Ghi nhật ký sách dạy nấu ăn¶
- tác giả:
Vinay Sajip <vinay_sajip tại red-dove dot com>
Trang này chứa một số công thức liên quan đến việc ghi nhật ký, những công thức này trước đây được cho là hữu ích. Để biết các liên kết đến thông tin hướng dẫn và tham khảo, vui lòng xem Các tài nguyên khác.
Sử dụng đăng nhập nhiều mô-đun¶
Nhiều lệnh gọi tới logging.getLogger('someLogger') trả về một tham chiếu đến cùng một đối tượng logger. Điều này đúng không chỉ trong cùng một mô-đun mà còn đúng trên các mô-đun miễn là nó nằm trong cùng một quy trình thông dịch Python. Nó đúng cho các tham chiếu đến cùng một đối tượng; Ngoài ra, mã ứng dụng có thể xác định và định cấu hình trình ghi nhật ký gốc trong một mô-đun và tạo (nhưng không định cấu hình) trình ghi nhật ký con trong một mô-đun riêng biệt và tất cả lệnh gọi của trình ghi nhật ký tới mô-đun con sẽ được chuyển tới mô-đun cha. Đây là một mô-đun chính:
nhập nhật ký
nhập phụ_module
trình ghi nhật ký # create với 'spam_application'
logger = log.getLogger('spam_application')
logger.setLevel(logging.DEBUG)
trình xử lý tệp # create ghi lại các thông báo gỡ lỗi
fh = log.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
Trình xử lý bảng điều khiển # create có mức nhật ký cao hơn
ch = ghi nhật ký.StreamHandler()
ch.setLevel(logging.ERROR)
# create định dạng và thêm nó vào trình xử lý
formatter = log.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(trình định dạng)
ch.setFormatter(trình định dạng)
# add các trình xử lý cho trình ghi nhật ký
logger.addHandler(fh)
logger.addHandler(ch)
logger.info('tạo một phiên bản của aux_module.Auxiliary')
a = phụ trợ_module.Auxiliary()
logger.info('đã tạo một phiên bản của aux_module.Auxiliary')
logger.info('gọi điện thoại phụ_module.Auxiliary.do_something')
a.do_something()
logger.info('đã hoàn thành phụ trợ_module.Auxiliary.do_something')
logger.info('gọi phụ_module.some_function()')
phụ_module.some_function()
logger.info('thực hiện với auxial_module.some_function()')
Đây là mô-đun phụ trợ:
nhập nhật ký
trình ghi nhật ký # create
module_logger = log.getLogger('spam_application.auxiliary')
lớp phụ trợ:
định nghĩa __init__(tự):
self.logger = log.getLogger('spam_application.auxiliary.Auxiliary')
self.logger.info('tạo một phiên bản của Phụ trợ')
def do_something(self):
self.logger.info('làm gì đó')
a = 1 + 1
self.logger.info('làm xong việc gì đó')
chắc chắn some_function():
module_logger.info('đã nhận được cuộc gọi tới "some_function"')
Đầu ra trông như thế này:
23-03-2005 23:47:11,663 - spam_application - INFO -
tạo một phiên bản của aux_module.Auxiliary
23-03-2005 23:47:11,665 - spam_application.auxiliary.Auxiliary - INFO -
tạo một phiên bản phụ trợ
23-03-2005 23:47:11,665 - spam_application - INFO -
đã tạo một phiên bản của aux_module.Auxiliary
23-03-2005 23:47:11,668 - spam_application - INFO -
gọi auxiliary_module.Auxiliary.do_something
23-03-2005 23:47:11,668 - spam_application.auxiliary.Auxiliary - INFO -
đang làm gì đó
23-03-2005 23:47:11,669 - spam_application.auxiliary.Auxiliary - INFO -
làm xong việc gì đó
23-03-2005 23:47:11,670 - spam_application - INFO -
đã hoàn thành phụ trợ_module.Auxiliary.do_something
23-03-2005 23:47:11,671 - spam_application - INFO -
gọi phụ trợ_module.some_function()
23-03-2005 23:47:11,672 - spam_application.auxiliary - INFO -
đã nhận được cuộc gọi tới 'some_function'
23-03-2005 23:47:11,673 - spam_application - INFO -
được thực hiện với phụ trợ_module.some_function()
Ghi nhật ký từ nhiều chủ đề¶
Đăng nhập từ nhiều chủ đề không đòi hỏi nỗ lực đặc biệt. Ví dụ sau đây cho thấy việc ghi nhật ký từ luồng chính (ban đầu) và một luồng khác:
nhập nhật ký
nhập luồng
thời gian nhập khẩu
công nhân def (arg):
trong khi không arg['stop']:
logging.debug('Xin chào từ myfunc')
thời gian.ngủ (0,5)
chắc chắn chính():
logging.basicConfig(level=logging.DEBUG, format='%(relativeCreated)6d %(threadName)s %(message)s')
thông tin = {'dừng': Sai}
thread = threading.Thread(target=worker, args=(info,))
thread.start()
trong khi Đúng:
thử:
logging.debug('Xin chào từ chính')
thời gian.ngủ (0,75)
ngoại trừ Bàn phímInterrupt:
thông tin['stop'] = Đúng
phá vỡ
thread.join()
nếu __name__ == '__main__':
chính()
Khi chạy, tập lệnh sẽ in nội dung như sau:
0 Chủ đề-1 Xin chào từ myfunc
3 MainThread Xin chào từ chính
505 Thread-1 Xin chào từ myfunc
755 MainThread Xin chào từ chính
1007 Chủ đề-1 Xin chào từ myfunc
1507 MainThread Xin chào từ chính
1508 Thread-1 Xin chào từ myfunc
2010 Chủ đề-1 Xin chào từ myfunc
2258 MainThread Xin chào từ chính
2512 Chủ đề-1 Xin chào từ myfunc
3009 MainThread Xin chào từ chính
3013 Chủ đề-1 Xin chào từ myfunc
3515 Thread-1 Xin chào từ myfunc
3761 MainThread Xin chào từ chính
4017 Chủ đề-1 Xin chào từ myfunc
4513 MainThread Xin chào từ chính
4518 Chủ đề-1 Xin chào từ myfunc
Điều này cho thấy đầu ra ghi nhật ký xen kẽ như người ta có thể mong đợi. Tất nhiên, cách tiếp cận này hoạt động cho nhiều chủ đề hơn được hiển thị ở đây.
Nhiều trình xử lý và định dạng¶
Trình ghi nhật ký là các đối tượng Python đơn giản. Phương thức addHandler() không có hạn ngạch tối thiểu hoặc tối đa cho số lượng trình xử lý bạn có thể thêm. Đôi khi, sẽ có ích nếu ứng dụng ghi lại tất cả các thông báo ở mọi mức độ nghiêm trọng vào một tệp văn bản đồng thời ghi lại các lỗi hoặc cao hơn vào bảng điều khiển. Để thiết lập tính năng này, chỉ cần định cấu hình trình xử lý thích hợp. Các cuộc gọi ghi nhật ký trong mã ứng dụng sẽ không thay đổi. Đây là một sửa đổi nhỏ đối với ví dụ về cấu hình dựa trên mô-đun đơn giản trước đó:
nhập nhật ký
logger = log.getLogger('simple_example')
logger.setLevel(logging.DEBUG)
trình xử lý tệp # create ghi lại các thông báo gỡ lỗi
fh = log.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
Trình xử lý bảng điều khiển # create có mức nhật ký cao hơn
ch = ghi nhật ký.StreamHandler()
ch.setLevel(logging.ERROR)
# create định dạng và thêm nó vào trình xử lý
formatter = log.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(trình định dạng)
fh.setFormatter(trình định dạng)
# add trình xử lý để ghi nhật ký
logger.addHandler(ch)
logger.addHandler(fh)
# mã 'ứng dụng'
logger.debug('thông báo gỡ lỗi')
logger.info('tin nhắn thông tin')
logger.warning('thông báo cảnh báo')
logger.error('thông báo lỗi')
logger.cript('thông báo quan trọng')
Lưu ý rằng mã 'ứng dụng' không quan tâm đến nhiều trình xử lý. Tất cả những gì thay đổi là việc bổ sung và cấu hình trình xử lý mới có tên fh.
Khả năng tạo trình xử lý mới với các bộ lọc có mức độ nghiêm trọng cao hơn hoặc thấp hơn có thể rất hữu ích khi viết và thử nghiệm một ứng dụng. Thay vì sử dụng nhiều câu lệnh print để gỡ lỗi, hãy sử dụng logger.debug: Không giống như các câu lệnh in mà bạn sẽ phải xóa hoặc nhận xét sau, các câu lệnh logger.debug có thể vẫn còn nguyên trong mã nguồn và không hoạt động cho đến khi bạn cần chúng lại. Vào thời điểm đó, thay đổi duy nhất cần thực hiện là sửa đổi mức độ nghiêm trọng của trình ghi nhật ký và/hoặc trình xử lý để gỡ lỗi.
Đăng nhập vào nhiều điểm đến¶
Giả sử bạn muốn đăng nhập vào bảng điều khiển và tệp với các định dạng tin nhắn khác nhau và trong các trường hợp khác nhau. Giả sử bạn muốn ghi nhật ký các thông báo có cấp độ DEBUG trở lên vào tệp và những thông báo đó ở cấp độ INFO trở lên vào bảng điều khiển. Cũng giả sử rằng tệp phải chứa dấu thời gian, nhưng thông báo trên bảng điều khiển thì không. Đây là cách bạn có thể đạt được điều này:
nhập nhật ký
# set đăng nhập vào tập tin - xem phần trước để biết thêm chi tiết
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
datefmt='%m-%d %H:%M',
tên tệp='/tmp/myapp.log',
filemode='w')
# define một Trình xử lý ghi tin nhắn INFO hoặc cao hơn vào sys.stderr
console = log.StreamHandler()
console.setLevel(logging.INFO)
# set một định dạng đơn giản hơn để sử dụng trên bảng điều khiển
trình định dạng = log.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
# tell trình xử lý để sử dụng định dạng này
console.setFormatter(formatter)
# add trình xử lý trình ghi nhật ký gốc
logging.getLogger().addHandler(console)
# Now, chúng ta có thể đăng nhập vào trình ghi nhật ký gốc hoặc bất kỳ trình ghi nhật ký nào khác. Đầu tiên là gốc...
logging.info('Jackdaws thích tượng nhân sư thạch anh lớn của tôi.')
# Now, hãy xác định một vài trình ghi nhật ký khác có thể đại diện cho các khu vực trong
# application:
logger1 = log.getLogger('myapp.area1')
logger2 = log.getLogger('myapp.area2')
logger1.debug('Tia gió nhanh chóng, làm Jim bực mình.')
logger1.info('Ngựa vằn nhảy nhanh như thế nào.')
logger2.warning('Jail zesty vixen kẻ đã cướp tiền từ lang băm.')
logger2.error('Năm pháp sư quyền anh nhảy rất nhanh.')
Khi bạn chạy cái này, trên bảng điều khiển bạn sẽ thấy
root : INFO Jackdaws thích tượng nhân sư thạch anh lớn của tôi.
myapp.area1 : INFO Ngựa vằn nhảy nhanh đến mức nào.
myapp.area2 : WARNING Jail zesty vixen đã nhận tiền từ lang băm.
myapp.area2 : ERROR Năm pháp sư quyền anh nhảy rất nhanh.
và trong tập tin bạn sẽ thấy một cái gì đó như
22-10 22:19 root INFO Jackdaws yêu tượng nhân sư thạch anh lớn của tôi.
22-10 22:19 myapp.area1 DEBUG Gió thổi nhanh, làm phiền lòng Jim.
22-10 22:19 myapp.area1 INFO Ngựa vằn nhảy nhanh đến mức khó chịu.
22-10 22:19 myapp.area2 WARNING Nhà tù zesty vixen đã cướp tiền từ lang băm.
22-10 22:19 myapp.area2 ERROR Năm pháp sư quyền anh nhảy rất nhanh.
Như bạn có thể thấy, thông báo DEBUG chỉ hiển thị trong tệp. Các tin nhắn khác được gửi đến cả hai điểm đến.
Ví dụ này sử dụng bảng điều khiển và trình xử lý tệp, nhưng bạn có thể sử dụng bất kỳ số lượng và tổ hợp trình xử lý nào bạn chọn.
Lưu ý rằng việc lựa chọn tên tệp nhật ký /tmp/myapp.log ở trên ngụ ý việc sử dụng vị trí tiêu chuẩn cho các tệp tạm thời trên hệ thống POSIX. Trên Windows, bạn có thể cần chọn tên thư mục khác cho nhật ký - chỉ cần đảm bảo rằng thư mục đó tồn tại và bạn có quyền tạo cũng như cập nhật các tệp trong đó.
Xử lý tùy chỉnh các cấp độ¶
Đôi khi, bạn có thể muốn thực hiện điều gì đó hơi khác so với cách xử lý cấp độ tiêu chuẩn trong trình xử lý, trong đó tất cả các cấp trên ngưỡng đều được trình xử lý xử lý. Để làm điều này, bạn cần sử dụng các bộ lọc. Hãy xem xét một tình huống mà bạn muốn sắp xếp mọi thứ như sau:
Gửi tin nhắn có mức độ nghiêm trọng
INFOvàWARNINGtớisys.stdoutGửi tin nhắn có mức độ nghiêm trọng
ERRORtrở lên tớisys.stderrGửi tin nhắn có mức độ nghiêm trọng
DEBUGtrở lên vào tệpapp.log
Giả sử bạn định cấu hình ghi nhật ký bằng JSON sau:
{
"phiên bản": 1,
"disable_ex hiện_loggers": sai,
"trình định dạng": {
"đơn giản": {
"format": "%(levelname)-8s - %(message)s"
}
},
"người xử lý": {
"thiết bị xuất chuẩn": {
"class": "logging.StreamHandler",
"cấp độ": "INFO",
"trình định dạng": "đơn giản",
"stream": "ext://sys.stdout"
},
"stderr": {
"class": "logging.StreamHandler",
"cấp độ": "ERROR",
"trình định dạng": "đơn giản",
"stream": "ext://sys.stderr"
},
"tập tin": {
"class": "logging.FileHandler",
"trình định dạng": "đơn giản",
"tên tệp": "app.log",
"chế độ": "w"
}
},
"gốc": {
"cấp độ": "DEBUG",
"người xử lý": [
"stderr",
"thiết bị xuất chuẩn",
"tập tin"
]
}
}
Cấu hình này thực hiện almost những gì chúng tôi muốn, ngoại trừ việc sys.stdout sẽ hiển thị các thông báo về mức độ nghiêm trọng ERROR và chỉ những sự kiện có mức độ nghiêm trọng này trở lên mới được theo dõi cũng như các thông báo INFO và WARNING. Để ngăn chặn điều này, chúng ta có thể thiết lập bộ lọc loại trừ những thông báo đó và thêm nó vào trình xử lý có liên quan. Điều này có thể được cấu hình bằng cách thêm phần filters song song với formatters và handlers:
{
"bộ lọc": {
"warnings_and_below": {
"()" : "__main__.filter_maker",
"cấp độ": "WARNING"
}
}
}
và thay đổi phần trên trình xử lý stdout để thêm nó:
{
"thiết bị xuất chuẩn": {
"class": "logging.StreamHandler",
"cấp độ": "INFO",
"trình định dạng": "đơn giản",
"stream": "ext://sys.stdout",
"bộ lọc": ["warnings_and_below"]
}
}
Bộ lọc chỉ là một hàm, vì vậy chúng ta có thể định nghĩa filter_maker (một hàm xuất xưởng) như sau:
def filter_maker(cấp độ):
cấp độ = getattr(ghi nhật ký, cấp độ)
bộ lọc def (bản ghi):
trả về bản ghi.levelno <= cấp
bộ lọc trở lại
Điều này chuyển đổi đối số chuỗi được truyền vào ở mức số và trả về một hàm chỉ trả về True nếu mức của bản ghi được truyền vào bằng hoặc thấp hơn mức được chỉ định. Lưu ý rằng trong ví dụ này, tôi đã xác định filter_maker trong tập lệnh thử nghiệm main.py mà tôi chạy từ dòng lệnh, vì vậy mô-đun của nó sẽ là __main__ - do đó có __main__.filter_maker trong cấu hình bộ lọc. Bạn sẽ cần thay đổi điều đó nếu bạn xác định nó trong một mô-đun khác.
Với bộ lọc được thêm vào, chúng ta có thể chạy main.py, đầy đủ là:
nhập json
nhập nhật ký
nhập log.config
CONFIG = '''
{
"phiên bản": 1,
"disable_ex hiện_loggers": sai,
"trình định dạng": {
"đơn giản": {
"format": "%(levelname)-8s - %(message)s"
}
},
"bộ lọc": {
"warnings_and_below": {
"()" : "__main__.filter_maker",
"cấp độ": "WARNING"
}
},
"người xử lý": {
"thiết bị xuất chuẩn": {
"class": "logging.StreamHandler",
"cấp độ": "INFO",
"trình định dạng": "đơn giản",
"stream": "ext://sys.stdout",
"bộ lọc": ["warnings_and_below"]
},
"stderr": {
"class": "logging.StreamHandler",
"cấp độ": "ERROR",
"trình định dạng": "đơn giản",
"stream": "ext://sys.stderr"
},
"tập tin": {
"class": "logging.FileHandler",
"trình định dạng": "đơn giản",
"tên tệp": "app.log",
"chế độ": "w"
}
},
"gốc": {
"cấp độ": "DEBUG",
"người xử lý": [
"stderr",
"thiết bị xuất chuẩn",
"tập tin"
]
}
}
'''
def filter_maker(cấp độ):
cấp độ = getattr(ghi nhật ký, cấp độ)
bộ lọc def (bản ghi):
trả về bản ghi.levelno <= cấp
bộ lọc trở lại
logging.config.dictConfig(json.loads(CONFIG))
logging.debug('Tin nhắn DEBUG')
logging.info('Một tin nhắn INFO')
logging.warning('Tin nhắn WARNING')
logging.error('Một tin nhắn ERROR')
logging.cript('Tin nhắn CRITICAL')
Và sau khi chạy nó như thế này:
python main.py 2>stderr.log>stdout.log
Chúng ta có thể thấy kết quả đúng như mong đợi:
thêm $ *.log
::::::::::::::
ứng dụng.log
::::::::::::::
DEBUG - Tin nhắn DEBUG
INFO - Một tin nhắn INFO
WARNING - Một tin nhắn WARNING
ERROR - Một tin nhắn ERROR
CRITICAL - Một tin nhắn CRITICAL
::::::::::::::
stderr.log
::::::::::::::
ERROR - Một tin nhắn ERROR
CRITICAL - Một tin nhắn CRITICAL
::::::::::::::
thiết bị xuất chuẩn.log
::::::::::::::
INFO - Một tin nhắn INFO
WARNING - Một tin nhắn WARNING
Ví dụ về cấu hình máy chủ¶
Dưới đây là ví dụ về mô-đun sử dụng máy chủ cấu hình ghi nhật ký:
nhập nhật ký
nhập log.config
thời gian nhập khẩu
hệ điều hành nhập khẩu
tập tin cấu hình ban đầu # read
logging.config.fileConfig('logging.conf')
# create và bắt đầu nghe trên cổng 9999
t = log.config.listen(9999)
t.start()
logger = log.getLogger('simpleExample')
thử:
# loop qua ghi lại các cuộc gọi để thấy sự khác biệt
Cấu hình # new được thực hiện cho đến khi nhấn Ctrl+C
trong khi Đúng:
logger.debug('thông báo gỡ lỗi')
logger.info('tin nhắn thông tin')
logger.warning('thông báo cảnh báo')
logger.error('thông báo lỗi')
logger.cript('thông báo quan trọng')
thời gian.ngủ(5)
ngoại trừ Bàn phímInterrupt:
# cleanup
ghi nhật ký.config.stopListening()
t.join()
Và đây là tập lệnh lấy tên tệp và gửi tệp đó đến máy chủ, đặt trước đúng độ dài được mã hóa nhị phân, làm cấu hình ghi nhật ký mới:
#!/usr/bin/env trăn
nhập ổ cắm, sys, struct
với open(sys.argv[1], 'rb') là f:
data_to_send = f.read()
HOST = 'localhost'
PORT = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
in('đang kết nối...')
s.connect((HOST, PORT))
print('gửi cấu hình...')
s.send(struct.pack('>L', len(data_to_send)))
s.send(data_to_send)
s.close()
in('hoàn thành')
Xử lý các trình xử lý chặn¶
Đôi khi, bạn phải yêu cầu trình xử lý ghi nhật ký của mình thực hiện công việc của họ mà không chặn luồng bạn đang đăng nhập. Điều này phổ biến trong các ứng dụng web, mặc dù tất nhiên nó cũng xảy ra trong các tình huống khác.
Thủ phạm phổ biến thể hiện hành vi chậm chạp là SMTPHandler: việc gửi email có thể mất nhiều thời gian vì một số lý do nằm ngoài tầm kiểm soát của nhà phát triển (ví dụ: cơ sở hạ tầng mạng hoặc thư hoạt động kém). Nhưng hầu như bất kỳ trình xử lý dựa trên mạng nào cũng có thể chặn: Ngay cả thao tác SocketHandler cũng có thể thực hiện truy vấn DNS ở mức cơ bản, tốc độ quá chậm (và truy vấn này có thể nằm sâu trong mã thư viện socket, bên dưới lớp Python và nằm ngoài tầm kiểm soát của bạn).
Một giải pháp là sử dụng cách tiếp cận hai phần. Đối với phần đầu tiên, chỉ đính kèm QueueHandler vào những trình ghi nhật ký được truy cập từ các luồng quan trọng về hiệu suất. Họ chỉ cần ghi vào hàng đợi của mình, hàng đợi này có thể được điều chỉnh kích thước đến dung lượng đủ lớn hoặc được khởi tạo mà không có giới hạn trên đối với kích thước của chúng. Việc ghi vào hàng đợi thường sẽ được chấp nhận nhanh chóng, mặc dù bạn có thể cần phải nắm bắt ngoại lệ queue.Full để đề phòng mã của mình. Nếu bạn là nhà phát triển thư viện có các luồng quan trọng về hiệu suất trong mã của họ, hãy nhớ ghi lại điều này (cùng với đề xuất chỉ đính kèm QueueHandlers vào trình ghi nhật ký của bạn) vì lợi ích của các nhà phát triển khác sẽ sử dụng mã của bạn.
Phần thứ hai của giải pháp là QueueListener, được thiết kế tương tự như QueueHandler. Một QueueListener rất đơn giản: nó vượt qua một hàng đợi và một số trình xử lý, đồng thời kích hoạt một luồng nội bộ lắng nghe hàng đợi của nó đối với các LogRecord được gửi từ QueueHandlers (hoặc bất kỳ nguồn nào khác của LogRecords). Zz005zz được xóa khỏi hàng đợi và được chuyển đến bộ xử lý để xử lý.
Ưu điểm của việc có một lớp QueueListener riêng biệt là bạn có thể sử dụng cùng một phiên bản để phục vụ nhiều QueueHandlers. Điều này thân thiện với tài nguyên hơn là có các phiên bản luồng của các lớp trình xử lý hiện có, sẽ tiêu tốn một luồng cho mỗi trình xử lý mà không mang lại lợi ích cụ thể nào.
Sau đây là một ví dụ về việc sử dụng hai lớp này (bỏ qua phần nhập):
que = queue.Queue(-1) # no giới hạn về kích thước
queue_handler=QueueHandler(que)
xử lý = ghi nhật ký.StreamHandler()
người nghe = QueueListener(que, handler)
root = log.getLogger()
root.addHandler(queue_handler)
formatter = log.Formatter('%(threadName)s: %(message)s')
handler.setFormatter(formatter)
người nghe.start()
Đầu ra nhật ký # The sẽ hiển thị chuỗi được tạo
sự kiện # the (luồng chính) chứ không phải sự kiện nội bộ
# thread giám sát hàng đợi nội bộ. Đây là cái gì
# you muốn xảy ra.
root.warning('Coi chừng!')
người nghe.stop()
mà khi chạy sẽ tạo ra:
Chủ đề chính: Hãy coi chừng!
Ghi chú
Mặc dù cuộc thảo luận trước đó không nói cụ thể về mã async mà là về trình xử lý ghi nhật ký chậm, cần lưu ý rằng khi đăng nhập từ mã async, mạng và thậm chí cả trình xử lý tệp có thể dẫn đến sự cố (chặn vòng lặp sự kiện) vì một số thao tác ghi nhật ký được thực hiện từ nội bộ asyncio. Tốt nhất, nếu bất kỳ mã không đồng bộ nào được sử dụng trong một ứng dụng, hãy sử dụng phương pháp ghi nhật ký ở trên để mọi mã chặn chỉ chạy trong luồng QueueListener.
Thay đổi trong phiên bản 3.5: Trước Python 3.5, QueueListener luôn chuyển mọi tin nhắn nhận được từ hàng đợi đến mọi trình xử lý mà nó được khởi tạo. (Điều này là do người ta cho rằng việc lọc cấp độ hoàn toàn được thực hiện ở phía bên kia, nơi hàng đợi được lấp đầy.) Từ phiên bản 3.5 trở đi, hành vi này có thể được thay đổi bằng cách chuyển đối số từ khóa respect_handler_level=True tới hàm tạo của trình nghe. Khi điều này được thực hiện, người nghe sẽ so sánh cấp độ của từng thông báo với cấp độ của trình xử lý và chỉ chuyển thông báo đến trình xử lý nếu thấy phù hợp.
Thay đổi trong phiên bản 3.14: QueueListener có thể được khởi động (và dừng) thông qua câu lệnh with. Ví dụ:
với QueueListener(que, handler) là người nghe:
trình nghe hàng đợi # The tự động bắt đầu
# when khối 'với' được nhập.
vượt qua
Trình nghe hàng đợi # The tự động dừng một lần
Khối # the 'with' đã được thoát.
Gửi và nhận các sự kiện ghi nhật ký trên mạng¶
Giả sử bạn muốn gửi các sự kiện ghi nhật ký qua mạng và xử lý chúng ở đầu nhận. Một cách đơn giản để thực hiện việc này là đính kèm phiên bản SocketHandler vào trình ghi nhật ký gốc ở đầu gửi:
nhập nhật ký, log.handlers
rootLogger = log.getLogger()
rootLogger.setLevel(logging.DEBUG)
socketHandler = log.handlers.SocketHandler('localhost',
log.handlers.DEFAULT_TCP_LOGGING_PORT)
# don không bận tâm đến trình định dạng, vì trình xử lý ổ cắm sẽ gửi sự kiện dưới dạng
# an dưa chua chưa được định dạng
rootLogger.addHandler(socketHandler)
# Now, chúng ta có thể đăng nhập vào trình ghi nhật ký gốc hoặc bất kỳ trình ghi nhật ký nào khác. Đầu tiên là gốc...
logging.info('Jackdaws thích tượng nhân sư thạch anh lớn của tôi.')
# Now, hãy xác định một vài trình ghi nhật ký khác có thể đại diện cho các khu vực trong
# application:
logger1 = log.getLogger('myapp.area1')
logger2 = log.getLogger('myapp.area2')
logger1.debug('Tia gió nhanh chóng, làm Jim bực mình.')
logger1.info('Ngựa vằn nhảy nhanh như thế nào.')
logger2.warning('Jail zesty vixen kẻ đã cướp tiền từ lang băm.')
logger2.error('Năm pháp sư quyền anh nhảy rất nhanh.')
Ở đầu nhận, bạn có thể thiết lập bộ thu bằng mô-đun socketserver. Đây là một ví dụ hoạt động cơ bản:
nhập khẩu dưa chua
nhập nhật ký
nhập log.handlers
nhập khẩu máy chủ ổ cắm
nhập cấu trúc
lớp LogRecordStreamHandler(socketserver.StreamRequestHandler):
"""Trình xử lý yêu cầu ghi nhật ký phát trực tuyến.
Về cơ bản, điều này sẽ ghi lại bản ghi bằng bất kỳ chính sách ghi nhật ký nào
được cấu hình cục bộ.
"""
xử lý def (tự):
"""
Xử lý nhiều yêu cầu - mỗi yêu cầu dự kiến có độ dài 4 byte,
theo sau là LogRecord ở định dạng dưa chua. Ghi lại bản ghi
theo bất kỳ chính sách nào được cấu hình cục bộ.
"""
trong khi Đúng:
chunk = self.connection.recv(4)
nếu len(chunk) < 4:
phá vỡ
slen = struct.unpack('>L', chunk)[0]
chunk = self.connection.recv(slen)
trong khi len(chunk) < slen:
chunk = chunk + self.connection.recv(slen - len(chunk))
obj = self.unPickle(chunk)
bản ghi = log.makeLogRecord(obj)
self.handleLogRecord(bản ghi)
def unPickle(tự, dữ liệu):
trả về Pickle.loads(dữ liệu)
def handLogRecord(self, record):
# if một tên được chỉ định, chúng tôi sử dụng trình ghi nhật ký được đặt tên thay vì tên
# implied theo kỷ lục.
nếu self.server.logname không phải là Không có:
tên = self.server.logname
khác:
tên = record.name
logger = log.getLogger(tên)
# N.B. Bản ghi EVERY được ghi lại. Điều này là do Logger.handle
# is thường được gọi là lọc cấp độ logger AFTER. Nếu bạn muốn
# to thực hiện lọc, thực hiện ở phía máy khách để tiết kiệm lãng phí
# cycles và băng thông mạng!
logger.handle(bản ghi)
lớp LogRecordSocketReceiver(socketserver.ThreadingTCPServer):
"""
Bộ thu ghi nhật ký dựa trên ổ cắm TCP đơn giản phù hợp để thử nghiệm.
"""
allow_reuse_address = Đúng
def __init__(self, Host='localhost',
port=logging.handlers.DEFAULT_TCP_LOGGING_PORT,
handler=LogRecordStreamHandler):
socketserver.ThreadingTCPServer.__init__(self, (host, port), handler)
tự.abort = 0
self.timeout = 1
self.logname = Không
def phục vụ_until_stopped(tự):
nhập khẩu chọn
hủy bỏ = 0
trong khi không hủy bỏ:
rd, wr, ex = select.select([self.socket.fileno()],
[], [],
tự.thời gian chờ)
nếu thứ:
self.handle_request()
hủy bỏ = self.abort
chắc chắn chính():
ghi nhật ký.basicConfig(
format='%(relativeCreated)5d %(name)-15s %(levelname)-8s %(message)s')
tcpserver = LogRecordSocketReceiver()
print('Sắp khởi động máy chủ TCP...')
tcpserver.serve_until_stopped()
nếu __name__ == '__main__':
chính()
Đầu tiên chạy máy chủ, sau đó chạy máy khách. Về phía máy khách, không có gì được in trên bảng điều khiển; về phía máy chủ, bạn sẽ thấy một cái gì đó như:
Sắp khởi động máy chủ TCP...
59 gốc INFO Jackdaws yêu thích nhân sư thạch anh lớn của tôi.
59 myapp.area1 DEBUG Gió thổi nhanh, làm Jim bực mình.
69 myapp.area1 INFO Ngựa vằn nhảy nhanh đến mức khó chịu.
69 myapp.area2 WARNING Nhà tù zesty vixen đã nhận tiền từ lang băm.
69 myapp.area2 ERROR Năm pháp sư quyền anh nhảy nhanh.
Lưu ý rằng có một số vấn đề bảo mật với dưa chua trong một số trường hợp. Nếu những điều này ảnh hưởng đến bạn, bạn có thể sử dụng sơ đồ tuần tự hóa thay thế bằng cách ghi đè phương thức makePickle() và triển khai phương án thay thế của bạn ở đó, cũng như điều chỉnh tập lệnh trên để sử dụng phương pháp tuần tự hóa thay thế của bạn.
Chạy trình nghe ổ cắm ghi nhật ký trong sản xuất¶
Để chạy trình xử lý ghi nhật ký trong sản xuất, bạn có thể cần sử dụng công cụ quản lý quy trình như Supervisor. Here is a Gist cung cấp các tệp cơ bản để chạy chức năng trên bằng Trình giám sát. Nó bao gồm các tập tin sau:
Tập tin |
Mục đích |
|---|---|
|
Tập lệnh Bash để chuẩn bị môi trường cho thử nghiệm |
|
Tệp cấu hình Người giám sát, có các mục dành cho người nghe và ứng dụng web đa quy trình |
|
Tập lệnh Bash để đảm bảo rằng Người giám sát đang chạy với cấu hình trên |
|
Chương trình nghe ổ cắm nhận các sự kiện nhật ký và ghi chúng vào một tệp |
|
Một ứng dụng web đơn giản thực hiện ghi nhật ký thông qua ổ cắm được kết nối với trình nghe |
|
Tệp cấu hình JSON cho ứng dụng web |
|
Tập lệnh Python để thực hiện ứng dụng web |
Ứng dụng web sử dụng Gunicorn, một máy chủ ứng dụng web phổ biến khởi động nhiều quy trình xử lý để xử lý các yêu cầu. Thiết lập ví dụ này cho thấy cách các công nhân có thể ghi vào cùng một tệp nhật ký mà không xung đột với nhau --- tất cả đều đi qua trình nghe ổ cắm.
Để kiểm tra các tệp này, hãy thực hiện như sau trong môi trường POSIX:
Tải xuống the Gist dưới dạng kho lưu trữ ZIP bằng nút Download ZIP.
Giải nén các tập tin trên từ kho lưu trữ vào một thư mục đầu.
Trong thư mục đầu, hãy chạy
bash prepare.shđể chuẩn bị sẵn sàng. Điều này tạo ra một thư mục conrunđể chứa các tệp nhật ký và liên quan đến Người giám sát, cũng như thư mục convenvđể chứa môi trường ảo trong đóbottle,gunicornvàsupervisorđược cài đặt.Chạy
bash ensure_app.shđể đảm bảo rằng Người giám sát đang chạy với cấu hình trên.Chạy
venv/bin/python client.pyđể thực thi ứng dụng web, điều này sẽ dẫn đến việc ghi các bản ghi vào nhật ký.Kiểm tra các tệp nhật ký trong thư mục con
run. Bạn sẽ thấy các dòng nhật ký gần đây nhất trong các tệp khớp với mẫuapp.log*. Chúng sẽ không theo bất kỳ thứ tự cụ thể nào vì chúng được xử lý đồng thời bởi các quy trình công nhân khác nhau theo cách không xác định.Bạn có thể tắt trình nghe và ứng dụng web bằng cách chạy
venv/bin/supervisorctl -c supervisor.conf shutdown.
Bạn có thể cần phải điều chỉnh các tệp cấu hình trong trường hợp hiếm gặp là các cổng được định cấu hình xung đột với thứ khác trong môi trường thử nghiệm của bạn.
Cấu hình mặc định sử dụng ổ cắm TCP trên cổng 9020. Bạn có thể sử dụng ổ cắm Miền Unix thay vì ổ cắm TCP bằng cách thực hiện như sau:
Trong
listener.json, thêm khóasocketkèm theo đường dẫn đến ổ cắm miền bạn muốn sử dụng. Nếu có khóa này, trình nghe sẽ lắng nghe trên ổ cắm miền tương ứng chứ không phải trên ổ cắm TCP (khóaportbị bỏ qua).Trong
webapp.json, thay đổi từ điển cấu hình trình xử lý ổ cắm để giá trịhostlà đường dẫn đến ổ cắm miền và đặt giá trịportthànhnull.
Thêm thông tin theo ngữ cảnh vào đầu ra ghi nhật ký của bạn¶
Đôi khi bạn muốn đầu ra ghi nhật ký chứa thông tin theo ngữ cảnh ngoài các tham số được chuyển cho lệnh gọi ghi nhật ký. Ví dụ: trong một ứng dụng nối mạng, có thể mong muốn ghi thông tin cụ thể của khách hàng vào nhật ký (ví dụ: tên người dùng hoặc địa chỉ IP của khách hàng từ xa). Mặc dù bạn có thể sử dụng tham số extra để đạt được điều này, nhưng việc truyền thông tin theo cách này không phải lúc nào cũng thuận tiện. Mặc dù việc tạo các phiên bản Logger trên cơ sở mỗi kết nối có thể rất hấp dẫn nhưng đây không phải là ý tưởng hay vì những phiên bản này không được thu thập rác. Mặc dù đây không phải là vấn đề trong thực tế, nhưng khi số lượng phiên bản Logger phụ thuộc vào mức độ chi tiết mà bạn muốn sử dụng khi ghi nhật ký ứng dụng, thì có thể khó quản lý nếu số lượng phiên bản Logger thực sự không bị giới hạn.
Sử dụng LoggerAdapters để truyền đạt thông tin theo ngữ cảnh¶
Một cách dễ dàng để bạn có thể chuyển thông tin theo ngữ cảnh ra đầu ra cùng với thông tin sự kiện ghi nhật ký là sử dụng lớp LoggerAdapter. Lớp này được thiết kế trông giống như Logger, do đó bạn có thể gọi debug(), info(), warning(), error(), exception(), critical() và log(). Các phương thức này có đặc điểm giống với các phương thức tương ứng của chúng trong Logger, vì vậy bạn có thể sử dụng hai loại phiên bản này thay thế cho nhau.
Khi bạn tạo một phiên bản của LoggerAdapter, bạn chuyển cho nó một phiên bản Logger và một đối tượng giống như dict chứa thông tin theo ngữ cảnh của bạn. Khi bạn gọi một trong các phương thức ghi nhật ký trên phiên bản của LoggerAdapter, nó sẽ ủy quyền cuộc gọi đến phiên bản cơ bản của Logger được chuyển cho hàm tạo của nó và sắp xếp để truyền thông tin theo ngữ cảnh trong lệnh gọi được ủy quyền. Đây là một đoạn mã của LoggerAdapter:
gỡ lỗi def(self, msg, /, *args, **kwargs):
"""
Ủy quyền lệnh gọi gỡ lỗi cho trình ghi nhật ký cơ bản, sau khi thêm
thông tin theo ngữ cảnh từ phiên bản bộ chuyển đổi này.
"""
tin nhắn, kwargs = self.process(tin nhắn, kwargs)
self.logger.debug(tin nhắn, *args, **kwargs)
Phương thức process() của LoggerAdapter là nơi thông tin theo ngữ cảnh được thêm vào đầu ra ghi nhật ký. Nó đã chuyển thông điệp và đối số từ khóa của lệnh gọi ghi nhật ký và gửi lại các phiên bản đã sửa đổi (có thể) của những phiên bản này để sử dụng trong lệnh gọi tới trình ghi nhật ký cơ bản. Việc triển khai mặc định của phương thức này chỉ để lại thông báo, nhưng chèn một khóa 'phụ' vào đối số từ khóa có giá trị là đối tượng giống như lệnh được truyền cho hàm tạo. Tất nhiên, nếu bạn đã chuyển đối số từ khóa 'bổ sung' trong lệnh gọi tới bộ điều hợp, nó sẽ bị ghi đè âm thầm.
Ưu điểm của việc sử dụng 'bổ sung' là các giá trị trong đối tượng giống dict được hợp nhất vào __dict__ của phiên bản LogRecord, cho phép bạn sử dụng các chuỗi tùy chỉnh với các phiên bản Formatter biết về khóa của đối tượng giống dict. Nếu bạn cần một phương pháp khác, ví dụ: nếu bạn muốn thêm hoặc nối thêm thông tin theo ngữ cảnh vào chuỗi tin nhắn, bạn chỉ cần phân lớp LoggerAdapter và ghi đè process() để thực hiện những gì bạn cần. Đây là một ví dụ đơn giản:
lớp CustomAdapter(logging.LoggerAdapter):
"""
Bộ điều hợp ví dụ này mong muốn đối tượng giống như dict được truyền vào có một
Khóa 'connid', có giá trị trong ngoặc được thêm vào trước thông điệp tường trình.
"""
quá trình def (tự, tin nhắn, kwargs):
return '[%s] %s' % (self.extra['connid'], msg), kwargs
mà bạn có thể sử dụng như thế này
logger = log.getLogger(__name__)
adapter = CustomAdapter(logger, {'connid': some_conn_id})
Sau đó, bất kỳ sự kiện nào bạn đăng nhập vào bộ điều hợp sẽ có giá trị some_conn_id được thêm vào thông báo tường trình.
Sử dụng các đối tượng không phải là dict để truyền thông tin theo ngữ cảnh¶
Bạn không cần chuyển một lệnh thực tế cho LoggerAdapter - bạn có thể chuyển một phiên bản của lớp triển khai __getitem__ và __iter__ để nó trông giống như một lệnh ghi nhật ký. Điều này sẽ hữu ích nếu bạn muốn tạo các giá trị một cách linh hoạt (trong khi các giá trị trong một lệnh sẽ không đổi).
Sử dụng Bộ lọc để truyền đạt thông tin theo ngữ cảnh¶
Bạn cũng có thể thêm thông tin theo ngữ cảnh vào đầu ra nhật ký bằng cách sử dụng Filter do người dùng xác định. Các phiên bản Filter được phép sửa đổi LogRecords được truyền cho chúng, bao gồm cả việc thêm các thuộc tính bổ sung mà sau đó có thể được xuất ra bằng chuỗi định dạng phù hợp hoặc nếu cần, một Formatter tùy chỉnh.
Ví dụ: trong một ứng dụng web, yêu cầu đang được xử lý (hoặc ít nhất là các phần thú vị của nó) có thể được lưu trữ trong biến threadlocal (threading.local), sau đó được truy cập từ Filter để thêm thông tin từ yêu cầu - chẳng hạn như địa chỉ IP từ xa và tên người dùng của người dùng từ xa - vào LogRecord, sử dụng tên thuộc tính 'ip' và 'user' như trong ví dụ LoggerAdapter ở trên. Trong trường hợp đó, chuỗi định dạng tương tự có thể được sử dụng để có được kết quả đầu ra tương tự như được hiển thị ở trên. Đây là một kịch bản ví dụ:
nhập nhật ký
từ lựa chọn nhập ngẫu nhiên
lớp ContextFilter(logging.Filter):
"""
Đây là bộ lọc đưa thông tin theo ngữ cảnh vào nhật ký.
Thay vì sử dụng thông tin theo ngữ cảnh thực tế, chúng tôi chỉ sử dụng ngẫu nhiên
dữ liệu trong bản demo này.
"""
USERS = ['jim', 'fred', 'sheila']
IPS = ['123.231.231.123', '127.0.0.1', '192.168.0.1']
bộ lọc def (tự, bản ghi):
record.ip = sự lựa chọn(ContextFilter.IPS)
record.user = sự lựa chọn(ContextFilter.USERS)
trả về Đúng
nếu __name__ == '__main__':
cấp độ = (logging.DEBUG,logging.INFO,logging.WARNING,logging.ERROR,logging.CRITICAL)
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)-15s %(name)-5s %(levelname)-8s IP: %(ip)-15s Người dùng: %(user)-8s %(message)s')
a1 = log.getLogger('a.b.c')
a2 = log.getLogger('d.e.f')
f = Bộ lọc bối cảnh()
a1.addFilter(f)
a2.addFilter(f)
a1.debug('Thông báo gỡ lỗi')
a1.info('Thông báo thông tin có %s', 'một số tham số')
cho x trong phạm vi (10):
lvl = sự lựa chọn (cấp độ)
lvlname = log.getLevelName(lvl)
a2.log(lvl, 'Một thông báo ở cấp độ %s với %d %s', lvlname, 2, 'parameters')
mà khi chạy sẽ tạo ra thứ gì đó như:
2010-09-06 22:38:15,292 a.b.c DEBUG IP: 123.231.231.123 Người dùng: fred Một thông báo gỡ lỗi
2010-09-06 22:38:15,300 a.b.c INFO IP: 192.168.0.1 Người dùng: sheila Một thông báo thông tin với một số thông số
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 127.0.0.1 Người dùng: sheila Một tin nhắn ở mức CRITICAL với 2 tham số
2010-09-06 22:38:15,300 d.e.f ERROR IP: 127.0.0.1 Người dùng: jim Một tin nhắn ở mức ERROR với 2 tham số
2010-09-06 22:38:15,300 d.e.f DEBUG IP: 127.0.0.1 Người dùng: sheila Một tin nhắn ở mức DEBUG với 2 tham số
2010-09-06 22:38:15,300 d.e.f ERROR IP: 123.231.231.123 Người dùng: fred Một tin nhắn ở mức ERROR với 2 tham số
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 192.168.0.1 Người dùng: jim Một thông báo ở cấp độ CRITICAL với 2 tham số
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 127.0.0.1 Người dùng: sheila Một thông báo ở cấp độ CRITICAL với 2 tham số
2010-09-06 22:38:15,300 d.e.f DEBUG IP: 192.168.0.1 Người dùng: jim Một thông báo ở mức DEBUG với 2 tham số
2010-09-06 22:38:15,301 d.e.f ERROR IP: 127.0.0.1 Người dùng: sheila Một tin nhắn ở mức ERROR với 2 tham số
2010-09-06 22:38:15,301 d.e.f DEBUG IP: 123.231.231.123 Người dùng: fred Một tin nhắn ở mức DEBUG với 2 tham số
2010-09-06 22:38:15,301 d.e.f INFO IP: 123.231.231.123 Người dùng: fred Một tin nhắn ở mức INFO với 2 tham số
Sử dụng contextvars¶
Kể từ Python 3.7, mô-đun contextvars đã cung cấp bộ nhớ cục bộ theo ngữ cảnh hoạt động cho cả nhu cầu xử lý threading và asyncio. Do đó, loại lưu trữ này thường được ưa chuộng hơn so với lưu trữ cục bộ. Ví dụ sau đây cho thấy cách thức, trong môi trường đa luồng, nhật ký có thể chứa thông tin theo ngữ cảnh, chẳng hạn như các thuộc tính yêu cầu được xử lý bởi ứng dụng web.
Với mục đích minh họa, giả sử bạn có các ứng dụng web khác nhau, mỗi ứng dụng độc lập với nhau nhưng chạy trong cùng một quy trình Python và sử dụng một thư viện chung cho chúng. Làm cách nào để mỗi ứng dụng này có nhật ký riêng, trong đó tất cả thông báo ghi nhật ký từ thư viện (và mã xử lý yêu cầu khác) đều được chuyển hướng đến tệp nhật ký của ứng dụng thích hợp, đồng thời đưa vào nhật ký thông tin ngữ cảnh bổ sung như IP máy khách, phương thức yêu cầu HTTP và tên người dùng máy khách?
Giả sử rằng thư viện có thể được mô phỏng bằng đoạn mã sau:
# webapplib.py
nhập nhật ký
thời gian nhập khẩu
logger = log.getLogger(__name__)
chắc chắn hữu ích():
# Just một sự kiện tiêu biểu được ghi lại từ thư viện
logger.debug('Xin chào từ webapplib!')
# Just ngủ một lát để các luồng khác có thể chạy
thời gian.ngủ (0,01)
Chúng ta có thể mô phỏng nhiều ứng dụng web bằng hai lớp đơn giản, Request và WebApp. Chúng mô phỏng cách hoạt động của các ứng dụng web theo luồng thực - mỗi yêu cầu được xử lý bởi một luồng:
# main.py
nhập khẩu argparse
từ bối cảnh nhập ContextVar
nhập nhật ký
hệ điều hành nhập khẩu
từ lựa chọn nhập ngẫu nhiên
nhập luồng
nhập ứng dụng web
logger = log.getLogger(__name__)
root = log.getLogger()
root.setLevel(logging.DEBUG)
Yêu cầu lớp:
"""
Một lớp yêu cầu giả đơn giản chỉ chứa phương thức yêu cầu HTTP giả,
địa chỉ IP của khách hàng và tên người dùng của khách hàng
"""
def __init__(tự, phương thức, ip, người dùng):
self.method = phương thức
self.ip = ip
self.user = người dùng
tập hợp yêu cầu giả # A sẽ được sử dụng trong mô phỏng - chúng tôi sẽ chỉ chọn
# from danh sách này một cách ngẫu nhiên. Lưu ý rằng tất cả các yêu cầu GET đều từ 192.168.2.XXX
# addresses, trong khi các yêu cầu POST đến từ địa chỉ 192.16.3.XXX. Ba người dùng
# are được thể hiện trong các yêu cầu mẫu.
REQUESTS = [
Yêu cầu('GET', '192.168.2.20', 'jim'),
Yêu cầu('POST', '192.168.3.20', 'fred'),
Yêu cầu('GET', '192.168.2.21', 'sheila'),
Yêu cầu('POST', '192.168.3.21', 'jim'),
Yêu cầu('GET', '192.168.2.22', 'fred'),
Yêu cầu('POST', '192.168.3.22', 'sheila'),
]
# Note rằng chuỗi định dạng bao gồm các tham chiếu để yêu cầu thông tin ngữ cảnh
# such làm phương thức HTTP, IP máy khách và tên người dùng
formatter = log.Formatter('%(threadName)-11s %(appName)s %(name)-9s %(user)-6s %(ip)s %(method)-4s %(message)s')
# Create các biến ngữ cảnh của chúng tôi. Những thứ này sẽ được điền khi bắt đầu yêu cầu
# processing và được sử dụng trong quá trình ghi nhật ký xảy ra trong quá trình xử lý đó
ctx_request = ContextVar('request')
ctx_appname = ContextVar('appname')
lớp InjectingFilter(logging.Filter):
"""
Bộ lọc đưa thông tin theo ngữ cảnh cụ thể vào nhật ký và đảm bảo
chỉ thông tin về một ứng dụng web cụ thể mới được đưa vào nhật ký của nó
"""
def __init__(tự, ứng dụng):
self.app = ứng dụng
bộ lọc def (tự, bản ghi):
yêu cầu = ctx_request.get()
record.method = request.method
record.ip = request.ip
record.user = request.user
record.appName = appName = ctx_appname.get()
trả về tên ứng dụng == self.app.name
lớp Ứng dụng web:
"""
Một lớp ứng dụng web giả có trình xử lý và bộ lọc riêng cho
nhật ký dành riêng cho ứng dụng web.
"""
def __init__(bản thân, tên):
self.name = tên
handler = log.FileHandler(name + '.log', 'w')
f = TiêmFilter(tự)
handler.setFormatter(formatter)
handler.addFilter(f)
root.addHandler(trình xử lý)
self.num_requests = 0
def process_request(tự, yêu cầu):
"""
Đây là phương pháp giả để xử lý yêu cầu. Nó được gọi là
chủ đề khác nhau cho mỗi yêu cầu. Chúng tôi lưu trữ thông tin ngữ cảnh vào
bối cảnh thay đổi trước khi làm bất cứ điều gì khác.
"""
ctx_request.set(yêu cầu)
ctx_appname.set(self.name)
self.num_requests += 1
logger.debug('Đã bắt đầu xử lý yêu cầu')
webapplib.useful()
logger.debug('Xử lý yêu cầu đã hoàn tất')
chắc chắn chính():
fn = os.path.splitext(os.path.basename(__file__))[0]
adhf = argparse.ArgumentDefaultsHelpFormatter
ap = argparse.ArgumentParser(formatter_class=adhf, prog=fn,
description='Mô phỏng một vài trang web '
'các ứng dụng xử lý một số'
'yêu cầu, hiển thị cách yêu cầu'
'ngữ cảnh có thể được sử dụng để'
'điền nhật ký')
aa = ap.add_argument
aa('--count', '-c', type=int, default=100, help='Có bao nhiêu yêu cầu mô phỏng')
tùy chọn = ap.parse_args()
# Create các ứng dụng web giả và đưa chúng vào danh sách mà chúng ta có thể sử dụng để chọn
# from ngẫu nhiên
app1 = WebApp('app1')
app2 = WebApp('app2')
ứng dụng = [app1, ứng dụng2]
chủ đề = []
# Add một trình xử lý chung sẽ nắm bắt tất cả các sự kiện
handler = log.FileHandler('app.log', 'w')
handler.setFormatter(formatter)
root.addHandler(trình xử lý)
# Generate gọi để xử lý yêu cầu
cho tôi trong phạm vi (options.count):
thử:
# Pick một ứng dụng ngẫu nhiên và yêu cầu ứng dụng đó xử lý
ứng dụng = sự lựa chọn (ứng dụng)
yêu cầu = lựa chọn(REQUESTS)
# Process yêu cầu trong chủ đề riêng của nó
t = threading.Thread(target=app.process_request, args=(request,))
chủ đề.append(t)
t.start()
ngoại trừ Bàn phímInterrupt:
phá vỡ
# Wait để các chủ đề kết thúc
cho t trong chủ đề:
t.join()
cho ứng dụng trong ứng dụng:
print('%s đã xử lý %s yêu cầu' % (app.name, app.num_requests))
nếu __name__ == '__main__':
chính()
Nếu bạn chạy phần trên, bạn sẽ thấy rằng khoảng một nửa số yêu cầu được chuyển vào app1.log và phần còn lại vào app2.log, đồng thời tất cả các yêu cầu đều được ghi vào app.log. Mỗi nhật ký dành riêng cho ứng dụng web sẽ chỉ chứa các mục nhật ký cho ứng dụng web đó và thông tin yêu cầu sẽ được hiển thị nhất quán trong nhật ký (tức là thông tin trong mỗi yêu cầu giả sẽ luôn xuất hiện cùng nhau trong một dòng nhật ký). Điều này được minh họa bằng đầu ra shell sau:
~/logging-contextual-webapp$ python main.py
app1 đã xử lý 51 yêu cầu
app2 đã xử lý 49 yêu cầu
~/logging-contextual-webapp$ wc -l *.log
153 ứng dụng1.log
147 ứng dụng2.log
300 ứng dụng.log
tổng cộng 600
~/logging-contextual-webapp$ head -3 app1.log
Thread-3 (process_request) app1 __main__ jim 192.168.3.21 POST Đã bắt đầu xử lý yêu cầu
Thread-3 (process_request) app1 webapplib jim 192.168.3.21 POST Xin chào từ webapplib!
Thread-5 (process_request) app1 __main__ jim 192.168.3.21 POST Đã bắt đầu xử lý yêu cầu
~/logging-contextual-webapp$ head -3 app2.log
Thread-1 (process_request) app2 __main__ sheila 192.168.2.21 GET Đã bắt đầu xử lý yêu cầu
Thread-1 (process_request) app2 webapplib sheila 192.168.2.21 GET Xin chào từ webapplib!
Thread-2 (process_request) app2 __main__ jim 192.168.2.20 GET Đã bắt đầu xử lý yêu cầu
~/logging-contextual-webapp$ head app.log
Thread-1 (process_request) app2 __main__ sheila 192.168.2.21 GET Đã bắt đầu xử lý yêu cầu
Thread-1 (process_request) app2 webapplib sheila 192.168.2.21 GET Xin chào từ webapplib!
Thread-2 (process_request) app2 __main__ jim 192.168.2.20 GET Đã bắt đầu xử lý yêu cầu
Thread-3 (process_request) app1 __main__ jim 192.168.3.21 POST Đã bắt đầu xử lý yêu cầu
Thread-2 (process_request) app2 webapplib jim 192.168.2.20 GET Xin chào từ webapplib!
Thread-3 (process_request) app1 webapplib jim 192.168.3.21 POST Xin chào từ webapplib!
Thread-4 (process_request) app2 __main__ fred 192.168.2.22 GET Đã bắt đầu xử lý yêu cầu
Thread-5 (process_request) app1 __main__ jim 192.168.3.21 POST Đã bắt đầu xử lý yêu cầu
Thread-4 (process_request) app2 webapplib fred 192.168.2.22 GET Xin chào từ webapplib!
Thread-6 (process_request) app1 __main__ jim 192.168.3.21 POST Đã bắt đầu xử lý yêu cầu
~/logging-contextual-webapp$ grep app1 app1.log | wc -l
153
~/logging-contextual-webapp$ grep app2 app2.log | wc -l
147
~/logging-contextual-webapp$ grep app1 app.log | wc -l
153
~/logging-contextual-webapp$ grep app2 app.log | wc -l
147
Truyền đạt thông tin theo ngữ cảnh trong trình xử lý¶
Mỗi Handler đều có chuỗi bộ lọc riêng. Nếu bạn muốn thêm thông tin theo ngữ cảnh vào LogRecord mà không rò rỉ thông tin đó cho các trình xử lý khác, bạn có thể sử dụng bộ lọc trả về LogRecord mới thay vì sửa đổi tại chỗ, như minh họa trong tập lệnh sau:
nhập bản sao
nhập nhật ký
bộ lọc def (bản ghi: log.LogRecord):
bản ghi = copy.copy(bản ghi)
record.user = 'jim'
hồ sơ trả lại
nếu __name__ == '__main__':
logger = ghi nhật ký.getLogger()
logger.setLevel(logging.INFO)
xử lý = ghi nhật ký.StreamHandler()
formatter = log.Formatter('%(message)s from %(user)-8s')
handler.setFormatter(formatter)
handler.addFilter(bộ lọc)
logger.addHandler(trình xử lý)
logger.info('Thông điệp tường trình')
Đăng nhập vào một tệp từ nhiều quy trình¶
Mặc dù việc ghi nhật ký là an toàn theo luồng và việc ghi nhật ký vào một tệp từ nhiều luồng trong một quy trình duy nhất được hỗ trợ is, việc ghi nhật ký vào một tệp từ multiple processes được hỗ trợ not vì không có cách tiêu chuẩn nào để tuần tự hóa quyền truy cập vào một tệp duy nhất trên nhiều quy trình trong Python. Nếu bạn cần đăng nhập vào một tệp từ nhiều quy trình, một cách để thực hiện việc này là yêu cầu tất cả các quy trình đăng nhập vào SocketHandler và có một quy trình riêng biệt triển khai máy chủ socket đọc từ ổ cắm và ghi nhật ký vào tệp. (Nếu muốn, bạn có thể dành một luồng trong một trong các quy trình hiện có để thực hiện chức năng này.) This section ghi lại cách tiếp cận này chi tiết hơn và bao gồm một bộ thu ổ cắm hoạt động có thể được sử dụng làm điểm khởi đầu để bạn điều chỉnh trong các ứng dụng của riêng mình.
Bạn cũng có thể viết trình xử lý của riêng mình sử dụng lớp Lock từ mô-đun multiprocessing để tuần tự hóa quyền truy cập vào tệp từ các quy trình của bạn. stdlib FileHandler và các lớp con không sử dụng multiprocessing.
Ngoài ra, bạn có thể sử dụng Queue và QueueHandler để gửi tất cả các sự kiện ghi nhật ký đến một trong các quy trình trong ứng dụng nhiều quy trình của bạn. Tập lệnh mẫu sau đây minh họa cách bạn có thể thực hiện việc này; trong ví dụ này, một quy trình nghe riêng biệt sẽ lắng nghe các sự kiện do các quy trình khác gửi và ghi nhật ký chúng theo cấu hình ghi nhật ký của chính nó. Mặc dù ví dụ này chỉ thể hiện một cách thực hiện (ví dụ: bạn có thể muốn sử dụng một chuỗi trình nghe thay vì một quy trình trình nghe riêng biệt -- việc triển khai sẽ tương tự), nó cho phép các cấu hình ghi nhật ký hoàn toàn khác nhau cho trình nghe và các quy trình khác trong ứng dụng của bạn và có thể được sử dụng làm cơ sở cho mã đáp ứng các yêu cầu cụ thể của riêng bạn:
# You sẽ cần những nội dung nhập này trong mã của riêng bạn
nhập nhật ký
nhập log.handlers
nhập đa xử lý
# Next hai dòng nhập chỉ dành cho bản demo này
từ lựa chọn nhập ngẫu nhiên, ngẫu nhiên
thời gian nhập khẩu
#
# Because bạn sẽ muốn xác định cấu hình ghi nhật ký cho người nghe và nhân viên,
# listener và các hàm xử lý công nhân lấy tham số cấu hình có thể gọi được
# for đang định cấu hình ghi nhật ký cho quá trình đó. Các chức năng này cũng được chuyển qua hàng đợi,
# which họ sử dụng để liên lạc.
#
# In, bạn có thể định cấu hình trình nghe theo cách bạn muốn, nhưng hãy lưu ý rằng trong phần này
Ví dụ về # simple, trình nghe không áp dụng logic cấp độ hoặc bộ lọc cho các bản ghi nhận được.
# In, bạn có thể muốn thực hiện logic này trong quy trình công nhân để tránh
# sending sẽ được lọc giữa các quy trình.
#
Kích thước # The của các tệp được xoay được làm nhỏ để bạn có thể xem kết quả dễ dàng.
def listen_configurer():
root = log.getLogger()
h = log.handlers.RotatingFileHandler('mptest.log', 'a', 300, 10)
f = log.Formatter('%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s')
h.setFormatter(f)
root.addHandler(h)
# This là vòng lặp cấp cao nhất của quy trình nghe: chờ ghi nhật ký sự kiện
# (LogRecords) trên hàng đợi và xử lý chúng, thoát khi bạn nhận được Không có cho một
# LogRecord.
def Listen_process(hàng đợi, trình cấu hình):
trình cấu hình()
trong khi Đúng:
thử:
bản ghi = queue.get()
nếu bản ghi là Không: # We gửi thông báo này dưới dạng trọng điểm để yêu cầu người nghe thoát.
phá vỡ
logger = log.getLogger(record.name)
Đã áp dụng mức logger.handle(record) # No hoặc logic bộ lọc - cứ làm đi!
ngoại trừ Ngoại lệ:
hệ thống nhập khẩu, truy nguyên
print('Rất tiếc! Sự cố:', file=sys.stderr)
traceback.print_exc(file=sys.stderr)
# Arrays được sử dụng để lựa chọn ngẫu nhiên trong bản demo này
LEVELS = [logging.DEBUG, log.INFO, log.WARNING,
log.ERROR, log.CRITICAL]
LOGGERS = ['a.b.c', 'd.e.f']
MESSAGES = [
'Tin nhắn ngẫu nhiên #1',
'Tin nhắn ngẫu nhiên #2',
'Tin nhắn ngẫu nhiên #3',
]
Cấu hình công nhân # The được thực hiện khi bắt đầu chạy quy trình công nhân.
# Note rằng trên Windows bạn không thể dựa vào ngữ nghĩa phân nhánh, vì vậy mỗi quy trình
# will chạy mã cấu hình ghi nhật ký khi nó khởi động.
def worker_configurer(hàng đợi):
h = log.handlers.QueueHandler(queue) # Just một trình xử lý cần thiết
root = log.getLogger()
root.addHandler(h)
# send tất cả tin nhắn, dành cho bản demo; không áp dụng mức độ hoặc logic bộ lọc nào khác.
root.setLevel(logging.DEBUG)
# This là vòng lặp cấp cao nhất của quy trình công nhân, chỉ ghi lại mười sự kiện với
# random can thiệp vào sự chậm trễ trước khi chấm dứt.
Tin nhắn in # The chỉ để bạn biết nó đang làm gì đó!
def worker_process(hàng đợi, trình cấu hình):
bộ cấu hình (hàng đợi)
tên = multiprocessing.current_process().name
print('Worker đã bắt đầu: %s' % name)
cho tôi trong phạm vi (10):
time.sleep(ngẫu nhiên())
logger = log.getLogger(lựa chọn(LOGGERS))
cấp độ = sự lựa chọn (LEVELS)
tin nhắn = lựa chọn(MESSAGES)
logger.log(cấp độ, tin nhắn)
print('Công nhân đã hoàn thành: %s' % name)
# Here là nơi dàn dựng bản demo. Tạo hàng đợi, tạo và bắt đầu
Trình nghe # the, tạo mười công nhân và khởi động chúng, đợi họ hoàn thành,
# then gửi None vào hàng đợi để báo cho người nghe kết thúc.
chắc chắn chính():
hàng đợi = multiprocessing.Queue(-1)
người nghe = multiprocessing.Process(target=listener_process,
args=(queue, listen_configurer))
người nghe.start()
công nhân = []
cho tôi trong phạm vi (10):
worker = multiprocessing.Process(target=worker_process,
args=(hàng đợi, worker_configurer))
công nhân.append(công nhân)
công nhân.start()
cho w trong công nhân:
w.join()
queue.put_nowait(Không có)
người nghe.join()
nếu __name__ == '__main__':
chính()
Một biến thể của tập lệnh trên giữ việc ghi nhật ký vào tiến trình chính, trong một luồng riêng biệt:
nhập nhật ký
nhập log.config
nhập log.handlers
từ quá trình nhập đa xử lý, hàng đợi
nhập khẩu ngẫu nhiên
nhập luồng
thời gian nhập khẩu
def logger_thread(q):
trong khi Đúng:
bản ghi = q.get()
nếu bản ghi là Không có:
phá vỡ
logger = log.getLogger(record.name)
logger.handle(bản ghi)
def worker_process(q):
qh = log.handlers.QueueHandler(q)
root = log.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(qh)
cấp độ = [logging.DEBUG, log.INFO, log.WARNING, log.ERROR,
log.CRITICAL]
logger = ['foo', 'foo.bar', 'foo.bar.baz',
'thư rác', 'spam.ham', 'spam.ham.eggs']
cho tôi trong phạm vi (100):
lvl = ngẫu nhiên.choice(cấp độ)
logger = log.getLogger(random.choice(loggers))
logger.log(lvl, 'Thông báo số %d', i)
nếu __name__ == '__main__':
q = Hàng đợi()
d = {
'phiên bản': 1,
'trình định dạng': {
'chi tiết': {
'lớp': 'logging.Formatter',
'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
}
},
'người xử lý': {
'bàn điều khiển': {
'lớp': 'logging.StreamHandler',
'cấp độ': 'INFO',
},
'tập tin': {
'lớp': 'logging.FileHandler',
'tên tệp': 'mplog.log',
'chế độ': 'w',
'người định dạng': 'chi tiết',
},
'tập tin ngu ngốc': {
'lớp': 'logging.FileHandler',
'tên tệp': 'mplog-foo.log',
'chế độ': 'w',
'người định dạng': 'chi tiết',
},
'lỗi': {
'lớp': 'logging.FileHandler',
'tên tệp': 'mplog-errors.log',
'chế độ': 'w',
'cấp độ': 'ERROR',
'người định dạng': 'chi tiết',
},
},
'người ghi nhật ký': {
'foo': {
'trình xử lý': ['foofile']
}
},
'gốc': {
'cấp độ': 'DEBUG',
'trình xử lý': ['console', 'file', 'errors']
},
}
công nhân = []
cho tôi trong phạm vi (5):
wp = Process(target=worker_process, name='worker %d' % (i + 1), args=(q,))
công nhân.append(wp)
wp.start()
ghi nhật ký.config.dictConfig(d)
lp = threading.Thread(target=logger_thread, args=(q,))
lp.start()
# At vào thời điểm này, quy trình chính có thể tự thực hiện một số công việc hữu ích
# Once xong rồi, có thể đợi công nhân nghỉ việc...
cho wp trong công nhân:
wp.join()
# And bây giờ cũng yêu cầu chuỗi ghi nhật ký hoàn tất
q.put(Không có)
lp.join()
Biến thể này cho thấy cách bạn có thể ví dụ: áp dụng cấu hình cho các trình ghi nhật ký cụ thể - ví dụ: Trình ghi nhật ký foo có một trình xử lý đặc biệt lưu trữ tất cả các sự kiện trong hệ thống con foo trong một tệp mplog-foo.log. Điều này sẽ được máy ghi nhật ký sử dụng trong quy trình chính (mặc dù các sự kiện ghi nhật ký được tạo trong quy trình của nhân viên) để chuyển các thông báo đến các đích thích hợp.
Sử dụng concurrent.futures.ProcessPoolExecutor¶
Nếu bạn muốn sử dụng concurrent.futures.ProcessPoolExecutor để bắt đầu các quy trình công nhân của mình, bạn cần tạo hàng đợi hơi khác một chút. Thay vì
hàng đợi = multiprocessing.Queue(-1)
bạn nên sử dụng
queue = multiprocessing.Manager().Queue(-1) # also hoạt động với các ví dụ trên
và sau đó bạn có thể thay thế việc tạo công nhân từ đây
công nhân = []
cho tôi trong phạm vi (10):
worker = multiprocessing.Process(target=worker_process,
args=(hàng đợi, worker_configurer))
công nhân.append(công nhân)
công nhân.start()
cho w trong công nhân:
w.join()
về điều này (nhớ lần nhập concurrent.futures đầu tiên):
với concurrent.futures.ProcessPoolExecutor(max_workers=10) là người thực thi:
cho tôi trong phạm vi (10):
executor.submit(worker_process, queue, worker_configurer)
Triển khai ứng dụng web bằng Gunicorn và uWSGI¶
Khi triển khai các ứng dụng web bằng Gunicorn hoặc uWSGI (hoặc tương tự), nhiều quy trình xử lý được tạo để xử lý các yêu cầu của khách hàng. Trong những môi trường như vậy, hãy tránh tạo trình xử lý dựa trên tệp trực tiếp trong ứng dụng web của bạn. Thay vào đó, hãy sử dụng SocketHandler để đăng nhập từ ứng dụng web tới trình nghe theo một quy trình riêng. Điều này có thể được thiết lập bằng công cụ quản lý quy trình như Người giám sát - xem Running a logging socket listener in production để biết thêm chi tiết.
Sử dụng xoay tập tin¶
Đôi khi bạn muốn để tệp nhật ký tăng đến một kích thước nhất định, sau đó mở tệp mới và đăng nhập vào đó. Bạn có thể muốn giữ lại một số lượng tệp nhất định và khi nhiều tệp đó đã được tạo, hãy xoay tệp sao cho số lượng tệp và kích thước của tệp vẫn bị giới hạn. Đối với kiểu sử dụng này, gói ghi nhật ký cung cấp RotatingFileHandler:
nhập khẩu toàn cầu
nhập nhật ký
nhập log.handlers
LOG_FILENAME = 'logging_rotatingfile_example.out'
# Set thiết lập một trình ghi nhật ký cụ thể với mức đầu ra mong muốn của chúng tôi
my_logger = log.getLogger('MyLogger')
my_logger.setLevel(logging.DEBUG)
# Add trình xử lý thông điệp tường trình cho trình ghi nhật ký
xử lý = log.handlers.RotatingFileHandler(
LOG_FILENAME, maxBytes=20, backupCount=5)
my_logger.addHandler(trình xử lý)
# Log một số tin nhắn
cho tôi trong phạm vi (20):
my_logger.debug('i = %d' % i)
# See tập tin nào được tạo
logfiles = glob.glob('%s*' % LOG_FILENAME)
cho tên tệp trong logfiles:
in (tên tệp)
Kết quả sẽ là 6 tệp riêng biệt, mỗi tệp có một phần lịch sử nhật ký của ứng dụng:
logging_rotatingfile_example.out
logging_rotatingfile_example.out.1
logging_rotatingfile_example.out.2
logging_rotatingfile_example.out.3
logging_rotatingfile_example.out.4
logging_rotatingfile_example.out.5
Tệp mới nhất luôn là logging_rotatingfile_example.out và mỗi khi đạt đến giới hạn kích thước, nó sẽ được đổi tên với hậu tố .1. Mỗi tệp sao lưu hiện có được đổi tên để tăng hậu tố (.1 trở thành .2, v.v.) và tệp .6 sẽ bị xóa.
Rõ ràng ví dụ này đặt độ dài nhật ký quá nhỏ so với một ví dụ cực đoan. Bạn muốn đặt maxBytes thành một giá trị thích hợp.
Sử dụng các kiểu định dạng thay thế¶
Khi tính năng ghi nhật ký được thêm vào thư viện chuẩn Python, cách duy nhất để định dạng thư có nội dung thay đổi là sử dụng phương thức %-formatting. Kể từ đó, Python đã có được hai cách tiếp cận định dạng mới: string.Template (được thêm vào Python 2.4) và str.format() (được thêm vào Python 2.6).
Ghi nhật ký (kể từ phiên bản 3.2) cung cấp khả năng hỗ trợ được cải thiện cho hai kiểu định dạng bổ sung này. Lớp Formatter đã được cải tiến để có thêm một tham số từ khóa tùy chọn có tên là style. Giá trị mặc định này là '%', nhưng các giá trị khác có thể có là '{' và '$', tương ứng với hai kiểu định dạng còn lại. Khả năng tương thích ngược được duy trì theo mặc định (như bạn mong đợi), nhưng bằng cách chỉ định rõ ràng tham số kiểu, bạn sẽ có khả năng chỉ định các chuỗi định dạng hoạt động với str.format() hoặc string.Template. Đây là phiên bảng điều khiển mẫu để hiển thị các khả năng:
>>> nhập nhật ký
>>> root = log.getLogger()
>>> root.setLevel(logging.DEBUG)
>>> xử lý = ghi nhật ký.StreamHandler()
>>> bf = log.Formatter('{asctime} {name} {levelname:8s} {message}',
... phong cách='{')
>>> handler.setFormatter(bf)
>>> root.addHandler(trình xử lý)
>>> logger = log.getLogger('foo.bar')
>>> logger.debug('Đây là tin nhắn DEBUG')
28-10-2010 15:11:55,341 foo.bar DEBUG Đây là tin nhắn DEBUG
>>> logger.critical('Đây là tin nhắn CRITICAL')
28-10-2010 15:12:11,526 foo.bar CRITICAL Đây là tin nhắn CRITICAL
>>> df =logging.Formatter('$asctime $name ${levelname} $message',
... phong cách='$')
>>> handler.setFormatter(df)
>>> logger.debug('Đây là tin nhắn DEBUG')
28-10-2010 15:13:06,924 foo.bar DEBUG Đây là tin nhắn DEBUG
>>> logger.critical('Đây là tin nhắn CRITICAL')
28-10-2010 15:13:11,494 foo.bar CRITICAL Đây là tin nhắn CRITICAL
>>>
Lưu ý rằng định dạng của thông báo ghi nhật ký cho đầu ra cuối cùng thành nhật ký hoàn toàn độc lập với cách tạo thông báo ghi nhật ký riêng lẻ. Điều đó vẫn có thể sử dụng định dạng %, như được hiển thị ở đây
>>> logger.error('Đây là một%s %s %s', 'other,', 'ERROR,', 'tin nhắn')
28-10-2010 15:19:29,833 foo.bar ERROR Đây là một tin nhắn khác, ERROR
>>>
Cuộc gọi ghi nhật ký (logger.debug(), logger.info(), v.v.) chỉ lấy các tham số vị trí cho chính thông báo ghi nhật ký thực tế, với các tham số từ khóa chỉ được sử dụng để xác định các tùy chọn về cách xử lý cuộc gọi ghi nhật ký thực tế (ví dụ: tham số từ khóa exc_info để cho biết thông tin theo dõi cần được ghi lại hoặc tham số từ khóa extra để biểu thị thông tin ngữ cảnh bổ sung cần được thêm vào nhật ký). Vì vậy, bạn không thể trực tiếp thực hiện cuộc gọi ghi nhật ký bằng cú pháp str.format() hoặc string.Template, vì bên trong gói ghi nhật ký sử dụng %-formatting để hợp nhất chuỗi định dạng và các đối số biến. Sẽ không có sự thay đổi nào trong khi vẫn duy trì khả năng tương thích ngược, vì tất cả lệnh gọi ghi nhật ký có sẵn trong mã hiện tại sẽ sử dụng chuỗi định dạng %.
Tuy nhiên, có một cách mà bạn có thể sử dụng định dạng {}- và $- để xây dựng các thông điệp tường trình riêng lẻ của mình. Hãy nhớ lại rằng đối với một tin nhắn, bạn có thể sử dụng một đối tượng tùy ý làm chuỗi định dạng tin nhắn và gói ghi nhật ký sẽ gọi str() trên đối tượng đó để lấy chuỗi định dạng thực tế. Hãy xem xét hai lớp sau:
lớp BraceMessage:
def __init__(self, fmt, /, *args, **kwargs):
self.fmt = fmt
self.args = args
self.kwargs = kwargs
chắc chắn __str__(tự):
trả về self.fmt.format(*self.args, **self.kwargs)
lớp DollarMessage:
def __init__(self, fmt, /, **kwargs):
self.fmt = fmt
self.kwargs = kwargs
chắc chắn __str__(tự):
từ Mẫu nhập chuỗi
return Mẫu(self.fmt).substitute(**self.kwargs)
Một trong hai thứ này có thể được sử dụng thay cho chuỗi định dạng, để cho phép sử dụng định dạng {}- hoặc $-để xây dựng phần "thông báo" thực tế xuất hiện trong đầu ra nhật ký được định dạng thay cho "%(message)s" hoặc "{message}" hoặc "$message". Sẽ hơi khó sử dụng khi sử dụng tên lớp bất cứ khi nào bạn muốn ghi nhật ký nội dung nào đó, nhưng sẽ khá hợp lý nếu bạn sử dụng bí danh như __ (dấu gạch dưới kép --- đừng nhầm lẫn với _, dấu gạch dưới đơn được sử dụng làm từ đồng nghĩa/bí danh cho gettext.gettext() hoặc anh em của nó).
Các lớp trên không có trong Python, mặc dù chúng đủ dễ dàng để sao chép và dán vào mã của riêng bạn. Chúng có thể được sử dụng như sau (giả sử rằng chúng được khai báo trong mô-đun có tên wherever):
>>> từ bất cứ đâu nhập BraceMessage dưới dạng __
>>> print(__('Tin nhắn với {0} {name}', 2, name='placeholders'))
Tin nhắn có 2 phần giữ chỗ
>>> Điểm lớp: đạt
...
>>> p = Điểm()
>>> p.x = 0,5
>>> p.y = 0,5
>>> print(__('Tin nhắn có tọa độ: ({point.x:.2f}, {point.y:.2f})',
... điểm=p))
Tin nhắn có tọa độ: (0,50, 0,50)
>>> từ bất cứ đâu nhập DollarMessage dưới dạng __
>>> print(__('Tin nhắn với $num $what', num=2, what='placeholders'))
Tin nhắn có 2 phần giữ chỗ
>>>
Mặc dù các ví dụ trên sử dụng print() để hiển thị cách hoạt động của định dạng, nhưng tất nhiên bạn sẽ sử dụng logger.debug() hoặc tương tự để ghi nhật ký thực sự bằng phương pháp này.
Một điều cần lưu ý là bạn không phải chịu hình phạt đáng kể nào về hiệu suất với phương pháp này: định dạng thực tế không xảy ra khi bạn thực hiện lệnh gọi ghi nhật ký mà là khi (và nếu) thông báo đã ghi nhật ký thực sự sắp được một trình xử lý xuất ra nhật ký. Vì vậy, điều hơi bất thường duy nhất có thể khiến bạn gặp khó khăn là các dấu ngoặc đơn đi xung quanh chuỗi định dạng và các đối số, không chỉ chuỗi định dạng. Đó là bởi vì ký hiệu __ chỉ là cú pháp cho lệnh gọi hàm tạo tới một trong các lớp XXXMessage.
Nếu muốn, bạn có thể sử dụng LoggerAdapter để đạt được hiệu ứng tương tự như trên, như trong ví dụ sau:
nhập nhật ký
Tin nhắn lớp:
def __init__(self, fmt, args):
self.fmt = fmt
self.args = args
chắc chắn __str__(tự):
trả về self.fmt.format(*self.args)
lớp StyleAdapter(logging.LoggerAdapter):
nhật ký def(bản thân, cấp độ, thông điệp, /, *args, stacklevel=1, **kwargs):
nếu self.isEnabledFor(cấp độ):
tin nhắn, kwargs = self.process(tin nhắn, kwargs)
self.logger.log(level, Message(msg, args), **kwargs,
cấp độ ngăn xếp=cấp độ ngăn xếp+1)
logger = StyleAdapter(logging.getLogger(__name__))
chắc chắn chính():
logger.debug('Xin chào, {}', 'thế giới!')
nếu __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
chính()
Tập lệnh trên sẽ ghi thông báo Hello, world! khi chạy với Python 3.8 trở lên.
Tùy chỉnh LogRecord¶
Mỗi sự kiện ghi nhật ký được thể hiện bằng một phiên bản LogRecord. Khi một sự kiện được ghi lại và không được lọc theo cấp độ của trình ghi nhật ký, một LogRecord sẽ được tạo, chứa thông tin về sự kiện và sau đó được chuyển đến trình xử lý cho trình ghi nhật ký đó (và tổ tiên của nó, bao gồm cả trình ghi nhật ký nơi việc truyền bá thêm lên hệ thống phân cấp bị vô hiệu hóa). Trước Python 3.2, chỉ có hai nơi thực hiện việc tạo này:
Logger.makeRecord(), được gọi trong quá trình ghi nhật ký sự kiện thông thường. Điều này gọi trực tiếpLogRecordđể tạo một phiên bản.makeLogRecord(), được gọi bằng từ điển chứa các thuộc tính sẽ được thêm vào LogRecord. Điều này thường được gọi khi một từ điển phù hợp đã được nhận qua mạng (ví dụ: ở dạng dưa chua quaSocketHandlerhoặc ở dạng JSON quaHTTPHandler).
Điều này thường có nghĩa là nếu bạn cần thực hiện bất kỳ điều gì đặc biệt với LogRecord, bạn phải thực hiện một trong những thao tác sau.
Tạo lớp con
Loggercủa riêng bạn, lớp này ghi đèLogger.makeRecord()và đặt nó bằngsetLoggerClass()trước khi bất kỳ trình ghi nhật ký nào mà bạn quan tâm được khởi tạo.Thêm
Filtervào trình ghi nhật ký hoặc trình xử lý để thực hiện thao tác đặc biệt cần thiết mà bạn cần khi phương thứcfilter()của nó được gọi.
Cách tiếp cận đầu tiên sẽ hơi khó sử dụng trong trường hợp (giả sử) một số thư viện khác nhau muốn làm những việc khác nhau. Mỗi người sẽ cố gắng thiết lập lớp con Logger của riêng mình và lớp nào thực hiện được điều này cuối cùng sẽ giành chiến thắng.
Cách tiếp cận thứ hai hoạt động khá tốt trong nhiều trường hợp, nhưng không cho phép bạn làm ví dụ: sử dụng một lớp con chuyên biệt của LogRecord. Các nhà phát triển thư viện có thể đặt bộ lọc phù hợp trên trình ghi nhật ký của họ, nhưng họ sẽ phải nhớ thực hiện việc này mỗi khi giới thiệu một trình ghi nhật ký mới (điều mà họ sẽ thực hiện đơn giản bằng cách thêm các gói hoặc mô-đun mới và thực hiện
logger = log.getLogger(__name__)
ở cấp độ mô-đun). Có lẽ có quá nhiều điều phải suy nghĩ. Các nhà phát triển cũng có thể thêm bộ lọc vào NullHandler được gắn vào trình ghi nhật ký cấp cao nhất của họ, nhưng điều này sẽ không được thực hiện nếu nhà phát triển ứng dụng gắn trình xử lý vào trình ghi nhật ký thư viện cấp thấp hơn --- vì vậy đầu ra từ trình xử lý đó sẽ không phản ánh ý định của nhà phát triển thư viện.
Trong Python 3.2 trở lên, việc tạo LogRecord được thực hiện thông qua một nhà máy mà bạn có thể chỉ định. Nhà máy chỉ là một lệnh gọi mà bạn có thể đặt bằng setLogRecordFactory() và thẩm vấn bằng getLogRecordFactory(). Nhà máy được gọi với cùng chữ ký với hàm tạo LogRecord, vì LogRecord là cài đặt mặc định cho nhà máy.
Cách tiếp cận này cho phép một nhà máy tùy chỉnh kiểm soát tất cả các khía cạnh của việc tạo LogRecord. Ví dụ: bạn có thể trả về một lớp con hoặc chỉ cần thêm một số thuộc tính bổ sung vào bản ghi sau khi được tạo bằng cách sử dụng mẫu tương tự như sau:
old_factory = log.getLogRecordFactory()
def record_factory(*args, **kwargs):
bản ghi = old_factory(*args, **kwargs)
record.custom_attribute = 0xdecafbad
hồ sơ trả lại
logging.setLogRecordFactory(record_factory)
Mẫu này cho phép các thư viện khác nhau liên kết các nhà máy lại với nhau và miễn là chúng không ghi đè lên các thuộc tính của nhau hoặc vô tình ghi đè lên các thuộc tính được cung cấp theo tiêu chuẩn thì sẽ không có gì đáng ngạc nhiên. Tuy nhiên, cần lưu ý rằng mỗi liên kết trong chuỗi sẽ thêm chi phí thời gian chạy cho tất cả các hoạt động ghi nhật ký và chỉ nên sử dụng kỹ thuật này khi việc sử dụng Filter không mang lại kết quả mong muốn.
Phân lớp QueueHandler và QueueListener- một ví dụ về ZeroMQ¶
Phân lớp QueueHandler¶
Bạn có thể sử dụng lớp con QueueHandler để gửi tin nhắn đến các loại hàng đợi khác, ví dụ như ổ cắm 'xuất bản' ZeroMQ. Trong ví dụ bên dưới, socket được tạo riêng biệt và được chuyển đến trình xử lý (dưới dạng 'hàng đợi'):
nhập zmq # using pyzmq, liên kết Python cho ZeroMQ
nhập bản ghi tuần tự hóa json # for một cách di động
ctx = zmq.Context()
sock = zmq.Socket(ctx, zmq.PUB) # or zmq.PUSH hoặc giá trị phù hợp khác
sock.bind('tcp://*:5556') # or mọi lúc mọi nơi
lớp ZeroMQSocketHandler(QueueHandler):
def enqueue(tự, ghi):
self.queue.send_json(record.__dict__)
xử lý = ZeroMQSocketHandler(sock)
Tất nhiên, có nhiều cách khác để tổ chức việc này, ví dụ như truyền dữ liệu mà trình xử lý cần để tạo ổ cắm
lớp ZeroMQSocketHandler(QueueHandler):
def __init__(self, uri, socktype=zmq.PUB, ctx=None):
self.ctx = ctx hoặc zmq.Context()
socket = zmq.Socket(self.ctx, socktype)
socket.bind(uri)
super().__init__(socket)
def enqueue(tự, ghi):
self.queue.send_json(record.__dict__)
def đóng (tự):
self.queue.close()
Phân lớp QueueListener¶
Bạn cũng có thể phân lớp QueueListener để nhận tin nhắn từ các loại hàng đợi khác, ví dụ như ổ cắm 'đăng ký' ZeroMQ. Đây là một ví dụ:
lớp ZeroMQSocketListener(QueueListener):
def __init__(self, uri, /, *handlers, **kwargs):
self.ctx = kwargs.get('ctx') hoặc zmq.Context()
socket = zmq.Socket(self.ctx, zmq.SUB)
socket.setsockopt_string(zmq.SUBSCRIBE, '') # subscribe cho mọi thứ
socket.connect(uri)
super().__init__(socket, *handlers, **kwargs)
def dequeue(tự):
tin nhắn = self.queue.recv_json()
trả lại log.makeLogRecord(tin nhắn)
Phân lớp QueueHandler và QueueListener- một ví dụ về pynng¶
Theo cách tương tự như phần trên, chúng ta có thể triển khai trình nghe và trình xử lý bằng cách sử dụng pynng, một Python liên kết với NNG, được coi là người kế thừa tinh thần của ZeroMQ. Đoạn mã sau minh họa - bạn có thể kiểm tra chúng trong môi trường đã cài đặt pynng. Để đa dạng, chúng tôi giới thiệu cho người nghe trước.
Phân lớp QueueListener¶
# listener.py
nhập json
nhập nhật ký
nhập log.handlers
nhập khẩu
DEFAULT_ADDR = "tcp://localhost:13232"
bị gián đoạn = Sai
lớp NNGSocketListener(logging.handlers.QueueListener):
def __init__(self, uri, /, *handlers, **kwargs):
# Have hết thời gian chờ để có thể bị gián đoạn và mở một
ổ cắm # subscriber
socket = pynng.Sub0(listen=uri, recv_timeout=500)
đăng ký # The b'' phù hợp với tất cả các chủ đề
chủ đề = kwargs.pop('topics', None) hoặc b''
socket.subscribe(chủ đề)
# We coi ổ cắm như một hàng đợi
super().__init__(socket, *handlers, **kwargs)
def dequeue(tự, chặn):
dữ liệu = Không có
# Keep lặp lại trong khi không bị gián đoạn và không nhận được dữ liệu nào qua
# socket
trong khi không bị gián đoạn:
thử:
dữ liệu = self.queue.recv(block=block)
phá vỡ
ngoại trừ pynng.Timeout:
vượt qua
ngoại trừ pynng.Closed: # sometimes xảy ra khi bạn nhấn Ctrl-C
phá vỡ
nếu dữ liệu là Không:
trả về Không có
# Get sự kiện ghi nhật ký được gửi từ nhà xuất bản
sự kiện = json.loads(data.decode('utf-8'))
trả về log.makeLogRecord(event)
def enqueue_sentinel(tự):
# Not được sử dụng trong triển khai này vì ổ cắm không thực sự là một
# queue
vượt qua
logging.getLogger('pynng').propagate = Sai
Listen = NNGSocketListener(DEFAULT_ADDR,logging.StreamHandler(), topic=b'')
người nghe.start()
print('Nhấn Ctrl-C để dừng.')
thử:
trong khi Đúng:
vượt qua
ngoại trừ Bàn phímInterrupt:
bị gián đoạn = Đúng
cuối cùng:
người nghe.stop()
Phân lớp QueueHandler¶
# sender.py
nhập json
nhập nhật ký
nhập log.handlers
thời gian nhập khẩu
nhập khẩu ngẫu nhiên
nhập khẩu
DEFAULT_ADDR = "tcp://localhost:13232"
lớp NNGSocketHandler(logging.handlers.QueueHandler):
def __init__(self, uri):
socket = pynng.Pub0(dial=uri, send_timeout=500)
super().__init__(socket)
def enqueue(tự, ghi):
# Send kỷ lục dưới dạng UTF-8 được mã hóa JSON
d = dict(record.__dict__)
dữ liệu = json.dumps(d)
self.queue.send(data.encode('utf-8'))
def đóng (tự):
self.queue.close()
logging.getLogger('pynng').propagate = Sai
trình xử lý = NNGSocketHandler(DEFAULT_ADDR)
# Make chắc chắn ID tiến trình ở đầu ra
logging.basicConfig(level=logging.DEBUG,
trình xử lý=[logging.StreamHandler(), trình xử lý],
format='%(levelname)-8s %(name)10s %(process)6s %(message)s')
cấp độ = (logging.DEBUG, log.INFO, log.WARNING, log.ERROR,
ghi nhật ký.CRITICAL)
logger_names = ('myapp', 'myapp.lib1', 'myapp.lib2')
tin nhắn = 1
trong khi Đúng:
# Just chọn ngẫu nhiên một số logger và cấp độ rồi đăng xuất
cấp độ = ngẫu nhiên.lựa chọn (cấp độ)
logger = log.getLogger(random.choice(logger_names))
logger.log(level, 'Message no. %5d' % msgno)
tin nhắn += 1
độ trễ = ngẫu nhiên.random() * 2 + 0,5
thời gian. ngủ (độ trễ)
Bạn có thể chạy hai đoạn mã trên trong các lệnh shell riêng biệt. Nếu chúng ta chạy trình nghe trong một shell và chạy trình gửi trong hai shell riêng biệt, chúng ta sẽ thấy nội dung như sau. Trong shell người gửi đầu tiên:
$ python sender.py
DEBUG myapp 613 Tin nhắn số. 1
WARNING myapp.lib2 613 Tin nhắn số. 2
CRITICAL myapp.lib2 613 Tin nhắn số. 3
WARNING myapp.lib2 613 Tin nhắn số. 4
CRITICAL myapp.lib1 613 Tin nhắn số. 5
DEBUG myapp 613 Tin nhắn số. 6
CRITICAL myapp.lib1 613 Số tin nhắn. 7
INFO myapp.lib1 613 Tin nhắn số. 8
(và vân vân)
Trong shell người gửi thứ hai:
$ python sender.py
INFO myapp.lib2 657 Tin nhắn số. 1
CRITICAL myapp.lib2 657 Tin nhắn số. 2
CRITICAL myapp 657 Tin nhắn số. 3
CRITICAL myapp.lib1 657 Tin nhắn số. 4
INFO myapp.lib1 657 Tin nhắn số. 5
WARNING myapp.lib2 657 Tin nhắn số. 6
CRITICAL myapp 657 Tin nhắn số. 7
DEBUG myapp.lib1 657 Tin nhắn số. 8
(và vân vân)
Trong trình nghe:
$ python Listen.py
Nhấn Ctrl-C để dừng.
DEBUG myapp 613 Tin nhắn số. 1
WARNING myapp.lib2 613 Tin nhắn số. 2
INFO myapp.lib2 657 Tin nhắn số. 1
CRITICAL myapp.lib2 613 Tin nhắn số. 3
CRITICAL myapp.lib2 657 Tin nhắn số. 2
CRITICAL myapp 657 Tin nhắn số. 3
WARNING myapp.lib2 613 Tin nhắn số. 4
CRITICAL myapp.lib1 613 Tin nhắn số. 5
CRITICAL myapp.lib1 657 Tin nhắn số. 4
INFO myapp.lib1 657 Tin nhắn số. 5
DEBUG myapp 613 Tin nhắn số. 6
WARNING myapp.lib2 657 Tin nhắn số. 6
CRITICAL myapp 657 Tin nhắn số. 7
CRITICAL myapp.lib1 613 Tin nhắn số. 7
INFO myapp.lib1 613 Tin nhắn số. 8
DEBUG myapp.lib1 657 Số tin nhắn. 8
(và vân vân)
Như bạn có thể thấy, việc ghi nhật ký từ hai quy trình của người gửi được xen kẽ vào đầu ra của người nghe.
Một ví dụ về cấu hình dựa trên từ điển¶
Dưới đây là ví dụ về từ điển cấu hình ghi nhật ký - nó được lấy từ documentation on the Django project. Từ điển này được chuyển tới dictConfig() để cấu hình có hiệu lực:
LOGGING = {
'phiên bản': 1,
'vô hiệu hóa_hiện_loggers': Sai,
'trình định dạng': {
'dài dòng': {
'định dạng': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'phong cách': '{',
},
'đơn giản': {
'định dạng': '{levelname} {tin nhắn}',
'phong cách': '{',
},
},
'bộ lọc': {
'đặc biệt': {
'()': 'project.logging. SpecialFilter',
'foo': 'thanh',
},
},
'người xử lý': {
'bàn điều khiển': {
'cấp độ': 'INFO',
'lớp': 'logging.StreamHandler',
'trình định dạng': 'đơn giản',
},
'mail_admin': {
'cấp độ': 'ERROR',
'lớp': 'Django.utils.log.AdminEmailHandler',
'bộ lọc': ['đặc biệt']
}
},
'người ghi nhật ký': {
'django': {
'trình xử lý': ['console'],
'tuyên truyền': Đúng,
},
'Django.request': {
'người xử lý': ['mail_admins'],
'cấp độ': 'ERROR',
'tuyên truyền': Sai,
},
'myproject.custom': {
'trình xử lý': ['console', 'mail_admins'],
'cấp độ': 'INFO',
'bộ lọc': ['đặc biệt']
}
}
}
Để biết thêm thông tin về cấu hình này, bạn có thể xem tài liệu relevant section của Django.
Sử dụng công cụ quay vòng và trình đặt tên để tùy chỉnh quá trình xử lý xoay vòng nhật ký¶
Một ví dụ về cách bạn có thể xác định tên và công cụ quay vòng được đưa ra trong tập lệnh chạy được sau đây, tập lệnh này hiển thị tính năng nén gzip của tệp nhật ký:
nhập gzip
nhập nhật ký
nhập log.handlers
hệ điều hành nhập khẩu
nhập khẩu
tên def (tên):
tên trả về + ".gz"
công cụ quay vòng def (nguồn, đích):
với open(source, 'rb') là f_in:
với gzip.open(dest, 'wb') là f_out:
im lặng.copyfileobj(f_in, f_out)
os.remove(nguồn)
rh = log.handlers.RotatingFileHandler('rotated.log', maxBytes=128, backupCount=5)
rh.rotator = công cụ quay vòng
rh.namer = người đặt tên
root = log.getLogger()
root.setLevel(logging.INFO)
root.addHandler(rh)
f = log.Formatter('%(asctime)s %(message)s')
rh.setFormatter(f)
cho tôi trong phạm vi (1000):
root.info(f'Message no. {i + 1}')
Sau khi chạy cái này, bạn sẽ thấy sáu tệp mới, năm trong số đó được nén:
$ ls đã xoay.log*
xoay.log xoay.log.2.gz xoay.log.4.gz
xoay.log.1.gz xoay.log.3.gz xoay.log.5.gz
$ zcat đã xoay.log.1.gz
2023-01-20 02:28:17,767 Tin nhắn số. 996
2023-01-20 02:28:17,767 Tin nhắn số. 997
2023-01-20 02:28:17,767 Tin nhắn số. 998
Một ví dụ đa xử lý phức tạp hơn¶
Ví dụ hoạt động sau đây cho thấy cách sử dụng tính năng ghi nhật ký với đa xử lý bằng tệp cấu hình. Các cấu hình khá đơn giản nhưng dùng để minh họa cách triển khai các cấu hình phức tạp hơn trong kịch bản đa xử lý thực tế.
Trong ví dụ này, quy trình chính sinh ra một quy trình nghe và một số quy trình công nhân. Mỗi quy trình chính, trình nghe và trình chạy có ba cấu hình riêng biệt (tất cả các trình chạy đều có chung cấu hình). Chúng ta có thể thấy việc ghi nhật ký vào quy trình chính, cách các công nhân đăng nhập vào QueueHandler và cách trình nghe triển khai QueueListener cũng như một cấu hình ghi nhật ký phức tạp hơn, đồng thời sắp xếp để gửi các sự kiện nhận được qua hàng đợi đến các trình xử lý được chỉ định trong cấu hình. Lưu ý rằng các cấu hình này hoàn toàn mang tính minh họa, nhưng bạn có thể điều chỉnh ví dụ này cho phù hợp với tình huống của riêng mình.
Đây là kịch bản - các tài liệu và nhận xét hy vọng sẽ giải thích cách hoạt động của nó
nhập nhật ký
nhập log.config
nhập log.handlers
từ quá trình nhập đa xử lý, Hàng đợi, Sự kiện, current_process
hệ điều hành nhập khẩu
nhập khẩu ngẫu nhiên
thời gian nhập khẩu
lớp MyHandler:
"""
Một trình xử lý đơn giản để ghi lại các sự kiện. Nó chạy trong quá trình nghe và
gửi các sự kiện tới người ghi nhật ký dựa trên tên trong bản ghi nhận được,
sau đó được hệ thống ghi nhật ký gửi đến người xử lý
được cấu hình cho những logger đó.
"""
xử lý def (tự, ghi):
nếu record.name == "root":
logger = ghi nhật ký.getLogger()
khác:
logger = log.getLogger(record.name)
nếu logger.isEnabledFor(record.levelno):
Tên quy trình # The được chuyển đổi chỉ để cho thấy rằng đó là người nghe
# doing ghi nhật ký vào tập tin và bảng điều khiển
record.processName = '%s (cho %s)' % (current_process().name, record.processName)
logger.handle(bản ghi)
def listen_process(q, stop_event, config):
"""
Điều này có thể được thực hiện trong quy trình chính, nhưng chỉ được thực hiện trong một quy trình riêng biệt.
quy trình nhằm mục đích minh họa.
Việc này khởi tạo việc ghi nhật ký theo cấu hình đã chỉ định,
khởi động trình nghe và chờ quá trình chính báo hiệu hoàn thành
thông qua sự kiện. Sau đó, người nghe sẽ dừng lại và quá trình sẽ thoát.
"""
ghi nhật ký.config.dictConfig(config)
người nghe = log.handlers.QueueListener(q, MyHandler())
người nghe.start()
nếu os.name == 'posix':
# On POSIX, trình ghi thiết lập sẽ được cấu hình trong
# parent, nhưng đáng lẽ phải bị vô hiệu hóa sau
cuộc gọi # dictConfig.
# On Windows, vì fork không được sử dụng nên trình ghi nhật ký thiết lập sẽ không
# exist ở trẻ em nên nó sẽ được tạo và thông báo
# would xuất hiện - do đó có mệnh đề "if posix".
logger = log.getLogger('setup')
logger.cript('Không nên xuất hiện, vì logger bị vô hiệu hóa ...')
stop_event.wait()
người nghe.stop()
def worker_process (cấu hình):
"""
Một số trong số này được sinh ra nhằm mục đích minh họa. trong
thực hành, chúng có thể là một tập hợp các quy trình không đồng nhất chứ không phải là
những cái giống hệt nhau.
Việc này khởi tạo việc ghi nhật ký theo cấu hình đã chỉ định,
và ghi lại hàng trăm tin nhắn với mức độ ngẫu nhiên để chọn ngẫu nhiên
người khai thác gỗ.
Một giấc ngủ nhỏ được thêm vào để cho phép các tiến trình khác có cơ hội chạy. Cái này
không thực sự cần thiết, nhưng nó trộn đầu ra từ các nguồn khác nhau
xử lý nhiều hơn một chút nếu nó bị bỏ đi.
"""
ghi nhật ký.config.dictConfig(config)
cấp độ = [logging.DEBUG, log.INFO, log.WARNING, log.ERROR,
log.CRITICAL]
logger = ['foo', 'foo.bar', 'foo.bar.baz',
'thư rác', 'spam.ham', 'spam.ham.eggs']
nếu os.name == 'posix':
# On POSIX, trình ghi thiết lập sẽ được cấu hình trong
# parent, nhưng đáng lẽ phải bị vô hiệu hóa sau
cuộc gọi # dictConfig.
# On Windows, vì fork không được sử dụng nên trình ghi nhật ký thiết lập sẽ không
# exist ở trẻ em nên nó sẽ được tạo và thông báo
# would xuất hiện - do đó có mệnh đề "if posix".
logger = log.getLogger('setup')
logger.cript('Không nên xuất hiện, vì logger bị vô hiệu hóa ...')
cho tôi trong phạm vi (100):
lvl = ngẫu nhiên.choice(cấp độ)
logger = log.getLogger(random.choice(loggers))
logger.log(lvl, 'Thông báo số %d', i)
thời gian.ngủ (0,01)
chắc chắn chính():
q = Hàng đợi()
Quá trình chính của # The có một cấu hình đơn giản để in ra bảng điều khiển.
config_init = {
'phiên bản': 1,
'người xử lý': {
'bàn điều khiển': {
'lớp': 'logging.StreamHandler',
'cấp độ': 'INFO'
}
},
'gốc': {
'trình xử lý': ['console'],
'cấp độ': 'DEBUG'
}
}
Cấu hình quy trình công nhân # The chỉ là một QueueHandler được gắn vào
# root logger, cho phép tất cả tin nhắn được gửi đến hàng đợi.
# We tắt trình ghi nhật ký hiện có để tắt trình ghi nhật ký "thiết lập" được sử dụng trong
quá trình # parent. Điều này là cần thiết trên POSIX vì trình ghi nhật ký sẽ
# be có đứa trẻ đi theo ngã ba().
config_worker = {
'phiên bản': 1,
'disable_being_loggers': Đúng,
'người xử lý': {
'xếp hàng': {
'lớp': 'logging.handlers.QueueHandler',
'xếp hàng': q
}
},
'gốc': {
'người xử lý': ['hàng đợi'],
'cấp độ': 'DEBUG'
}
}
Cấu hình quy trình nghe # The cho thấy tính linh hoạt hoàn toàn của
Tuy nhiên, cấu hình # logging có sẵn để gửi sự kiện đến trình xử lý
# you muốn.
# We tắt trình ghi nhật ký hiện có để tắt trình ghi nhật ký "thiết lập" được sử dụng trong
quá trình # parent. Điều này là cần thiết trên POSIX vì trình ghi nhật ký sẽ
# be có đứa trẻ đi theo ngã ba().
config_listener = {
'phiên bản': 1,
'disable_being_loggers': Đúng,
'trình định dạng': {
'chi tiết': {
'lớp': 'logging.Formatter',
'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
},
'đơn giản': {
'lớp': 'logging.Formatter',
'định dạng': '%(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
}
},
'người xử lý': {
'bàn điều khiển': {
'lớp': 'logging.StreamHandler',
'trình định dạng': 'đơn giản',
'cấp độ': 'INFO'
},
'tập tin': {
'lớp': 'logging.FileHandler',
'tên tệp': 'mplog.log',
'chế độ': 'w',
'người định dạng': 'chi tiết'
},
'tập tin ngu ngốc': {
'lớp': 'logging.FileHandler',
'tên tệp': 'mplog-foo.log',
'chế độ': 'w',
'người định dạng': 'chi tiết'
},
'lỗi': {
'lớp': 'logging.FileHandler',
'tên tệp': 'mplog-errors.log',
'chế độ': 'w',
'người định dạng': 'chi tiết',
'cấp độ': 'ERROR'
}
},
'người ghi nhật ký': {
'foo': {
'trình xử lý': ['foofile']
}
},
'gốc': {
'trình xử lý': ['console', 'file', 'errors'],
'cấp độ': 'DEBUG'
}
}
# Log một số sự kiện ban đầu, chỉ để cho thấy rằng việc đăng nhập vào cấp độ gốc có hiệu quả
# normally.
log.config.dictConfig(config_initial)
logger = log.getLogger('setup')
logger.info('Sắp tạo công nhân ...')
công nhân = []
cho tôi trong phạm vi (5):
wp = Process(target=worker_process, name='worker %d' % (i + 1),
args=(config_worker,))
công nhân.append(wp)
wp.start()
logger.info('Worker đã bắt đầu: %s', wp.name)
logger.info('Sắp tạo trình nghe ...')
stop_event=Sự kiện()
lp = Quá trình(target=listener_process, name='listener',
args=(q, stop_event, config_listener))
lp.start()
logger.info('Đã bắt đầu nghe')
# We hiện đang ở lại để các công nhân hoàn thành công việc của mình.
cho wp trong công nhân:
wp.join()
# Workers đã xong, giờ có thể dừng nghe.
# Logging ở cha mẹ vẫn hoạt động bình thường.
logger.info('Yêu cầu người nghe dừng lại ...')
stop_event.set()
lp.join()
logger.info('Tất cả đã xong.')
nếu __name__ == '__main__':
chính()
Chèn BOM vào tin nhắn được gửi tới SysLogHandler¶
RFC 5424 yêu cầu gửi thông báo Unicode tới trình nền nhật ký hệ thống dưới dạng một tập hợp byte có cấu trúc sau: thành phần pure-ASCII tùy chọn, theo sau là Dấu thứ tự byte UTF-8 (BOM), tiếp theo là Unicode được mã hóa bằng UTF-8. (Xem relevant section of the specification.)
Trong Python 3.1, mã đã được thêm vào SysLogHandler để chèn BOM vào tin nhắn, nhưng thật không may, nó đã được triển khai không chính xác, với BOM xuất hiện ở đầu tin nhắn và do đó không cho phép bất kỳ thành phần pure-ASCII nào xuất hiện trước nó.
Vì hành vi này bị hỏng nên mã chèn BOM không chính xác sẽ bị xóa khỏi Python 3.2.4 trở lên. Tuy nhiên, nó không được thay thế và nếu bạn muốn tạo các thông báo tuân thủ RFC 5424 bao gồm BOM, một chuỗi pure-ASCII tùy chọn trước nó và Unicode tùy ý sau nó, được mã hóa bằng UTF-8, thì bạn cần làm như sau:
Đính kèm phiên bản
Formattervào phiên bảnSysLogHandlercủa bạn, bằng chuỗi định dạng như:'phần ASCII\ufeffUnicode'
Điểm mã Unicode U+FEFF, khi được mã hóa bằng UTF-8, sẽ được mã hóa dưới dạng UTF-8 BOM -- chuỗi byte
b'\xef\xbb\xbf'.Thay thế phần ASCII bằng bất kỳ phần giữ chỗ nào bạn thích, nhưng hãy đảm bảo rằng dữ liệu xuất hiện trong đó sau khi thay thế luôn là ASCII (theo cách đó, nó sẽ không thay đổi sau khi mã hóa UTF-8).
Thay thế phần Unicode bằng bất kỳ phần giữ chỗ nào bạn thích; nếu dữ liệu xuất hiện ở đó sau khi thay thế chứa các ký tự nằm ngoài phạm vi ASCII thì không sao -- nó sẽ được mã hóa bằng UTF-8.
Thông báo được định dạng will được mã hóa bằng cách sử dụng mã hóa UTF-8 bởi SysLogHandler. Nếu tuân theo các quy tắc trên, bạn sẽ có thể tạo thông báo tuân thủ RFC 5424. Nếu không, việc ghi nhật ký có thể không phàn nàn, nhưng tin nhắn của bạn sẽ không tuân thủ RFC 5424 và trình nền nhật ký hệ thống của bạn có thể phàn nàn.
Triển khai ghi nhật ký có cấu trúc¶
Mặc dù hầu hết các thông điệp ghi nhật ký đều nhằm mục đích để con người đọc và do đó không thể phân tích cú pháp bằng máy một cách dễ dàng, nhưng có thể có những trường hợp bạn muốn xuất thông báo ở định dạng có cấu trúc mà is có thể được phân tích cú pháp bởi một chương trình (không cần các biểu thức chính quy phức tạp để phân tích thông điệp tường trình). Điều này dễ dàng đạt được bằng cách sử dụng gói ghi nhật ký. Có một số cách có thể đạt được điều này, nhưng sau đây là một cách tiếp cận đơn giản sử dụng JSON để tuần tự hóa sự kiện theo cách mà máy có thể phân tích cú pháp:
nhập json
nhập nhật ký
Thông báo cấu trúc lớp:
def __init__(self, message, /, **kwargs):
self.message = tin nhắn
self.kwargs = kwargs
chắc chắn __str__(tự):
return '%s >>> %s' % (self.message, json.dumps(self.kwargs))
_ = StructuredMessage # optional, để cải thiện khả năng đọc
logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.info(_('tin nhắn 1', foo='bar', bar='baz', num=123, fnum=123.456))
Nếu đoạn script trên được chạy, nó sẽ in:
tin nhắn 1 >>> {"fnum": 123.456, "num": 123, "bar": "baz", "foo": "bar"}
Lưu ý rằng thứ tự các mục có thể khác nhau tùy theo phiên bản Python được sử dụng.
Nếu cần xử lý chuyên biệt hơn, bạn có thể sử dụng bộ mã hóa JSON tùy chỉnh, như trong ví dụ hoàn chỉnh sau:
nhập json
nhập nhật ký
Bộ mã hóa lớp (json.JSONEncode):
mặc định mặc định (tự, o):
nếu isinstance(o, set):
trả về bộ dữ liệu(o)
Elif isinstance(o, str):
return o.encode('unicode_escape').decode('ascii')
trả về super().default(o)
Thông báo cấu trúc lớp:
def __init__(self, message, /, **kwargs):
self.message = tin nhắn
self.kwargs = kwargs
chắc chắn __str__(tự):
s = Bộ mã hóa().encode(self.kwargs)
trả về '%s >>> %s' % (self.message, s)
_ = StructuredMessage # optional, để cải thiện khả năng đọc
chắc chắn chính():
logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.info(_('message 1', set_value={1, 2, 3}, snowman='\u2603'))
nếu __name__ == '__main__':
chính()
Khi đoạn script trên được chạy, nó sẽ in:
tin nhắn 1 >>> {"snowman": "\u2603", "set_value": [1, 2, 3]}
Lưu ý rằng thứ tự các mục có thể khác nhau tùy theo phiên bản Python được sử dụng.
Tùy chỉnh trình xử lý với dictConfig()¶
Đôi khi bạn muốn tùy chỉnh trình xử lý ghi nhật ký theo những cách cụ thể và nếu bạn sử dụng dictConfig(), bạn có thể thực hiện việc này mà không cần phân lớp con. Ví dụ: hãy xem xét rằng bạn có thể muốn đặt quyền sở hữu tệp nhật ký. Trên POSIX, việc này được thực hiện dễ dàng bằng shutil.chown(), nhưng trình xử lý tệp trong stdlib không cung cấp hỗ trợ tích hợp. Bạn có thể tùy chỉnh việc tạo trình xử lý bằng cách sử dụng một hàm đơn giản như:
def sở hữu_file_handler(tên tệp, mode='a', mã hóa=Không, chủ sở hữu=Không):
nếu chủ sở hữu:
nếu không os.path.exists(tên tệp):
open(tên tệp, 'a').close()
Shutil.chown(tên file, *chủ sở hữu)
trả lại log.FileHandler(tên tệp, chế độ, mã hóa)
Sau đó, bạn có thể chỉ định, trong cấu hình ghi nhật ký được chuyển tới dictConfig(), trình xử lý ghi nhật ký sẽ được tạo bằng cách gọi hàm này
LOGGING = {
'phiên bản': 1,
'vô hiệu hóa_hiện_loggers': Sai,
'trình định dạng': {
'mặc định': {
'format': '%(asctime)s %(levelname)s %(name)s %(message)s'
},
},
'người xử lý': {
'tệp':{
Các giá trị # The bên dưới được lấy ra từ từ điển này và
# used để tạo trình xử lý, đặt cấp độ của trình xử lý và
định dạng # its.
'()': owner_file_handler,
'cấp độ':'DEBUG',
'trình định dạng': 'mặc định',
Các giá trị # The bên dưới được chuyển tới trình tạo trình xử lý có thể gọi được
đối số từ khóa # as.
'chủ sở hữu': ['xung', 'xung'],
'tên tệp': 'chowntest.log',
'chế độ': 'w',
'mã hóa': 'utf-8',
},
},
'gốc': {
'trình xử lý': ['file'],
'cấp độ': 'DEBUG',
},
}
Trong ví dụ này, tôi đang thiết lập quyền sở hữu bằng cách sử dụng người dùng và nhóm pulse, chỉ nhằm mục đích minh họa. Đặt nó lại với nhau thành một tập lệnh hoạt động, chowntest.py:
nhập nhật ký, log.config, os, Shutil
def sở hữu_file_handler(tên tệp, mode='a', mã hóa=Không, chủ sở hữu=Không):
nếu chủ sở hữu:
nếu không os.path.exists(tên tệp):
open(tên tệp, 'a').close()
Shutil.chown(tên file, *chủ sở hữu)
trả lại log.FileHandler(tên tệp, chế độ, mã hóa)
LOGGING = {
'phiên bản': 1,
'vô hiệu hóa_hiện_loggers': Sai,
'trình định dạng': {
'mặc định': {
'format': '%(asctime)s %(levelname)s %(name)s %(message)s'
},
},
'người xử lý': {
'tệp':{
Các giá trị # The bên dưới được lấy ra từ từ điển này và
# used để tạo trình xử lý, đặt cấp độ của trình xử lý và
định dạng # its.
'()': owner_file_handler,
'cấp độ':'DEBUG',
'trình định dạng': 'mặc định',
Các giá trị # The bên dưới được chuyển tới trình tạo trình xử lý có thể gọi được
đối số từ khóa # as.
'chủ sở hữu': ['xung', 'xung'],
'tên tệp': 'chowntest.log',
'chế độ': 'w',
'mã hóa': 'utf-8',
},
},
'gốc': {
'trình xử lý': ['file'],
'cấp độ': 'DEBUG',
},
}
log.config.dictConfig(LOGGING)
logger = log.getLogger('mylogger')
logger.debug('Thông báo gỡ lỗi')
Để chạy cái này, có thể bạn sẽ cần chạy dưới dạng root:
$ sudo python3.3 chowntest.py
$ cat chowntest.log
2013-11-05 09:34:51,128 DEBUG mylogger Một thông báo gỡ lỗi
$ ls -l chowntest.log
-rw-r--r-- 1 xung xung 55 2013-11-05 09:34 chowntest.log
Lưu ý rằng ví dụ này sử dụng Python 3.3 vì đó là nơi shutil.chown() xuất hiện. Cách tiếp cận này sẽ hoạt động với mọi phiên bản Python hỗ trợ dictConfig() - cụ thể là Python 2.7, 3.2 trở lên. Với các phiên bản trước 3.3, bạn sẽ cần triển khai thay đổi quyền sở hữu thực tế bằng cách sử dụng ví dụ: os.chown().
Trong thực tế, chức năng tạo trình xử lý có thể nằm trong một mô-đun tiện ích nào đó trong dự án của bạn. Thay vì dòng trong cấu hình:
'()': owner_file_handler,
bạn có thể sử dụng ví dụ:
'()': 'ext://project.util.owned_file_handler',
trong đó project.util có thể được thay thế bằng tên thực của gói chứa hàm. Trong tập lệnh làm việc ở trên, sử dụng 'ext://__main__.owned_file_handler' sẽ hoạt động. Ở đây, lệnh gọi thực tế được giải quyết bằng dictConfig() từ thông số kỹ thuật ext://.
Hy vọng ví dụ này cũng chỉ ra cách bạn có thể triển khai các loại thay đổi tệp khác - ví dụ: thiết lập các bit quyền POSIX cụ thể - theo cách tương tự, sử dụng os.chmod().
Tất nhiên, cách tiếp cận này cũng có thể được mở rộng cho các loại trình xử lý khác ngoài FileHandler - ví dụ: một trong các trình xử lý tệp xoay hoặc một loại trình xử lý khác hoàn toàn.
Sử dụng các kiểu định dạng cụ thể trong toàn bộ ứng dụng của bạn¶
Trong Python 3.2, Formatter đã nhận được tham số từ khóa style, mặc dù được đặt mặc định là % để tương thích ngược, nhưng vẫn cho phép thông số kỹ thuật của { hoặc $ hỗ trợ các phương pháp định dạng được hỗ trợ bởi str.format() và string.Template. Lưu ý rằng điều này chi phối định dạng của thông báo ghi nhật ký cho đầu ra cuối cùng thành nhật ký và hoàn toàn trực giao với cách xây dựng thông báo ghi nhật ký riêng lẻ.
Cuộc gọi ghi nhật ký (debug(), info(), v.v.) chỉ lấy các tham số vị trí cho chính thông báo ghi nhật ký thực tế, với các tham số từ khóa chỉ được sử dụng để xác định các tùy chọn về cách xử lý cuộc gọi ghi nhật ký (ví dụ: tham số từ khóa exc_info để cho biết rằng thông tin truy nguyên cần được ghi lại hoặc tham số từ khóa extra để biểu thị thông tin ngữ cảnh bổ sung sẽ được thêm vào nhật ký). Vì vậy, bạn không thể trực tiếp thực hiện cuộc gọi ghi nhật ký bằng cú pháp str.format() hoặc string.Template, vì bên trong gói ghi nhật ký sử dụng %-formatting để hợp nhất chuỗi định dạng và các đối số biến. Sẽ không có sự thay đổi nào trong khi vẫn duy trì khả năng tương thích ngược, vì tất cả lệnh gọi ghi nhật ký có sẵn trong mã hiện tại sẽ sử dụng chuỗi định dạng %.
Đã có đề xuất liên kết kiểu định dạng với trình ghi nhật ký cụ thể, nhưng cách tiếp cận đó cũng gặp phải vấn đề tương thích ngược vì mọi mã hiện có đều có thể sử dụng tên trình ghi nhật ký nhất định và sử dụng định dạng %.
Để việc ghi nhật ký có thể hoạt động tương tác giữa mọi thư viện bên thứ ba và mã của bạn, các quyết định về định dạng cần được đưa ra ở cấp độ lệnh gọi ghi nhật ký riêng lẻ. Điều này mở ra một số cách có thể cung cấp các kiểu định dạng thay thế.
Sử dụng nhà máy LogRecord¶
Trong Python 3.2, cùng với những thay đổi về Formatter được đề cập ở trên, gói ghi nhật ký có khả năng cho phép người dùng thiết lập các lớp con LogRecord của riêng họ bằng cách sử dụng hàm setLogRecordFactory(). Bạn có thể sử dụng điều này để đặt lớp con LogRecord của riêng mình, điều này thực hiện Điều đúng bằng cách ghi đè phương thức getMessage(). Việc triển khai lớp cơ sở của phương thức này là nơi diễn ra định dạng msg % args và là nơi bạn có thể thay thế định dạng thay thế của mình; tuy nhiên, bạn nên cẩn thận để hỗ trợ tất cả các kiểu định dạng và cho phép định dạng % làm mặc định, để đảm bảo khả năng tương tác với mã khác. Cũng cần cẩn thận khi gọi str(self.msg), giống như việc triển khai cơ sở.
Tham khảo tài liệu tham khảo về setLogRecordFactory() và LogRecord để biết thêm thông tin.
Sử dụng các đối tượng tin nhắn tùy chỉnh¶
Có một cách khác, có lẽ đơn giản hơn mà bạn có thể sử dụng định dạng {}- và $- để tạo thông điệp tường trình riêng lẻ của mình. Bạn có thể nhớ lại (từ Sử dụng các đối tượng tùy ý làm tin nhắn) rằng khi ghi nhật ký, bạn có thể sử dụng một đối tượng tùy ý làm chuỗi định dạng thông báo và gói ghi nhật ký sẽ gọi str() trên đối tượng đó để lấy chuỗi định dạng thực tế. Hãy xem xét hai lớp sau:
lớp BraceMessage:
def __init__(self, fmt, /, *args, **kwargs):
self.fmt = fmt
self.args = args
self.kwargs = kwargs
chắc chắn __str__(tự):
trả về self.fmt.format(*self.args, **self.kwargs)
lớp DollarMessage:
def __init__(self, fmt, /, **kwargs):
self.fmt = fmt
self.kwargs = kwargs
chắc chắn __str__(tự):
từ Mẫu nhập chuỗi
return Mẫu(self.fmt).substitute(**self.kwargs)
Bạn có thể sử dụng một trong hai chuỗi này thay cho chuỗi định dạng để cho phép sử dụng định dạng {}- hoặc $-để xây dựng phần "thông báo" thực tế xuất hiện trong đầu ra nhật ký được định dạng thay cho “%(message)s” hoặc “{message}” hoặc “$message”. Nếu bạn thấy hơi khó sử dụng tên lớp bất cứ khi nào bạn muốn ghi nhật ký nội dung nào đó, bạn có thể làm cho nó dễ chịu hơn nếu bạn sử dụng bí danh như M hoặc _ cho tin nhắn (hoặc có thể là __, nếu bạn đang sử dụng _ để bản địa hóa).
Ví dụ về cách tiếp cận này được đưa ra dưới đây. Đầu tiên, định dạng bằng str.format():
>>> __ = BraceMessage
>>> print(__('Tin nhắn với {0} {1}', 2, 'placeholders'))
Tin nhắn có 2 phần giữ chỗ
>>> Điểm lớp: đạt
...
>>> p = Điểm()
>>> p.x = 0,5
>>> p.y = 0,5
>>> print(__('Thông báo có tọa độ: ({point.x:.2f}, {point.y:.2f})', point=p))
Tin nhắn có tọa độ: (0,50, 0,50)
Thứ hai, định dạng bằng string.Template:
>>> __ = Tin nhắn Dollar
>>> print(__('Tin nhắn với $num $what', num=2, what='placeholders'))
Tin nhắn có 2 phần giữ chỗ
>>>
Một điều cần lưu ý là bạn không phải chịu hình phạt đáng kể nào về hiệu suất với phương pháp này: định dạng thực tế không xảy ra khi bạn thực hiện lệnh gọi ghi nhật ký mà là khi (và nếu) thông báo đã ghi nhật ký thực sự sắp được một trình xử lý xuất ra nhật ký. Vì vậy, điều hơi bất thường duy nhất có thể khiến bạn gặp khó khăn là các dấu ngoặc đơn đi xung quanh chuỗi định dạng và các đối số, không chỉ chuỗi định dạng. Đó là bởi vì ký hiệu __ chỉ là cú pháp cho lệnh gọi hàm tạo tới một trong các lớp XXXMessage được hiển thị ở trên.
Định cấu hình bộ lọc với dictConfig()¶
Bạn can định cấu hình các bộ lọc bằng dictConfig(), mặc dù thoạt nhìn có thể không rõ ràng về cách thực hiện (do đó có công thức này). Vì Filter là lớp bộ lọc duy nhất có trong thư viện chuẩn và không có khả năng đáp ứng nhiều yêu cầu (nó chỉ có ở đó dưới dạng lớp cơ sở), nên thông thường bạn sẽ cần xác định lớp con Filter của riêng mình bằng một phương thức filter() được ghi đè. Để thực hiện việc này, hãy chỉ định khóa () trong từ điển cấu hình cho bộ lọc, chỉ định một lệnh gọi có thể được sử dụng để tạo bộ lọc (một lớp là rõ ràng nhất, nhưng bạn có thể cung cấp bất kỳ lệnh gọi nào trả về một phiên bản Filter). Đây là một ví dụ hoàn chỉnh:
nhập nhật ký
nhập log.config
hệ thống nhập khẩu
lớp MyFilter(logging.Filter):
def __init__(self, param=None):
self.param = param
bộ lọc def (tự, bản ghi):
nếu self.param là Không:
cho phép = Đúng
khác:
allow = self.param không có trong record.msg
nếu cho phép:
record.msg = 'đã thay đổi: ' + record.msg
cho phép trả lại
LOGGING = {
'phiên bản': 1,
'bộ lọc': {
'bộ lọc của tôi': {
'()': Bộ lọc của tôi,
'param': 'không có mặt',
}
},
'người xử lý': {
'bàn điều khiển': {
'lớp': 'logging.StreamHandler',
'bộ lọc': ['myfilter']
}
},
'gốc': {
'cấp độ': 'DEBUG',
'trình xử lý': ['console']
},
}
nếu __name__ == '__main__':
log.config.dictConfig(LOGGING)
log.debug('xin chào')
log.debug('xin chào - noshow')
Ví dụ này cho thấy cách bạn có thể chuyển dữ liệu cấu hình đến đối tượng có thể gọi được để xây dựng phiên bản đó, dưới dạng tham số từ khóa. Khi chạy, đoạn script trên sẽ in:
đã thay đổi: xin chào
điều này cho thấy bộ lọc đang hoạt động như được định cấu hình.
Một vài điểm bổ sung cần lưu ý:
Nếu bạn không thể tham chiếu trực tiếp đến hàm có thể gọi được trong cấu hình (ví dụ: nếu nó nằm trong một mô-đun khác và bạn không thể nhập nó trực tiếp vào nơi có từ điển cấu hình), thì bạn có thể sử dụng biểu mẫu
ext://...như được mô tả trong Truy cập vào các đối tượng bên ngoài. Ví dụ: bạn có thể sử dụng văn bản'ext://__main__.MyFilter'thay vìMyFiltertrong ví dụ trên.Cũng như đối với các bộ lọc, kỹ thuật này cũng có thể được sử dụng để định cấu hình các trình xử lý và định dạng tùy chỉnh. Xem Đối tượng do người dùng xác định để biết thêm thông tin về cách hỗ trợ ghi nhật ký bằng cách sử dụng các đối tượng do người dùng xác định trong cấu hình của nó và xem công thức sách nấu ăn khác Tùy chỉnh trình xử lý với dictConfig() ở trên.
Định dạng ngoại lệ tùy chỉnh¶
Có thể đôi khi bạn muốn thực hiện định dạng ngoại lệ tùy chỉnh - vì mục đích tranh luận, giả sử bạn muốn chính xác một dòng cho mỗi sự kiện được ghi lại, ngay cả khi có thông tin ngoại lệ. Bạn có thể thực hiện việc này bằng lớp trình định dạng tùy chỉnh, như trong ví dụ sau:
nhập nhật ký
lớp OneLineExceptionFormatter(logging.Formatter):
định dạng defException(tự, exc_info):
"""
Định dạng một ngoại lệ để nó in trên một dòng.
"""
result = super().formatException(exc_info)
trả về định dạng repr(result) # or thành một dòng theo cách bạn muốn
định dạng def (tự, bản ghi):
s = super().format(bản ghi)
nếu bản ghi.exc_text:
s = s.replace('\n', '') + '|'
trả lại s
def configure_logging():
fh = log.FileHandler('output.txt', 'w')
f = OneLineExceptionFormatter('%(asctime)s|%(levelname)s|%(message)s|',
'%d/%m/%Y %H:%M:%S')
fh.setFormatter(f)
root = log.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(fh)
chắc chắn chính():
configure_logging()
logging.info('Tin nhắn mẫu')
thử:
x = 1/0
ngoại trừ ZeroDivisionError là e:
logging.Exception('ZeroDivisionError: %s', e)
nếu __name__ == '__main__':
chính()
Khi chạy, nó tạo ra một tệp có chính xác hai dòng:
28/01/2015 07:21:23|INFO|Tin nhắn mẫu|
28/01/2015 07:21:23|ERROR|ZeroDivisionError: chia cho zero|'Traceback (most recent call last):\n File "logtest7.py", line 30, in main\n x = 1 / 0\nZeroDivisionError: division by zero'|
Mặc dù cách xử lý ở trên rất đơn giản nhưng nó chỉ ra cách có thể định dạng thông tin ngoại lệ theo ý thích của bạn. Mô-đun traceback có thể hữu ích cho các nhu cầu chuyên biệt hơn.
Nói thông điệp ghi nhật ký¶
Có thể có những tình huống mong muốn các thông điệp ghi nhật ký được hiển thị ở dạng âm thanh thay vì dạng hiển thị. Điều này rất dễ thực hiện nếu bạn có sẵn chức năng chuyển văn bản thành giọng nói (TTS) trong hệ thống của mình, ngay cả khi nó không có liên kết Python. Hầu hết các hệ thống TTS đều có chương trình dòng lệnh mà bạn có thể chạy và chương trình này có thể được gọi từ trình xử lý bằng subprocess. Ở đây giả định rằng các chương trình dòng lệnh TTS sẽ không tương tác với người dùng hoặc mất nhiều thời gian để hoàn thành và tần suất của các tin nhắn được ghi lại sẽ không cao đến mức khiến người dùng tràn ngập các tin nhắn và có thể chấp nhận nói từng tin nhắn một thay vì đồng thời. Việc triển khai ví dụ bên dưới sẽ đợi một tin nhắn được nói trước khi tin nhắn tiếp theo được xử lý và điều này có thể khiến các trình xử lý khác phải chờ. Dưới đây là một ví dụ ngắn thể hiện cách tiếp cận, giả định rằng gói espeak TTS có sẵn
nhập nhật ký
nhập khẩu quy trình con
hệ thống nhập khẩu
lớp TTSHandler(logging.Handler):
def phát ra (tự, ghi):
msg = self.format(bản ghi)
# Speak chậm rãi bằng giọng nữ tiếng Anh
cmd = ['espeak', '-s150', '-ven+f3', msg]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
# wait để chương trình kết thúc
p.communicate()
def configure_logging():
h = TTSHandler()
root = log.getLogger()
root.addHandler(h)
trình định dạng mặc định # the chỉ trả về tin nhắn
root.setLevel(logging.DEBUG)
chắc chắn chính():
log.info('Xin chào')
log.debug('Tạm biệt')
nếu __name__ == '__main__':
configure_logging()
sys.exit(chính())
Khi chạy, tập lệnh này sẽ nói "Xin chào" và sau đó là "Tạm biệt" bằng giọng nữ.
Tất nhiên, cách tiếp cận trên có thể được điều chỉnh cho phù hợp với các hệ thống TTS khác và thậm chí cả các hệ thống khác có thể xử lý tin nhắn thông qua các chương trình bên ngoài chạy từ dòng lệnh.
Lưu vào bộ đệm các thông báo ghi nhật ký và xuất chúng có điều kiện¶
Có thể có những trường hợp bạn muốn ghi thông điệp vào một khu vực tạm thời và chỉ xuất chúng nếu một điều kiện nhất định xảy ra. Ví dụ: bạn có thể muốn bắt đầu ghi nhật ký các sự kiện gỡ lỗi trong một hàm và nếu hàm đó hoàn thành mà không có lỗi thì bạn không muốn làm lộn xộn nhật ký với thông tin gỡ lỗi đã thu thập nhưng nếu có lỗi, bạn muốn tất cả thông tin gỡ lỗi được xuất ra cùng với lỗi.
Dưới đây là ví dụ cho thấy cách bạn có thể thực hiện việc này bằng cách sử dụng công cụ trang trí cho các chức năng mà bạn muốn ghi nhật ký hoạt động theo cách này. Nó sử dụng logging.handlers.MemoryHandler, cho phép lưu vào bộ đệm các sự kiện đã ghi cho đến khi một số điều kiện xảy ra, tại thời điểm đó các sự kiện được lưu vào bộ đệm là flushed - được chuyển đến một trình xử lý khác (trình xử lý target) để xử lý. Theo mặc định, MemoryHandler sẽ bị xóa khi bộ đệm của nó bị đầy hoặc khi nhìn thấy một sự kiện có mức lớn hơn hoặc bằng ngưỡng chỉ định. Bạn có thể sử dụng công thức này với lớp con chuyên dụng hơn là MemoryHandler nếu bạn muốn có hành vi xả tùy chỉnh.
Tập lệnh ví dụ có một hàm đơn giản, foo, chỉ duyệt qua tất cả các cấp độ ghi nhật ký, ghi vào sys.stderr để cho biết nó sắp đăng nhập ở cấp độ nào và sau đó thực sự ghi lại một thông báo ở cấp độ đó. Bạn có thể chuyển tham số tới foo, nếu đúng, tham số này sẽ ghi ở cấp độ ERROR và CRITICAL - nếu không, nó chỉ ghi ở cấp độ DEBUG, INFO và WARNING.
Tập lệnh chỉ sắp xếp để trang trí foo bằng một trình trang trí sẽ thực hiện ghi nhật ký có điều kiện được yêu cầu. Trình trang trí lấy một trình ghi nhật ký làm tham số và gắn một trình xử lý bộ nhớ trong suốt thời gian gọi đến hàm được trang trí. Trình trang trí có thể được tham số hóa bổ sung bằng cách sử dụng trình xử lý đích, mức độ xảy ra quá trình xả và dung lượng cho bộ đệm (số lượng bản ghi được lưu vào bộ đệm). Các giá trị này mặc định là StreamHandler ghi lần lượt vào sys.stderr, logging.ERROR và 100.
Đây là kịch bản:
nhập nhật ký
từ log.handlers nhập MemoryHandler
hệ thống nhập khẩu
logger = log.getLogger(__name__)
logger.addHandler(logging.NullHandler())
def log_if_errors(logger, target_handler=Không, tuôn ra_level=Không, dung lượng=Không):
nếu target_handler là Không có:
target_handler = ghi nhật ký.StreamHandler()
nếu tuôn ra_level là Không có:
Flush_level = log.ERROR
nếu dung lượng là Không có:
công suất = 100
handler = MemoryHandler(dung lượng, flushLevel=flush_level, target=target_handler)
trang trí def (fn):
trình bao bọc def(*args, **kwargs):
logger.addHandler(trình xử lý)
thử:
trả về fn(*args, **kwargs)
ngoại trừ Ngoại lệ:
logger.Exception('cuộc gọi thất bại')
nâng cao
cuối cùng:
super(MemoryHandler, handler).flush()
logger.removeHandler(trình xử lý)
giấy gói trả lại
trở lại trang trí
chắc chắn write_line(s):
sys.stderr.write('%s\n' % s)
def foo(thất bại=Sai):
write_line('sắp đăng nhập tại DEBUG ...')
logger.debug('Thực sự đã đăng nhập tại DEBUG')
write_line('sắp đăng nhập tại INFO ...')
logger.info('Thực sự đã đăng nhập tại INFO')
write_line('sắp đăng nhập tại WARNING ...')
logger.warning('Thực sự đã đăng nhập tại WARNING')
nếu thất bại:
write_line('sắp đăng nhập tại ERROR ...')
logger.error('Thực sự đã đăng nhập tại ERROR')
write_line('sắp đăng nhập tại CRITICAL ...')
logger.cript('Thực sự đã đăng nhập tại CRITICAL')
trở lại thất bại
trang trí_foo = log_if_errors(logger)(foo)
nếu __name__ == '__main__':
logger.setLevel(logging.DEBUG)
write_line('Gọi foo chưa được trang trí bằng Sai')
khẳng định không phải foo(Sai)
write_line('Gọi foo chưa được trang trí bằng True')
khẳng định foo(Đúng)
write_line('Gọi foo được trang trí bằng Sai')
khẳng định không trang trí_foo(Sai)
write_line('Gọi foo được trang trí bằng True')
khẳng định trang trí_foo(Đúng)
Khi tập lệnh này được chạy, kết quả đầu ra sau sẽ được quan sát:
Gọi foo chưa được trang trí bằng Sai
sắp đăng nhập tại DEBUG ...
sắp đăng nhập tại INFO ...
sắp đăng nhập tại WARNING ...
Gọi foo chưa được trang trí bằng True
sắp đăng nhập tại DEBUG ...
sắp đăng nhập tại INFO ...
sắp đăng nhập tại WARNING ...
sắp đăng nhập tại ERROR ...
sắp đăng nhập tại CRITICAL ...
Gọi foo được trang trí bằng Sai
sắp đăng nhập tại DEBUG ...
sắp đăng nhập tại INFO ...
sắp đăng nhập tại WARNING ...
Gọi foo được trang trí bằng True
sắp đăng nhập tại DEBUG ...
sắp đăng nhập tại INFO ...
sắp đăng nhập tại WARNING ...
sắp đăng nhập tại ERROR ...
Thực tế đã đăng nhập tại DEBUG
Thực tế đã đăng nhập tại INFO
Thực tế đã đăng nhập tại WARNING
Thực tế đã đăng nhập tại ERROR
sắp đăng nhập tại CRITICAL ...
Thực tế đã đăng nhập tại CRITICAL
Như bạn có thể thấy, kết quả ghi nhật ký thực tế chỉ xảy ra khi một sự kiện được ghi lại có mức độ nghiêm trọng là ERROR trở lên, nhưng trong trường hợp đó, mọi sự kiện trước đó ở mức độ nghiêm trọng thấp hơn cũng được ghi lại.
Tất nhiên bạn có thể sử dụng các phương tiện trang trí thông thường:
@log_if_errors(logger)
def foo(thất bại=Sai):
...
Gửi tin nhắn ghi nhật ký tới email, có tính năng lưu vào bộ đệm¶
Để minh họa cách bạn có thể gửi thông điệp tường trình qua email, sao cho một số lượng thông báo nhất định được gửi cho mỗi email, bạn có thể phân lớp BufferingHandler. Trong ví dụ sau, ví dụ mà bạn có thể điều chỉnh cho phù hợp với nhu cầu cụ thể của mình, một khai thác thử nghiệm đơn giản được cung cấp cho phép bạn chạy tập lệnh với các đối số dòng lệnh chỉ định những gì bạn thường cần để gửi mọi thứ qua SMTP. (Chạy tập lệnh đã tải xuống với đối số -h để xem các đối số bắt buộc và tùy chọn.)
nhập nhật ký
nhập log.handlers
nhập smtplib
lớp BufferingSMTPHandler(logging.handlers.BufferingHandler):
def __init__(self, mailhost, port, username, pass, fromaddr, toaddrs,
môn học, năng lực):
logging.handlers.BufferingHandler.__init__(tự, dung lượng)
self.mailhost = mailhost
self.mailport = cổng
self.username = tên người dùng
self.password = mật khẩu
self.fromaddr = fromaddr
if isinstance(toaddrs, str):
toaddrs = [toaddrs]
self.toaddrs = toaddrs
self.subject = chủ đề
self.setFormatter(logging.Formatter("%(asctime)s %(levelname)-5s %(message)s"))
def tuôn ra (tự):
nếu len(self.buffer) > 0:
thử:
smtp = smtplib.SMTP(self.mailhost, self.mailport)
smtp.starttls()
smtp.login(self.username, self.password)
msg = "Từ: %s\r\nTới: %s\r\nChủ đề: %s\r\n\r\n" % (self.fromaddr, ','.join(self.toaddrs), self.subject)
để ghi vào self.buffer:
s = self.format(bản ghi)
tin nhắn = tin nhắn + s + "\r\n"
smtp.sendmail(self.fromaddr, self.toaddrs, tin nhắn)
smtp.quit()
ngoại trừ Ngoại lệ:
nếu ghi nhật ký.raiseExceptions:
nâng cao
self.buffer = []
nếu __name__ == '__main__':
nhập khẩu argparse
ap = argparse.ArgumentParser()
aa = ap.add_argument
aa('máy chủ', metavar='HOST', help='SMTP máy chủ')
aa('--port', '-p', type=int, default=587, help='SMTP port')
aa('người dùng', metavar='USER', help='SMTP tên người dùng')
aa('mật khẩu', metavar='PASSWORD', help='SMTP mật khẩu')
aa('to', metavar='TO', help='Địa chỉ email')
aa('sender', metavar='SENDER', help='Địa chỉ email của người gửi')
aa('--chủ đề', '-s',
default='Kiểm tra email ghi nhật ký từ mô-đun ghi nhật ký Python (đệm)',
help='Chủ đề email')
tùy chọn = ap.parse_args()
logger = ghi nhật ký.getLogger()
logger.setLevel(logging.DEBUG)
h = BufferingSMTPHandler(options.host, options.port, options.user,
tùy chọn.mật khẩu, tùy chọn.sender,
tùy chọn.to, tùy chọn.chủ đề, 10)
logger.addHandler(h)
cho tôi trong phạm vi (102):
logger.info("Chỉ số thông tin = %d", i)
h.flush()
h.close()
Nếu bạn chạy tập lệnh này và máy chủ SMTP của bạn được thiết lập chính xác, bạn sẽ thấy rằng nó sẽ gửi 11 email đến địa chỉ mà bạn chỉ định. Mười email đầu tiên, mỗi email sẽ có mười tin nhắn tường trình và email thứ mười một sẽ có hai tin nhắn. Điều đó tạo nên 102 tin nhắn như được chỉ định trong tập lệnh.
Định dạng lần sử dụng UTC (GMT) thông qua cấu hình¶
Đôi khi bạn muốn định dạng thời gian bằng UTC, việc này có thể được thực hiện bằng cách sử dụng một lớp như UTCFormatter, được hiển thị bên dưới:
nhập nhật ký
thời gian nhập khẩu
lớp UTCFormatter(logging.Formatter):
công cụ chuyển đổi = time.gmtime
và sau đó bạn có thể sử dụng UTCFormatter trong mã của mình thay vì Formatter. Nếu bạn muốn thực hiện điều đó thông qua cấu hình, bạn có thể sử dụng dictConfig() API với cách tiếp cận được minh họa bằng ví dụ hoàn chỉnh sau:
nhập nhật ký
nhập log.config
thời gian nhập khẩu
lớp UTCFormatter(logging.Formatter):
công cụ chuyển đổi = time.gmtime
LOGGING = {
'phiên bản': 1,
'vô hiệu hóa_hiện_loggers': Sai,
'trình định dạng': {
'utc': {
'()': UTCFormatter,
'format': '%(asctime)s %(message)s',
},
'địa phương': {
'format': '%(asctime)s %(message)s',
}
},
'người xử lý': {
'bảng điều khiển1': {
'lớp': 'logging.StreamHandler',
'trình định dạng': 'utc',
},
'bảng điều khiển2': {
'lớp': 'logging.StreamHandler',
'trình định dạng': 'cục bộ',
},
},
'gốc': {
'trình xử lý': ['console1', 'console2'],
}
}
nếu __name__ == '__main__':
log.config.dictConfig(LOGGING)
logging.warning('Giờ địa phương là %s', time.asctime())
Khi tập lệnh này được chạy, nó sẽ in nội dung như sau:
2015-10-17 12:53:29,501 Giờ địa phương là Thứ Bảy ngày 17 tháng 10 13:53:29 2015
2015-10-17 13:53:29,501 Giờ địa phương là Thứ bảy ngày 17 tháng 10 13:53:29 2015
hiển thị cách định dạng thời gian theo cả giờ địa phương và UTC, một cho mỗi trình xử lý.
Sử dụng trình quản lý bối cảnh để ghi nhật ký có chọn lọc¶
Đôi khi sẽ rất hữu ích nếu bạn tạm thời thay đổi cấu hình ghi nhật ký và hoàn nguyên cấu hình đó sau khi thực hiện một thao tác nào đó. Đối với điều này, trình quản lý bối cảnh là cách rõ ràng nhất để lưu và khôi phục bối cảnh ghi nhật ký. Dưới đây là một ví dụ đơn giản về trình quản lý bối cảnh như vậy, cho phép bạn tùy ý thay đổi cấp độ ghi nhật ký và thêm trình xử lý ghi nhật ký hoàn toàn trong phạm vi của trình quản lý bối cảnh:
nhập nhật ký
hệ thống nhập khẩu
lớp LoggingContext:
def __init__(self, logger, level=None, handler=None, close=True):
self.logger = người ghi nhật ký
self.level = cấp độ
self.handler = người xử lý
self.close = đóng
chắc chắn __enter__(tự):
nếu self.level không phải là Không có:
self.old_level = self.logger.level
self.logger.setLevel(self.level)
nếu self.handler:
self.logger.addHandler(self.handler)
def __exit__(self, et, ev, tb):
nếu self.level không phải là Không có:
self.logger.setLevel(self.old_level)
nếu self.handler:
self.logger.removeHandler(self.handler)
nếu self.handler và self.close:
self.handler.close()
# implicit trả về Không => không nuốt ngoại lệ
Nếu bạn chỉ định một giá trị cấp độ, cấp độ của trình ghi nhật ký sẽ được đặt thành giá trị đó trong phạm vi khối with được trình quản lý bối cảnh quản lý. Nếu bạn chỉ định một trình xử lý, nó sẽ được thêm vào bộ ghi khi vào khối và bị xóa khi thoát khỏi khối. Bạn cũng có thể yêu cầu người quản lý đóng trình xử lý cho bạn khi thoát khỏi khối - bạn có thể thực hiện việc này nếu không cần trình xử lý nữa.
Để minh họa cách hoạt động của nó, chúng ta có thể thêm khối mã sau vào đoạn mã trên
nếu __name__ == '__main__':
logger = log.getLogger('foo')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.INFO)
logger.info('1. Cái này sẽ chỉ xuất hiện một lần trên stderr.')
logger.debug('2. Cái này sẽ không xuất hiện.')
với LoggingContext(logger, level=logging.DEBUG):
logger.debug('3. Cái này sẽ xuất hiện một lần trên stderr.')
logger.debug('4. Cái này sẽ không xuất hiện.')
h = log.StreamHandler(sys.stdout)
với LoggingContext(logger, level=logging.DEBUG, handler=h, close=True):
logger.debug('5. Cái này sẽ xuất hiện hai lần - một lần trên stderr và một lần trên stdout.')
logger.info('6. Cái này sẽ chỉ xuất hiện một lần trên stderr.')
logger.debug('7. Cái này sẽ không xuất hiện.')
Ban đầu, chúng tôi đặt cấp độ của trình ghi nhật ký thành INFO, vì vậy thông báo #1 xuất hiện còn thông báo #2 thì không. Sau đó, chúng tôi tạm thời thay đổi cấp độ thành DEBUG trong khối with sau và do đó thông báo #3 xuất hiện. Sau khi thoát khỏi khối, cấp độ của trình ghi nhật ký được khôi phục về INFO và do đó thông báo #4 không xuất hiện. Trong khối with tiếp theo, chúng tôi đặt lại cấp độ thành DEBUG nhưng cũng thêm một trình xử lý ghi vào sys.stdout. Do đó, thông báo #5 xuất hiện hai lần trên bảng điều khiển (một lần qua stderr và một lần qua stdout). Sau khi câu lệnh with hoàn thành, trạng thái vẫn như trước nên thông báo #6 xuất hiện (như tin nhắn #1) trong khi tin nhắn #7 thì không (giống như tin nhắn #2).
Nếu chúng ta chạy script kết quả thì kết quả như sau:
$ trăn logctx.py
1. Điều này sẽ chỉ xuất hiện một lần trên stderr.
3. Điều này sẽ xuất hiện một lần trên stderr.
5. Điều này sẽ xuất hiện hai lần - một lần trên thiết bị xuất chuẩn và một lần trên thiết bị xuất chuẩn.
5. Điều này sẽ xuất hiện hai lần - một lần trên thiết bị xuất chuẩn và một lần trên thiết bị xuất chuẩn.
6. Điều này sẽ chỉ xuất hiện một lần trên stderr.
Nếu chúng tôi chạy lại nó, nhưng chuyển stderr sang /dev/null, chúng tôi sẽ thấy thông báo sau, đây là thông báo duy nhất được ghi vào stdout:
$ python logctx.py 2>/dev/null
5. Điều này sẽ xuất hiện hai lần - một lần trên thiết bị xuất chuẩn và một lần trên thiết bị xuất chuẩn.
Một lần nữa, nhưng chuyển stdout sang /dev/null, chúng tôi nhận được:
$ python logctx.py >/dev/null
1. Điều này sẽ chỉ xuất hiện một lần trên stderr.
3. Điều này sẽ xuất hiện một lần trên stderr.
5. Điều này sẽ xuất hiện hai lần - một lần trên thiết bị xuất chuẩn và một lần trên thiết bị xuất chuẩn.
6. Điều này sẽ chỉ xuất hiện một lần trên stderr.
Trong trường hợp này, thông báo #5 được in ra stdout không xuất hiện như mong đợi.
Tất nhiên, cách tiếp cận được mô tả ở đây có thể được khái quát hóa, chẳng hạn như gắn tạm thời các bộ lọc ghi nhật ký. Lưu ý rằng đoạn mã trên hoạt động trong Python 2 cũng như Python 3.
Mẫu khởi động ứng dụng CLI¶
Đây là một ví dụ cho thấy cách bạn có thể:
Sử dụng mức ghi nhật ký dựa trên đối số dòng lệnh
Gửi tới nhiều tiểu ban trong các tệp riêng biệt, tất cả đều ghi nhật ký ở cùng cấp một cách nhất quán
Sử dụng cấu hình đơn giản, tối thiểu
Giả sử chúng ta có một ứng dụng dòng lệnh có nhiệm vụ dừng, khởi động hoặc khởi động lại một số dịch vụ. Điều này có thể được tổ chức nhằm mục đích minh họa dưới dạng tệp app.py là tập lệnh chính cho ứng dụng, với các lệnh riêng lẻ được triển khai trong start.py, stop.py và restart.py. Giả sử thêm rằng chúng ta muốn kiểm soát mức độ chi tiết của ứng dụng thông qua đối số dòng lệnh, mặc định là logging.INFO. Đây là một cách mà app.py có thể được viết:
nhập khẩu argparse
nhập khẩu nhập khẩu
nhập nhật ký
hệ điều hành nhập khẩu
hệ thống nhập khẩu
def chính(args=None):
scriptname = os.path.basename(__file__)
trình phân tích cú pháp = argparse.ArgumentParser(scriptname)
cấp độ = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
Parser.add_argument('--log-level', default='INFO', Choices=levels)
subparsers = parser.add_subparsers(dest='command',
help='Các lệnh có sẵn:')
start_cmd = subparsers.add_parser('start', help='Start a service')
start_cmd.add_argument('name', metavar='NAME',
help='Tên dịch vụ cần bắt đầu')
stop_cmd = subparsers.add_parser('stop',
help='Dừng một hoặc nhiều dịch vụ')
stop_cmd.add_argument('names', metavar='NAME', nargs='+',
help='Tên dịch vụ cần dừng')
restart_cmd = subparsers.add_parser('khởi động lại',
help='Khởi động lại một hoặc nhiều dịch vụ')
restart_cmd.add_argument('names', metavar='NAME', nargs='+',
help='Tên dịch vụ cần khởi động lại')
tùy chọn = trình phân tích cú pháp.parse_args()
Tất cả mã # the để gửi lệnh đều có thể có trong tệp này. Vì mục đích
# of chỉ minh họa, chúng tôi triển khai từng lệnh trong một mô-đun riêng biệt.
thử:
mod = importlib.import_module(options.command)
cmd = getattr(mod, 'lệnh')
ngoại trừ (ImportError, AttributionError):
print('Không thể tìm thấy mã lệnh \'%s\'' % options.command)
trở lại 1
# Could tìm hiểu ở đây và tải cấu hình từ tệp hoặc từ điển
logging.basicConfig(level=options.log_level,
format='%(levelname)s %(name)s %(message)s')
cmd(tùy chọn)
nếu __name__ == '__main__':
sys.exit(chính())
Và các lệnh start, stop và restart có thể được triển khai trong các mô-đun riêng biệt, như vậy để bắt đầu:
# start.py
nhập nhật ký
logger = log.getLogger(__name__)
lệnh def (tùy chọn):
logger.debug('Sắp bắt đầu %s', options.name)
# actually xử lý lệnh ở đây ...
logger.info('Đã bắt đầu dịch vụ \'%s\'.', options.name)
và do đó để dừng
# stop.py
nhập nhật ký
logger = log.getLogger(__name__)
lệnh def (tùy chọn):
n = len(options.names)
nếu n == 1:
số nhiều = ''
services = '\'%s\'' % options.names[0]
khác:
số nhiều = 's'
services = ', '.join('\'%s\'' % tên cho tên trong options.names)
i = services.rfind(', ')
dịch vụ = dịch vụ[:i] + ' và ' + dịch vụ[i + 2:]
logger.debug('Sắp dừng %s', dịch vụ)
# actually xử lý lệnh ở đây ...
logger.info('Đã dừng dịch vụ %s%s.', services, số nhiều)
và tương tự để khởi động lại
# restart.py
nhập nhật ký
logger = log.getLogger(__name__)
lệnh def (tùy chọn):
n = len(options.names)
nếu n == 1:
số nhiều = ''
services = '\'%s\'' % options.names[0]
khác:
số nhiều = 's'
services = ', '.join('\'%s\'' % tên cho tên trong options.names)
i = services.rfind(', ')
dịch vụ = dịch vụ[:i] + ' và ' + dịch vụ[i + 2:]
logger.debug('Sắp khởi động lại %s', dịch vụ)
# actually xử lý lệnh ở đây ...
logger.info('Đã khởi động lại dịch vụ %s%s.', services, số nhiều)
Nếu chúng tôi chạy ứng dụng này với cấp độ nhật ký mặc định, chúng tôi sẽ nhận được kết quả như thế này:
$ python app.py bắt đầu foo
INFO start Đã bắt đầu dịch vụ 'foo'.
$ python app.py dừng thanh foo
INFO stop Đã dừng dịch vụ 'foo' và 'bar'.
$ python app.py khởi động lại foo bar baz
khởi động lại INFO Đã khởi động lại các dịch vụ 'foo', 'bar' và 'baz'.
Từ đầu tiên là cấp độ ghi nhật ký và từ thứ hai là tên mô-đun hoặc gói của nơi sự kiện được ghi lại.
Nếu chúng ta thay đổi cấp độ ghi nhật ký thì chúng ta có thể thay đổi thông tin được gửi tới nhật ký. Ví dụ: nếu chúng tôi muốn biết thêm thông tin:
$ python app.py --log-level DEBUG bắt đầu foo
DEBUG start Sắp bắt đầu foo
INFO start Đã bắt đầu dịch vụ 'foo'.
$ python app.py --log-level DEBUG dừng thanh foo
DEBUG dừng Sắp dừng 'foo' và 'bar'
INFO stop Đã dừng dịch vụ 'foo' và 'bar'.
$ python app.py --log-level DEBUG khởi động lại foo bar baz
DEBUG restart Sắp khởi động lại 'foo', 'bar' và 'baz'
khởi động lại INFO Đã khởi động lại các dịch vụ 'foo', 'bar' và 'baz'.
Và nếu chúng ta muốn ít hơn:
$ python app.py --log-level WARNING bắt đầu foo
$ python app.py --log-level WARNING dừng thanh foo
$ python app.py --log-level WARNING khởi động lại foo bar baz
Trong trường hợp này, các lệnh không in bất cứ thứ gì ra bảng điều khiển, vì không có gì ở cấp độ WARNING trở lên được chúng ghi lại.
Qt GUI để ghi nhật ký¶
Thỉnh thoảng một câu hỏi được đặt ra là về cách đăng nhập vào ứng dụng GUI. Khung Qt là khung giao diện người dùng đa nền tảng phổ biến với các liên kết Python sử dụng thư viện PySide2 hoặc PyQt5.
Ví dụ sau đây cho thấy cách đăng nhập vào Qt GUI. Phần này giới thiệu một lớp QtHandler đơn giản có một lớp có thể gọi được, lớp này sẽ là một vị trí trong luồng chính thực hiện cập nhật GUI. Một luồng công việc cũng được tạo để hiển thị cách bạn có thể đăng nhập vào GUI từ cả giao diện người dùng (thông qua nút để ghi nhật ký thủ công) cũng như một luồng công việc đang hoạt động ở chế độ nền (ở đây, chỉ ghi nhật ký tin nhắn ở các cấp độ ngẫu nhiên với độ trễ ngắn ngẫu nhiên ở giữa).
Chuỗi công việc được triển khai bằng cách sử dụng lớp QThread của Qt thay vì mô-đun threading, vì có những trường hợp người ta phải sử dụng QThread, cung cấp khả năng tích hợp tốt hơn với các thành phần Qt khác.
Mã sẽ hoạt động với các bản phát hành gần đây của bất kỳ PySide6, PyQt6, PySide2 hoặc PyQt5 nào. Bạn sẽ có thể điều chỉnh cách tiếp cận với các phiên bản Qt trước đó. Vui lòng tham khảo các nhận xét trong đoạn mã để biết thêm thông tin chi tiết.
nhập nhật ký
nhập khẩu ngẫu nhiên
hệ thống nhập khẩu
thời gian nhập khẩu
# Deal với những khác biệt nhỏ giữa các gói Qt khác nhau
thử:
từ PySide6 nhập QtCore, QtGui, QtWidgets
Tín hiệu = QtCore.Signal
Khe = QtCore.Slot
ngoại trừ lỗi nhập khẩu:
thử:
từ PyQt6 nhập QtCore, QtGui, QtWidgets
Tín hiệu = QtCore.pyqtSignal
Vị trí = QtCore.pyqtSlot
ngoại trừ lỗi nhập khẩu:
thử:
từ PySide2 nhập QtCore, QtGui, QtWidgets
Tín hiệu = QtCore.Signal
Khe = QtCore.Slot
ngoại trừ lỗi nhập khẩu:
từ PyQt5 nhập QtCore, QtGui, QtWidgets
Tín hiệu = QtCore.pyqtSignal
Vị trí = QtCore.pyqtSlot
logger = log.getLogger(__name__)
#
# Signals cần phải được chứa trong QObject hoặc lớp con để hoạt động chính xác
# initialized.
Trình báo hiệu #
class(QtCore.QObject):
tín hiệu = Tín hiệu (str, log.LogRecord)
#
# Output sang Qt GUI chỉ được cho là xảy ra trên luồng chính. Vì vậy, điều này
# handler được thiết kế để có chức năng khe cắm được thiết lập để chạy trong hệ thống chính
# thread. Trong ví dụ này, hàm lấy một đối số chuỗi là một
thông báo nhật ký # formatted và bản ghi nhật ký đã tạo ra nó. Được định dạng
# string chỉ là một sự tiện lợi - bạn có thể định dạng một chuỗi cho đầu ra theo bất kỳ cách nào
# you giống như trong chính chức năng khe cắm.
#
# You chỉ định chức năng khe để thực hiện bất kỳ cập nhật GUI nào bạn muốn. Người xử lý
# doesn không biết hoặc không quan tâm đến các thành phần giao diện người dùng cụ thể.
#
class QtHandler(logging.Handler):
def __init__(self, slotfunc, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signaller = Người báo hiệu()
self.signaller.signal.connect(slotfunc)
def phát ra (tự, ghi):
s = self.format(bản ghi)
self.signaller.signal.emit(s, record)
#
Ví dụ # This sử dụng QThreads, có nghĩa là các luồng ở cấp độ Python
# are đặt tên gì đó như "Dummy-1". Hàm bên dưới lấy tên Qt của
chủ đề # current.
#
def ctname():
trả về QtCore.QThread.currentThread().objectName()
#
# Used để tạo các mức ngẫu nhiên để ghi nhật ký.
#
LEVELS = (logging.DEBUG, log.INFO, log.WARNING, log.ERROR,
ghi nhật ký.CRITICAL)
#
Lớp công nhân # This đại diện cho công việc được thực hiện trong một luồng riêng biệt với
chủ đề # main. Cách khởi động luồng để thực hiện công việc là thông qua một lần nhấn nút
# that kết nối với một khe trong Worker.
#
# Because, giá trị threadName mặc định trong LogRecord không được sử dụng nhiều, chúng tôi thêm vào
# a qThreadName chứa tên QThread như đã tính ở trên và chuyển tên đó
# value trong từ điển "bổ sung" được sử dụng để cập nhật LogRecord với
tên # QThread.
#
Nhân viên mẫu # This chỉ xuất các tin nhắn một cách tuần tự, xen kẽ với
# random trì hoãn thứ tự vài giây.
Công nhân #
class(QtCore.QObject):
@Slot()
bắt đầu chắc chắn (tự):
thêm = {'qThreadName': ctname() }
logger.debug('Đã bắt đầu công việc', extra=extra)
tôi = 1
# Let luồng chạy cho đến khi bị gián đoạn. Điều này cho phép làm sạch hợp lý
chấm dứt # thread.
trong khi không phải QtCore.QThread.currentThread().isInterruptionRequested():
độ trễ = 0,5 + ngẫu nhiên.random() * 2
thời gian. ngủ (độ trễ)
thử:
nếu ngẫu nhiên.random() < 0,1:
raise ValueError('Ngoại lệ được nêu ra: %d' % i)
khác:
cấp độ = ngẫu nhiên.choice(LEVELS)
logger.log(level, 'Thông báo sau độ trễ của %3.1f: %d', độ trễ, i, extra=extra)
ngoại trừ ValueError là e:
logger.Exception('Thất bại: %s', e, extra=extra)
tôi += 1
#
# Implement một giao diện người dùng đơn giản cho ví dụ về sách dạy nấu ăn này. Điều này chứa:
#
# * Cửa sổ chỉnh sửa văn bản chỉ đọc chứa các thông điệp tường trình được định dạng
# * Một nút để bắt đầu công việc và ghi nội dung vào một chủ đề riêng
# * Một nút để ghi lại nội dung nào đó từ luồng chính
# * Nút để xóa cửa sổ nhật ký
Cửa sổ #
class(QtWidgets.QWidget):
COLORS = {
logging.DEBUG: 'đen',
logging.INFO: 'màu xanh',
logging.WARNING: 'màu cam',
logging.ERROR: 'đỏ',
logging.CRITICAL: 'tím',
}
def __init__(tự, ứng dụng):
siêu().__init__()
self.app = ứng dụng
self.textedit = te = QtWidgets.QPlainTextEdit(self)
# Set bất kể phông chữ đơn cách mặc định dành cho nền tảng là gì
f = QtGui.QFont('nosuchfont')
nếu hasattr(f, 'Monospace'):
f.setStyleHint(f.Monospace)
khác:
f.setStyleHint(f.StyleHint.Monospace) # for Qt6
te.setFont(f)
te.setReadOnly(Đúng)
PB = QtWidgets.QPushButton
self.work_button = PB('Bắt đầu công việc nền', self)
self.log_button = PB('Ghi tin nhắn ở mức ngẫu nhiên', self)
self.clear_button = PB('Xóa cửa sổ nhật ký', self)
self.handler = h = QtHandler(self.update_status)
# Remember để sử dụng qThreadName thay vì threadName trong chuỗi định dạng.
fs = '%(asctime)s %(qThreadName)-12s %(levelname)-8s %(message)s'
trình định dạng = log.Formatter(fs)
h.setFormatter(trình định dạng)
logger.addHandler(h)
# Set lên để chấm dứt QThread khi chúng tôi thoát
app.aboutToQuit.connect(self.force_quit)
# Lay ra tất cả các vật dụng
bố cục = QtWidgets.QVBoxLayout(self)
bố cục.addWidget(te)
bố cục.addWidget(self.work_button)
bố cục.addWidget(self.log_button)
bố cục.addWidget(self.clear_button)
self.setFixedSize(900, 400)
# Connect các khe cắm và tín hiệu không hoạt động
self.log_button.clicked.connect(self.manual_update)
self.clear_button.clicked.connect(self.clear_display)
# Start một luồng công nhân mới và kết nối các khe cho công nhân
self.start_thread()
self.work_button.clicked.connect(self.worker.start)
# Once đã bắt đầu, nút này sẽ bị tắt
self.work_button.clicked.connect(lambda : self.work_button.setEnabled(Sai))
def start_thread(tự):
self.worker = Công nhân()
self.worker_thread = QtCore.QThread()
self.worker.setObjectName('Worker')
self.worker_thread.setObjectName('WorkerThread') # for qThreadName
self.worker.moveToThread(self.worker_thread)
# This sẽ bắt đầu một vòng lặp sự kiện trong chuỗi công việc
self.worker_thread.start()
def kill_thread(tự):
# Just bảo công nhân dừng lại, sau đó bảo nó thoát và đợi
# to xảy ra
self.worker_thread.requestInterruption()
nếu self.worker_thread.isRunning():
self.worker_thread.quit()
self.worker_thread.wait()
khác:
print('worker đã thoát.')
def Force_quit(tự):
# For sử dụng khi cửa sổ đóng
nếu self.worker_thread.isRunning():
self.kill_thread()
Các hàm # The bên dưới cập nhật giao diện người dùng và chạy trong luồng chính vì
# that là nơi thiết lập các khe cắm
@Slot(str, log.LogRecord)
def update_status(bản thân, trạng thái, bản ghi):
color = self.COLORS.get(record.levelno, 'đen')
s = '<pre><font color="%s">%s</font></pre>' % (màu sắc, trạng thái)
self.textedit.appendHtml(s)
@Slot()
def manual_update(tự):
Hàm # This sử dụng thông báo được định dạng được truyền vào nhưng cũng sử dụng
# information khỏi bản ghi để định dạng tin nhắn theo cách thích hợp
# color theo mức độ nghiêm trọng (mức độ) của nó.
cấp độ = ngẫu nhiên.choice(LEVELS)
thêm = {'qThreadName': ctname() }
logger.log(level, 'Đăng nhập thủ công!', extra=extra)
@Slot()
def Clear_display(tự):
self.textedit.clear()
chắc chắn chính():
QtCore.QThread.currentThread().setObjectName('MainThread')
logging.getLogger().setLevel(logging.DEBUG)
ứng dụng = QtWidgets.QApplication(sys.argv)
ví dụ = Cửa sổ (ứng dụng)
ví dụ.show()
nếu hasattr(ứng dụng, 'exec'):
rc = app.exec()
khác:
rc = app.exec_()
sys.exit(rc)
nếu __name__=='__main__':
chính()
Đăng nhập vào syslog với sự hỗ trợ của RFC5424¶
Mặc dù RFC 5424 có từ năm 2009, hầu hết các máy chủ nhật ký hệ thống đều được cấu hình theo mặc định để sử dụng RFC 3164 cũ hơn, ra mắt từ năm 2001. Khi logging được thêm vào Python vào năm 2003, nó đã hỗ trợ giao thức trước đó (và duy nhất hiện có) vào thời điểm đó. Kể từ khi RFC 5424 xuất hiện, do nó chưa được triển khai rộng rãi trên các máy chủ nhật ký hệ thống nên chức năng SysLogHandler vẫn chưa được cập nhật.
RFC 5424 chứa một số tính năng hữu ích như hỗ trợ dữ liệu có cấu trúc và nếu bạn cần đăng nhập vào máy chủ nhật ký hệ thống có hỗ trợ cho nó, bạn có thể làm như vậy với trình xử lý phân lớp trông giống như thế này:
nhập ngày giờ dưới dạng dt
nhập log.handlers
nhập lại
ổ cắm nhập khẩu
thời gian nhập khẩu
lớp SysLogHandler5424(logging.handlers.SysLogHandler):
tz_offset = re.compile(r'([+-]\d{2})(\d{2})$')
đã thoát = re.compile(r'([\]"\\])')
def __init__(self, *args, **kwargs):
self.msgid = kwargs.pop('msgid', None)
self.appname = kwargs.pop('appname', None)
super().__init__(*args, **kwargs)
định dạng def (tự, bản ghi):
phiên bản = 1
asctime = dt.datetime.fromtimestamp(record.created).isoformat()
m = self.tz_offset.match(time.strftime('%z'))
has_offset = Sai
nếu m và time.timezone:
giờ, phút = m.groups()
nếu int(giờ) hoặc int(phút):
has_offset = Đúng
nếu không có has_offset:
thời gian tăng dần += 'Z'
khác:
thời gian tăng dần += f'{hrs}:{mins}'
thử:
tên máy chủ = socket.gethostname()
ngoại trừ Ngoại lệ:
tên máy chủ = '-'
tên ứng dụng = self.appname hoặc '-'
procid = record.process
msgstr = '-'
msg = super().format(bản ghi)
sdata = '-'
nếu hasattr(bản ghi, 'dữ liệu có cấu trúc'):
sd = record.structured_data
# This phải là một lệnh trong đó các khóa là SD-ID và giá trị là a
# dict ánh xạ PARAM-NAME tới PARAM-VALUE (tham khảo RFC để biết những điều này
# mean)
# There không có lỗi khi kiểm tra ở đây - nó hoàn toàn mang tính minh họa và bạn
# can điều chỉnh mã này để sử dụng trong môi trường sản xuất
bộ phận = []
người thay thế def(m):
g = m.groups()
trả về '\\' + g[0]
đối với sdid, dv trong sd.items():
phần = f'[{sdid}'
cho k, v trong dv.items():
s = str(v)
s = self.escaped.sub(người thay thế, s)
phần += f' {k}="{s}"'
phần += ']'
parts.append(part)
sdata = ''.join(phần)
return f'{version} {asctime} {hostname} {appname} {procid} {msgid} {sdata} {msg}'
Bạn cần phải làm quen với RFC 5424 để hiểu đầy đủ đoạn mã trên và có thể bạn có các nhu cầu hơi khác một chút (ví dụ: về cách bạn chuyển dữ liệu cấu trúc vào nhật ký). Tuy nhiên, những điều trên phải phù hợp với nhu cầu cụ thể của bạn. Với trình xử lý ở trên, bạn sẽ truyền dữ liệu có cấu trúc bằng cách sử dụng cái gì đó như thế này
sd = {
'foo@12345': {'bar': 'baz', 'baz': 'bozz', 'fizz': r'buzz'},
'foo@54321': {'rab': 'baz', 'zab': 'bozz', 'zzif': r'buzz'}
}
thêm = {'dữ liệu có cấu trúc': sd}
tôi = 1
logger.debug('Thông báo %d', i, extra=extra)
Cách xử lý trình ghi nhật ký như luồng đầu ra¶
Đôi khi, bạn cần giao tiếp với API của bên thứ ba để ghi vào một đối tượng giống như tệp nhưng bạn muốn chuyển hướng đầu ra của API tới một trình ghi nhật ký. Bạn có thể thực hiện việc này bằng cách sử dụng một lớp bao bọc trình ghi nhật ký bằng API giống như tệp. Đây là một đoạn script ngắn minh họa một lớp như vậy:
nhập nhật ký
lớp LoggerWriter:
def __init__(bản thân, người ghi nhật ký, cấp độ):
self.logger = người ghi nhật ký
self.level = cấp độ
def write(tự, tin nhắn):
if message != '\n': # avoid in dòng mới, nếu bạn thích
self.logger.log(self.level, tin nhắn)
def tuôn ra (tự):
# doesn thực sự không làm bất cứ điều gì, nhưng có thể được mong đợi giống như một tập tin
# object - tùy chọn tùy thuộc vào tình huống của bạn
vượt qua
def đóng (tự):
# doesn thực sự không làm bất cứ điều gì, nhưng có thể được mong đợi giống như một tập tin
# object - tùy chọn tùy thuộc vào tình huống của bạn. Bạn có thể muốn
# to đặt cờ để các cuộc gọi viết sau này sẽ đưa ra một ngoại lệ
vượt qua
chắc chắn chính():
logging.basicConfig(level=logging.DEBUG)
logger = log.getLogger('demo')
info_fp = LoggerWriter(logger,logging.INFO)
debug_fp = LoggerWriter(logger,logging.DEBUG)
print('Một tin nhắn INFO', file=info_fp)
print('Tin nhắn DEBUG', file=debug_fp)
nếu __name__ == "__main__":
chính()
Khi tập lệnh này được chạy, nó sẽ in
INFO:demo:Một tin nhắn INFO
DEBUG:demo:Một tin nhắn DEBUG
Bạn cũng có thể sử dụng LoggerWriter để chuyển hướng sys.stdout và sys.stderr bằng cách thực hiện một số việc như thế này:
hệ thống nhập khẩu
sys.stdout = LoggerWriter(logger,logging.INFO)
sys.stderr = LoggerWriter(logger,logging.WARNING)
Bạn nên thực hiện việc ghi nhật ký định cấu hình after này cho nhu cầu của mình. Trong ví dụ trên, lệnh gọi basicConfig() thực hiện điều này (sử dụng giá trị sys.stderr before, nó bị ghi đè bởi một phiên bản LoggerWriter). Sau đó, bạn sẽ nhận được loại kết quả này:
>>> in('Foo')
INFO:demo:Foo
>>> print('Bar', file=sys.stderr)
WARNING:bản demo:Thanh
>>>
Tất nhiên, các ví dụ ở trên hiển thị đầu ra theo định dạng được basicConfig() sử dụng, nhưng bạn có thể sử dụng một bộ định dạng khác khi định cấu hình ghi nhật ký.
Lưu ý rằng với sơ đồ trên, bạn phần nào phải phụ thuộc vào việc lưu vào bộ nhớ đệm và trình tự các cuộc gọi ghi mà bạn đang chặn. Ví dụ: với định nghĩa LoggerWriter ở trên, nếu bạn có đoạn mã
sys.stderr = LoggerWriter(logger,logging.WARNING)
1/0
sau đó chạy tập lệnh sẽ dẫn đến
WARNING:demo:Traceback (cuộc gọi gần đây nhất):
WARNING:demo: Tệp "/home/runner/cookbook-loggerwriter/test.py", dòng 53, trong <module>
WARNING:demo:
WARNING:demo:main()
WARNING:demo: Tệp "/home/runner/cookbook-loggerwriter/test.py", dòng 49, trong main
WARNING:demo:
WARNING:bản demo:1 / 0
WARNING:demo:ZeroDivisionError
WARNING:demo::
WARNING:demo:chia cho 0
Như bạn có thể thấy, kết quả đầu ra này không lý tưởng. Đó là vì mã cơ bản ghi vào sys.stderr thực hiện nhiều lần ghi, mỗi lần ghi dẫn đến một dòng được ghi riêng (ví dụ: ba dòng cuối cùng ở trên). Để giải quyết vấn đề này, bạn cần đệm mọi thứ và chỉ xuất các dòng nhật ký khi nhìn thấy dòng mới. Hãy sử dụng cách triển khai LoggerWriter tốt hơn một chút:
lớp BufferingLoggerWriter(LoggerWriter):
def __init__(bản thân, người ghi nhật ký, cấp độ):
super().__init__(logger, cấp độ)
self.buffer = ''
def write(tự, tin nhắn):
nếu '\n' không có trong tin nhắn:
self.buffer += tin nhắn
khác:
phần = message.split('\n')
nếu self.buffer:
s = self.buffer + parts.pop(0)
self.logger.log(self.level, s)
self.buffer = parts.pop()
cho một phần trong các bộ phận:
self.logger.log(self.level, một phần)
Điều này chỉ đệm nội dung cho đến khi nhìn thấy dòng mới và sau đó ghi lại các dòng hoàn chỉnh. Với phương pháp này, bạn sẽ có được kết quả tốt hơn:
WARNING:demo:Traceback (cuộc gọi gần đây nhất):
WARNING:demo: Tệp "/home/runner/cookbook-loggerwriter/main.py", dòng 55, trong <module>
WARNING: demo: main()
WARNING:demo: Tệp "/home/runner/cookbook-loggerwriter/main.py", dòng 52, trong main
WARNING:bản demo: 1/0
WARNING:demo:ZeroDivisionError: chia cho 0
Cách xử lý thống nhất các dòng mới trong đầu ra ghi nhật ký¶
Thông thường, các tin nhắn được ghi lại (chẳng hạn như bảng điều khiển hoặc tệp) chỉ bao gồm một dòng văn bản. Tuy nhiên, đôi khi cần phải xử lý các tin nhắn có nhiều dòng - cho dù chuỗi định dạng ghi nhật ký chứa dòng mới hay dữ liệu nhật ký chứa dòng mới. Nếu bạn muốn xử lý các tin nhắn như vậy một cách thống nhất, sao cho mỗi dòng trong tin nhắn được ghi lại có định dạng thống nhất như thể nó được ghi riêng biệt, bạn có thể thực hiện việc này bằng cách sử dụng một trình xử lý mixin, như trong đoạn mã sau:
# Assume đây là mô-đun mymixins.py
nhập bản sao
lớp MultilineMixin:
def phát ra (tự, ghi):
s = record.getMessage()
nếu '\n' không có trong s:
super().emit(bản ghi)
khác:
dòng = s.splitlines()
rec = copy.copy(bản ghi)
rec.args = Không
cho dòng trong dòng:
rec.msg = dòng
super().emit(rec)
Bạn có thể sử dụng mixin như trong đoạn script sau:
nhập nhật ký
từ mymixins nhập MultilineMixin
logger = log.getLogger(__name__)
lớp StreamHandler(MultilineMixin, log.StreamHandler):
vượt qua
nếu __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)-9s %(message)s',
trình xử lý = [StreamHandler()])
logger.debug('Dòng đơn')
logger.debug('Nhiều dòng:\nđánh lừa tôi một lần ...')
logger.debug('Một dòng đơn khác')
logger.debug('Nhiều dòng:\n%s', 'đánh lừa tôi ...\nkhông thể bị lừa lần nữa')
Tập lệnh khi chạy sẽ in nội dung như sau:
2025-07-02 13:54:47,234 DEBUG Dòng đơn
2025-07-02 13:54:47,234 DEBUG Nhiều dòng:
2025-07-02 13:54:47,234 DEBUG lừa tôi một lần ...
2025-07-02 13:54:47,234 DEBUG Một dòng đơn khác
2025-07-02 13:54:47,234 DEBUG Nhiều dòng:
2025-07-02 13:54:47,234 DEBUG lừa tôi ...
2025-07-02 13:54:47,234 DEBUG không thể bị lừa lần nữa
Mặt khác, nếu bạn lo ngại về log injection, bạn có thể sử dụng trình định dạng thoát khỏi dòng mới, theo ví dụ sau:
nhập nhật ký
logger = log.getLogger(__name__)
lớp EscapingFormatter(logging.Formatter):
định dạng def (tự, bản ghi):
s = super().format(bản ghi)
return s.replace('\n', r'\n')
nếu __name__ == '__main__':
h = ghi nhật ký.StreamHandler()
h.setFormatter(EscapingFormatter('%(asctime)s %(levelname)-9s %(message)s'))
logging.basicConfig(level=logging.DEBUG, handlers = [h])
logger.debug('Dòng đơn')
logger.debug('Nhiều dòng:\nđánh lừa tôi một lần ...')
logger.debug('Một dòng đơn khác')
logger.debug('Nhiều dòng:\n%s', 'đánh lừa tôi ...\nkhông thể bị lừa lần nữa')
Tất nhiên, bạn có thể sử dụng bất kỳ sơ đồ thoát nào có ý nghĩa nhất đối với bạn. Tập lệnh khi chạy sẽ tạo ra kết quả như thế này:
2025-07-09 06:47:33,783 DEBUG Dòng đơn
2025-07-09 06:47:33,783 DEBUG Nhiều dòng:\nlừa tôi một lần ...
2025-07-09 06:47:33,783 DEBUG Một dòng đơn khác
2025-07-09 06:47:33,783 DEBUG Nhiều dòng:\nlừa tôi ...\nkhông thể bị lừa lần nữa
Hành vi thoát không thể là hành vi mặc định của stdlib vì nó sẽ phá vỡ khả năng tương thích ngược.
Những kiểu mẫu cần tránh¶
Mặc dù các phần trước đã mô tả các cách thực hiện những việc bạn có thể cần làm hoặc giải quyết, nhưng điều đáng nói là một số kiểu sử dụng unhelpful và do đó nên tránh trong hầu hết các trường hợp. Các phần sau đây không theo thứ tự cụ thể.
Mở cùng một tệp nhật ký nhiều lần¶
Trên Windows, thông thường bạn sẽ không thể mở cùng một tệp nhiều lần vì điều này sẽ dẫn đến lỗi "tệp đang được sử dụng bởi quy trình khác". Tuy nhiên, trên nền tảng POSIX bạn sẽ không gặp bất kỳ lỗi nào nếu mở cùng một tệp nhiều lần. Điều này có thể được thực hiện một cách vô tình, ví dụ bằng cách:
Thêm một trình xử lý tệp nhiều lần tham chiếu đến cùng một tệp (ví dụ: do lỗi sao chép/dán/quên thay đổi).
Mở hai tệp trông khác nhau vì chúng có tên khác nhau nhưng giống nhau vì một tệp là liên kết tượng trưng với tệp kia.
Phân nhánh một quy trình, theo đó cả cha và con đều có tham chiếu đến cùng một tệp. Ví dụ: điều này có thể thông qua việc sử dụng mô-đun
multiprocessing.
Việc mở một tệp nhiều lần có thể khiến appear hoạt động hầu hết thời gian nhưng có thể dẫn đến một số vấn đề trong thực tế:
Đầu ra ghi nhật ký có thể bị cắt xén do nhiều luồng hoặc tiến trình cố gắng ghi vào cùng một tệp. Mặc dù việc ghi nhật ký bảo vệ chống lại việc sử dụng đồng thời cùng một phiên bản trình xử lý bởi nhiều luồng, nhưng không có biện pháp bảo vệ nào như vậy nếu hai luồng khác nhau cố gắng ghi đồng thời bằng cách sử dụng hai phiên bản trình xử lý khác nhau trỏ đến cùng một tệp.
Nỗ lực xóa một tệp (ví dụ: trong khi xoay tệp) âm thầm không thành công vì có một tham chiếu khác trỏ đến tệp đó. Điều này có thể dẫn đến sự nhầm lẫn và lãng phí thời gian gỡ lỗi - các mục nhật ký xuất hiện ở những nơi không mong muốn hoặc bị mất hoàn toàn. Hoặc một tệp được cho là đã được di chuyển vẫn giữ nguyên vị trí và tăng kích thước một cách bất ngờ mặc dù chức năng xoay dựa trên kích thước được cho là đã được áp dụng.
Sử dụng các kỹ thuật được nêu trong Đăng nhập vào một tệp từ nhiều quy trình để tránh những vấn đề như vậy.
Sử dụng trình ghi nhật ký làm thuộc tính trong một lớp hoặc chuyển chúng dưới dạng tham số¶
Mặc dù có thể có những trường hợp bất thường mà bạn cần phải làm điều này, nhưng nhìn chung không có ích gì vì người ghi nhật ký là người độc thân. Mã luôn có thể truy cập một phiên bản trình ghi nhật ký cụ thể theo tên bằng cách sử dụng logging.getLogger(name), do đó, việc chuyển các phiên bản xung quanh và giữ chúng làm thuộc tính phiên bản là vô nghĩa. Lưu ý rằng trong các ngôn ngữ khác như Java và C#, trình ghi nhật ký thường là thuộc tính lớp tĩnh. Tuy nhiên, mẫu này không có ý nghĩa trong Python, trong đó mô-đun (chứ không phải lớp) là đơn vị phân rã phần mềm.
Thêm các trình xử lý khác ngoài NullHandler vào trình ghi nhật ký trong thư viện¶
Định cấu hình ghi nhật ký bằng cách thêm trình xử lý, trình định dạng và bộ lọc là trách nhiệm của nhà phát triển ứng dụng chứ không phải của nhà phát triển thư viện. Nếu bạn đang duy trì một thư viện, hãy đảm bảo rằng bạn không thêm trình xử lý vào bất kỳ trình ghi nhật ký nào ngoài phiên bản NullHandler.
Tạo nhiều logger¶
Trình ghi nhật ký là các trình đơn không bao giờ được giải phóng trong quá trình thực thi tập lệnh và do đó, việc tạo nhiều trình ghi nhật ký sẽ sử dụng hết bộ nhớ mà sau đó không thể giải phóng được. Thay vì tạo một trình ghi nhật ký, ví dụ: được xử lý hoặc kết nối mạng được thực hiện, hãy sử dụng existing mechanisms để chuyển thông tin theo ngữ cảnh vào nhật ký của bạn và hạn chế trình ghi nhật ký được tạo cho các khu vực mô tả trong ứng dụng của bạn (nói chung là các mô-đun, nhưng đôi khi chi tiết hơn một chút).
Các tài nguyên khác¶
Xem thêm
- Mô-đun
logging tham chiếu API cho mô-đun ghi nhật ký.
- Mô-đun
logging.config Cấu hình API cho mô-đun ghi nhật ký.
- Mô-đun
logging.handlers Trình xử lý hữu ích đi kèm với mô-đun ghi nhật ký.