跳转至

尚未翻译

本页面尚未翻译成中文,因此显示英文原文。 帮助翻译

logging Module Complexity

The logging module routes records from loggers, through filters, to handlers that format and write them. Its cost is decided by one question asked before anything else happens: is this record going to be emitted at all? A call below the effective level returns after one cached dictionary lookup, having built no LogRecord and formatted none of its arguments. A call at or above it pays for the record, the walk up the logger hierarchy, every filter on the way, and one formatting and one write per handler that accepts it.

Nothing is held beyond the record being processed, except by the handlers that buffer on purpose. The module does keep a registry of every logger ever named and a weak reference to every handler ever built, and setLevel(), shutdown() and dictConfig() are linear in one of those rather than in anything the call names.

Records are the unit throughout. a is the ancestors between a logger and the root, h is the handlers found on that walk, F is the filter calls made for one record (the originating logger's filters, then those of each handler the record reaches) and k is the characters in one handler's formatted output. L is the entries in the logger registry, loggers and placeholders alike; H is the live handlers in the process; d and ℓ are the dotted components and the characters of a logger name; f is the characters in a format string; e is the keys in an extra mapping; p is the characters in a path; t is the frames in a traceback, chained causes included; D is the frames on the call stack; r is the records in a buffer or queue; E is the entries in a log file's directory; and b is a rotating handler's backupCount.

Emission bounds cover the standard stream handlers, constant-cost filters and ordinary %-style messages from a fixed call site with no extra, exc_info or stack_info; those three add the makeRecord(), formatException() and findCaller(stack_info=True) rows. A custom filter, formatter, namer or callback adds its own cost; hashing and equality of keys and handlers are treated as O(1); and the destination's work - a stream's write, a socket's send, an SMTP session - counts as one operation per record. Space excludes output retained by the destination.

A suppressed call is cheap, not free

logger.debug(...) under an INFO level is one cache lookup, which is why the %s form matters: with logger.debug('user %s', user) the argument is never converted, while an f-string has already been built before debug() is called.

Complexity Reference

Logger

Operation Time Space Notes
logging.getLogger(name=None) - known name O(1) O(1) A dict lookup in the registry; no name, or 'root', returns the root logger
logging.getLogger(name) - first time O((d + c)·ℓ) O(d·ℓ) Each missing ancestor gets a placeholder keyed by its prefix, and those prefixes are retained; c = loggers already registered below a placeholder this name replaces, each revisited to compare and repoint its parent
logging.Logger(name, level=NOTSET) O(1) O(1) Direct construction bypasses the registry: no entry, no parent until one is assigned, and a level cache outside every sweep, so even its own setLevel() leaves cached answers stale
Logger.debug(msg, *args, **kwargs), Logger.info(msg, *args, **kwargs), Logger.warning(msg, *args, **kwargs), Logger.error(msg, *args, **kwargs), Logger.critical(msg, *args, **kwargs), Logger.exception(msg, *args, exc_info=True, **kwargs), Logger.log(level, msg, *args, **kwargs), Logger.fatal(msg, *args, **kwargs) - suppressed O(1) cached; O(a) on the first call per level O(1) One lookup in the logger's level cache; no record, no formatting, no filter
The same call - emitted O(a + h + F + h·k) O(k) auxiliary findCaller(), the record, the walk up the hierarchy, every filter reached, and one formatting and one write per accepting handler; log() with a non-integer level raises TypeError when raiseExceptions is true and otherwise does nothing
Logger.warn(msg, *args, **kwargs) as warning() as warning() Deprecated: raises a DeprecationWarning on every call before delegating
Logger.isEnabledFor(level) O(1) cached; O(a) on a miss O(1) The answer is cached per level; a miss consults logging.disable()'s threshold and getEffectiveLevel()
Logger.getEffectiveLevel() O(a) O(1) Walks toward the root until a logger with a level set; not cached
Logger.setLevel(level) O(L) O(1) Clears the level cache of every registered logger, not just this one
Logger.hasHandlers() O(a) O(1) Stops at the first logger with handlers, or the first with propagate false
Logger.addHandler(hdlr), Logger.removeHandler(hdlr) O(n) O(1) n = handlers already on this logger, scanned for equality; a handler is added once
Logger.handle(record) O(F + a + h) plus handler work O(1) Skipped entirely when disabled; the logger's filters, then callHandlers()
Logger.callHandlers(record) O(a + h) plus each accepting handler's handle() O(1) Walks toward the root unless propagate is false; a record that finds no handler anywhere goes to logging.lastResort
Logger.makeRecord(name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None) O(p + e) O(p + e) The record, then each extra key checked against the record's attributes and copied in; a collision raises KeyError
Logger.findCaller(stack_info=False, stacklevel=1) O(s) O(1) s = stacklevel plus logging's own frames: it stops at the first caller, so the depth of the stack below does not matter
Logger.findCaller(stack_info=True, stacklevel=1) O(D) O(D) Formats every frame on the stack, reading each one's source line
Logger.getChild(suffix) O(ℓ), then as getLogger() O(ℓ) Joins the names, even for a logger that already exists, and asks the registry
Logger.getChildren() O(L) O(children) Python 3.12+; scans the whole registry for direct children
Logger.name, Logger.level, Logger.parent, Logger.propagate, Logger.handlers, Logger.filters, Logger.disabled O(1) O(1) Attributes. Assigning level directly leaves every cached isEnabledFor() answer stale; setLevel() is what clears them
logging.root, Logger.root, Logger.manager O(1) O(1) The root logger and the registry that owns every named logger

LogRecord

Operation Time Space Notes
logging.LogRecord(name, level, pathname, lineno, msg, args, exc_info, func=None, sinfo=None) O(p) O(p) Attribute assignment: pathname is split for filename and module; the process id, thread name and, on 3.12+, the asyncio task name are read; msg % args is not done here
LogRecord.getMessage() O(k) O(k) str(msg) % args, redone on every call; each handler's formatter calls it again
logging.makeLogRecord(dict) O(e) O(e) A blank record whose __dict__ is updated with the e keys; how a pickled record from a SocketHandler is rebuilt
logging.getLogRecordFactory(), logging.setLogRecordFactory(factory) O(1) O(1) The callable that builds each record

Handler

Operation Time Space Notes
logging.Handler(level=NOTSET) O(1) O(1) Registers a weak reference for shutdown() and creates an RLock
Handler.setLevel(level), Handler.setFormatter(fmt), Handler.level, Handler.formatter O(1) O(1) A level name is resolved through one dict lookup
Handler.name, Handler.get_name(), Handler.set_name(name) O(1) O(1) Assigning a name registers the handler for getHandlerByName(); close() unregisters it
Handler.createLock(), Handler.acquire(), Handler.release() O(1) O(1) emit() runs under this lock, so threads sharing a handler serialize on it
Handler.handle(record) O(F) plus emit() O(1) The handler's filters, then emit() under the lock; returns a false value when a filter rejected the record, and otherwise the record (3.12+) or True
Handler.format(record) O(k) O(k) The handler's formatter, or the module default of %(message)s
Handler.emit(record) O(1) O(1) Raises NotImplementedError; subclasses do the work
Handler.flush() O(1) O(1) A no-op here; StreamHandler flushes its stream
Handler.close() O(1) O(1) Drops the name registration; subclasses close streams and sockets
Handler.handleError(record) O(D + t) O(D + t) With logging.raiseExceptions true, writes the traceback and the whole call stack to sys.stderr; with it false, nothing
Handler.addFilter(filter), Handler.removeFilter(filter) O(n) O(1) n = filters already on the handler, scanned for equality
logging.getHandlerByName(name) O(1) O(1) Python 3.12+; a lookup in the named-handler registry
logging.getHandlerNames() O(n) O(n) Python 3.12+; a fresh frozenset of the n registered names
logging.lastResort O(1) O(1) A WARNING-level handler on sys.stderr, used only by a record that found no handler at all on its walk

StreamHandler, FileHandler and NullHandler

Operation Time Space Notes
logging.StreamHandler(stream=None) O(1) O(1) sys.stderr by default; the stream is never closed by the handler
StreamHandler.emit(record) O(k) O(k) Formats, writes msg + terminator in one write(), then calls flush(): every record is followed by a flush
StreamHandler.flush() O(1) O(1) The stream's own flush(), under the handler lock
StreamHandler.setStream(stream) O(1) O(1) Flushes the old stream and returns it, or None when the stream is unchanged
StreamHandler.terminator O(1) O(1) The string appended to each record, '\n' by default
logging.FileHandler(filename, mode='a', encoding=None, delay=False, errors=None) O(p) O(p) Resolves and keeps the absolute path, then opens the file unless delay=True
FileHandler.emit(record) O(k) O(k) Opens the file on the first record when delayed, then StreamHandler.emit(): a write() and a flush() per record
FileHandler.close() O(1) O(1) Flushes and closes the file
logging.NullHandler() O(1) O(1) What a library installs so its records have somewhere to go
NullHandler.handle(record), NullHandler.emit(record) O(1) O(1) Both are stubs; handle() is overridden, so the handler's filters are never consulted
NullHandler.createLock() O(1) O(1) Sets no lock at all

Formatter and BufferingFormatter

Operation Time Space Notes
logging.Formatter(fmt=None, datefmt=None, style='%', validate=True, *, defaults=None) O(f) O(f) validate=True scans the format string once and raises ValueError for a field that does not fit the style; '$' compiles a string.Template. A defaults mapping is merged with the record's attributes on every format(), so it costs its size per record
Formatter.format(record) O(k + t) O(k + t) getMessage(), the time if the format uses it, the fields, then the exception text and stack info if present. The exception text is formatted once per record and cached in record.exc_text, so a second handler pays no t
Formatter.formatMessage(record) O(k) O(k) The format string applied to the record's attributes
Formatter.formatTime(record, datefmt=None) O(1) O(1) A time.localtime() and a time.strftime(); fixed-size output for a fixed datefmt
Formatter.formatException(ei) O(t) O(t) traceback.print_exception() into a string; each frame's source line is read through linecache, which reads the file the first time
Formatter.formatStack(stack_info) O(1) O(1) Returns its argument; the cost was paid in findCaller()
Formatter.usesTime() O(f) O(1) A substring search for the time field, repeated on every format()
Formatter.converter, Formatter.default_time_format, Formatter.default_msec_format O(1) O(1) Attributes; set converter to time.gmtime for UTC timestamps
logging.BufferingFormatter(linefmt=None) O(1) O(1) Stores the line formatter; no record is formatted
BufferingFormatter.format(records) O(r + K) with amortized string growth O(r + K) K = total characters of the r formatted lines, concatenated one at a time between the header and footer
BufferingFormatter.formatHeader(records), BufferingFormatter.formatFooter(records) O(1) O(1) Return ''; subclasses override them
logging.BASIC_FORMAT O(1) O(1) The format string basicConfig() defaults to

Filter and Filterer

Operation Time Space Notes
logging.Filter(name='') O(1) O(1) Stores the name and its length
Filter.filter(record) O(ℓ) O(1) Compares the filter's name as a dotted prefix of the record's logger name; an empty name passes everything
Filterer.addFilter(filter), Filterer.removeFilter(filter) - on a Logger or a Handler O(n) O(1) n = filters already attached, scanned for equality; a filter is attached once
Filterer.filter(record) - Logger.filter(record), Handler.filter(record) O(F) O(1) Each filter in order, stopping at the first that rejects; on 3.12+ a filter may return a LogRecord, which replaces the record for the rest of the chain

LoggerAdapter

Operation Time Space Notes
logging.LoggerAdapter(logger, extra=None, merge_extra=False) O(1) O(1) Wraps a logger; merge_extra is Python 3.13+
LoggerAdapter.process(msg, kwargs) O(1); O(e) with merge_extra O(e) with merge_extra Replaces the call's extra with the adapter's; merge_extra=True builds a merged dict instead
LoggerAdapter.debug(msg, *args, **kwargs), LoggerAdapter.info(msg, *args, **kwargs), LoggerAdapter.warning(msg, *args, **kwargs), LoggerAdapter.error(msg, *args, **kwargs), LoggerAdapter.critical(msg, *args, **kwargs), LoggerAdapter.exception(msg, *args, exc_info=True, **kwargs), LoggerAdapter.log(level, msg, *args, **kwargs), LoggerAdapter.warn(msg, *args, **kwargs) as Logger, plus process() as Logger The level is checked first, so a suppressed call never reaches process()
LoggerAdapter.isEnabledFor(level), LoggerAdapter.getEffectiveLevel(), LoggerAdapter.hasHandlers(), LoggerAdapter.setLevel(level) as Logger as Logger Delegated to the wrapped logger
LoggerAdapter.logger, LoggerAdapter.extra, LoggerAdapter.manager, LoggerAdapter.name O(1) O(1) Attributes; manager and name read through to the logger

Module-Level Functions

Operation Time Space Notes
logging.debug(msg, *args, **kwargs), logging.info(msg, *args, **kwargs), logging.warning(msg, *args, **kwargs), logging.error(msg, *args, **kwargs), logging.critical(msg, *args, **kwargs), logging.exception(msg, *args, exc_info=True, **kwargs), logging.log(level, msg, *args, **kwargs), logging.fatal(msg, *args, **kwargs), logging.warn(msg, *args, **kwargs) as the root logger's call as the root logger's call A root with no handlers first gets basicConfig(); warn() also raises a DeprecationWarning
logging.disable(level=CRITICAL) O(L) O(1) Sets one threshold and clears every logger's level cache, the same as setLevel()
logging.shutdown(handlerList=_handlerList) O(H) plus each handler's flush and close O(H) auxiliary Copies the weak references to every live handler, newest first, and flushes and closes each; from 3.12 a handler with flushOnClose false is closed without the flush. Registered with atexit
logging.basicConfig(**kwargs) O(1) when the root has handlers and force is false; O(n² + m² + L) otherwise O(n + m) n = handlers supplied (one by default), each added with the duplicate scan; m = the root's existing handlers, each removed with a scan and closed when force=True; L only when level is given
logging.getLoggerClass(), logging.setLoggerClass(klass) O(1) O(1) The class getLogger() instantiates; setLoggerClass() checks it subclasses Logger
logging.captureWarnings(capture) O(1) O(1) Swaps warnings.showwarning; each captured warning is then rendered by warnings.formatwarning() and emitted through the py.warnings logger
logging.raiseExceptions O(1) O(1) Whether handleError() reports to sys.stderr

Levels and Constants

Operation Time Space Notes
logging.addLevelName(level, levelName) O(1) O(1) Two dict entries, number to name and name to number
logging.getLevelName(level) O(1) O(1) A dict lookup in either direction; an unknown level returns 'Level N' rather than raising
logging.getLevelNamesMapping() O(v) O(v) Python 3.11+; v = registered names, copied into a new dict
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, logging.WARN, logging.ERROR, logging.CRITICAL, logging.FATAL O(1) O(1) Integer constants; WARN and FATAL are the deprecated spellings
logging.handlers.DEFAULT_TCP_LOGGING_PORT, logging.handlers.DEFAULT_UDP_LOGGING_PORT, logging.handlers.DEFAULT_HTTP_LOGGING_PORT, logging.handlers.DEFAULT_SOAP_LOGGING_PORT, logging.handlers.SYSLOG_UDP_PORT, logging.handlers.SYSLOG_TCP_PORT, logging.config.DEFAULT_LOGGING_CONFIG_PORT O(1) O(1) Integer port numbers

logging.config

Operation Time Space Notes
logging.config.dictConfig(config) O(C + H + L log L + (n + c)·L), plus flush and close work O(C + L + H) C = size of the config, though h handlers or filters on one target attach through the addHandler() and addFilter() scans, O(h²). Every live handler is flushed and closed first (H); the L registered names are sorted, and each of the n configured loggers scans them for its descendants; every existing logger the config does not name is disabled unless disable_existing_loggers is false, except the c existing descendants of named loggers, which are reset to inherit from them, each reset an O(L) cache clear of its own. incremental=True skips all of that and only sets levels and propagate: O(C + n·L), because every level set still clears every cache
logging.config.fileConfig(fname, defaults=None, disable_existing_loggers=True, encoding=None) O(S + H + L log L + (n + c)·L), plus flush and close work O(S + L + H) S = characters in the file, parsed by configparser; handler arguments are evaluated with eval(); then the same handler closing and existing-logger pass as dictConfig()
logging.config.listen(port=DEFAULT_LOGGING_CONFIG_PORT, verify=None) O(1) O(1) Returns a thread that is not yet started; nothing is bound until start(). Each configuration received then runs dictConfig() or fileConfig()
logging.config.stopListening() O(1) O(1) Sets a flag the server checks after each one-second poll and after any request it is serving; the socket closes then

BaseRotatingHandler, RotatingFileHandler and TimedRotatingFileHandler

Operation Time Space Notes
logging.handlers.BaseRotatingHandler(filename, mode, encoding=None, delay=False, errors=None) O(p) O(p) A FileHandler; not meant to be instantiated directly
BaseRotatingHandler.emit(record), RotatingFileHandler.emit(record), TimedRotatingFileHandler.emit(record) shouldRollover() + doRollover() when due + FileHandler.emit() O(k), plus doRollover()'s when due The rollover check runs on every record
BaseRotatingHandler.rotation_filename(default_name) O(1) O(1) Returns the name unchanged, or whatever namer returns
BaseRotatingHandler.rotate(source, dest) O(1) O(1) An os.path.exists() and an os.rename(), or whatever rotator does
BaseRotatingHandler.namer, BaseRotatingHandler.rotator O(1) O(1) None by default; callables that customize the two steps above
logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False, errors=None) O(p) O(p) A maxBytes of 0 never rolls over
RotatingFileHandler.shouldRollover(record) O(k) O(k) With maxBytes set, formats the record to measure it, so an emitted record is formatted twice, except the first record into an empty file from 3.12.6; before 3.12.6 it also stats the path on every record, and from 3.12.6 only when a rollover is due
RotatingFileHandler.doRollover() O(b) O(1) Renames each backup up one number, an exists() and a rename() apiece, then reopens the file
logging.handlers.TimedRotatingFileHandler(filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None, errors=None) O(p) O(p) Stats the file for its modification time and computes the first rollover
TimedRotatingFileHandler.computeRollover(currentTime) O(1) O(1) Arithmetic, with a DST adjustment for local time
TimedRotatingFileHandler.shouldRollover(record) O(1) O(1) A clock read and a comparison; the record is not formatted
TimedRotatingFileHandler.getFilesToDelete() O(E + m log m) O(E + m) Lists the whole directory and prefix-matches every entry; m = matching backups, sorted so the oldest go first. With a namer, each entry is regex-searched and the namer called per candidate
TimedRotatingFileHandler.doRollover() O(E + m log m); O(1) with backupCount=0 O(E + m) A strftime(), a rename, getFilesToDelete() and one remove() per surplus backup, then reopens

