ข้อความบันทึกปรากฏสองครั้งด้วย Python Logging


101

ฉันใช้การบันทึก Python และด้วยเหตุผลบางประการข้อความทั้งหมดของฉันจึงปรากฏขึ้นสองครั้ง

ฉันมีโมดูลสำหรับกำหนดค่าการบันทึก:

# BUG: It's outputting logging messages twice - not sure why - it's not the propagate setting.
def configure_logging(self, logging_file):
    self.logger = logging.getLogger("my_logger")
    self.logger.setLevel(logging.DEBUG)
    self.logger.propagate = 0
    # Format for our loglines
    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    # Setup console logging
    ch = logging.StreamHandler()
    ch.setLevel(logging.DEBUG)
    ch.setFormatter(formatter)
    self.logger.addHandler(ch)
    # Setup file logging as well
    fh = logging.FileHandler(LOG_FILENAME)
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(formatter)
    self.logger.addHandler(fh)

ต่อมาฉันเรียกวิธีนี้เพื่อกำหนดค่าการบันทึก:

if __name__ == '__main__':
    tom = Boy()
    tom.configure_logging(LOG_FILENAME)
    tom.buy_ham()

จากนั้นกล่าวว่าโมดูล buy_ham ฉันจะเรียก:

self.logger.info('Successfully able to write to %s' % path)

และด้วยเหตุผลบางประการข้อความทั้งหมดจึงปรากฏขึ้นสองครั้ง ฉันแสดงความคิดเห็นจากตัวจัดการสตรีมคนหนึ่ง แต่ก็ยังคงเหมือนเดิม แปลกนิดหน่อยไม่แน่ใจว่าทำไมถึงเกิดขึ้น ... ฮ่า ๆ สมมติว่าฉันพลาดสิ่งที่ชัดเจน

ไชโยวิคเตอร์


1
แน่ใจหรือว่าconfigure_logging()ไม่ถูกเรียกสองครั้ง (เช่นจากตัวสร้างด้วย)? มีการสร้าง Boy () เพียงชุดเดียวหรือไม่
Jacek Konieczny

1
การใช้self.logger.handlers = [ch]แทนจะช่วยแก้ปัญหานี้ได้แม้ว่าจะเป็นการดีที่สุดเพื่อให้แน่ใจว่าคุณจะไม่เรียกใช้โค้ดนี้ซ้ำสองครั้งตัวอย่างเช่นใช้if not self.loggerในช่วงเริ่มต้น
Ninjakannon

คำตอบ:


135

คุณกำลังเรียกconfigure_loggingสองครั้ง (อาจอยู่ใน__init__วิธีการBoy): getLoggerจะส่งคืนวัตถุเดียวกัน แต่addHandlerไม่ได้ตรวจสอบว่ามีการเพิ่มตัวจัดการที่คล้ายกันลงในเครื่องตัดไม้หรือไม่

ลองติดตามการโทรไปยังวิธีการนั้นและกำจัดหนึ่งในนั้น หรือตั้งค่าแฟlogging_initializedล็กเริ่มต้นเป็นFalseใน__init__วิธีการBoyและเปลี่ยนconfigure_loggingเพื่อไม่ทำอะไรเลยหากlogging_initializedเป็นTrueและตั้งค่าเป็นTrueหลังจากที่คุณเตรียมใช้งานตัวบันทึกข้อมูลแล้ว

หากโปรแกรมของคุณสร้างหลายBoyอินสแตนซ์คุณจะต้องเปลี่ยนวิธีทำสิ่งต่างๆด้วยconfigure_loggingฟังก์ชันส่วนกลางที่เพิ่มตัวจัดการและBoy.configure_loggingวิธีการเริ่มต้นself.loggerแอตทริบิวต์เท่านั้น

อีกวิธีในการแก้ปัญหานี้คือการตรวจสอบแอตทริบิวต์ตัวจัดการของคนตัดไม้ของคุณ:

logger = logging.getLogger('my_logger')
if not logger.handlers:
    # create the handlers and call logger.addHandler(logging_handler)

1
ใช่คุณพูดถูก - ฉันโง่ ฉันเรียกมันในinitเช่นเดียวกับที่อื่นอย่างชัดเจน ฮ่า ๆ. ขอบคุณ =)
victorhooi

ขอบคุณ. โซลูชันของคุณช่วยฉันได้ในวันนี้
ForeverLearner

1
ในกรณีของฉันพวกเขาปรากฏตัว 6 ครั้ง ฉันสงสัยว่าเพราะฉันประกาศคนตัดไม้ประเภทเดียวกันใน 6 คลาส oop
ตอบดู

