Skip to content

Commit 2eb4738

Browse files
improve Application Insights logging and telemetry handling
1 parent e729a8d commit 2eb4738

21 files changed

Lines changed: 2031 additions & 1397 deletions

File tree

docs/LocalDevelopmentSetup.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,6 @@ Enable detailed logging by setting these environment variables in your `.env` fi
780780

781781
```bash
782782
APP_LOGGING_LEVEL=DEBUG
783-
APP_LOGGING_ENABLE=True
784783
```
785784

786785
## Related Documentation

infra/main.bicep

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,10 @@ module avmContainerApp 'br/public:avm/res/app/container-app:0.19.0' = {
10051005
name: 'AZURE_LOGGING_PACKAGES'
10061006
value: ''
10071007
}
1008+
{
1009+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1010+
value: enableMonitoring ? applicationInsights.outputs.connectionString : ''
1011+
}
10081012
]
10091013
}
10101014
]
@@ -1065,6 +1069,10 @@ module avmContainerApp_API 'br/public:avm/res/app/container-app:0.19.0' = {
10651069
name: 'AZURE_LOGGING_PACKAGES'
10661070
value: ''
10671071
}
1072+
{
1073+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1074+
value: enableMonitoring ? applicationInsights.outputs.connectionString : ''
1075+
}
10681076
]
10691077
probes: [
10701078
// Liveness Probe - Checks if the app is still running
@@ -1270,6 +1278,10 @@ module avmContainerApp_Workflow 'br/public:avm/res/app/container-app:0.19.0' = {
12701278
name: 'AZURE_LOGGING_PACKAGES'
12711279
value: ''
12721280
}
1281+
{
1282+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1283+
value: enableMonitoring ? applicationInsights.outputs.connectionString : ''
1284+
}
12731285
]
12741286
}
12751287
]
@@ -1642,6 +1654,10 @@ module avmContainerApp_update 'br/public:avm/res/app/container-app:0.19.0' = {
16421654
name: 'AZURE_LOGGING_PACKAGES'
16431655
value: ''
16441656
}
1657+
{
1658+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1659+
value: enableMonitoring ? applicationInsights.outputs.connectionString : ''
1660+
}
16451661
]
16461662
}
16471663
]
@@ -1717,6 +1733,10 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.19.0' =
17171733
name: 'AZURE_LOGGING_PACKAGES'
17181734
value: ''
17191735
}
1736+
{
1737+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1738+
value: enableMonitoring ? applicationInsights.outputs.connectionString : ''
1739+
}
17201740
]
17211741
probes: [
17221742
// Liveness Probe - Checks if the app is still running
@@ -1843,6 +1863,10 @@ module avmContainerApp_Workflow_update 'br/public:avm/res/app/container-app:0.19
18431863
name: 'AZURE_LOGGING_PACKAGES'
18441864
value: ''
18451865
}
1866+
{
1867+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1868+
value: enableMonitoring ? applicationInsights.outputs.connectionString : ''
1869+
}
18461870
]
18471871
}
18481872
]

src/ContentProcessor/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ azure-ai-inference==1.0.0b9
33
azure-appconfiguration==1.7.2
44
azure-core==1.38.0
55
azure-identity==1.25.1
6+
azure-monitor-events-extension==0.1.0
7+
azure-monitor-opentelemetry==1.6.10
68
azure-storage-blob==12.28.0
79
azure-storage-queue==12.15.0
810
certifi==2026.1.4

