Skip to main content

Azure Functions Logging

Part of the Azure Functions Python DX Toolkit — dogfood-tested by azure-functions-cookbook-python.

PyPI Downloads Python Version CI Release Security Scans codecov pre-commit Docs License: MIT

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

Why this exists

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
Worker logs orphaned from the invocation trace Python is the only Functions worker runtime without built-in OpenTelemetry invocation middleware, so worker log records are orphaned with span_id=0 unless you activate a span in every handler yourself Binds the host's W3C trace context so your OpenTelemetry logs inherit the invocation's trace_id / span_id

What it does

  • Invocation context — auto-injects invocation_id, function_name, cold_start, and host_instance_id (the scaled-out worker instance) into every log
  • Structured JSON output — Application Insights-ready NDJSON format for production
  • Noise controlSamplingFilter rate-limits chatty third-party loggers
  • PII protectionRedactionFilter masks 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 — both customDimensions-parsed and raw-message shapes are valid in production.

OpenTelemetry trace correlation

Python is the only Azure Functions worker runtime without built-in OpenTelemetry invocation middleware. The host emits a W3C traceparent for every invocation, but the Python worker never activates it in-process — so unless you activate a span yourself, worker log records are stamped with span_id=0 and get orphaned from the host's invocation span.

You can close this gap by hand, but it's manual and easy to forget. structlog, Loguru, and stdlib logging all rely on OpenTelemetry's LoggingHandler / LoggingInstrumentor, which only stamp trace_id / span_id when there is an active span in the process. Because the Python worker never activates one, Microsoft's documented pattern is to extract the host's traceparent and start a span yourself — in every handler. Miss it in one path and those records silently fall back to span_id=0.

azure-functions-logging fills that gap. Opt in with activate_trace_context=True (requires the [otel] extra) and the library binds the host's W3C trace context for the duration of the handler, so your existing OpenTelemetry log records inherit the invocation's trace_id / span_id:

from azure_functions_logging import logging_context, setup_logging

setup_logging(activate_trace_context=True)  # requires: pip install azure-functions-logging[otel]

with logging_context(context):
    logger.info("processing")  # OpenTelemetry record inherits the invocation trace_id / span_id

This is correlation, not tracing — the library never creates, records, or exports spans itself. It is complementary to (not a replacement for) OpenTelemetry or the Application Insights SDK, which remain responsible for producing spans. See the OpenTelemetry trace correlation guide.

Prefer per-call activation? Skip the process-wide default and pass it directly: with logging_context(context, activate_trace_context=True):.

Pipeline at a glance

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 ContextFilter when use_record_factory=True.

Before / After

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}

Local terminal — without azure-functions-logging

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"}}

Local terminal — with azure-functions-logging

Every log carries invocation_id and cold_start. Queryable in Application Insights. Zero print() 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 the message column. Examples for both shapes are below.

Application Insights — Before / After

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):

App Insights Logs — before

Afterazure-functions-logging with inject_context(context) (invocation_id, function_name, cold_start populated):

App Insights Logs — after

Drill-down by invocation_id — one query, one execution, all logs in sequence:

App Insights Logs — invocation drill-down

Transaction Search — visual execution timeline with cold_start, structured fields, and per-event offsets:

App Insights Transaction Search

Query in Application Insights

When JSON fields are parsed into customDimensions

traces
| where customDimensions.invocation_id == "abc-123-def"
| project timestamp, message, customDimensions.cold_start, customDimensions.function_name
| order by timestamp asc

Find all cold starts in the last hour:

traces
| where customDimensions.cold_start == "true"
| where timestamp > ago(1h)
| summarize count() by bin(timestamp, 5m)

When JSON remains in the message column

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 asc

Find 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)

What this package does not do

This package does not own:

  • Replacing stdlib logging — it wraps and enriches Python's standard logging, never replaces it
  • Distributed tracing — it binds the Azure Functions host's W3C trace context so your existing OpenTelemetry log records inherit the invocation span's trace_id / span_id, but it never creates, records, or exports spans itself — correlation, not tracing. Use OpenTelemetry or the Application Insights SDK to produce spans. See OpenTelemetry trace correlation
  • API documentation — use azure-functions-openapi for API documentation and spec generation

Installation

pip install azure-functions-logging

Quick Start

import 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_app

Verify locally and on Azure

After deploying (see docs/deployment.md), the same request produces the same response in both environments.

Local

