API Reference¶
This reference documents the public API exported by azure_functions_logging.
Use this page together with:
- Configuration for setup behavior by environment.
- Usage Guide for complete implementation patterns.
- Examples for runnable snippets.
setup_logging¶
Configure logging for the current environment. Behavior depends on the detected environment:
- Azure / Core Tools (
use_record_factory=False, default): InstallsContextFilteron the root logger's existing handlers and on the root logger itself (so records emitted directly on the root logger also carry context fields; note this does not cover records that merely propagate up from named child loggers — those are filtered only by the handler-level filters). Does NOT add new handlers or modify the root logger level (respectshost.jsonconfiguration). Iffunctions_formatteris provided, it is applied to every root handler before the filter is added. Whenuse_record_factory=True, noContextFilteris attached; context injection happens via the globalLogRecordFactoryinstead. - Standalone local development: Adds a
StreamHandlerwithColorFormatterorJsonFormatterto the specified logger (or root logger iflogger_nameis None). Sets the level. Pass an explicitlogger_nameto avoid modifying the root logger; when a handler is added to a named logger this way, itspropagateflag is set toFalseso records are not also emitted by an ancestor (e.g. root) handler, which would double-log.
Reconfiguration (calling setup_logging() more than once).
Repeat calls are safe and never double-install filters or handlers. The
exact contract depends on the detected environment:
- Standalone local mode is fully idempotent per
logger_name: state is tracked in a process-wide set, so the first call for a givenlogger_namewins and later calls for that same name return early —level,format, and the added handler are applied only once. Configure a differentlogger_nameto set up an additional logger. - Azure / Core Tools mode is idempotent per
(logger_name, use_record_factory)at the handler level: each root handler is given theContextFilter(andfunctions_formatter, if supplied) exactly once, tracked by aWeakSet. Repeat calls therefore recover by picking up any handlers the host attached after the previous call, without duplicating filters. The root logger's level and handler list are never modified (host.jsonowns the level).
Switching use_record_factory between calls. Enabling it
(False -> True) installs the global LogRecordFactory and strips
any ContextFilter this package previously installed on the target and
root loggers, so context is injected by exactly one mechanism. The two
modes keep isolated per-signature state, so a later ContextFilter call
never reuses a factory-mode instance (or vice versa). Switching back
(True -> False) installs a fresh ContextFilter but does not
uninstall the already-installed global factory; prefer a single, consistent
use_record_factory value per process.
Switching activate_trace_context. A bare setup_logging() (the
default activate_trace_context=None) leaves the previously configured
activation default untouched; pass True/False to change it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
int
|
Logging level for local development. Ignored in Azure/Core Tools. |
INFO
|
format
|
str
|
Log output format for local development. Supported values are
|
'color'
|
logger_name
|
str | None
|
Optional logger name to configure. When None, configures the root logger (local dev) or installs filter on root handlers (Azure). |
None
|
functions_formatter
|
Formatter | None
|
Optional custom formatter applied to all root handlers when running inside Azure/Core Tools. Useful for injecting a custom JSON formatter or third-party formatter without losing ContextFilter integration. |
None
|
host_json_path
|
Path | str | None
|
Optional explicit path to a |
None
|
use_record_factory
|
bool
|
When True, install the global |
False
|
extra_context_vars
|
dict[str, ContextVar[Any]] | None
|
Optional mapping of |
None
|
activate_trace_context
|
bool | None
|
Sets the process-wide default that
|
None
|
.. warning::
``use_record_factory=True`` modifies the **global**
``logging.LogRecordFactory``, which affects all loggers in the process
(including third-party libraries). The four context field names
(``invocation_id``, ``function_name``, ``trace_id``, ``cold_start``)
become reserved LogRecord attributes — passing them via ``extra=`` to
stdlib loggers will raise ``KeyError``. Prefer :class:`FunctionLogger`
(which sanitizes ``extra`` keys automatically) when this option is on.
Source code in src/azure_functions_logging/_setup.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | |
Usage Notes¶
- Call once during startup.
- Default format is
"color". - In Azure/Core Tools runtime, filter-only behavior avoids duplicate handlers.
Example¶
import logging
from azure_functions_logging import setup_logging
setup_logging(level=logging.INFO, format="json")
Example: Named Target Logger¶
Example: Invalid Format Handling¶
from azure_functions_logging import setup_logging
try:
setup_logging(format="pretty")
except ValueError:
pass
get_logger¶
Create a FunctionLogger wrapping a standard logging.Logger.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Logger name. Typically |
None
|
Returns:
| Type | Description |
|---|---|
FunctionLogger
|
A |
Source code in src/azure_functions_logging/__init__.py
Usage Notes¶
- Returns a
FunctionLoggerwrapper over a standard logger. - Pass
__name__for module-level identity. - Use the wrapper methods like standard logging methods.
Example¶
from azure_functions_logging import get_logger, setup_logging
setup_logging()
logger = get_logger(__name__)
logger.info("module logger ready")
Example: Root Logger Wrapper¶
from azure_functions_logging import get_logger, setup_logging
setup_logging()
root_logger = get_logger()
root_logger.warning("root logger event")
FunctionLogger¶
Wrapper around a standard logging.Logger with context binding.
FunctionLogger forwards the common logging methods (debug,
info, warning, error, critical, exception, log)
through :meth:_log, which merges bound context and sanitizes reserved
extra keys. Any other attribute of the underlying logging.Logger
(addHandler, handlers, propagate, level, getChild,
...) is delegated via :meth:__getattr__, so a FunctionLogger
behaves like the stdlib Logger it wraps.
The bind() method returns a new wrapper with additional context
fields that are merged into extra on each log call.
Context from bind() is supplementary to the ContextFilter-based
context (invocation_id, function_name, etc.) which is set globally via
inject_context().
Source code in src/azure_functions_logging/_logger.py
name
property
¶
Return the name of the underlying logger.
__getattr__(name)
¶
Delegate unknown attributes to the wrapped logging.Logger.
__getattr__ only runs when normal lookup fails, so it never
shadows FunctionLogger's own slots or methods. It forwards the rest
of the stdlib Logger surface (addHandler, removeHandler,
addFilter, handlers, propagate, level, disabled,
parent, manager, getChild, ...) to self._logger.
Source code in src/azure_functions_logging/_logger.py
bind(**kwargs)
¶
Return a new FunctionLogger with additional bound context.
The returned logger shares the same underlying logging.Logger
but carries merged context fields. This is an immutable operation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**kwargs
|
Any
|
Context key-value pairs to bind. |
{}
|
Returns:
| Type | Description |
|---|---|
FunctionLogger
|
A new |
Source code in src/azure_functions_logging/_logger.py
clear_context()
¶
critical(msg, *args, **kwargs)
¶
debug(msg, *args, **kwargs)
¶
error(msg, *args, **kwargs)
¶
exception(msg, *args, **kwargs)
¶
Log an ERROR message with exception info.
fatal(msg, *args, **kwargs)
¶
Alias for :meth:critical (mirrors logging.Logger.fatal).
Implemented explicitly rather than delegated so bound context and reserved-key sanitization still apply.
Source code in src/azure_functions_logging/_logger.py
getEffectiveLevel()
¶
hasHandlers()
¶
info(msg, *args, **kwargs)
¶
isEnabledFor(level)
¶
log(level, msg, *args, **kwargs)
¶
Log msg at the given level, mirroring logging.Logger.log.
Honors the same bind < extra < kwargs merge precedence
as :meth:info / :meth:warning / etc. and applies the same
reserved-key sanitization.
Source code in src/azure_functions_logging/_logger.py
setLevel(level)
¶
warn(msg, *args, **kwargs)
¶
Deprecated alias for :meth:warning (mirrors logging.Logger.warn).
Implemented explicitly rather than delegated so bound context and reserved-key sanitization still apply.
Source code in src/azure_functions_logging/_logger.py
Usage Notes¶
bind()returns a new immutable logger wrapper with merged context.clear_context()clears bound context on that wrapper instance.- Logging methods mirror standard logger API.
Example: Binding Context¶
from azure_functions_logging import get_logger, setup_logging
setup_logging(format="json")
logger = get_logger("checkout")
request_logger = logger.bind(request_id="r-100", user_id="u-55")
request_logger.info("checkout started")
Example: Chained Binding¶
base = get_logger("service")
l1 = base.bind(tenant_id="tenant-a")
l2 = l1.bind(operation="import")
l2.info("import queued")
Example: Clearing Bound Context¶
log = get_logger("demo").bind(session="s-1")
log.info("before clear")
log.clear_context()
log.info("after clear")
Example: Exception Logging¶
log = get_logger("errors")
try:
raise RuntimeError("boom")
except RuntimeError:
log.exception("operation failed", phase="load")
JsonFormatter¶
Bases: Formatter
Structured JSON log formatter.
Output is newline-delimited JSON (NDJSON), with one JSON object per log line. Context fields (invocation_id, function_name, etc.) are included when present on the LogRecord (set by ContextFilter).
Unserializable values in extra are coerced to strings via
:func:_json_default rather than dropping the log record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_string_length
|
int
|
Maximum character length for each native string value
in |
2048
|
truncate_native_strings
|
bool
|
When |
False
|
Source code in src/azure_functions_logging/_json_formatter.py
format(record)
¶
Format a log record as one NDJSON object.
Fully fail-safe: every step (message rendering, exception formatting,
timestamp, JSON serialization) is wrapped so that a hostile
__str__ / __repr__, a malformed exc_info triple, or a
cyclic extra payload never raises out of format(). Worst-case
we still emit a single valid JSON object with sentinel values.
Source code in src/azure_functions_logging/_json_formatter.py
Usage Notes¶
- Use indirectly via
setup_logging(format="json")for most cases. - Produces one JSON object per line (NDJSON style).
- Includes context fields when available on log records.
- Pass
truncate_native_strings=Trueto clip string values inextraatmax_string_lengthcharacters (default 2048); truncated values are suffixed with…. Only strings inextraare affected (recursively through dicts/lists) —message, ints, floats, and booleans are left intact.
Example: Automatic Selection¶
from azure_functions_logging import get_logger, setup_logging
setup_logging(format="json")
logger = get_logger("api")
logger.info("json formatter active", version="v1")
Example: Manual Formatter Wiring¶
import logging
from azure_functions_logging import JsonFormatter, get_logger
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
target = logging.getLogger("manual")
target.handlers = [handler]
target.setLevel(logging.INFO)
logger = get_logger("manual")
logger.info("manual formatter configured")
SamplingFilter¶
Bases: Filter
Rate-limit a logger to emit at most rate records per window seconds.
Useful for high-frequency loggers (e.g. per-request HTTP logs, polling loops) that can saturate the Azure Functions gRPC channel.
All records that exceed the rate cap are silently dropped. Records at WARNING and above are always passed through, regardless of the cap.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
int
|
Maximum number of records to pass per window. Must be >= 1. |
100
|
window
|
float
|
Rolling time window in seconds. Default: 1.0. |
1.0
|
name
|
str
|
Optional logger-name scope. When set, only matching loggers are subject to sampling; non-matching records pass through unchanged. Empty string matches all loggers (default). |
''
|
per_logger
|
bool
|
When False (default), all matching records share one rate
bucket per filter instance. When True, each |
False
|
Example::
filter = SamplingFilter(rate=10, window=1.0)
handler.addFilter(filter)
Source code in src/azure_functions_logging/_filters.py
filter(record)
¶
Return True to emit the record, False to drop it.
Source code in src/azure_functions_logging/_filters.py
Usage Notes¶
WARNINGand above always pass.name=scopes sampling to matching logger names; non-matching records bypass sampling.per_logger=Falseshares one bucket across all matching records on the filter instance.per_logger=Truegives eachrecord.namean independent bucket/window.
Example: Per-Logger Buckets for Azure SDK Logs¶
import logging
from azure_functions_logging import SamplingFilter
for handler in logging.getLogger().handlers:
handler.addFilter(SamplingFilter(rate=10, window=1.0, name="azure", per_logger=True))
RedactionFilter¶
Bases: Filter
Mask PII / sensitive values on LogRecord extra attributes in-place.
Iterates over all non-standard attributes on the LogRecord and
replaces the value of any key whose normalized name is in
sensitive_keys with "***".
Key normalization: lowercased, hyphens replaced with underscores.
This means X-Functions-Key matches the entry x_functions_key.
This filter mutates the record in-place so both ColorFormatter and
JsonFormatter see redacted values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sensitive_keys
|
Iterable[str] | None
|
Iterable of key names to redact (case-insensitive,
hyphen-insensitive). When None, uses the built-in default set
(25 keys):
|
None
|
name
|
str
|
Optional logger-name scope. When set, only matching loggers are subject to redaction; non-matching records pass through unchanged. |
''
|
patterns
|
Iterable[str | Pattern[str]] | None
|
Optional iterable of secret patterns (compiled |
None
|
Note
The default set includes credential which may over-redact
in codebases that use generic attribute names. Pass an explicit
sensitive_keys set if false positives occur.
Example::
filter = RedactionFilter()
handler.addFilter(filter)
Source code in src/azure_functions_logging/_filters.py
filter(record)
¶
Redact sensitive fields on the record. Always returns True.
Source code in src/azure_functions_logging/_filters.py
Value / pattern-based redaction¶
By default RedactionFilter is key-based: it masks values whose key is
sensitive, but not secrets embedded in a free-text message or string extra
(e.g. logger.info("connecting with token=ghp_...")). Pass patterns= to
enable opt-in value-based redaction of the rendered message and string
extras:
from azure_functions_logging import DEFAULT_REDACTION_PATTERNS, RedactionFilter
flt = RedactionFilter(patterns=DEFAULT_REDACTION_PATTERNS)
It is off by default because pattern scanning has a hot-path cost and can
produce false positives. DEFAULT_REDACTION_PATTERNS is a curated set of
high-confidence secrets (bearer tokens, key=value secrets, Azure
connection-string keys / SAS sig=, AWS access-key IDs, GitHub tokens, JWTs);
patterns with a keep named group preserve a readable prefix (e.g. token=)
while masking only the value. You may also pass your own regex strings or
compiled patterns. Substitution failures never raise (see the design
principle that redaction failures are silent).
DEFAULT_REDACTION_PATTERNS¶
AttributeFlattenFilter¶
Bases: Filter
Flatten nested dict extras into dotted scalar attributes in-place.
OpenTelemetry attributes only permit scalars and homogeneous arrays. A
nested dict passed via extra (e.g. order={"id": 1}) is silently
dropped by the OTel SDK. This filter rewrites such attributes into dotted
scalar keys (order.id) so the data survives export.
The filter mutates the record in-place, removing the original nested-dict attribute and adding one attribute per leaf. It is opt-in: it has no effect unless explicitly attached to a handler/logger.
Behavior:
- Nested dicts are flattened recursively to dotted keys.
- Lists / heterogeneous arrays are left unchanged (emitted as-is under
their dotted key). OTel accepts homogeneous scalar arrays; heterogeneous
or nested-object arrays remain the caller's responsibility.
- Scalar attributes and reserved LogRecord keys are never touched.
- Empty dicts contribute no keys (the attribute is removed).
- Cyclic references are dropped; over-deep structures (beyond
max_depth) are emitted as-is at the depth boundary.
.. note::
This filter rewrites the record's attributes, so it also affects
**non-OTel** consumers reading the same record — e.g. a
:class:`JsonFormatter` on the root handler will emit ``order.id``
instead of a nested ``order`` object. To avoid changing your JSON log
shape, attach this filter only to the OpenTelemetry ``LoggingHandler``
rather than to the root handler.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Optional logger-name scope. When set, only matching loggers are flattened; non-matching records pass through unchanged. |
''
|
separator
|
str
|
Delimiter joining nested keys. Default |
'.'
|
max_depth
|
int
|
Maximum recursion depth before a nested dict is emitted
as-is. Default |
_FLATTEN_MAX_DEPTH
|
Example::
filter = AttributeFlattenFilter()
handler.addFilter(filter)
Source code in src/azure_functions_logging/_filters.py
filter(record)
¶
Flatten nested-dict fields on the record. Always returns True.
Source code in src/azure_functions_logging/_filters.py
Usage Notes¶
- Opt-in only. Attach it to a handler/logger to flatten nested
dictextras into dotted scalar keys (e.g.order={"id": 1}becomesorder.id=1). - Intended for OpenTelemetry pipelines, where nested
dictattributes are silently dropped by the OTel SDK. - Lists / heterogeneous arrays are left unchanged (emitted as-is under their
dotted key). Flattening does not recurse into lists, so a
list-of-dicts such as
items=[{"id": 1}]is passed through verbatim rather than expanded intoitems.0.id. - Non-string dict keys are skipped (they cannot form a queryable dotted path).
- On key collision — e.g. a nested
{"a": {"b": 1}}and a literal{"a.b": 2}both mapping toa.b— the first value in iteration order wins and later collisions are dropped silently.
Attach to specific handlers, not the root logger blindly
This filter mutates the LogRecord in place, rewriting nested-dict
attributes into new dotted-key attributes. Attach it only to the handlers
that need flattened output (e.g. your OpenTelemetry handler). Installing it
on the root logger changes the record schema for every downstream
handler, which can surprise formatters that expect the original nested
attribute.
from azure_functions_logging import AttributeFlattenFilter
handler.addFilter(AttributeFlattenFilter())
inject_context¶
Set invocation context from an Azure Functions context object.
Extracts invocation_id, function_name, trace_id, and cold_start from the provided context and stores them in contextvars.
This function is safe to call with any object. Missing or inaccessible attributes are silently ignored (Principle 3: context injection failures never cause application failures).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
Any
|
An Azure Functions context object (func.Context). |
required |
Returns:
| Type | Description |
|---|---|
ContextTokens
|
A mapping of ContextVar to Token that can be passed to |
ContextTokens
|
|
Source code in src/azure_functions_logging/_context.py
Usage Notes¶
- Call at the start of every function invocation.
- Sets invocation metadata in context variables.
- Enables automatic cold start field in output.
Example: Azure Function Entrypoint¶
import azure.functions as func
from azure_functions_logging import get_logger, inject_context, setup_logging
setup_logging(format="json")
logger = get_logger(__name__)
app = func.FunctionApp()
@app.route(route="status")
def status(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
inject_context(context)
logger.info("status request")
return func.HttpResponse("ok")
Example: Safe with Partial Context Object¶
from azure_functions_logging import get_logger, inject_context, setup_logging
class PartialContext:
invocation_id = "local-123"
setup_logging(format="json")
logger = get_logger("partial")
inject_context(PartialContext())
logger.info("partial context accepted")
with_context¶
Decorator that automatically injects invocation context.
Can be used with or without arguments::
@with_context
def handler(req, context):
...
@with_context(param="ctx")
def handler(req, ctx):
...
The decorator:
- Finds the
contextparameter (by name, default"context") - Calls
inject_context(context)before the handler body - Restores the previous context in
finallyafter the handler returns
Both sync and async handlers are supported.
See Also
How correlation works, how the id reaches your handler: https://yeongseon.dev/azure-functions-python/logging/how-correlation-works/#2-how-it-reaches-your-handler
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
_F | None
|
The handler function (when used without parentheses). |
None
|
param
|
str
|
Name of the parameter that receives the Azure Functions
context object. Defaults to |
_DEFAULT_PARAM
|
activate_trace_context
|
bool | None
|
When |
None
|
strict
|
bool
|
When |
False
|
lifecycle
|
bool
|
When |
False
|
lifecycle_level
|
int
|
Log level for the start/end lifecycle records when
|
INFO
|
Warns:
| Type | Description |
|---|---|
RuntimeWarning
|
If the decorated handler cannot receive the context
argument, making injection an ineffective no-op. The Azure Functions
worker only supplies |
Source code in src/azure_functions_logging/_decorator.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | |
get_logging_metadata¶
Return logging metadata if the function was decorated with with_context.
Returns None if the function has no logging metadata attached.
Source code in src/azure_functions_logging/_decorator.py
Usage Notes¶
- Returns the logging metadata dict attached by the
with_contextdecorator, orNonewhen the function was not decorated. - Signature:
get_logging_metadata(func: Any) -> dict[str, Any] | None.
Example¶
from azure_functions_logging import get_logging_metadata, with_context
@with_context
def handler(req, context=None):
...
metadata = get_logging_metadata(handler)
# -> {"version": 1, "context_param": "context"} or None if not decorated
logging_context¶
Context manager wrapping inject_context + restore_context.
Recommended pattern when handlers don't use the with_context decorator::
def handler(req, context):
with logging_context(context):
logger.info("processing")
...
Guarantees context is restored to its previous state even if the body raises, supporting safe nesting of contexts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
Any
|
An Azure Functions context object (func.Context). |
required |
activate_trace_context
|
bool | None
|
When |
None
|
Source code in src/azure_functions_logging/_context.py
propagate_context¶
Bind the current invocation context to func for background-thread execution.
Invocation context is stored in :mod:contextvars, which do not propagate
to :class:~concurrent.futures.ThreadPoolExecutor workers or manually created
:class:threading.Thread targets. Wrapping a callable with
propagate_context snapshots the current invocation context fields
(invocation_id, function_name, trace_id, span_id,
cold_start) at wrap time and re-applies them inside the wrapper when
it later runs on another thread, then restores the previous values on exit so
pooled threads never leak context between tasks.
Wrap the callable inside the invocation whose context should be propagated, immediately before handing work to a thread or executor::
from concurrent.futures import ThreadPoolExecutor
def handler(req, context):
with logging_context(context):
with ThreadPoolExecutor() as pool:
pool.submit(propagate_context(do_work, context=context), payload)
When an Azure Functions context object is supplied, the worker's
thread_local_storage.invocation_id is also set for the duration of the
call (and restored afterwards) so the worker's own logging handler correlates
records emitted from the background thread. This is best-effort and
duck-typed: any missing attribute or error is silently ignored (Principle 3:
context propagation failures never crash the caller). No azure-functions
import is required.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
Callable[_P, _R]
|
The callable to run on a background thread. Called with whatever positional/keyword arguments the returned wrapper receives. |
required |
context
|
Any
|
Optional Azure Functions context object ( |
None
|
Returns:
| Type | Description |
|---|---|
Callable[_P, _R]
|
A wrapper around func that applies the snapshotted context on entry and |
Callable[_P, _R]
|
restores the previous state on exit. Reusable and concurrency-safe: it may |
Callable[_P, _R]
|
be submitted to multiple threads simultaneously. |
See Also
How correlation works, background threads: https://yeongseon.dev/azure-functions-python/logging/how-correlation-works/#4-why-background-threads-lose-the-id
Source code in src/azure_functions_logging/_context.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | |
Usage Notes¶
- Binds the current invocation context to a callable so it can run on a
ThreadPoolExecutorworker or a manually createdthreading.Thread, whichcontextvarsdo not reach on their own. - Wrap inside the invocation, immediately before submitting the work; the context is snapshotted at wrap time.
- Pass
context=contextto also propagate the Azure worker'sthread_local_storage.invocation_id; propagation failures are silent and never crash the caller.
Example: ThreadPoolExecutor¶
from concurrent.futures import ThreadPoolExecutor
import azure.functions as func
from azure_functions_logging import logging_context, propagate_context
def handler(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
with logging_context(context):
with ThreadPoolExecutor() as pool:
future = pool.submit(propagate_context(do_work, context=context), payload)
future.result() # log records from do_work carry the invocation context
return func.HttpResponse("ok")
propagating_executor¶
Return a :class:PropagatingExecutor wrapping pool (or a new pool).
Ergonomic entry point for background-thread context propagation that removes
the per-submit :func:propagate_context boilerplate. See
:class:PropagatingExecutor for full semantics.
Example::
from concurrent.futures import ThreadPoolExecutor
from azure_functions_logging import logging_context, propagating_executor
with logging_context(context):
# Wrap an existing pool ...
pool = propagating_executor(ThreadPoolExecutor(max_workers=4), context=context)
# ... or let the helper create and own one:
with propagating_executor(max_workers=4, context=context) as pool:
pool.submit(do_work, payload)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pool
|
Executor | None
|
Existing executor to wrap, or |
None
|
context
|
Any
|
Optional Azure Functions |
None
|
**pool_kwargs
|
Any
|
Forwarded to a newly created pool; rejected when pool is provided. |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
PropagatingExecutor
|
class: |
PropagatingExecutor
|
/ :meth: |
Source code in src/azure_functions_logging/_context.py
PropagatingExecutor¶
Bases: Executor
An :class:~concurrent.futures.Executor that auto-propagates invocation context.
contextvars do not follow work handed to a
:class:~concurrent.futures.ThreadPoolExecutor worker, so records emitted from
pooled threads lose their invocation_id unless every submitted callable is
wrapped with :func:propagate_context. This executor removes that per-submit
boilerplate: it wraps each callable passed to :meth:submit / :meth:map with
:func:propagate_context at submission time, snapshotting the invocation
context bound on the submitting thread.
Propagation stays explicit and opt-in at the executor boundary — the library
never monkeypatches :mod:threading or :mod:concurrent.futures. Work submitted
to a plain executor is unaffected. Propagation failures never crash the caller
(Principle 3): a failed context application degrades to running the callable
without context rather than raising.
The executor either wraps an existing pool or lazily creates a
:class:~concurrent.futures.ThreadPoolExecutor::
from azure_functions_logging import logging_context, propagating_executor
def handler(req, context):
with logging_context(context):
with propagating_executor(context=context) as pool:
pool.submit(do_work, payload) # record carries invocation_id
Passing context= also propagates the Azure worker's
thread_local_storage.invocation_id for the duration of each call, matching
:func:propagate_context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pool
|
Executor | None
|
An existing :class: |
None
|
context
|
Any
|
Optional Azure Functions |
None
|
**pool_kwargs
|
Any
|
Forwarded to :class: |
{}
|
Source code in src/azure_functions_logging/_context.py
pool
property
¶
The wrapped (or lazily created) underlying executor.
map(fn, *iterables, timeout=None, chunksize=1)
¶
Like :meth:Executor.map, wrapping fn for context propagation.
Source code in src/azure_functions_logging/_context.py
shutdown(wait=True, *, cancel_futures=False)
¶
Shut down the underlying executor (see :meth:Executor.shutdown).
submit(fn, /, *args, **kwargs)
¶
Submit fn, wrapping it so the current invocation context propagates.
Source code in src/azure_functions_logging/_context.py
Usage Notes¶
- Ergonomic alternative to wrapping every callable with
propagate_context: the executor auto-wraps each callable passed tosubmit/mapat submission time, snapshotting the invocation context bound on the submitting thread. - Propagation stays explicit and opt-in at the executor boundary — the
library never monkeypatches
threading/concurrent.futures. Work submitted to a plain executor is unaffected. propagating_executor(pool)wraps an existing executor; with nopoolit creates and owns a newThreadPoolExecutorfrom the forwarded keyword arguments (e.g.max_workers=).- Pass
context=contextto also propagate the Azure worker'sthread_local_storage.invocation_id; propagation failures never crash the caller.
Example: propagating executor¶
import azure.functions as func
from azure_functions_logging import logging_context, propagating_executor
def handler(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
with logging_context(context):
with propagating_executor(max_workers=4, context=context) as pool:
# No per-submit propagate_context() needed — records from do_work
# carry the invocation context.
futures = [pool.submit(do_work, item) for item in payload]
for future in futures:
future.result()
return func.HttpResponse("ok")
reset_context¶
Clear every invocation context variable.
Use this for test teardown or defensive full cleanup. For normal context management, prefer token-based restore::
tokens = inject_context(context)
try:
...
finally:
restore_context(tokens)
because token-based restore preserves any outer context.
Safe to call repeatedly. Setting to None is the documented "absent"
state for every context field (matches ContextVar defaults).
Source code in src/azure_functions_logging/_context.py
restore_context¶
Restore context variables to their previous state using tokens.
Tokens are single-use and must be restored in the same context where
they were created. Calling this function twice with the same tokens
raises RuntimeError from contextvars.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tokens
|
ContextTokens
|
Mapping returned by |
required |
Source code in src/azure_functions_logging/_context.py
ContextTokens¶
End-to-End API Example¶
import logging
import azure.functions as func
from azure_functions_logging import get_logger, inject_context, setup_logging
setup_logging(level=logging.INFO, format="json")
logger = get_logger(__name__)
app = func.FunctionApp()
@app.route(route="orders")
def orders(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
inject_context(context)
req_logger = logger.bind(route="/orders", method=req.method)
req_logger.info("orders request started")
req_logger.info("orders request completed")
return func.HttpResponse("ok")