-
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 3 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,10 @@ module app 'core/host/container-app-upsert.bicep' = { | |||||||||||||||||||||||
| name: 'AZURE_COSMOSDB_CONTAINER' | ||||||||||||||||||||||||
| value: cosmosDbContainer | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| 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 | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,21 +5,37 @@ | |||||||
| 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_middleware import OpenTelemetryMiddleware | ||||||||
pamelafox marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||
|
|
||||||||
| load_dotenv(override=True) | ||||||||
| RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "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") | ||||||||
pamelafox marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| logger = logging.getLogger("ExpensesMCP") | ||||||||
| logger.setLevel(logging.INFO) | ||||||||
|
|
||||||||
| # Configure OpenTelemetry tracing | ||||||||
| 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
pamelafox marked this conversation as resolved.
Outdated
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 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,83 @@ | ||
| 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, | ||
| "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}", | ||
| 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.