WatchedFileHandler

Operation Time Space Notes
logging.handlers.WatchedFileHandler(filename, mode='a', encoding=None, delay=False, errors=None) O(p) O(p) Opens the file and records its device and inode
WatchedFileHandler.reopenIfNeeded() O(1) O(1) One os.stat() of the path; reopens when the device or inode changed or the file is gone
WatchedFileHandler.emit(record) O(k) O(k) reopenIfNeeded() then FileHandler.emit(): a stat(), a write() and a flush() per record

SocketHandler and DatagramHandler

Operation Time Space Notes
logging.handlers.SocketHandler(host, port) O(1) O(1) No connection until the first record; port=None means a Unix domain socket at host
SocketHandler.makeSocket(timeout=1) name resolution, then one connection attempt per resolved address until one succeeds O(1) timeout bounds each attempt, not the name resolution before them
SocketHandler.createSocket() O(1) between attempts O(1) After a failed connect, no retry before retryStart seconds, doubling by retryFactor up to retryMax; a record emitted meanwhile is still pickled, then dropped without a connection attempt
SocketHandler.makePickle(record) O(k + A) O(k + A) A = size of the record's attribute dict, values included: the dict is copied with msg merged, args and exc_info cleared and message dropped, then pickled behind a 4-byte length; with exc_info the record is formatted first so exc_text travels
SocketHandler.send(s) one sendall() O(1) Connects first when there is no socket
SocketHandler.emit(record) O(k + A) plus one send O(k + A) makePickle() then send()
SocketHandler.handleError(record) O(1); Handler.handleError() otherwise Closes the socket when closeOnError is true, so the next record reconnects
SocketHandler.close() O(1) O(1) Closes the socket
logging.handlers.DatagramHandler(host, port) O(1) O(1) The same pickle, over UDP
DatagramHandler.makeSocket() O(1) O(1) An unconnected datagram socket
DatagramHandler.send(s) one sendto() O(1) No delivery guarantee and no partial sends
DatagramHandler.emit(record) O(k + A) plus one send O(k + A) As SocketHandler.emit()

