Observability & Telemetry Patterns
Production-ready observability for Azure Container Apps using OpenTelemetry and Application Insights. Every application in this template includes structured logging, distributed tracing, and health checks.
Community skills: For additional monitoring skills, see the microsoft/skills community repository.
Core Requirements
Every application MUST have:
- Structured logging — JSON-formatted, never
print()/console.log() - Health check endpoint —
GET /healthreturning 200 OK - OpenTelemetry tracing — Distributed traces exported to Application Insights
- Configuration via environment —
APPLICATION_INSIGHTS_CONNECTION_STRINGandLOG_LEVEL
1. Python — FastAPI / Gunicorn
Dependencies (pyproject.toml)
dependencies = [
"opentelemetry-api>=1.27.0,<2.0.0",
"opentelemetry-sdk>=1.27.0,<2.0.0",
"opentelemetry-instrumentation-fastapi>=0.48.0,<0.49.0",
"opentelemetry-instrumentation-httpx>=0.48.0,<0.49.0",
"azure-monitor-opentelemetry-exporter>=1.0.0,<2.0.0",
]
Tracing Setup (utils/tracing.py)
import logging
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
logger = logging.getLogger(__name__)
def setup_tracing(
service_name: str,
service_version: str = "1.0.0",
connection_string: str | None = None,
) -> None:
"""Initialize OpenTelemetry tracing with Azure Monitor export."""
resource = Resource.create({
"service.name": service_name,
"service.version": service_version,
})
provider = TracerProvider(resource=resource)
if connection_string:
exporter = AzureMonitorTraceExporter(
connection_string=connection_string
)
provider.add_span_processor(BatchSpanProcessor(exporter))
logger.info("Azure Monitor tracing enabled")
else:
logger.warning("No APPLICATION_INSIGHTS_CONNECTION_STRING — tracing to console only")
trace.set_tracer_provider(provider)
# Auto-instrument frameworks
FastAPIInstrumentor.instrument()
HTTPXClientInstrumentor.instrument()
Structured Logging (utils/logging_config.py)
import logging
import json
import sys
from datetime import datetime, timezone
class JsonFormatter(logging.Formatter):
"""JSON-formatted log output for production environments."""
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info and record.exc_info[0] is not None:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data)
def setup_logging(log_level: str = "INFO") -> None:
"""Configure structured logging for the application."""
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
root_logger.handlers = [handler]
# Quiet noisy libraries
logging.getLogger("azure").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
Health Check (main.py)
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health_check():
"""Health check endpoint for container orchestration."""
return {"status": "healthy", "service": "my-app"}
Application Startup (main.py)
import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from utils.tracing import setup_tracing
from utils.logging_config import setup_logging
@asynccontextmanager
async def lifespan(app: FastAPI):
setup_logging(os.getenv("LOG_LEVEL", "INFO"))
setup_tracing(
service_name="my-app",
connection_string=os.getenv("APPLICATION_INSIGHTS_CONNECTION_STRING"),
)
logger = logging.getLogger(__name__)
logger.info("Application started")
yield
logger.info("Application shutting down")
app = FastAPI(lifespan=lifespan)
2. Python — Gradio
Gradio applications use the same tracing/logging modules. Key difference: Gradio has its own server, so instrument with a custom middleware.
import gradio as gr
import logging
from utils.logging_config import setup_logging
from utils.tracing import setup_tracing
setup_logging(os.getenv("LOG_LEVEL", "INFO"))
setup_tracing(
service_name="my-gradio-app",
connection_string=os.getenv("APPLICATION_INSIGHTS_CONNECTION_STRING"),
)
logger = logging.getLogger(__name__)
demo = gr.Blocks()
with demo:
gr.Markdown("# My App")
# ... components ...
if __name__ == "__main__":
logger.info("Starting Gradio application")
demo.launch(
server_name="0.0.0.0",
server_port=int(os.getenv("GRADIO_SERVER_PORT", "80")),
)
3. Python — Streamlit
Streamlit runs its own web server. Configure logging early in the entry point.
# streamlit_app.py
import streamlit as st
import logging
import os
from utils.logging_config import setup_logging
setup_logging(os.getenv("LOG_LEVEL", "INFO"))
logger = logging.getLogger(__name__)
st.set_page_config(page_title="My App", layout="wide")
# Health check: Streamlit exposes /_stcore/health automatically
logger.info("Streamlit application loaded")
Streamlit health check: Built-in at /_stcore/health. No custom endpoint needed.
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:80/_stcore/health || exit 1
4. TypeScript — Express / Node.js
Dependencies (package.json)
{
"dependencies": {
"@azure/monitor-opentelemetry": "^1.7.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-node": "^0.54.0",
"@opentelemetry/instrumentation-express": "^0.42.0",
"winston": "^3.14.0"
}
}
Tracing Setup (utils/tracing.ts)
import { useAzureMonitor, AzureMonitorOpenTelemetryOptions } from "@azure/monitor-opentelemetry";
export function setupTracing(connectionString?: string): void {
if (!connectionString) {
console.warn("No APPLICATION_INSIGHTS_CONNECTION_STRING — tracing disabled");
return;
}
const options: AzureMonitorOpenTelemetryOptions = {
azureMonitorExporterOptions: { connectionString },
instrumentationOptions: {
http: { enabled: true },
},
};
useAzureMonitor(options);
}
Structured Logging (utils/logger.ts)
import winston from "winston";
const logger = winston.createLogger({
level: process.env.LOG_LEVEL?.toLowerCase() || "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [new winston.transports.Console()],
});
// Suppress verbose library logs
logger.on("error", () => {});
export default logger;
Health Check (routes/health.ts)
import { Router, Request, Response } from "express";
const router = Router();
router.get("/health", (_req: Request, res: Response) => {
res.json({ status: "healthy", service: "my-node-app" });
});
export default router;
5. TypeScript — React (Client-side)
Dependencies (package.json)
{
"dependencies": {
"@microsoft/applicationinsights-react-js": "^17.3.0",
"@microsoft/applicationinsights-web": "^3.3.0"
}
}
Telemetry Service (services/telemetry.ts)
import { ApplicationInsights } from "@microsoft/applicationinsights-web";
import { ReactPlugin } from "@microsoft/applicationinsights-react-js";
const reactPlugin = new ReactPlugin();
let appInsights: ApplicationInsights | null = null;
export function initializeTelemetry(connectionString: string): void {
if (!connectionString || appInsights) return;
appInsights = new ApplicationInsights({
config: {
connectionString,
extensions: [reactPlugin],
enableAutoRouteTracking: true,
enableCorsCorrelation: true,
},
});
appInsights.loadAppInsights();
}
export function trackEvent(name: string, properties?: Record<string, string>): void {
appInsights?.trackEvent({ name }, properties);
}
export function trackException(error: Error): void {
appInsights?.trackException({ exception: error });
}
export { reactPlugin };
Structured Logger (utils/logger.ts)
type LogLevel = "debug" | "info" | "warn" | "error";
const LOG_LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };
const currentLevel = (import.meta.env.VITE_LOG_LEVEL?.toLowerCase() || "info") as LogLevel;
function shouldLog(level: LogLevel): boolean {
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
}
export const logger = {
debug: (msg: string, data?: unknown) => shouldLog("debug") && console.debug(JSON.stringify({ level: "DEBUG", msg, data, ts: new Date().toISOString() })),
info: (msg: string, data?: unknown) => shouldLog("info") && console.info(JSON.stringify({ level: "INFO", msg, data, ts: new Date().toISOString() })),
warn: (msg: string, data?: unknown) => shouldLog("warn") && console.warn(JSON.stringify({ level: "WARN", msg, data, ts: new Date().toISOString() })),
error: (msg: string, data?: unknown) => shouldLog("error") && console.error(JSON.stringify({ level: "ERROR", msg, data, ts: new Date().toISOString() })),
};
6. .NET — ASP.NET Core
Dependencies (.csproj)
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
Program.cs Setup
using Serilog;
using Azure.Monitor.OpenTelemetry.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Structured logging with Serilog
builder.Host.UseSerilog((context, configuration) =>
configuration
.ReadFrom.Configuration(context.Configuration)
.WriteTo.Console(outputTemplate:
"{Timestamp:yyyy-MM-ddTHH:mm:ss.fffZ} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
);
// OpenTelemetry with Azure Monitor
builder.Services.AddOpenTelemetry()
.UseAzureMonitor();
// Health checks
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.MapControllers();
app.Run();
7. Bicep — Application Insights Resource
module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.12.0' = {
name: 'log-analytics'
params: {
name: '${abbrs.operationalInsightsWorkspaces}${environmentName}'
location: location
tags: tags
}
}
module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = {
name: 'application-insights'
params: {
name: '${abbrs.insightsComponents}${environmentName}'
location: location
tags: tags
workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
}
}
// Output for application consumption
output APPLICATION_INSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString
Container App Environment Variables
environmentVariables: [
{
name: 'APPLICATION_INSIGHTS_CONNECTION_STRING'
value: monitoring.outputs.applicationInsightsConnectionString
}
{
name: 'LOG_LEVEL'
value: 'INFO'
}
]
8. Dockerfile Health Checks
Python (FastAPI)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:80/health').raise_for_status()"
Node.js
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "fetch('http://localhost:80/health').then(r => r.ok ? process.exit(0) : process.exit(1))"
.NET
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:80/health || exit 1
Streamlit
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:80/_stcore/health || exit 1
Anti-Patterns
| Don't | Do Instead |
|---|---|
print("Debug: value =", x) |
logger.debug("value=%s", x) |
console.log("error", err) |
logger.error("message", { error: err }) |
| Hard-coded connection string | os.getenv("APPLICATION_INSIGHTS_CONNECTION_STRING") |
| No health endpoint | GET /health returning JSON status |
| Verbose framework logs in prod | Set library loggers to WARNING |
References
- Azure Monitor OpenTelemetry
- Application Insights for Python
- Application Insights for Node.js
- Application Insights for .NET
Converted and distributed by TomeVault — claim your Tome and manage your conversions.