5
ฉันต้องการแบ่งปันประสบการณ์ของฉันที่นี่: สำหรับแอปพลิเคชัน Flask ที่ฉันพัฒนาขึ้นข้อความบันทึกปรากฏมากกว่าสองครั้ง ฉันจะบอกว่าพวกเขากำลังเพิ่มขึ้นในไฟล์บันทึกเนื่องจากเมื่อโหลดแอปพลิเคชันและโมดูลแล้วloggerตัวแปรที่ใช้ไม่ใช่ตัวแปรที่สร้างอินสแตนซ์จากคลาสของฉัน แต่loggerตัวแปรอยู่ใน Python3 cache และตัวจัดการถูกเพิ่มทุกๆ 60 วินาทีโดย AppScheduler ที่ฉันกำหนดค่าไว้ ดังนั้นนี่if not logger.handlersเป็นวิธีที่ชาญฉลาดในการหลีกเลี่ยงปรากฏการณ์ประเภทนี้ ขอบคุณสำหรับการแก้ปัญหาเพื่อน :)!
ivanleoncz

2
ฉันพบปัญหานี้ในแอพ Flask วิธีนี้แก้ไขปัญหาสำหรับข้อความบันทึกที่สร้างขึ้นในแอปขวดหลัก แต่แอปของฉันใช้ฟังก์ชันในโมดูลไลบรารีและข้อความเหล่านั้นจากไลบรารีนั้นยังคงได้รับการบันทึกหลายครั้ง ฉันไม่ทราบวิธีแก้ไขปัญหานี้
Cas

27

หากคุณพบปัญหานี้และคุณไม่ได้เพิ่มตัวจัดการสองครั้งให้ดูคำตอบของ abarnert ที่นี่

จากเอกสาร :

หมายเหตุ: หากคุณแนบตัวจัดการกับคนตัดไม้และบรรพบุรุษของมันอย่างน้อยหนึ่งตัวเครื่องอาจส่งบันทึกเดียวกันหลายครั้ง โดยทั่วไปคุณไม่จำเป็นต้องแนบตัวจัดการกับคนตัดไม้มากกว่าหนึ่งคน - หากคุณเพียงแค่แนบเข้ากับเครื่องตัดไม้ที่เหมาะสมซึ่งอยู่สูงสุดในลำดับชั้นของคนตัดไม้ระบบจะเห็นเหตุการณ์ทั้งหมดที่บันทึกโดยผู้ตัดไม้รุ่นลูกหลานทั้งหมดหากมีการแพร่กระจาย การตั้งค่าถูกตั้งค่าเป็น True สถานการณ์ทั่วไปคือการแนบตัวจัดการกับ root logger เท่านั้นและเพื่อให้การขยายพันธุ์ดูแลส่วนที่เหลือ

ดังนั้นหากคุณต้องการตัวจัดการที่กำหนดเองใน "การทดสอบ" และคุณไม่ต้องการให้ข้อความของมันไปที่ตัวจัดการรากคำตอบนั้นง่ายมาก: ปิดpropagateแฟล็ก:

logger.propagate = False

1
นั่นคือคำตอบที่ดีที่สุด มันไม่ตรงกับวัตถุประสงค์ของโปสเตอร์ (ข้อผิดพลาดทางตรรกะในการเข้ารหัส) แต่ส่วนใหญ่มักจะเป็นเช่นนี้
Artem

ไชโย นี่คือสาเหตุที่แท้จริงของรายการซ้ำ (สำหรับกรณีทั่วไปส่วนใหญ่)
Mr.

นี่เป็นปัญหากับรหัสของฉันด้วย ขอบคุณมาก.
Harshit

คำตอบที่ดีที่สุดที่เคยมีมา ขอบคุณ!
Foivos Ts

เหตุใดจึงไม่สามารถเป็นค่าเริ่มต้นได้ ฉันสร้างคนตัดไม้ทั้งหมดของฉันภายใต้ "รูท" + โครงสร้างไดเรกทอรีดังนั้นฉันจึงสามารถควบคุมทุกอย่างได้อย่างง่ายดายจากคนตัดไม้ 'root'
MortenB

8

ตัวจัดการจะถูกเพิ่มทุกครั้งที่คุณโทรจากภายนอก ลองลบตัวจัดการหลังจากที่คุณทำงานเสร็จ:

self.logger.removeHandler(ch)

1
ฉันใช้logger.handlers.pop() ใน python 2.7 เคล็ดลับ
radtek

7

ฉันเป็นมือใหม่หัดงู แต่ดูเหมือนจะใช้ได้ผลสำหรับฉัน (Python 2.7)

while logger.handlers:
     logger.handlers.pop()

4

ในกรณีของฉันฉันจะตั้งค่า logger.propagate = Falseเพื่อป้องกันการพิมพ์ซ้ำซ้อน

ในโค้ดด้านล่างหากคุณนำออกlogger.propagate = Falseคุณจะเห็นการพิมพ์สองครั้ง

import logging
from typing import Optional

_logger: Optional[logging.Logger] = None

def get_logger() -> logging.Logger:
    global _logger
    if _logger is None:
        raise RuntimeError('get_logger call made before logger was setup!')
    return _logger