SysLogHandler

Operation Time Space Notes
logging.handlers.SysLogHandler(address=('localhost', SYSLOG_UDP_PORT), facility=LOG_USER, socktype=None, timeout=None) one createSocket() O(1) A str address is a Unix socket, connected now, silently if it fails; Windows has no Unix sockets and raises AttributeError for one. A host tuple costs a getaddrinfo() and, for a stream socket, a connect. timeout is Python 3.14+
SysLogHandler.createSocket() as above O(1) Python 3.11+; called again from emit() whenever there is no socket. On 3.10 the constructor does the same work inline and emit() reconnects only a Unix socket
SysLogHandler.encodePriority(facility, priority), SysLogHandler.mapPriority(levelName) O(1) O(1) Dict lookups; an unknown level name maps to 'warning'
SysLogHandler.emit(record) O(k) plus one send O(k) Formats, prefixes ident and the priority, encodes as UTF-8 and appends a NUL when append_nul; one send per record, and a failed Unix-socket send reconnects and retries once
SysLogHandler.close() O(1) O(1) Closes the socket
SysLogHandler.ident, SysLogHandler.append_nul, SysLogHandler.priority_names, SysLogHandler.facility_names, SysLogHandler.priority_map O(1) O(1) Attributes; the three dicts map names to the LOG_* integers