curl -s http://localhost:7071/api/logme?correlation_id=demo-123
{"logged": true, "correlation_id": "demo-123"}

Azure

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.

Core capabilities

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.

Invocation context

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_start semantics. cold_start=True means the first invocation observed by this Python worker process after module load — not a platform-level cold-start metric.

Worker instance. Every record also carries host_instance_id, a best-effort identifier of the worker instance that produced the log (resolved from WEBSITE_INSTANCE_IDWEBSITE_POD_NAMECONTAINER_NAMEsocket.gethostname()). It is complementary to, but not guaranteed equal to, Application Insights' cloud_RoleInstance.

Usage: context injection · API: with_context

Structured JSON output

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

host.json conflict detection

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

Noise control & PII redaction

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

Context binding

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.

Usage: context binding

Global LogRecordFactory (opt-in)

setup_logging(use_record_factory=True) installs a global LogRecordFactory that 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

Local vs cloud

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

When to use

  • You need structured, queryable logs in Application Insights
  • You want invocation_id correlation 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.json config silently suppresses logs and you don't know why

Documentation

Ecosystem

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

For AI Coding Assistants

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:

  1. Preserves host configuration — In Azure / Core Tools, no handlers are added and the root logger level is left to host.json; ContextFilter is 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), pass use_record_factory=True to setup_logging() to guarantee context coverage. In standalone local mode, setup_logging(logger_name=None) configures the root logger (sets level, adds a StreamHandler if none exist).
  2. Context injection is contextvar-based — Not thread-local, works with asyncio
  3. Idempotent setup — Calling setup_logging() multiple times is safe
  4. Two environments, two behaviors:
    • Azure/Core Tools: install ContextFilter on existing root handlers and on the root logger itself; do not add handlers or change the root level (respects host.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.
  5. Test-friendly:
    • inject_context() accepts any object (no hard dependency on azure.functions.Context)
    • with_context decorator works with sync and async handlers
    • Use reset_context() in test teardown if needed

When generating code:

  • Import only from azure_functions_logging public API (no underscores)
  • Call setup_logging() at module level or handler startup (not per-request)
  • Prefer with logging_context(context): in handlers; use raw inject_context(context) only with try/finally restore_context(tokens)
  • Use logger.bind(key=value) for per-request fields (not direct logger.extra)
  • Use with_context decorator if you prefer to inject context implicitly per-handler
  • Call get_logging_metadata(func) to inspect @with_context metadata on a function (returns dict[str, Any] | None)
  • Apply RedactionFilter for PII fields, SamplingFilter for 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")

Disclaimer

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.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

azure_functions_logging-0.10.1.tar.gz (877.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

azure_functions_logging-0.10.1-py3-none-any.whl (49.5 kB view details)

Uploaded Python 3

File details

Details for the file azure_functions_logging-0.10.1.tar.gz.

File metadata

  • Download URL: azure_functions_logging-0.10.1.tar.gz
  • Upload date:
  • Size: 877.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for azure_functions_logging-0.10.1.tar.gz
Algorithm Hash digest
SHA256 e0c0eab3af32ab01a9498c6d65336231ff0eb783a6b804c5ff05379d5ba351c0
MD5 05ae9503bd5e19438310cac66297fa40
BLAKE2b-256 2f244a662e4cadbbb00a18d455d0e3d28e0d8e57f24329beea3250a87dd59d3a

See more details on using hashes here.

Provenance

The following attestation bundles were made for azure_functions_logging-0.10.1.tar.gz:

Publisher: publish-pypi.yml on yeongseon/azure-functions-logging-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file azure_functions_logging-0.10.1-py3-none-any.whl.

File metadata

File hashes

Hashes for azure_functions_logging-0.10.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0467120dbbedbf0cf92df9d0eb118eacd6e8aac44230b9f80fa69e456d9295f8
MD5 c7d7b96317b05181be9d329ef653233e
BLAKE2b-256 32655b32c00ed6436810d648bdcca58951916fabf3f2087bfd24e8ce8069f427

See more details on using hashes here.

Provenance

The following attestation bundles were made for azure_functions_logging-0.10.1-py3-none-any.whl:

Publisher: publish-pypi.yml on yeongseon/azure-functions-logging-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.10.2

2 files

This release

0.10.1 This release

2 files

0.10.0

2 files

0.9.0

2 files

0.8.1

2 files

0.8.0

2 files

0.7.7

2 files

0.7.6

2 files

0.7.5

2 files

0.7.4

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.5.3

2 files

0.5.2

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page