Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions infra/aca.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -58,6 +59,10 @@ module app 'core/host/container-app-upsert.bicep' = {
name: 'AZURE_COSMOSDB_CONTAINER'
value: cosmosDbContainer
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: applicationInsightsConnectionString
}
Comment on lines +63 to +66
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Application Insights connection string is empty (when useMonitoring is false), the environment variable APPLICATIONINSIGHTS_CONNECTION_STRING will be set to an empty string in the container app. This could potentially trigger the Azure Monitor instrumentation setup in deployed_mcp.py (line 35-37) with an invalid connection string, leading to errors. Consider either not adding the environment variable when it's empty, or checking for non-empty values in the Python code before calling configure_azure_monitor().

Suggested change
{
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 uses AI. Check for mistakes.
Comment on lines +65 to +66
Copy link

Copilot AI Dec 3, 2025

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.

Copilot uses AI. Check for mistakes.
]
targetPort: 8000
}
Expand Down
17 changes: 17 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -669,6 +683,7 @@ module aca 'aca.bicep' = {
cosmosDbAccount: cosmosDb.outputs.name
cosmosDbDatabase: cosmosDbDatabaseName
cosmosDbContainer: cosmosDbContainerName
applicationInsightsConnectionString: useMonitoring ? applicationInsights.outputs.connectionString : ''
exists: acaExists
}
}
Expand Down Expand Up @@ -738,3 +753,5 @@ 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

output APPLICATIONINSIGHTS_CONNECTION_STRING string = useMonitoring ? applicationInsights.outputs.connectionString : ''
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies = [
"azure-ai-agents>=1.1.0",
"agent-framework>=1.0.0b251016",
"azure-cosmos>=4.9.0",
"azure-monitor-opentelemetry>=1.6.4",
"logfire>=3.11.0",
]

[dependency-groups]
Expand Down
34 changes: 31 additions & 3 deletions servers/deployed_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,49 @@
from enum import Enum
from typing import Annotated

import logfire
from azure.cosmos.aio import CosmosClient
from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential
from azure.monitor.opentelemetry import configure_azure_monitor
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
from dotenv import load_dotenv
from fastmcp import FastMCP
from opentelemetry import trace
from opentelemetry.sdk.trace.export import BatchSpanProcessor

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")
logger = logging.getLogger("ExpensesMCP")
logger.setLevel(logging.INFO)

# Configure OpenTelemetry tracing
APPLICATIONINSIGHTS_CONNECTION_STRING = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
if APPLICATIONINSIGHTS_CONNECTION_STRING:
logger.info("Setting up Azure Monitor instrumentation")
configure_azure_monitor()

# Use Logfire to instrument MCP tool calls
logfire.configure(
service_name="expenses-mcp",
send_to_logfire=False, # Send spans to Application Insights, not Logfire backend
)
logfire.instrument_mcp()

if APPLICATIONINSIGHTS_CONNECTION_STRING:
logger.info("Adding Azure Monitor Trace Exporter to TracerProvider")
tracer_provider = trace.get_tracer_provider()
exporter = AzureMonitorTraceExporter(connection_string=APPLICATIONINSIGHTS_CONNECTION_STRING)
span_processor = BatchSpanProcessor(exporter)
tracer_provider.add_span_processor(span_processor)

# Cosmos DB configuration from environment variables
AZURE_COSMOSDB_ACCOUNT = os.environ["AZURE_COSMOSDB_ACCOUNT"]
AZURE_COSMOSDB_DATABASE = os.environ["AZURE_COSMOSDB_DATABASE"]
AZURE_COSMOSDB_CONTAINER = os.environ["AZURE_COSMOSDB_CONTAINER"]
RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "true"
AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID", "")

# Configure Cosmos DB client and container
Expand Down
49 changes: 49 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.