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 fields logged in exception tracebacks | RedactionFilter with pattern matching |
- 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
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 get_logger, inject_context, setup_logging
setup_logging()
logger = get_logger(__name__)
app = func.FunctionApp()
@app.route(route="orders")
def process_order(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
inject_context(context)
logger.info("Processing order", order_id="o-999")
return func.HttpResponse("OK")Local terminal output (colorized):
10:30:00 INFO function_app Processing order [invocation_id=abc-123-def, function_name=process_order, cold_start=true]
Production output (NDJSON for Application Insights):
{"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. The queries below assume structured JSON fields appear in
customDimensions.
Once your logs flow as structured JSON, query them in Application Insights:
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)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, inject_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:
inject_context(context) # binds invocation_id, function_name, cold_start
logger.info("Request received")
# {"level": "INFO", "invocation_id": "abc-123", "cold_start": true, ...}
return func.HttpResponse("OK")Start the Functions host locally (using the e2e example app):
func startAfter 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.
inject_context(context) should be the first line of every handler. It binds:
invocation_id— unique per execution, correlates all logs for one requestfunction_name— the Azure Functions function nametrace_id— trace context from the platformcold_start—Trueon first invocation of this worker process
def my_function(req, context):
inject_context(context)
logger.info("handler started")
# every log from here carries invocation_id and cold_startWithout inject_context(), these fields are None in every log line.
For less boilerplate, use the with_context decorator instead of calling inject_context() manually:
import azure.functions as func
from azure_functions_logging import get_logger, setup_logging, with_context
setup_logging()
logger = get_logger(__name__)
app = func.FunctionApp()
@app.route(route="hello")
@with_context
def hello(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
logger.info("Request received")
return func.HttpResponse("OK")The decorator finds the context parameter by name, calls inject_context() before your handler runs, and resets context variables in finally after it returns.
Custom parameter name:
@with_context(param="ctx")
def hello(req: func.HttpRequest, ctx: func.Context) -> func.HttpResponse:
...Both sync and async handlers are supported.
Use JSON format when logs feed Application Insights or any aggregation system:
setup_logging(format="json")Output per log line (NDJSON — one JSON object per line):
{"timestamp": "2024-01-15T10:30:00+00:00", "level": "INFO", "logger": "my_module",
"message": "order accepted", "invocation_id": "abc-123", "function_name": "OrderHandler",
"cold_start": false, "trace_id": "00-abc...", "exception": null,
"extra": {"order_id": "o-999"}}Extra fields appear in extra and are indexable in Application Insights:
logger.info("order accepted", order_id="o-999", tenant_id="t-1")If your host.json suppresses log levels that your app emits, you get this warning at startup:
WARNING: host.json logLevel.default is 'Warning'. Logs below WARNING will be suppressed in Azure.
Recommended host.json baseline:
{
"version": "2.0",
"logging": {
"logLevel": {
"default": "Information",
"Function": "Information"
}
}
}Suppress chatty third-party loggers without removing them:
from azure_functions_logging import SamplingFilter, setup_logging
import logging
setup_logging()
# Only log 1 in 10 azure-core messages
logging.getLogger("azure").addFilter(SamplingFilter(rate=0.1))
# Silence urllib3 completely in production
logging.getLogger("urllib3").setLevel(logging.WARNING)Strip sensitive fields before they reach Application Insights:
from azure_functions_logging import RedactionFilter, setup_logging
import logging
setup_logging()
root = logging.getLogger()
root.addFilter(RedactionFilter(patterns=["password", "token", "secret"]))Any log record where the message or extra fields match a pattern will have those values replaced with [REDACTED].
| Environment | Format | Behavior |
|---|---|---|
| Local terminal | color (default) |
Colorized [TIME] [LEVEL] [LOGGER] message |
| Azure / Core Tools | json |
NDJSON, no ANSI codes, host-managed handlers |
| CI / pipeline | json |
NDJSON, machine-parseable |
setup_logging() detects FUNCTIONS_WORKER_RUNTIME and WEBSITE_INSTANCE_ID to choose the right path automatically. In Azure, it installs context filters without adding handlers (avoids duplicate output from the host pipeline).
Attach request-scoped metadata to every log without passing it through every call:
def process_order(order_id: str) -> None:
order_logger = logger.bind(order_id=order_id, region="eastus")
order_logger.info("processing started") # includes order_id + region
order_logger.info("processing complete") # same metadata, new messageCreate bound loggers per-invocation. Do not cache them at module level.
- 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
- 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 | OpenAPI spec generation and Swagger UI |
| azure-functions-validation | Request/response validation and serialization |
| azure-functions-db | Database bindings for SQL, PostgreSQL, MySQL, SQLite, and Cosmos DB |
| azure-functions-langgraph | LangGraph deployment adapter for Azure Functions |
| azure-functions-scaffold | Project scaffolding CLI |
| azure-functions-logging | Structured logging and observability |
| azure-functions-doctor | Pre-deploy diagnostic CLI |
| azure-functions-durable-graph | Manifest-first graph runtime with Durable Functions (experimental) |
| azure-functions-python-cookbook | Recipes and examples |
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:
- Never modify root logger — Only install filters/formatters on handlers
- 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: Only install filters on existing root handlers (respects host.json)
- Local dev: Add ColorFormatter or JsonFormatter handler to specified logger
- Test-friendly:
inject_context()accepts any object (no hard dependency on azure.functions.Context)with_contextdecorator works with sync and async handlers- Use
context.reset_context_vars()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) - Call
inject_context(context)as first line of handler - Use
logger.bind(key=value)for per-request fields (not direct logger.extra) - Apply
RedactionFilterfor PII fields,SamplingFilterfor high-volume logs
Example Pattern:
from azure_functions_logging import setup_logging, get_logger, inject_context
# Module level
setup_logging()
logger = get_logger(__name__)
# Per handler
def my_function(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
inject_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