def set_logger(name:str, level=logging.DEBUG) -> None:
    global _logger
    if _logger is not None:
        raise RuntimeError('_logger is already setup!')
    _logger = logging.getLogger(name)
    _logger.handlers.clear()
    _logger.setLevel(level)
    ch = logging.StreamHandler()
    ch.setLevel(level)
    # warnings.filterwarnings("ignore", "(Possibly )?corrupt EXIF data", UserWarning)
    ch.setFormatter(_get_formatter())
    _logger.addHandler(ch)
    _logger.propagate = False # otherwise root logger prints things again


def _get_formatter() -> logging.Formatter:
    return logging.Formatter(
        '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')

นี่คือประเด็นที่ฉันมี ขอบคุณ
q0987

คำตอบที่ดี; การเพิ่มlogger.propagate = Falseเป็นวิธีแก้ปัญหาในการป้องกันการล็อกซ้ำสองครั้งในแอปพลิเคชัน Flask ที่โฮสต์โดย Waitress เมื่อเข้าสู่app.loggerอินสแตนซ์ของ Flask
bluebinary

1

การโทรเพื่อlogging.debug()โทรlogging.basicConfig()หากไม่มีการติดตั้งเครื่องจัดการรูท สิ่งนี้เกิดขึ้นสำหรับฉันในกรอบการทดสอบที่ฉันไม่สามารถควบคุมคำสั่งที่กรณีทดสอบเริ่มทำงานได้ รหัสเริ่มต้นของฉันกำลังติดตั้งรหัสที่สอง ค่าเริ่มต้นใช้การบันทึก BASIC_FORMAT ที่ฉันไม่ต้องการ


ฉันคิดว่านี่คือสิ่งที่เกิดขึ้นสำหรับฉัน คุณป้องกันการสร้างตัวบันทึกคอนโซลโดยอัตโนมัติได้อย่างไร
Robert

@ โรเบิร์ตมันเกี่ยวกับการตรวจสอบให้แน่ใจว่าคุณได้เริ่มต้นด้วยคนตัดไม้ที่คุณต้องการก่อนการโทรเข้าบันทึกครั้งแรก กรอบการทดสอบสามารถบดบังสิ่งนี้ได้ แต่ควรมีวิธีดำเนินการ นอกจากนี้หากคุณกำลังประมวลผลหลายขั้นตอนคุณต้องทำเช่นเดียวกันกับแต่ละกระบวนการ
JimB

1

ดูเหมือนว่าหากคุณส่งออกบางสิ่งไปยังคนตัดไม้ (โดยไม่ตั้งใจ) จากนั้นกำหนดค่ามันก็สายเกินไป ตัวอย่างเช่นในรหัสของฉันฉันมี

logging.warning("look out)"

...
ch = logging.StreamHandler(sys.stdout)
root = logging.getLogger()
root.addHandler(ch)

root.info("hello")

ฉันจะได้รับสิ่งที่ต้องการ (ไม่สนใจตัวเลือกรูปแบบ)

look out
hello
hello

และทุกอย่างถูกเขียนถึง stdout สองครั้ง ฉันเชื่อว่านี่เป็นเพราะการเรียกครั้งแรกเพื่อlogging.warningสร้างตัวจัดการใหม่โดยอัตโนมัติจากนั้นฉันก็เพิ่มตัวจัดการอื่นอย่างชัดเจน ปัญหานี้หายไปเมื่อฉันลบการlogging.warningโทรครั้งแรกโดยไม่ได้ตั้งใจ


0

ฉันได้รับสถานการณ์แปลก ๆ ที่บันทึกของคอนโซลเพิ่มขึ้นเป็นสองเท่า แต่บันทึกไฟล์ของฉันไม่ใช่ หลังจากขุดได้หลายตันฉันก็คิดออก

โปรดทราบว่าแพ็คเกจของบุคคลภายนอกสามารถลงทะเบียนคนตัดไม้ได้ นี่คือสิ่งที่ต้องระวัง (และในบางกรณีไม่สามารถป้องกันได้) ในหลายกรณีรหัสของบุคคลที่สามจะตรวจสอบเพื่อดูว่ามีรูทที่มีอยู่หรือไม่ตัวจัดการตัวตัดไม้หรือไม่ และหากไม่มีให้ลงทะเบียนตัวจัดการคอนโซลใหม่

วิธีแก้ปัญหาของฉันคือการลงทะเบียนคอนโซลคนตัดไม้ที่ระดับราก:

rootLogger = logging.getLogger()  # note no text passed in--that's how we grab the root logger
if not rootLogger.handlers:
        ch = logging.StreamHandler()
        ch.setLevel(logging.INFO)
        ch.setFormatter(logging.Formatter('%(process)s|%(levelname)s] %(message)s'))
        rootLogger.addHandler(ch)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.