src/ContentProcessorAPI/app/application.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
application context used by request handlers.
99
"""
1010

11+
import logging
1112
import os
1213
import warnings
1314
from datetime import datetime
@@ -26,6 +27,12 @@
2627
from app.routers.logics.schemasetvault import SchemaSets
2728
from app.routers.logics.schemavault import Schemas
2829

30+
# Azure Monitor and OpenTelemetry imports
31+
from azure.monitor.opentelemetry import configure_azure_monitor
32+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
33+
34+
logger = logging.getLogger(__name__)
35+
2936
# PyMongo emits a compatibility warning when it detects Azure Cosmos DB (Mongo API).
3037
# This is informational and is commonly suppressed to keep logs clean.
3138
warnings.filterwarnings(
@@ -79,6 +86,7 @@ def initialize(self):
7986
self.app.include_router(http_probes)
8087
self._register_dependencies()
8188
self._config_routers()
89+
self._configure_telemetry()
8290

8391
def _config_routers(self):
8492
"""Mount feature routers onto the FastAPI application."""
@@ -119,3 +127,23 @@ def _register_dependencies(self):
119127

120128
def run(self, host: str = "0.0.0.0", port: int = 8000, reload: bool = True):
121129
"""No-op; the ASGI server (uvicorn) is launched externally."""
130+
131+
def _configure_telemetry(self):
132+
"""Configure Azure Monitor and instrument FastAPI for OpenTelemetry."""
133+
connection_string = self.application_context.configuration.applicationinsights_connection_string
134+
if connection_string:
135+
configure_azure_monitor(
136+
connection_string=connection_string,
137+
enable_live_metrics=True,
138+
)
139+
FastAPIInstrumentor.instrument_app(
140+
self.app,
141+
excluded_urls="startup,health",
142+
)
143+
logger.info(
144+
"Application Insights configured with live metrics and FastAPI instrumentation enabled"
145+
)
146+
else:
147+
logger.warning(
148+
"No Application Insights connection string found. Telemetry disabled."
149+
)

src/ContentProcessorAPI/app/libs/application/application_configuration.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ class AppConfiguration(ModelBaseSettings):
4646
app_message_queue_extract: Extraction message-queue name.
4747
app_cps_max_filesize_mb: Maximum upload file size in megabytes.
4848
app_logging_level: Application log level.
49-
app_logging_enable: Whether application logging is enabled.
5049
azure_package_logging_level: Log level for Azure SDK packages.
5150
azure_logging_packages: Comma-separated Azure package logger names.
5251
"""
@@ -65,6 +64,6 @@ class AppConfiguration(ModelBaseSettings):
6564
app_message_queue_extract: str
6665
app_cps_max_filesize_mb: int
6766
app_logging_level: str
68-
app_logging_enable: bool = False
6967
azure_package_logging_level: str
7068
azure_logging_packages: str
69+
applicationinsights_connection_string: str = ""

src/ContentProcessorAPI/app/libs/base/application_base.py

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def __init__(self, env_file_path: str | None = None, **data):
5959
1. Load ``.env`` from *env_file_path* (or derive from subclass location).
6060
2. Read Azure App Configuration and inject values into ``os.environ``.
6161
3. Populate ``application_context`` with config and Azure credentials.
62-
4. Configure Python logging if enabled in config.
62+
4. Configure Python logging unconditionally.
6363
5. Call ``self.initialize()``.
6464
6565
Args:
@@ -80,28 +80,39 @@ def __init__(self, env_file_path: str | None = None, **data):
8080

8181
self.application_context.set_configuration(AppConfiguration())
8282

