Part of the Azure Functions Python DX Toolkit — dogfood-tested by azure-functions-cookbook-python.
Read this in: 한국어 | 日本語 | 简体中文
Invocation-aware observability for Azure Functions Python v2.
Surfaces invocation_id, detects cold starts, warns on host.json misconfig, and outputs Application Insights-ready structured logs — without replacing Python's standard logging.
Part of the Azure Functions Python DX Toolkit → Bring FastAPI-like developer experience to Azure Functions
Azure Functions Python logging has specific failure modes that generic logging libraries don't address:
| Problem | What happens | This library |
|---|---|---|
host.json log level conflict |
Your INFO logs silently disappear in Azure |
Detects and warns at startup |
No invocation_id in logs |
Impossible to correlate logs to a specific execution | Auto-injects from context object |
| Cold start invisible | No signal when a new worker instance starts | Detects automatically on first inject_context() |
| Noisy third-party loggers | azure-core, urllib3 flood your Application Insights |
SamplingFilter / RedactionFilter |
| Local vs cloud output mismatch | Colorized output breaks in production pipelines | Environment-aware formatter switching |
| PII leaking into logs | Sensitive values accidentally logged as extra fields | RedactionFilter with key-based redaction |
- Invocation context — auto-injects
invocation_id,function_name,cold_startinto every log - Structured JSON output — Application Insights-ready NDJSON format for production
- Noise control —
SamplingFilterrate-limits chatty third-party loggers - PII protection —
RedactionFiltermasks sensitive fields before they reach log aggregation
Scope disclaimer. This package writes structured JSON to Python
logging/ stdout. How those fields appear in Application Insights depends on the Azure Functions host, worker, logging configuration, and ingestion pipeline. The library does not own ingestion or schema mapping — bothcustomDimensions-parsed and raw-messageshapes are valid in production.
flowchart TD
A["setup_logging()"] -->|Azure / Core Tools| B[Azure host handler]
A -->|local dev| C[Console/Color handler]
D["inject_context() / with_context / logging_context"] --> E[contextvars]
E --> F{injection mode}
F -->|"default"| G[ContextFilter]
F -->|"use_record_factory=True"| H[LogRecordFactory]
G --> I[FunctionLogger]
H --> I
B --> I
C --> I
I --> J[JsonFormatter / ColorFormatter]
J --> K[Host / stdout → Application Insights]
The two injection modes are mutually exclusive: do not attach
ContextFilterwhenuse_record_factory=True.
Without azure-functions-logging — plain print() output, no context, no structure:
import azure.functions as func
app = func.FunctionApp()
@app.route(route="orders")
def process_order(req: func.HttpRequest) -> func.HttpResponse:
print("Processing order") # no invocation_id, no structure
print(f"Order: {req.get_json()}") # PII may leak, no log level
return func.HttpResponse("OK")Terminal output:
Processing order
Order: {'customer': 'Alice', 'total': 99.99}
No invocation ID. No log level. Hard to correlate in Application Insights.
With azure-functions-logging — structured, queryable, production-ready:
import azure.functions as func
from azure_functions_logging import JsonFormatter, get_logger, logging_context, setup_logging
setup_logging(functions_formatter=JsonFormatter())
logger = get_logger(__name__)
app = func.FunctionApp()
@app.route(route="orders")
def process_order(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
with logging_context(context):
logger.info("Processing order", order_id="o-999")
return func.HttpResponse("OK")Local terminal output when run standalone (e.g. python app.py, color formatter):
10:30:00 INFO function_app Processing order [invocation_id=abc-123-def, function_name=process_order, cold_start=true]
Production output under func start / Azure (Application Insights NDJSON, applied because functions_formatter is set):
{"timestamp": "2024-01-15T10:30:00+00:00", "level": "INFO", "logger": "function_app",
"message": "Processing order", "invocation_id": "abc-123-def",
"function_name": "process_order", "trace_id": null, "cold_start": true,
"exception": null, "extra": {"order_id": "o-999"}}Every log carries
invocation_idandcold_start. Queryable in Application Insights. Zeroprint()statements.
Note: The exact Application Insights schema depends on your ingestion pipeline. In some deployments JSON fields are parsed into
customDimensions; in others the JSON stays inside themessagecolumn. Examples for both shapes are below.
The following screenshots are from a real deployed Azure Functions app queried in Application Insights Logs.
Before — plain logging.info(), no azure-functions-logging (context fields not injected — invocation_id, function_name, cold_start are absent from the payload):
After — azure-functions-logging with inject_context(context) (invocation_id, function_name, cold_start populated):
Drill-down by invocation_id — one query, one execution, all logs in sequence:
Transaction Search — visual execution timeline with cold_start, structured fields, and per-event offsets:
traces
| where customDimensions.invocation_id == "abc-123-def"
| project timestamp, message, customDimensions.cold_start, customDimensions.function_name
| order by timestamp ascFind all cold starts in the last hour:
traces
| where customDimensions.cold_start == "true"
| where timestamp > ago(1h)
| summarize count() by bin(timestamp, 5m)traces
| extend payload = parse_json(message)
| where tostring(payload.invocation_id) == "abc-123-def"
| project timestamp, tostring(payload.message), tostring(payload.cold_start), tostring(payload.function_name)
| order by timestamp ascFind all cold starts in the last hour:
traces
| extend payload = parse_json(message)
| where tostring(payload.cold_start) == "true"
| where timestamp > ago(1h)
| summarize count() by bin(timestamp, 5m)This package does not own:
- Replacing stdlib logging — it wraps and enriches Python's standard
logging, never replaces it - Distributed tracing — use OpenTelemetry or Application Insights SDK for end-to-end trace correlation
- API documentation — use
azure-functions-openapifor API documentation and spec generation
pip install azure-functions-loggingimport azure.functions as func
from azure_functions_logging import get_logger, logging_context, setup_logging
setup_logging()
logger = get_logger(__name__)
app = func.FunctionApp()
@app.route(route="hello")
def hello(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
with logging_context(context): # binds invocation_id, function_name, cold_start; restores previous context on exit
logger.info("Request received")
# log record now carries invocation_id, function_name, cold_start
return func.HttpResponse("OK")logging_context is the recommended primary pattern: it injects context on enter and always restores the previous context on exit (even when the handler raises), which prevents stale context from leaking into the next invocation on a reused worker.
For lower-level control or when integrating with custom middleware, use token-based restore:
from azure_functions_logging import inject_context, restore_context
# Assumes `logger` and `context` are in scope (see Quick Start).
tokens = inject_context(context)
try:
logger.info("Request received")
finally:
restore_context(tokens)Use reset_context() only when you intentionally want to clear all context (e.g. test teardown).
Start the Functions host locally (using the e2e example app):
func start --script-root examples/e2e_appAfter deploying (see docs/deployment.md), the same request produces the same response in both environments.
curl -s http://localhost:7071/api/logme?correlation_id=demo-123{"logged": true, "correlation_id": "demo-123"}curl -s "https://<your-app>.azurewebsites.net/api/logme?correlation_id=demo-123"{"logged": true, "correlation_id": "demo-123"}Verified against a temporary Azure Functions deployment in koreacentral (Python 3.12, Consumption plan). Response captured and URL anonymized.
Every capability below has a full how-to on the documentation site — this section summarizes what each does and links to the single source, so the README stays a quick overview rather than a second copy of the docs.
logging_context(context) (see Quick Start) binds invocation_id, function_name, trace_id, and cold_start for the duration of a handler and always restores the previous context on exit. For lower-level control use inject_context() / restore_context(), or the @with_context decorator to inject implicitly (sync and async handlers).
cold_startsemantics.cold_start=Truemeans the first invocation observed by this Python worker process after module load — not a platform-level cold-start metric.
→ Usage: context injection · API: with_context
Pass setup_logging(functions_formatter=JsonFormatter()) to emit Application Insights-ready NDJSON on host-managed handlers (or format="json" for standalone/CI). Extra fields land under extra; opt into truncate_native_strings=True to clip long string values.
→ Usage: JSON output · API: JsonFormatter
At startup the library warns when your host.json — or AzureFunctionsJobHost__logging__logLevel__... app-setting overrides — suppresses levels your app emits. host.json is auto-discovered by walking up from the working directory (or AzureWebJobsScriptRoot); pass host_json_path= to override.
→ Configuration: host.json conflict · Troubleshooting
SamplingFilter rate-limits chatty third-party loggers (e.g. azure-core, urllib3); RedactionFilter masks sensitive keys (passwords, tokens, secrets, connection strings, and more — case-insensitive, recursive) before logs reach aggregation. Attach either to your root handlers, and pass sensitive_keys=[...] to customize redaction.
→ API: SamplingFilter · API: RedactionFilter
logger.bind(key=value) returns a logger that attaches request-scoped metadata to every subsequent log without threading it through each call. Create bound loggers per-invocation; don't cache them at module level.
install_context_factory() injects context at record-creation time so every LogRecord carries it regardless of handler/filter wiring — useful when handlers are added after setup_logging() or loggers bypass the filter chain. It is mutually exclusive with the default ContextFilter mode.
→ Configuration: use_record_factory · API: install_context_factory
setup_logging() detects FUNCTIONS_WORKER_RUNTIME: colorized human-readable output locally, host-managed NDJSON in Azure / Core Tools (context filters only — no duplicate handlers), and machine-parseable JSON in CI.
→ Configuration: environment detection
- You need structured, queryable logs in Application Insights
- You want
invocation_idcorrelation across all logs for a single request - You need cold start detection without custom instrumentation
- You want PII redaction or noise control for third-party loggers
- Your
host.jsonconfig silently suppresses logs and you don't know why
- Full docs: yeongseon.github.io/azure-functions-logging-python
- Configuration reference
- Troubleshooting guide
- API reference
This package is part of the Azure Functions Python DX Toolkit.
Design principle: azure-functions-logging owns structured logging and invocation-aware observability. It enriches Python's standard logging — it does not replace it. Adjacent concerns belong to azure-functions-openapi (API documentation and spec generation), azure-functions-validation (request/response validation and serialization), and azure-functions-langgraph (LangGraph runtime exposure).
| Package | Role |
|---|---|
| azure-functions-openapi-python | OpenAPI spec generation and Swagger UI |
| azure-functions-validation-python | Request/response validation and serialization |
| azure-functions-db-python | SQLAlchemy-powered DB integration helpers (poll-based pseudo trigger, input/output/client injection) |
| azure-functions-langgraph-python | LangGraph deployment adapter for Azure Functions |
| azure-functions-scaffold-python | Project scaffolding CLI |
| azure-functions-logging-python | Structured logging and observability |
| azure-functions-doctor-python | Pre-deploy diagnostic CLI |
| azure-functions-durable-graph-python | Manifest-first graph runtime with Durable Functions (experimental) |
| azure-functions-knowledge-python | Knowledge retrieval (RAG) decorators |
| azure-functions-cookbook-python | Dogfood examples — runnable recipes that exercise the full toolkit |
This package provides structured logging for Azure Functions with zero modifications to stdlib logging.
LLM-Friendly Resources:
llms.txt— Concise API reference and quick start (repo root)llms-full.txt— Complete API signatures, patterns, and design principles (repo root)
Key Implementation Details for Code Generation:
- Preserves host configuration — In Azure / Core Tools, no handlers are added and the root logger level is left to
host.json;ContextFilteris installed on existing root handlers and on the root logger itself (so direct calls on the root logger carry context). For records that propagate from named child loggers to handlers attached later (e.g. by the host or third-party libraries), passuse_record_factory=Truetosetup_logging()to guarantee context coverage (the standaloneinstall_context_factory()is deprecated). In standalone local mode,setup_logging(logger_name=None)configures the root logger (sets level, adds aStreamHandlerif none exist). - Context injection is contextvar-based — Not thread-local, works with asyncio
- Idempotent setup — Calling setup_logging() multiple times is safe
- Two environments, two behaviors:
- Azure/Core Tools: install
ContextFilteron existing root handlers and on the root logger itself; do not add handlers or change the root level (respectshost.json). - Standalone local: set the target/root logger level; add a
StreamHandler(ColorFormatter or JsonFormatter) only if no handlers exist, otherwise just attach filters to existing handlers.
- Azure/Core Tools: install
- Test-friendly:
inject_context()accepts any object (no hard dependency on azure.functions.Context)with_contextdecorator works with sync and async handlers- Use
reset_context()in test teardown if needed
When generating code:
- Import only from
azure_functions_loggingpublic API (no underscores) - Call
setup_logging()at module level or handler startup (not per-request) - Prefer
with logging_context(context):in handlers; use rawinject_context(context)only withtry/finally restore_context(tokens) - Use
logger.bind(key=value)for per-request fields (not direct logger.extra) - Use
with_contextdecorator if you prefer to inject context implicitly per-handler - Call
get_logging_metadata(func)to inspect@with_contextmetadata on a function (returnsdict[str, Any] | None) - Apply
RedactionFilterfor PII fields,SamplingFilterfor high-volume logs
Example Pattern:
from azure_functions_logging import get_logger, logging_context, setup_logging
# Module level
setup_logging()
logger = get_logger(__name__)
# Per handler
def my_function(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
with logging_context(context):
req_logger = logger.bind(correlation_id=req.params.get("id"))
req_logger.info("Processing")
return func.HttpResponse("OK")This project is an independent community project and is not affiliated with, endorsed by, or maintained by Microsoft.
Azure and Azure Functions are trademarks of Microsoft Corporation.
MIT





