-
Notifications
You must be signed in to change notification settings - Fork 36
Add app insights and Logfire instrumentation #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ param openAiEndpoint string | |||||||||||||||||||||||
| param cosmosDbAccount string | ||||||||||||||||||||||||
| param cosmosDbDatabase string | ||||||||||||||||||||||||
| param cosmosDbContainer string | ||||||||||||||||||||||||
| param applicationInsightsConnectionString string = '' | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| resource acaIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { | ||||||||||||||||||||||||
| name: identityName | ||||||||||||||||||||||||
|
|
@@ -58,6 +59,11 @@ module app 'core/host/container-app-upsert.bicep' = { | |||||||||||||||||||||||
| name: 'AZURE_COSMOSDB_CONTAINER' | ||||||||||||||||||||||||
| value: cosmosDbContainer | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| // We typically store sensitive values in secrets, but App Insights connection strings are not considered highly sensitive | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' | ||||||||||||||||||||||||
| value: applicationInsightsConnectionString | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
pamelafox marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+63
to
+66
|
||||||||||||||||||||||||
| { | |
| name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' | |
| value: applicationInsightsConnectionString | |
| } | |
| // Only add the environment variable if the connection string is non-empty | |
| ...(empty(applicationInsightsConnectionString) ? [] : [ | |
| { | |
| name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' | |
| value: applicationInsightsConnectionString | |
| } | |
| ]) |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
APPLICATIONINSIGHTS_CONNECTION_STRING is set as a plain environment variable for the container app, increasing chances of accidental exposure via logs, crash dumps, or diagnostics endpoints. Attackers with limited foothold (e.g., read access to env or logs) could exfiltrate it and push malicious telemetry. Fix: store this value as a secret (Container Apps secrets or Key Vault) and reference it from the app; avoid placing it directly in environmentVariables.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -155,6 +155,20 @@ module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0 | |||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // Application Insights for telemetry | ||||||||
| module applicationInsights 'br/public:avm/res/insights/component:0.4.2' = if (useMonitoring) { | ||||||||
| name: 'applicationinsights' | ||||||||
| scope: resourceGroup | ||||||||
| params: { | ||||||||
| name: '${prefix}-appinsights' | ||||||||
| location: location | ||||||||
| tags: tags | ||||||||
| workspaceResourceId: logAnalyticsWorkspace.?outputs.resourceId | ||||||||
| kind: 'web' | ||||||||
| applicationType: 'web' | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // https://learn.microsoft.com/en-us/azure/container-apps/firewall-integration?tabs=consumption-only | ||||||||
| module containerAppsNSG 'br/public:avm/res/network/network-security-group:0.5.1' = if (useVnet) { | ||||||||
| name: 'containerAppsNSG' | ||||||||
|
|
@@ -669,6 +683,7 @@ module aca 'aca.bicep' = { | |||||||
| cosmosDbAccount: cosmosDb.outputs.name | ||||||||
| cosmosDbDatabase: cosmosDbDatabaseName | ||||||||
| cosmosDbContainer: cosmosDbContainerName | ||||||||
| applicationInsightsConnectionString: useMonitoring ? applicationInsights.outputs.connectionString : '' | ||||||||
| exists: acaExists | ||||||||
| } | ||||||||
| } | ||||||||
|
|
@@ -738,3 +753,6 @@ output AZURE_COSMOSDB_ACCOUNT string = cosmosDb.outputs.name | |||||||
| output AZURE_COSMOSDB_ENDPOINT string = cosmosDb.outputs.endpoint | ||||||||
| output AZURE_COSMOSDB_DATABASE string = cosmosDbDatabaseName | ||||||||
| output AZURE_COSMOSDB_CONTAINER string = cosmosDbContainerName | ||||||||
|
|
||||||||
| // We typically do not output sensitive values, but App Insights connection strings are not considered highly sensitive | ||||||||
| output APPLICATIONINSIGHTS_CONNECTION_STRING string = useMonitoring ? applicationInsights.outputs.connectionString : '' | ||||||||
pamelafox marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+757
to
+758
|
||||||||
| // We typically do not output sensitive values, but App Insights connection strings are not considered highly sensitive | |
| output APPLICATIONINSIGHTS_CONNECTION_STRING string = useMonitoring ? applicationInsights.outputs.connectionString : '' | |
| // Application Insights connection string is sensitive and should not be output as a deployment output. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,10 @@ dependencies = [ | |||||
| "azure-ai-agents>=1.1.0", | ||||||
| "agent-framework>=1.0.0b251016", | ||||||
| "azure-cosmos>=4.9.0", | ||||||
| "azure-monitor-opentelemetry>=1.6.4", | ||||||
| "opentelemetry-instrumentation-starlette>=0.49b0", | ||||||
|
||||||
| "opentelemetry-instrumentation-starlette>=0.49b0", | |
| "opentelemetry-instrumentation-starlette>=0.59b0,<0.60", |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,25 +1,49 @@ | ||||||||||||||||||||||||||
| """Run with: cd servers && uvicorn deployed_mcp:app --host 0.0.0.0 --port 8000""" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||
| from datetime import date | ||||||||||||||||||||||||||
| from enum import Enum | ||||||||||||||||||||||||||
| from typing import Annotated | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import logfire | ||||||||||||||||||||||||||
| from azure.core.settings import settings | ||||||||||||||||||||||||||
| from azure.cosmos.aio import CosmosClient | ||||||||||||||||||||||||||
| from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential | ||||||||||||||||||||||||||
| from azure.monitor.opentelemetry import configure_azure_monitor | ||||||||||||||||||||||||||
| from dotenv import load_dotenv | ||||||||||||||||||||||||||
| from fastmcp import FastMCP | ||||||||||||||||||||||||||
| from opentelemetry.instrumentation.starlette import StarletteInstrumentor | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| from opentelemetry_middleware import OpenTelemetryMiddleware | ||||||||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||||||||
| from servers.opentelemetry_middleware import OpenTelemetryMiddleware | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "true" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| load_dotenv(override=True) | ||||||||||||||||||||||||||
| if not RUNNING_IN_PRODUCTION: | ||||||||||||||||||||||||||
| load_dotenv(override=True) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s") | ||||||||||||||||||||||||||
| logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(message)s") | ||||||||||||||||||||||||||
| logger = logging.getLogger("ExpensesMCP") | ||||||||||||||||||||||||||
| logger.setLevel(logging.INFO) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Configure OpenTelemetry tracing, either via Azure Monitor or Logfire | ||||||||||||||||||||||||||
| # We don't support both at the same time due to potential conflicts with tracer providers | ||||||||||||||||||||||||||
| if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"): | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"): | |
| connection_string = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") | |
| if connection_string and connection_string.strip(): |
pamelafox marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Azure SDK tracing implementation setting (settings.tracing_implementation = "opentelemetry") is only configured when using Logfire, but not when using Azure Monitor. This could lead to inconsistent Azure SDK tracing behavior between the two observability platforms. Consider setting this configuration before the conditional blocks (line 33) to ensure Azure SDK always uses OpenTelemetry tracing regardless of the export destination.
| if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"): | |
| logger.info("Setting up Azure Monitor instrumentation") | |
| configure_azure_monitor() | |
| elif os.getenv("LOGFIRE_PROJECT_NAME"): | |
| logger.info("Setting up Logfire instrumentation") | |
| settings.tracing_implementation = "opentelemetry" # Configure Azure SDK to use OpenTelemetry tracing | |
| settings.tracing_implementation = "opentelemetry" # Ensure Azure SDK always uses OpenTelemetry tracing | |
| if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"): | |
| logger.info("Setting up Azure Monitor instrumentation") | |
| configure_azure_monitor() | |
| elif os.getenv("LOGFIRE_PROJECT_NAME"): | |
| logger.info("Setting up Logfire instrumentation") |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The send_to_logfire=True parameter is redundant when LOGFIRE_PROJECT_NAME is set. The logfire.configure() function automatically enables sending to Logfire when a project name is configured. This parameter should be omitted for clarity.
| logfire.configure(service_name="expenses-mcp", send_to_logfire=True) | |
| logfire.configure(service_name="expenses-mcp") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| from fastmcp.server.middleware import Middleware, MiddlewareContext | ||
| from opentelemetry import trace | ||
| from opentelemetry.trace import Status, StatusCode | ||
|
|
||
|
|
||
| class OpenTelemetryMiddleware(Middleware): | ||
| """Middleware that creates OpenTelemetry spans for MCP operations.""" | ||
|
|
||
| def __init__(self, tracer_name: str): | ||
| self.tracer = trace.get_tracer(tracer_name) | ||
|
|
||
| async def on_call_tool(self, context: MiddlewareContext, call_next): | ||
| """Create a span for each tool call with detailed attributes.""" | ||
| tool_name = context.message.name | ||
|
|
||
| with self.tracer.start_as_current_span( | ||
| f"tool.{tool_name}", | ||
| attributes={ | ||
| "mcp.method": context.method, | ||
| "mcp.source": context.source, | ||
| "mcp.tool.name": tool_name, | ||
| # If arguments are sensitive, consider omitting or sanitizing them | ||
| # If arguments are long/nested, consider adding a size or depth limit | ||
| "mcp.tool.arguments": str(context.message.arguments), | ||
pamelafox marked this conversation as resolved.
Show resolved
Hide resolved
pamelafox marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| ) as span: | ||
| try: | ||
| result = await call_next(context) | ||
| span.set_attribute("mcp.tool.success", True) | ||
| span.set_status(Status(StatusCode.OK)) | ||
| return result | ||
| except Exception as e: | ||
| span.set_attribute("mcp.tool.success", False) | ||
| span.set_attribute("mcp.tool.error", str(e)) | ||
| span.set_status(Status(StatusCode.ERROR, str(e))) | ||
| span.record_exception(e) | ||
| raise | ||
|
|
||
| async def on_read_resource(self, context: MiddlewareContext, call_next): | ||
| """Create a span for each resource read.""" | ||
| resource_uri = str(getattr(context.message, "uri", "unknown")) | ||
|
|
||
| with self.tracer.start_as_current_span( | ||
| f"resource.{resource_uri}", | ||
| attributes={ | ||
| "mcp.method": context.method, | ||
| "mcp.source": context.source, | ||
| "mcp.resource.uri": resource_uri, | ||
| }, | ||
| ) as span: | ||
| try: | ||
| result = await call_next(context) | ||
| span.set_attribute("mcp.resource.success", True) | ||
| span.set_status(Status(StatusCode.OK)) | ||
| return result | ||
| except Exception as e: | ||
| span.set_attribute("mcp.resource.success", False) | ||
| span.set_attribute("mcp.resource.error", str(e)) | ||
| span.set_status(Status(StatusCode.ERROR, str(e))) | ||
| span.record_exception(e) | ||
| raise | ||
|
|
||
| async def on_get_prompt(self, context: MiddlewareContext, call_next): | ||
| """Create a span for each prompt retrieval.""" | ||
| prompt_name = getattr(context.message, "name", "unknown") | ||
|
|
||
| with self.tracer.start_as_current_span( | ||
| f"prompt.{prompt_name}", | ||
|
Comment on lines
+17
to
+68
|
||
| attributes={ | ||
| "mcp.method": context.method, | ||
| "mcp.source": context.source, | ||
| "mcp.prompt.name": prompt_name, | ||
| }, | ||
| ) as span: | ||
| try: | ||
| result = await call_next(context) | ||
| span.set_attribute("mcp.prompt.success", True) | ||
| span.set_status(Status(StatusCode.OK)) | ||
| return result | ||
| except Exception as e: | ||
| span.set_attribute("mcp.prompt.success", False) | ||
| span.set_attribute("mcp.prompt.error", str(e)) | ||
| span.set_status(Status(StatusCode.ERROR, str(e))) | ||
| span.record_exception(e) | ||
| raise | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.