83-
if self.application_context.configuration.app_logging_enable:
84-
logging_level = getattr(
85-
logging, self.application_context.configuration.app_logging_level
83+
# Configure logging unconditionally
84+
logging_level = getattr(
85+
logging,
86+
self.application_context.configuration.app_logging_level,
87+
logging.INFO,
88+
)
89+
logging.basicConfig(
90+
level=logging_level,
91+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
92+
)
93+
94+
# Suppress noisy Azure SDK and OpenTelemetry internal loggers
95+
logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(logging.WARNING)
96+
logging.getLogger("azure.core.pipeline.policies._universal").setLevel(logging.WARNING)
97+
logging.getLogger("opentelemetry.sdk").setLevel(logging.WARNING)
98+
logging.getLogger("azure.monitor.opentelemetry.exporter.export._base").setLevel(logging.WARNING)
99+
100+
if self.application_context.configuration.azure_logging_packages:
101+
azure_level = getattr(
102+
logging,
103+
self.application_context.configuration.azure_package_logging_level.upper(),
104+
logging.WARNING,
86105
)
87-
logging.basicConfig(level=logging_level)
88-
89-
if self.application_context.configuration.azure_logging_packages:
90-
azure_level = getattr(
91-
logging,
92-
self.application_context.configuration.azure_package_logging_level.upper(),
93-
logging.WARNING,
94-
)
95-
for logger_name in filter(
96-
None,
97-
(
98-
pkg.strip()
99-
for pkg in self.application_context.configuration.azure_logging_packages.split(
100-
","
101-
)
102-
),
103-
):
104-
logging.getLogger(logger_name).setLevel(azure_level)
106+
for logger_name in filter(
107+
None,
108+
(
109+
pkg.strip()
110+
for pkg in self.application_context.configuration.azure_logging_packages.split(
111+
","
112+
)
113+
),
114+
):
115+
logging.getLogger(logger_name).setLevel(azure_level)
105116

106117
self.initialize()
107118

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Utility for tracking custom events to Application Insights."""
5+
6+
import logging
7+
import os
8+
9+
from azure.monitor.events.extension import track_event
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def track_event_if_configured(event_name: str, event_data: dict):
15+
"""Track custom event to Application Insights if configured.
16+
17+
Args:
18+
event_name: Name of the event to track.
19+
event_data: Dictionary of event properties.
20+
"""
21+
instrumentation_key = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
22+
if instrumentation_key:
23+
track_event(event_name, event_data)
24+
else:
25+
logger.warning(
26+
"Skipping track_event for %s: Application Insights is not configured",
27+
event_name,
28+
)

src/ContentProcessorAPI/app/routers/claimprocessor.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
ClaimBatchProcessRepository.
1010
"""
1111

12+
import logging
1213
import uuid
1314
from enum import Enum
1415

@@ -18,6 +19,9 @@
1819
from sas.cosmosdb.mongo.repository import SortField
1920

2021
from app.libs.base.typed_fastapi import TypedFastAPI
22+
from app.libs.logging.event_utils import track_event_if_configured
23+
24+
logger = logging.getLogger(__name__)
2125
from app.routers.logics.claimbatchpocessor import (
2226
ClaimBatchProcessor,
2327
ClaimBatchProcessRepository,
@@ -324,6 +328,11 @@ async def start_claim_process(
324328
try:
325329
batch_processor.enqueue_claim_request_for_processing(claim_process_request=data)
326330
except Exception as e:
331+
track_event_if_configured("ClaimProcessError", {
332+
"claim_id": data.claim_process_id,
333+
"error": str(e),
334+
"error_type": type(e).__name__,
335+
})
327336
return JSONResponse(
328337
status_code=400,
329338
content={
@@ -344,6 +353,10 @@ async def start_claim_process(
344353
)
345354
)
346355

356+
track_event_if_configured("ClaimProcessSubmitted", {
357+
"claim_id": data.claim_process_id,
358+
})
359+
347360
return JSONResponse(
348361
status_code=202,
349362
headers={"Location": f"/claims/{data.claim_process_id}/status"},

src/ContentProcessorAPI/app/routers/contentprocessor.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import datetime
1212
import io
13+
import logging
1314
import urllib.parse
1415
import uuid
1516
from enum import Enum
@@ -19,6 +20,9 @@
1920
from pymongo.results import UpdateResult
2021

2122
from app.libs.base.typed_fastapi import TypedFastAPI
23+
from app.libs.logging.event_utils import track_event_if_configured
24+
25+
logger = logging.getLogger(__name__)
2226
from app.routers.logics.claimbatchpocessor import ClaimBatchProcessRepository
2327
from app.utils.mime_types import MimeTypesDetection
2428
from app.utils.upload_validation import (
@@ -204,6 +208,14 @@ async def Submit_File_With_MetaData(
204208

205209
content_processor.enqueue_message(submit_queue_message)
206210

211+
track_event_if_configured("FileSubmitted", {
212+
"process_id": process_id,
213+
"file_name": safe_filename,
214+
"schema_id": schema_id,
215+
"metadata_id": metadata_id,
216+
"size_bytes": str(size_bytes),
217+
})
218+
207219
file_size_mb = size_bytes / (1024 * 1024)
208220

209221
status_url = f"/contentprocessor/status/{process_id}"
@@ -271,6 +283,10 @@ async def get_status(
271283
collection_name=app.app_context.configuration.app_cosmos_container_process,
272284
)
273285

286+
track_event_if_configured("ProcessStatusQueried", {
287+
"process_id": process_id,
288+
})
289+
274290
if process_status is None:
275291
return JSONResponse(
276292
status_code=404,
@@ -481,6 +497,10 @@ async def update_process_result(
481497
},
482498
)
483499
else:
500+
track_event_if_configured("ProcessResultUpdated", {
501+
"process_id": process_id,
502+
"update_type": type(content_update_request).__name__,
503+
})
484504
return JSONResponse(
485505
status_code=200,
486506
content={

0 commit comments

Comments
 (0)