SMTPHandler, HTTPHandler and NTEventLogHandler

Operation Time Space Notes
logging.handlers.SMTPHandler(mailhost, fromaddr, toaddrs, subject, credentials=None, secure=None, timeout=5.0) O(1) O(1) Stores the addresses; nothing is connected
SMTPHandler.getSubject(record) O(1) O(1) The fixed subject; override it for a record-dependent one
SMTPHandler.emit(record) one SMTP session O(k) Connects, optionally STARTTLS and login, sends and quits: a complete session for every record, on the emitting thread
logging.handlers.HTTPHandler(host, url, method='GET', secure=False, credentials=None, context=None) O(1) O(1) Validates the method; nothing is connected
HTTPHandler.mapLogRecord(record) O(1) O(1) Returns the record's own __dict__, not a copy
HTTPHandler.getConnection(host, secure) O(1) O(1) An http.client connection object; the connect happens in the request
HTTPHandler.emit(record) O(A + V) plus one request O(A + V) URL-encodes every attribute of the record (A attributes, V characters of values) into the query string or body, sends one request per record and waits for the response
logging.handlers.NTEventLogHandler(appname, dllname=None, logtype='Application') one registry write O(1) Windows with pywin32: registers the event source. Elsewhere it prints a notice and every emit() is a no-op
NTEventLogHandler.emit(record) O(k) plus one ReportEvent O(k) Windows only
NTEventLogHandler.getMessageID(record), NTEventLogHandler.getEventCategory(record), NTEventLogHandler.getEventType(record) O(1) O(1) Return 1, 0 and a lookup in the level-to-type map
NTEventLogHandler.close() O(1) O(1) Leaves the registry entry in place

