The Story That Explains Why Logging Exists ✈️Logging क्यों है — समझाने वाली कहानी ✈️一个讲清日志为何存在的故事 ✈️
When something goes wrong at 30,000 feet, Pilot A has nothing. Pilot B replays the last minutes and sees exactly which valve failed, and when.
print() is the sticky note. Python's logging module is the black box: every message has a level, a time and a source, and you decide — in one place — which messages are kept and where they go.जब 30,000 feet पर कुछ गड़बड़ होती है, तो Pilot A के पास कुछ नहीं होता। Pilot B आख़िरी मिनट दोबारा चलाता है और ठीक-ठीक देखता है कि कौन-सा valve कब ख़राब हुआ।
print() वह पर्ची है। Python का logging module black box है: हर message का एक level, एक समय और एक स्रोत होता है, और आप — एक ही जगह — तय करते हैं कि कौन-से messages रखे जाएँ और वे कहाँ जाएँ।当 30,000 英尺高空出问题时,飞行员 A 什么也没有。飞行员 B 回放最后几分钟,准确看到哪个阀门在何时失效。
print() 就是便利贴。Python 的 logging 模块就是黑匣子:每条消息都有 级别、时间 和 来源,你可以在一个地方决定保留哪些消息、送到哪里。Logging is not "printing with extra steps". It is a separate channel: your code only says what happened and how serious it is. Configuration — written once, at start-up — decides what is kept and where it goes: the screen, a file, a log server. Change the configuration, and not one line of your business code changes.
Logging "थोड़ी ज़्यादा मेहनत वाला print" नहीं है। यह एक अलग channel है: आपका code सिर्फ़ बताता है कि क्या हुआ और वह कितना गंभीर है। Configuration — start-up पर एक बार लिखी गई — तय करती है कि क्या रखा जाए और कहाँ जाए: screen, file, log server। Configuration बदलें, और आपके business code की एक भी line नहीं बदलती।
日志不是"多了几步的 print"。它是一条独立的通道:你的代码只说明 发生了什么、有多严重。配置 — 在启动时写一次 — 决定保留什么、送到哪里:屏幕、文件、日志服务器。修改配置,业务代码一行都不用动。
Look for the interactive boxes: the Volume Knob (Section 03), the Log Router (06) — checked against the real logging module on 3,840 cases — the Formatter Builder (07) and Rotating Files (08).
Interactive डिब्बे खोजें: Volume Knob (Section 03), Log Router (06) — असली logging module से 3,840 cases पर जाँचा गया — Formatter Builder (07) और Rotating Files (08)।
请留意这些互动框:音量旋钮(第 03 节)、日志路由器(06)— 在 3,840 个用例上与真实 logging 模块核对过 — 格式化器构建器(07)和 轮转文件(08)。
Why print() Fails in Real Apps 🧻असली Apps में print() क्यों नाकाम है 🧻为什么 print() 在真实应用中不够用 🧻
| Problemसमस्या问题 | Resultनतीजा后果 |
|---|---|
| no levelकोई level नहीं没有级别 | debug noise mixes with real errorsdebug शोर असली errors में मिल जाता है调试噪音与真正的错误混在一起 |
| no time, no sourceन समय, न स्रोत没有时间、没有来源 | you cannot tell when or whereपता नहीं चलता कब और कहाँ无法知道何时、何处 |
| one destinationएक ही जगह只有一个去处 | cannot send errors to a fileerrors को file में नहीं भेज सकते无法把错误单独写入文件 |
| no off switchबंद करने का switch नहीं没有开关 | delete them all before releaserelease से पहले सब हटाने पड़ते हैं发布前只能全部删掉 |
| Featureसुविधा特性 | Benefitफ़ायदा好处 |
|---|---|
| 5 levels5 levels5 个级别 | turn noise down with one lineएक line से शोर कम करें一行配置降低噪音 |
| automatic time, file, lineअपने आप समय, file, line自动记录时间、文件、行号 | every line tells its storyहर line अपनी कहानी बताती है每行都自带来龙去脉 |
| many handlersकई handlers多个处理器 | screen + file + server at oncescreen + file + server एक साथ屏幕 + 文件 + 服务器同时输出 |
| thread-safethread-safe线程安全 | lines from threads do not mixthreads की lines आपस में नहीं मिलतीं多线程的日志行不会交错 |
import logging
print("print: User 42 logged in") # no level, no time, no source
logging.info("This INFO line is hidden") # default level is WARNING
logging.warning("Disk usage at 82%") # shown: WARNING and above
logging.error("Failed to send email")
Notice two things in the real output. The INFO line is missing — the default level is WARNING. And log lines go to stderr, not stdout, so you can send your program's normal output and its logs to different places.
असली output में दो बातें देखें। INFO line ग़ायब है — default level WARNING है। और log lines stdout पर नहीं, stderr पर जाती हैं, इसलिए आप program का सामान्य output और logs अलग-अलग जगह भेज सकते हैं।
注意真实输出中的两点。INFO 那一行不见了 — 默认级别是 WARNING。而且日志行输出到 stderr 而不是 stdout,所以你可以把程序的正常输出和日志送到不同的地方。
The Five Log Levels 🎚️पाँच Log Levels 🎚️五个日志级别 🎚️
Every message carries a level, and every level is a number. A logger drops any message whose number is below its threshold.
हर message का एक level होता है, और हर level एक नंबर है। Logger हर उस message को गिरा देता है जिसका नंबर उसकी सीमा से नीचे है।
每条消息都带有一个级别,每个级别都是一个数字。记录器会丢弃数字 低于 阈值的所有消息。
With level=WARNING (the default), DEBUG and INFO are dropped.
level=WARNING (default) के साथ DEBUG और INFO गिरा दिए जाते हैं।
在 level=WARNING(默认值)下,DEBUG 和 INFO 会被丢弃。
import logging
logging.basicConfig(level=logging.DEBUG) # show everything from DEBUG up
logging.debug("Loop iteration i=5, value=42")
logging.info("Server started on port 8000")
logging.warning("Disk usage at 82%")
logging.error("Failed to send email to user@example.com")
logging.critical("Database connection pool exhausted!")
print(logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)
Try It — The Volume Knobकरके देखें — Volume Knob动手试试 — 音量旋钮
Here is a real log from a small shop app, captured at DEBUG. Turn the knob and watch which lines survive at each level.
यह एक छोटे shop app का असली log है, DEBUG पर रिकॉर्ड किया गया। Knob घुमाएँ और देखें हर level पर कौन-सी lines बचती हैं।
这是一个小商店应用的真实日志,在 DEBUG 级别下采集。转动旋钮,看看每个级别下哪些行会留下来。
Your PC → DEBUG. Test server → INFO. Production → INFO or WARNING. Read the level from configuration (an environment variable or a config file), so changing it never needs a code change.
आपका PC → DEBUG। Test server → INFO। Production → INFO या WARNING। Level configuration (environment variable या config file) से पढ़ें, ताकि उसे बदलने के लिए कभी code न बदलना पड़े।
你的电脑 → DEBUG。测试服务器 → INFO。生产环境 → INFO 或 WARNING。从配置(环境变量或配置文件)读取级别,这样修改它永远不用改代码。
The Anatomy of the Logging System 🔧Logging System की बनावट 🔧日志系统的构造 🔧
log.info(...). Loggers form a tree by dotted name: myapp.db is a child of myapp.
जिसे आप बुलाते हैं। log.info(...)। Loggers बिंदु वाले नाम से tree बनाते हैं: myapp.db, myapp का child है।
你调用的对象。log.info(...)。记录器按点号名称组成树:myapp.db 是 myapp 的子节点。
The logger checks its level once. After that, only handler levels and filters decide, at every logger on the way up.
Logger अपना level एक बार जाँचता है। उसके बाद ऊपर जाते हुए हर logger पर सिर्फ़ handler levels और filters फ़ैसला करते हैं।
记录器只检查一次自己的级别。之后,在向上经过的每个记录器上,只由 处理器 的级别和过滤器决定。
Named Loggers — Use getLogger(__name__) 🌳नाम वाले Loggers — getLogger(__name__) इस्तेमाल करें 🌳命名记录器 — 使用 getLogger(__name__) 🌳
Calling logging.info(...) uses the root logger. In real code, every module creates its own logger with logging.getLogger(__name__). __name__ is the module's dotted path, so the loggers form a tree that matches your packages — and you can turn each branch up or down separately.
logging.info(...) बुलाने से root logger इस्तेमाल होता है। असली code में हर module logging.getLogger(__name__) से अपना logger बनाता है। __name__ module का बिंदु वाला path है, इसलिए loggers आपके packages जैसा ही tree बनाते हैं — और आप हर शाखा को अलग से ऊपर-नीचे कर सकते हैं।
调用 logging.info(...) 使用的是 根 记录器。真实代码中,每个模块都用 logging.getLogger(__name__) 创建自己的记录器。__name__ 是模块的点号路径,所以记录器会组成一棵与包结构一致的树 — 你可以分别调高或调低每个分支。
# myapp/db.py
import logging
log = logging.getLogger(__name__) # becomes "myapp.db"
def query(sql):
log.debug("Executing SQL: %s", sql)
log.info("Query finished in %d ms", 42)
# myapp/api.py
import logging
from myapp import db
log = logging.getLogger(__name__) # becomes "myapp.api"
def get_user(user_id):
log.info("GET /users/%s", user_id)
db.query(f"SELECT * FROM users WHERE id = {user_id}")
import logging
from myapp import api
logging.basicConfig(level=logging.DEBUG, format="%(levelname)-7s %(name)-10s %(message)s")
api.get_user(42)
# Turn ONE module down - no code change inside myapp
logging.getLogger("myapp.db").setLevel(logging.WARNING)
print("--- myapp.db set to WARNING ---")
api.get_user(7)
The Logger Tree and Effective LevelsLogger Tree और Effective Levels记录器树与有效级别
import logging
pool = logging.getLogger("myapp.db.pool")
print(pool.parent.name) # "myapp" and "myapp.db" do not exist yet!
app = logging.getLogger("myapp")
db = logging.getLogger("myapp.db")
print(pool.parent.name, db.parent.name, app.parent.name) # now the tree is complete
app.setLevel(logging.ERROR)
print(pool.level, logging.getLevelName(pool.getEffectiveLevel())) # 0 = NOTSET -> inherits
db.setLevel(logging.DEBUG)
print(logging.getLevelName(pool.getEffectiveLevel())) # the nearest set level wins
print(app.getChildren()) # Python 3.12+
| LoggerLogger记录器 | ParentParent父级 | If its level is NOTSET…अगर इसका level NOTSET है…若其级别为 NOTSET… |
|---|---|---|
root | — | starts at WARNINGWARNING से शुरू初始为 WARNING |
myapp | root | uses root's levelroot का level लेता है使用 root 的级别 |
myapp.db | myapp | uses myapp's level, or root'smyapp का level लेता है, या root का使用 myapp 的级别,否则 root 的 |
myapp.db.pool | myapp.db | walks up until a level is setlevel मिलने तक ऊपर चलता है一直向上找到设置了级别的祖先 |
The first line of the example prints root: when myapp.db.pool was created, myapp and myapp.db did not exist yet. As soon as they are created, Python fixes the tree. You never need to create parents first.
Example की पहली line root छापती है: जब myapp.db.pool बना, तब myapp और myapp.db अभी मौजूद नहीं थे। जैसे ही वे बनते हैं, Python tree ठीक कर देता है। Parents पहले बनाने की कभी ज़रूरत नहीं।
示例第一行打印 root:创建 myapp.db.pool 时,myapp 和 myapp.db 还不存在。它们一旦被创建,Python 就会修正这棵树。你从不需要先创建父记录器。
Try It — The Log Router 🚦करके देखें — Log Router 🚦动手试试 — 日志路由器 🚦
This is the heart of logging. Set the levels, switch propagate on or off, then send a record from any logger and watch where it goes. The router follows the same rules as Logger.callHandlers(), and it was checked against the real logging module on all 3,840 combinations.
यही logging का दिल है। Levels set करें, propagate चालू या बंद करें, फिर किसी भी logger से record भेजें और देखें वह कहाँ जाता है। Router वही नियम मानता है जो Logger.callHandlers(), और इसे असली logging module से सभी 3,840 combinations पर जाँचा गया है।
这是日志的核心。设置级别,打开或关闭 propagate,然后从任一记录器发送一条记录,看它去了哪里。路由器遵循与 Logger.callHandlers() 相同的规则,并在全部 3,840 种组合上与真实 logging 模块核对过。
① Send myapp.db at DEBUG — dropped at once, because the effective level comes from root (WARNING). ② Set myapp.db to DEBUG and myapp to ERROR, then send DEBUG: it still reaches app.log! A parent's logger level is not checked during propagation. ③ Set myapp propagate to False: the console stops showing myapp lines — a common way to avoid duplicates.
① myapp.db से DEBUG भेजें — तुरंत गिरा, क्योंकि effective level root (WARNING) से आता है। ② myapp.db को DEBUG और myapp को ERROR करें, फिर DEBUG भेजें: यह फिर भी app.log तक पहुँचता है! Propagation के दौरान parent का logger level नहीं जाँचा जाता। ③ myapp का propagate False करें: console पर myapp की lines आनी बंद — duplicates से बचने का आम तरीका।
① 从 myapp.db 发送 DEBUG — 立即被丢弃,因为有效级别来自 root(WARNING)。② 把 myapp.db 设为 DEBUG、myapp 设为 ERROR,再发送 DEBUG:它仍然到达了 app.log!传递过程中不会检查父 记录器 的级别。③ 把 myapp 的 propagate 设为 False:控制台不再显示 myapp 的行 — 这是避免重复的常用办法。
Formatters — Making Logs Readable 🖋️Formatters — Logs को पढ़ने लायक बनाना 🖋️格式化器 — 让日志易读 🖋️
| PlaceholderPlaceholder占位符 | Meaningमतलब含义 | Exampleउदाहरण示例 |
|---|---|---|
%(asctime)s | time (default has milliseconds)समय (default में milliseconds)时间(默认带毫秒) | 2026-07-12 14:32:07,213 |
%(name)s | logger namelogger का नाम记录器名称 | shop.checkout |
%(levelname)s | level as textlevel text के रूप में级别文本 | INFO |
%(levelno)d | level as numberlevel नंबर के रूप में级别数字 | 20 |
%(module)s | module namemodule का नाम模块名 | checkout |
%(filename)s | file namefile का नाम文件名 | checkout.py |
%(funcName)s | function namefunction का नाम函数名 | place_order |
%(lineno)d | line numberline नंबर行号 | 42 |
%(threadName)s | thread namethread का नाम线程名 | MainThread |
%(process)d | process IDprocess ID进程 ID | 4242 |
%(message)s | your text with the args filled inआपका text, args भरकर填好参数的消息文本 | Order ORD-8821 placed for Rs 1299.50 |
import logging
FMT = "%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s"
logging.basicConfig(level=logging.INFO, format=FMT, datefmt="%Y-%m-%d %H:%M:%S")
log = logging.getLogger("shop.checkout")
log.info("Order %s placed for Rs %.2f", "ORD-8821", 1299.5)
log.warning("Coupon %s expired but was accepted", "DIWALI25")
Try It — Formatter Builderकरके देखें — Formatter Builder动手试试 — 格式化器构建器
Click placeholders or type your own format. The result is formatted exactly like Python does it, for two real LogRecords. Try a misspelt field, or a format with no field at all.
Placeholders पर click करें या अपना format लिखें। नतीजा दो असली LogRecords के लिए ठीक वैसे ही format होता है जैसे Python करता है। ग़लत spelling वाला field, या बिना field वाला format आज़माएँ।
点击占位符或输入你自己的格式。结果会按照 Python 的方式,为两条真实的 LogRecord 进行格式化。试试拼错的字段,或完全没有字段的格式。
Handlers — Sending Logs Everywhere 📮Handlers — Logs को हर जगह भेजना 📮处理器 — 把日志送到各处 📮
sys.stderr by default. The usual choice for the console, and for containers that collect stdout/stderr.sys.stderr। Console के लिए, और stdout/stderr इकट्ठा करने वाले containers के लिए आम पसंद।sys.stderr。控制台以及收集 stdout/stderr 的容器的常用选择。Console + Rotating FileConsole + Rotating File控制台 + 轮转文件
import logging
from logging.handlers import RotatingFileHandler
log = logging.getLogger("myapp")
log.setLevel(logging.DEBUG) # the logger lets everything in
fmt = logging.Formatter("%(levelname)-8s | %(name)s | %(message)s")
console = logging.StreamHandler() # stderr by default
console.setLevel(logging.WARNING) # the screen: WARNING and up
console.setFormatter(fmt)
file_h = RotatingFileHandler("myapp.log", maxBytes=5_000_000, backupCount=5, encoding="utf-8")
file_h.setLevel(logging.DEBUG) # the file: everything
file_h.setFormatter(fmt)
log.addHandler(console)
log.addHandler(file_h)
log.debug("goes to the FILE only")
log.info("goes to the FILE only")
log.warning("goes to BOTH")
log.error("goes to BOTH")
file_h.close()
print("--- myapp.log ---")
print(open("myapp.log", encoding="utf-8").read(), end="")
Set the logger wide open (DEBUG) and let each handler choose what it keeps: the file takes everything, the screen only WARNING and up. That is the standard pattern.
Logger को पूरा खुला (DEBUG) रखें और हर handler को तय करने दें कि वह क्या रखे: file सब कुछ लेती है, screen सिर्फ़ WARNING और ऊपर। यही मानक तरीका है।
把 记录器 完全放开(DEBUG),让每个 处理器 自己选择保留什么:文件全收,屏幕只显示 WARNING 及以上。这是标准做法。
Try It — Rotating Filesकरके देखें — Rotating Files动手试试 — 轮转文件
A RotatingFileHandler with a tiny limit: 100 bytes and 3 backups. Step through 14 writes and watch the files move down when the limit is reached. Every snapshot was taken from the real files on disk.
बहुत छोटी सीमा वाला RotatingFileHandler: 100 bytes और 3 backups। 14 writes में कदम-दर-कदम चलें और सीमा पर files को नीचे खिसकते देखें। हर snapshot disk की असली files से लिया गया है।
一个上限极小的 RotatingFileHandler:100 字节、3 个备份。逐步执行 14 次写入,看看达到上限时文件如何依次后移。每个快照都取自磁盘上的真实文件。
Logging is thread-safe, but several processes writing (and rotating) the same file can lose or mix lines. With multiprocessing or several web workers, send records through a QueueHandler to one listener, give each process its own file, or log to stderr and let the platform collect it.
Logging thread-safe है, पर एक ही file में लिखने (और rotate करने) वाले कई processes lines खो या मिला सकते हैं। multiprocessing या कई web workers के साथ records को QueueHandler से एक listener तक भेजें, हर process को अपनी file दें, या stderr पर log करें और platform को इकट्ठा करने दें।
日志是线程安全的,但多个 进程 写入(并轮转)同一个文件,可能丢行或串行。使用 multiprocessing 或多个 Web worker 时,请通过 QueueHandler 把记录发给同一个监听器,或给每个进程单独的文件,或输出到 stderr 由平台收集。
Logging Exceptions — Keep the Traceback 🧯Exceptions log करना — Traceback रखें 🧯记录异常 — 保留回溯 🧯
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s")
log = logging.getLogger("calc")
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
log.error("Bad: %s", e) # message only - no traceback
log.exception("Failed to divide %s by %s", a, b) # ERROR + full traceback
return None
divide(10, 0)
1. log.exception("msg") — ERROR level + traceback; use it inside except.
2. log.warning("msg", exc_info=True) — any level + traceback.
3. log.error("msg", exc_info=err) — attach a specific exception object, even outside except.
1. log.exception("msg") — ERROR level + traceback; except के अंदर इस्तेमाल करें।
2. log.warning("msg", exc_info=True) — कोई भी level + traceback।
3. log.error("msg", exc_info=err) — कोई ख़ास exception object जोड़ें, except के बाहर भी।
1. log.exception("msg") — ERROR 级别 + 回溯;在 except 中使用。
2. log.warning("msg", exc_info=True) — 任意级别 + 回溯。
3. log.error("msg", exc_info=err) — 附加指定的异常对象,即使在 except 之外。
dictConfig — The Production Pattern 🏭dictConfig — Production का तरीका 🏭dictConfig — 生产环境的做法 🏭
In a real app you describe the whole setup — formatters, handlers, loggers — in one dictionary, and apply it once at start-up. It can be loaded from a JSON or YAML file, and changed per environment.
असली app में आप पूरा setup — formatters, handlers, loggers — एक dictionary में लिखते हैं, और start-up पर एक बार लागू करते हैं। इसे JSON या YAML file से load किया जा सकता है, और हर environment के लिए बदला जा सकता है।
在真实应用中,你把整套配置 — 格式化器、处理器、记录器 — 写进一个字典,启动时应用一次。它可以从 JSON 或 YAML 文件加载,并按环境区分。
import logging, logging.config
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {"format": "%(levelname)-8s | %(name)s | %(message)s"},
"detailed": {"format": "%(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s"},
},
"handlers": {
"console": {"class": "logging.StreamHandler", "level": "INFO", "formatter": "standard"},
"file": {"class": "logging.handlers.RotatingFileHandler", "level": "DEBUG", "formatter": "detailed",
"filename": "app.log", "maxBytes": 5_000_000, "backupCount": 5, "encoding": "utf-8"},
"errors": {"class": "logging.handlers.RotatingFileHandler", "level": "ERROR", "formatter": "detailed",
"filename": "errors.log", "maxBytes": 5_000_000, "backupCount": 10, "encoding": "utf-8"},
},
"loggers": {
"myapp": {"handlers": ["console", "file", "errors"], "level": "DEBUG", "propagate": False},
"urllib3": {"level": "WARNING"}, # silence a chatty library
},
"root": {"handlers": ["console"], "level": "WARNING"},
}
logging.config.dictConfig(LOGGING)
log = logging.getLogger("myapp.orders")
log.debug("Validating order #100")
log.info("Order #100 processed")
log.error("Payment gateway timeout for order #101")
logging.getLogger("urllib3").info("Starting new HTTPS connection") # dropped
print(sorted(logging.getHandlerNames())) # Python 3.12+
for name in ["app.log", "errors.log"]:
print(f"--- {name} ---")
print(open(name, encoding="utf-8").read(), end="")
All of the setup is in one place and reviewed as one unit. Keep "disable_existing_loggers": False, or loggers created before the call (for example by imported libraries) are switched off. Since Python 3.12, logging.getHandlerByName("file") gives you a configured handler back by its name.
पूरा setup एक जगह है और एक इकाई के रूप में review होता है। "disable_existing_loggers": False रखें, वरना call से पहले बने loggers (जैसे imported libraries के) बंद हो जाते हैं। Python 3.12 से logging.getHandlerByName("file") configured handler को उसके नाम से वापस देता है।
所有配置集中在一处,作为一个整体审查。请保留 "disable_existing_loggers": False,否则调用之前创建的记录器(例如被导入的库创建的)会被关闭。从 Python 3.12 起,logging.getHandlerByName("file") 可以按名称取回已配置的处理器。
Real-World Example — Logging a Flask App 🌐असली उदाहरण — Flask App की Logging 🌐实战示例 — 为 Flask 应用记录日志 🌐
This example really runs: Flask's test client sends three requests without starting a server. You see the access lines, a warning, and a full traceback for the crash — all from one dictConfig.
यह उदाहरण सच में चलता है: Flask का test client बिना server शुरू किए तीन requests भेजता है। आप access lines, एक warning, और crash का पूरा traceback देखते हैं — सब एक dictConfig से।
这个示例是真实运行的:Flask 的测试客户端在不启动服务器的情况下发送了三个请求。你会看到访问日志、一条警告,以及崩溃的完整回溯 — 全部来自一个 dictConfig。
import logging, logging.config, time
from flask import Flask, request, g
logging.config.dictConfig({
"version": 1,
"formatters": {"default": {"format": "%(levelname)-8s | %(name)s | %(message)s"}},
"handlers": {"console": {"class": "logging.StreamHandler", "formatter": "default"}},
"root": {"handlers": ["console"], "level": "INFO"},
})
app = Flask(__name__)
log = logging.getLogger("shop.api")
@app.before_request
def start_timer():
g.start = time.perf_counter()
@app.after_request
def log_request(response):
ms = (time.perf_counter() - g.start) * 1000
log.info("%s %s -> %d (%s ms)", request.method, request.path, response.status_code,
"<1" if ms < 1 else round(ms))
return response
@app.route("/order/<int(signed=True):order_id>")
def get_order(order_id):
if order_id < 0:
log.warning("Rejecting negative order id: %s", order_id)
return {"error": "bad id"}, 400
if order_id == 13:
raise RuntimeError("database unavailable")
return {"id": order_id, "total": 42.0}
@app.errorhandler(Exception)
def unhandled(err):
log.exception("Unhandled error on %s", request.path)
return {"error": "internal"}, 500
client = app.test_client() # send requests without starting a server
for path in ["/order/42", "/order/-1", "/order/13"]:
client.get(path)
Structured (JSON) Logging 🧾Structured (JSON) Logging 🧾结构化(JSON)日志 🧾
Log tools such as Elasticsearch, Loki or Datadog work best with one JSON object per line: every key becomes a searchable field. Use the python-json-logger package, or write a small formatter yourself.
Elasticsearch, Loki या Datadog जैसे log tools हर line पर एक JSON object के साथ सबसे अच्छा काम करते हैं: हर key खोजने लायक field बन जाती है। python-json-logger package इस्तेमाल करें, या ख़ुद एक छोटा formatter लिखें।
Elasticsearch、Loki、Datadog 等日志工具最适合每行一个 JSON 对象:每个键都成为可搜索的字段。可以用 python-json-logger 包,也可以自己写一个小格式化器。
import logging, json
from pythonjsonlogger.json import JsonFormatter # pip install python-json-logger
log = logging.getLogger("shop")
log.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter("%(levelname)s %(name)s %(message)s"))
log.addHandler(handler)
log.info("Order placed", extra={"order_id": "ORD-8821", "user_id": 42, "amount": 1299.5, "currency": "INR"})
# No package? A tiny JSON formatter with only the standard library:
class SimpleJson(logging.Formatter):
def format(self, record):
data = {"level": record.levelname, "logger": record.name, "msg": record.getMessage()}
data.update(getattr(record, "ctx", {}))
return json.dumps(data, ensure_ascii=False)
handler.setFormatter(SimpleJson())
log.info("Refund issued", extra={"ctx": {"order_id": "ORD-8821", "amount": 1299.5}})
In python-json-logger 3.x and later, import JsonFormatter from pythonjsonlogger.json. The old from pythonjsonlogger import jsonlogger still appears in many tutorials but is deprecated.
python-json-logger 3.x और बाद में JsonFormatter को pythonjsonlogger.json से import करें। पुराना from pythonjsonlogger import jsonlogger अब भी बहुत से tutorials में दिखता है पर deprecated है।
在 python-json-logger 3.x 及以后版本中,请从 pythonjsonlogger.json 导入 JsonFormatter。旧写法 from pythonjsonlogger import jsonlogger 仍出现在许多教程里,但已被弃用。
Performance — Lazy Formatting ⚡Performance — आलसी Formatting ⚡性能 — 延迟格式化 ⚡
import logging
logging.basicConfig(level=logging.INFO) # DEBUG is OFF
log = logging.getLogger("perf")
class User:
def __str__(self):
print(" (building the user text...)")
return "Priya"
def expensive_call():
print(" (expensive_call ran!)")
return [1, 2, 3]
user = User()
print("1. f-string:")
log.debug(f"User {user}") # text is built anyway, then thrown away
print("2. %-style:")
log.debug("User %s", user) # str() never runs
print("3. %-style with a function call:")
log.debug("Result %s", expensive_call()) # the ARGUMENT still runs!
print("4. isEnabledFor guard:")
if log.isEnabledFor(logging.DEBUG):
log.debug("Result %s", expensive_call()) # nothing runs
print("done")
| PatternPattern写法 |
|---|
log.debug(f"User {user}") |
log.debug("User " + str(user)) |
| The text is built even when DEBUG is off.DEBUG बंद हो तब भी text बनता है।即使 DEBUG 关闭,文本也会被构建。 |
| PatternPattern写法 |
|---|
log.debug("User %s", user) |
if log.isEnabledFor(logging.DEBUG): ... |
| Formatting happens only if the record is emitted.Formatting तभी होती है जब record निकलता है।只有记录真正输出时才格式化。 |
Example 3 above shows it: log.debug("Result %s", expensive_call()) still runs expensive_call(), because Python evaluates arguments before calling any function. Only the string building is skipped. For truly expensive values, wrap the call in if log.isEnabledFor(logging.DEBUG):.
ऊपर का उदाहरण 3 यही दिखाता है: log.debug("Result %s", expensive_call()) फिर भी expensive_call() चलाता है, क्योंकि Python कोई भी function बुलाने से पहले arguments निकालता है। सिर्फ़ string बनाना छूटता है। सचमुच महँगी values के लिए call को if log.isEnabledFor(logging.DEBUG): में लपेटें।
上面的第 3 个例子说明了这一点:log.debug("Result %s", expensive_call()) 仍然会运行 expensive_call(),因为 Python 在调用任何函数之前都会先求值参数。被跳过的只是 字符串拼接。对真正昂贵的值,请把调用包在 if log.isEnabledFor(logging.DEBUG): 里。
Libraries, Noise, Secrets and Context 🧰Libraries, शोर, Secrets और Context 🧰库、噪音、机密与上下文 🧰
Silencing a Chatty Libraryबातूनी Library को शांत करना让啰嗦的库安静下来
import logging
logging.basicConfig(level=logging.DEBUG, format="%(levelname)-7s %(name)s: %(message)s")
noisy = logging.getLogger("urllib3.connectionpool") # pretend this is the library
noisy.debug("Starting new HTTPS connection (1): api.example.com:443")
noisy.debug('"GET /v1/users HTTP/1.1" 200 512')
logging.getLogger("urllib3").setLevel(logging.WARNING) # one line in YOUR app
noisy.debug("Starting new HTTPS connection (2): api.example.com:443")
noisy.warning("Retrying after connection broken")
If You Write a Library: NullHandler, Never basicConfigअगर आप Library लिखते हैं: NullHandler, कभी basicConfig नहीं如果你在写库:用 NullHandler,绝不用 basicConfig
# Inside a LIBRARY you publish (e.g. mylib/__init__.py):
import logging
logging.getLogger("mylib").addHandler(logging.NullHandler()) # never basicConfig()!
def work():
logging.getLogger("mylib").info("working...") # silent unless the APP configures logging
work()
print("an app that never configured logging sees nothing from mylib")
logging.basicConfig(level=logging.INFO, format="%(name)s: %(message)s") # the APP decides
work()
Hiding Passwords with a FilterFilter से Passwords छिपाना用过滤器隐藏密码
import logging, re
class Redact(logging.Filter):
PATTERN = re.compile(r"(password|token)=\S+", re.IGNORECASE)
def filter(self, record):
record.msg = self.PATTERN.sub(r"\1=***", record.getMessage())
record.args = ()
return True # keep the (cleaned) record
handler = logging.StreamHandler()
handler.addFilter(Redact())
handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
log = logging.getLogger("auth")
log.addHandler(handler)
log.setLevel(logging.INFO)
log.info("Login user=%s password=%s", "priya", "hunter2")
log.info("Calling API with token=abc123XYZ")
Since Python 3.12, a filter can return a new LogRecord instead of changing the original. A filter on one handler can then clean the record without affecting the other handlers.
Python 3.12 से filter मूल record बदलने के बजाय एक नया LogRecord लौटा सकता है। तब एक handler का filter दूसरे handlers पर असर डाले बिना record साफ़ कर सकता है।
从 Python 3.12 起,过滤器可以返回一个 新的 LogRecord,而不是修改原记录。这样某个处理器上的过滤器可以清理记录,而不影响其他处理器。
Adding Context to Every Lineहर Line में Context जोड़ना为每一行添加上下文
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s [req=%(request_id)s] %(message)s")
base = logging.getLogger("shop")
log = logging.LoggerAdapter(base, {"request_id": "a1b2c3"}) # adds context to every line
log.info("Cart loaded")
log.warning("Item %s is out of stock", "SKU-77")
Common Pitfalls 🕳️आम ग़लतियाँ 🕳️常见陷阱 🕳️
import logging
# 1. No configuration at all: the "last resort" handler prints WARNING+ to stderr
log = logging.getLogger("app")
log.info("lost - nobody shows INFO")
log.warning("shown by the last-resort handler")
# 2. Adding a handler on every call = duplicate lines
def get_logger():
lg = logging.getLogger("dup")
lg.addHandler(logging.StreamHandler()) # BUG: runs every time
return lg
get_logger(); get_logger().warning("printed twice!")
# 3. basicConfig() does nothing once the root logger has handlers
logging.warning("this call quietly runs basicConfig() first")
logging.basicConfig(level=logging.DEBUG, format=">> %(message)s") # ignored!
logging.debug("not shown")
logging.basicConfig(level=logging.DEBUG, format=">> %(message)s", force=True)
logging.debug("shown after force=True")
| Pitfallग़लती陷阱 | Symptomलक्षण症状 | Fixहल解决办法 |
|---|---|---|
| Handler added on every callहर call पर handler जोड़ना每次调用都添加处理器 | every line appears 2, 3, 4… timesहर line 2, 3, 4… बार每行出现 2、3、4……次 | configure once at start-up; or check if not log.handlersstart-up पर एक बार configure करें; या if not log.handlers जाँचें只在启动时配置一次;或检查 if not log.handlers |
| Parent and child both have handlersParent और child दोनों पर handlers父子记录器都有处理器 | duplicate linesदोहरी lines重复的行 | put handlers on one logger, or set propagate=Falsehandlers एक ही logger पर रखें, या propagate=False只在一个记录器上放处理器,或设置 propagate=False |
| No configurationकोई configuration नहीं没有配置 | INFO/DEBUG vanish; WARNING+ printed bareINFO/DEBUG ग़ायब; WARNING+ सादा छपता हैINFO/DEBUG 消失;WARNING+ 只打印消息本身 | call basicConfig() or dictConfig() at start-upstart-up पर basicConfig() या dictConfig()启动时调用 basicConfig() 或 dictConfig() |
basicConfig() after a log calllog call के बाद basicConfig()在日志调用之后才调用 basicConfig() | your settings are ignoredआपकी settings अनदेखी你的设置被忽略 | configure first, or pass force=True (3.8+)पहले configure करें, या force=True (3.8+)先配置,或传入 force=True(3.8+) |
log.error(str(e))log.error(str(e))log.error(str(e)) | no tracebacktraceback नहीं没有回溯 | log.exception(...) inside exceptexcept में log.exception(...)在 except 中使用 log.exception(...) |
log.warn(...)log.warn(...)log.warn(...) | DeprecationWarningDeprecationWarningDeprecationWarning | use log.warning(...)log.warning(...) इस्तेमाल करें使用 log.warning(...) |
| Plain FileHandler on a serverServer पर सादा FileHandler服务器上使用普通 FileHandler | disk slowly fills updisk धीरे-धीरे भरती है磁盘慢慢被写满 | Rotating or TimedRotating handlerRotating या TimedRotating handler使用 Rotating 或 TimedRotating 处理器 |
logging vs print vs loguru ⚖️logging बनाम print बनाम loguru ⚖️logging、print 与 loguru ⚖️
| Featureसुविधा特性 | print() | logging (stdlib) | loguru (pip) |
|---|---|---|---|
| LevelsLevels级别 | ❌ | ✅ 5 + custom | ✅ 7 + custom |
| Time, file, lineसमय, file, line时间、文件、行号 | ❌ | ✅ | ✅ |
| Many destinationsकई जगहें多个去处 | ❌ | ✅ handlers | ✅ sinks |
| Set-up effortSetup की मेहनत配置成本 | noneकोई नहीं无 | medium (dictConfig)मध्यम (dictConfig)中等(dictConfig) | one lineएक line一行 |
| Ships with PythonPython के साथ आता है随 Python 自带 | ✅ | ✅ | ❌ |
| Used by frameworks and librariesFrameworks और libraries इस्तेमाल करते हैं框架和库使用 | — | ✅ everywhere✅ हर जगह✅ 无处不在 | your app onlyसिर्फ़ आपका app仅限你的应用 |
| Best forकिसके लिए बेहतर最适合 | quick experimentsजल्दी के प्रयोग快速试验 | any real projectहर असली project任何真实项目 | scripts and small toolsscripts और छोटे tools脚本和小工具 |
For libraries, frameworks and team projects, use the standard logging module — every library you install already logs through it. loguru is pleasant for personal scripts, and it can capture standard logging records too.
Libraries, frameworks और team projects के लिए standard logging module इस्तेमाल करें — आपकी install की हर library पहले से इसी से log करती है। loguru निजी scripts के लिए सुखद है, और वह standard logging records भी पकड़ सकता है।
对于库、框架和团队项目,请使用标准的 logging 模块 — 你安装的每个库本来就通过它记录日志。loguru 适合个人脚本,它也能接收标准 logging 的记录。
Golden Rules and Quiz 🏆सुनहरे नियम और Quiz 🏆黄金法则与小测验 🏆
log = logging.getLogger(__name__) at the top of every module.log = logging.getLogger(__name__) लिखें।log = logging.getLogger(__name__)。dictConfig). Libraries only add a NullHandler.dictConfig)। Libraries सिर्फ़ NullHandler जोड़ती हैं।dictConfig)。库只添加 NullHandler。log.debug("x=%s", x). Guard expensive values with isEnabledFor.log.debug("x=%s", x)। महँगी values को isEnabledFor से बचाएँ।log.debug("x=%s", x)。昂贵的值用 isEnabledFor 保护。except, call log.exception(...) so the traceback is kept.except के अंदर log.exception(...) बुलाएँ ताकि traceback रहे।except 中调用 log.exception(...),保留回溯。QueueHandler when several processes log.QueueHandler।QueueHandler。3.12: Logger.getChildren(), getHandlerByName(), getHandlerNames(), the taskName attribute for asyncio, and filters that return a new record. 3.13: LoggerAdapter(..., merge_extra=True) merges your extra= with the adapter's. 3.15: basicConfig(formatter=...) accepts a ready Formatter.
3.12: Logger.getChildren(), getHandlerByName(), getHandlerNames(), asyncio के लिए taskName attribute, और नया record लौटाने वाले filters। 3.13: LoggerAdapter(..., merge_extra=True) आपके extra= को adapter वाले से मिलाता है। 3.15: basicConfig(formatter=...) तैयार Formatter स्वीकार करता है।
3.12:Logger.getChildren()、getHandlerByName()、getHandlerNames()、用于 asyncio 的 taskName 属性,以及可返回新记录的过滤器。3.13:LoggerAdapter(..., merge_extra=True) 会合并你的 extra= 与适配器自带的。3.15:basicConfig(formatter=...) 可直接接受一个 Formatter。
Quick Quiz — 8 Questionsछोटा Quiz — 8 सवाल小测验 — 8 道题
Your code says what happened through getLogger(__name__) and five levels; configuration — written once with dictConfig — decides what is kept and where it goes. A record is checked once against the logger's effective level, then offered to every handler on the way up the tree. Keep tracebacks with log.exception, format lazily, rotate your files, and never log secrets.
आपका code getLogger(__name__) और पाँच levels से बताता है कि क्या हुआ; configuration — dictConfig से एक बार लिखी गई — तय करती है कि क्या रखा जाए और कहाँ जाए। Record को logger के effective level से एक बार जाँचा जाता है, फिर tree में ऊपर जाते हुए हर handler को दिया जाता है। log.exception से tracebacks रखें, आलसी formatting करें, files rotate करें, और secrets कभी log न करें।
你的代码通过 getLogger(__name__) 和五个级别说明 发生了什么;配置 — 用 dictConfig 写一次 — 决定保留什么、送到哪里。一条记录只按记录器的有效级别检查一次,然后在沿树向上的途中交给每个处理器。用 log.exception 保留回溯,延迟格式化,轮转文件,绝不记录机密。