BufferingHandler and MemoryHandler

Operation Time Space Notes
logging.handlers.BufferingHandler(capacity) O(1) O(1) An empty list
BufferingHandler.emit(record) O(1) amortized O(1) Appends the record, then flush() when shouldFlush() says so; records are held unformatted
BufferingHandler.shouldFlush(record) O(1) O(1) True when the buffer has reached capacity
BufferingHandler.flush() O(r) O(1) Empties the buffer and does nothing else with the records
BufferingHandler.close() O(r) O(1) flush(), then Handler.close()
logging.handlers.MemoryHandler(capacity, flushLevel=ERROR, target=None, flushOnClose=True) O(1) O(1) A BufferingHandler that forwards to target
MemoryHandler.shouldFlush(record) O(1) O(1) True when the buffer is full or the record's level reaches flushLevel
MemoryHandler.setTarget(target) O(1) O(1) Under the handler lock
MemoryHandler.flush() O(r) plus the target's handle() per record O(1) Each buffered record goes to target.handle(), which runs the target's filters but does not check its level. Without a target the buffer is kept, so it grows without bound
MemoryHandler.close() O(r) plus the target's handle() per record O(1) flush() when flushOnClose, then drops the target; with flushOnClose=False the records stay in the buffer

QueueHandler and QueueListener

Operation Time Space Notes
logging.handlers.QueueHandler(queue) O(1) O(1) Any object with put_nowait()
QueueHandler.prepare(record) O(k + A) O(k + A) Formats the record on the calling thread, then returns a shallow copy whose msg and message are the formatted text and whose args, exc_info, exc_text and stack_info are None; anything else on the record, extra included, travels as it is
QueueHandler.enqueue(record) O(1) O(1) put_nowait(); a full bounded queue raises queue.Full into handleError() and the record is dropped
QueueHandler.emit(record) O(k + A) O(k + A) enqueue(prepare(record))
QueueHandler.listener O(1) O(1) Python 3.12+; None unless dictConfig() built the listener
logging.handlers.QueueListener(queue, *handlers, respect_handler_level=False) O(1) O(1) Stores the handlers; no thread yet
QueueListener.start() O(1) O(1) Starts one daemon thread; from 3.13.4 a second start() raises RuntimeError
QueueListener.stop() O(r) plus handling O(1) Enqueues the sentinel behind every pending record and joins, so it returns only after all r have been handled; a full bounded queue makes the sentinel's put_nowait() raise queue.Full instead. From 3.13 a second stop() is a no-op, and on 3.14+ the listener is a context manager
QueueListener.dequeue(block), QueueListener.enqueue_sentinel() O(1) O(1) get() and put_nowait() on the queue
QueueListener.prepare(record) O(1) O(1) Returns the record unchanged
QueueListener.handle(record) O(n) plus each handler's handle() O(1) n = the listener's handlers; every one of them receives the record regardless of its level unless respect_handler_level is true

The Level Check Is the Whole Story

import io
import logging

stream = io.StringI