Skip to content

Commit 73511bd

Browse files
authored
Merge pull request #96 from PerfectThymeTech/marvinbuss/azure_monitor_manual
Switch to Manual Azure Monitor Setup
2 parents 34223ec + 4167aca commit 73511bd

File tree

8 files changed

+171
-33
lines changed

8 files changed

+171
-33
lines changed

.github/workflows/_containerTemplate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
- name: Install cosign
5757
uses: sigstore/cosign-installer@v3.3.0
5858
id: install_cosign
59-
if: github.event_name != 'pull_request'
59+
if: github.event_name == 'release'
6060
with:
6161
cosign-release: 'v2.2.0'
6262

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import Any
1+
from typing import Annotated
22

3-
from fastapi import APIRouter
3+
import httpx
4+
from fastapi import APIRouter, Header
45
from fastapp.models.sample import SampleRequest, SampleResponse
56
from fastapp.utils import setup_logging
67

@@ -11,7 +12,14 @@
1112

1213
@router.post("/sample", response_model=SampleResponse, name="sample")
1314
async def post_predict(
14-
data: SampleRequest,
15+
data: SampleRequest, x_forwarded_for: Annotated[str, Header()] = ""
1516
) -> SampleResponse:
1617
logger.info(f"Received request: {data}")
18+
logger.info(f"IP of sender: {x_forwarded_for}")
19+
20+
# Sample request
21+
async with httpx.AsyncClient() as client:
22+
response = await client.get("https://www.bing.com")
23+
logger.info(f"Received response status code: {response.status_code}")
24+
1725
return SampleResponse(output=f"Hello {data.input}")

code/function/fastapp/core/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ class Settings(BaseSettings):
1010
APP_VERSION: str = "v0.0.1"
1111
API_V1_STR: str = "/v1"
1212
LOGGING_LEVEL: int = logging.INFO
13+
LOGGING_SAMPLING_RATIO: float = 1.0
14+
LOGGING_SCHEDULE_DELAY: int = 5000
1315
DEBUG: bool = False
1416
APPLICATIONINSIGHTS_CONNECTION_STRING: str = Field(
1517
default="", env="APPLICATIONINSIGHTS_CONNECTION_STRING"
1618
)
17-
MY_SECRET_CONFIG: str = Field(default="", env="MY_SECRET_CONFIG")
19+
WEBSITE_NAME: str = Field(default="test", alias="WEBSITE_SITE_NAME")
20+
WEBSITE_INSTANCE_ID: str = Field(default="0", alias="WEBSITE_INSTANCE_ID")
21+
MY_SECRET_CONFIG: str = Field(default="", alias="MY_SECRET_CONFIG")
1822

1923

2024
settings = Settings()

code/function/fastapp/main.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1+
from contextlib import asynccontextmanager
2+
13
from fastapi import FastAPI
24
from fastapp.api.v1.api_v1 import api_v1_router
35
from fastapp.core.config import settings
4-
from fastapp.utils import setup_tracer
6+
from fastapp.utils import setup_opentelemetry
7+
8+
9+
@asynccontextmanager
10+
async def lifespan(app: FastAPI) -> None:
11+
"""Gracefully start the application before the server reports readiness."""
12+
setup_opentelemetry(app=app)
13+
yield
14+
pass
15+
16+
17+
def lifespan_sync(app: FastAPI) -> None:
18+
"""Gracefully start the application before the server reports readiness."""
19+
setup_opentelemetry(app=app)
520

621

722
def get_app() -> FastAPI:
@@ -11,24 +26,14 @@ def get_app() -> FastAPI:
1126
"""
1227
app = FastAPI(
1328
title=settings.PROJECT_NAME,
29+
description="",
1430
version=settings.APP_VERSION,
1531
openapi_url="/openapi.json",
1632
debug=settings.DEBUG,
33+
lifespan=lifespan,
1734
)
1835
app.include_router(api_v1_router, prefix=settings.API_V1_STR)
1936
return app
2037

2138

2239
app = get_app()
23-
24-
25-
@app.on_event("startup")
26-
async def startup_event():
27-
"""Gracefully start the application before the server reports readiness."""
28-
setup_tracer(app=app)
29-
30-
31-
@app.on_event("shutdown")
32-
async def shutdown_event():
33-
"""Gracefully close connections before shutdown of the server."""
34-
pass

code/function/fastapp/utils.py

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
import logging
22
from logging import Logger
33

4+
from azure.monitor.opentelemetry import configure_azure_monitor
5+
46
# from azure.identity import ManagedIdentityCredential
5-
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
7+
from azure.monitor.opentelemetry.exporter import (
8+
ApplicationInsightsSampler,
9+
AzureMonitorLogExporter,
10+
AzureMonitorMetricExporter,
11+
AzureMonitorTraceExporter,
12+
)
613
from fastapi import FastAPI
714
from fastapp.core.config import settings
15+
from opentelemetry import trace
16+
from opentelemetry._logs import set_logger_provider
817
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
18+
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
19+
from opentelemetry.instrumentation.system_metrics import SystemMetricsInstrumentor
20+
from opentelemetry.metrics import set_meter_provider
21+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
22+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
23+
from opentelemetry.sdk.metrics import MeterProvider
24+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
925
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
1026
from opentelemetry.sdk.trace import TracerProvider
1127
from opentelemetry.sdk.trace.export import BatchSpanProcessor
28+
from opentelemetry.trace import Tracer, get_tracer_provider, set_tracer_provider
1229

1330

1431
def setup_logging(module) -> Logger:
@@ -21,26 +38,101 @@ def setup_logging(module) -> Logger:
2138
logger.propagate = False
2239

2340
# Create stream handler
24-
logger_stream_handler = logging.StreamHandler()
25-
logger_stream_handler.setFormatter(
41+
stream_handler = logging.StreamHandler()
42+
stream_handler.setFormatter(
2643
logging.Formatter("[%(asctime)s] [%(levelname)s] [%(module)-8.8s] %(message)s")
2744
)
28-
logger.addHandler(logger_stream_handler)
45+
logger.addHandler(stream_handler)
2946
return logger
3047

3148

32-
def setup_tracer(app: FastAPI):
49+
def setup_tracer(module) -> Tracer:
50+
"""Setup tracer and event handler.
51+
52+
RETURNS (Tracer): The tracer object to create spans.
53+
"""
54+
tracer = trace.get_tracer(module)
55+
return tracer
56+
57+
58+
def setup_opentelemetry(app: FastAPI):
3359
"""Setup tracer for Open Telemetry.
3460
3561
app (FastAPI): The app to be instrumented by Open Telemetry.
3662
RETURNS (None): Nothing is being returned.
3763
"""
3864
if settings.APPLICATIONINSIGHTS_CONNECTION_STRING:
3965
# credential = ManagedIdentityCredential()
40-
exporter = AzureMonitorTraceExporter.from_connection_string(
66+
resource = Resource.create(
67+
{
68+
"service.name": settings.WEBSITE_NAME,
69+
"service.namespace": settings.WEBSITE_NAME,
70+
"service.instance.id": settings.WEBSITE_INSTANCE_ID,
71+
}
72+
)
73+
74+
# Create logger provider
75+
logger_exporter = AzureMonitorLogExporter.from_connection_string(
4176
settings.APPLICATIONINSIGHTS_CONNECTION_STRING,
4277
# credential=credential
4378
)
44-
tracer = TracerProvider(resource=Resource({SERVICE_NAME: "api"}))
45-
tracer.add_span_processor(BatchSpanProcessor(exporter))
46-
FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer)
79+
logger_provider = LoggerProvider(resource=resource)
80+
logger_provider.add_log_record_processor(
81+
BatchLogRecordProcessor(
82+
exporter=logger_exporter,
83+
schedule_delay_millis=settings.LOGGING_SCHEDULE_DELAY,
84+
)
85+
)
86+
set_logger_provider(logger_provider)
87+
handler = LoggingHandler(
88+
level=settings.LOGGING_LEVEL, logger_provider=logger_provider
89+
)
90+
logging.getLogger().addHandler(handler)
91+
92+
# Create tracer provider
93+
tracer_exporter = AzureMonitorTraceExporter.from_connection_string(
94+
settings.APPLICATIONINSIGHTS_CONNECTION_STRING,
95+
# credential=credential
96+
)
97+
sampler = ApplicationInsightsSampler(
98+
sampling_ratio=settings.LOGGING_SAMPLING_RATIO
99+
)
100+
tracer_provider = TracerProvider(resource=resource, sampler=sampler)
101+
tracer_provider.add_span_processor(
102+
BatchSpanProcessor(
103+
span_exporter=tracer_exporter,
104+
schedule_delay_millis=settings.LOGGING_SCHEDULE_DELAY,
105+
)
106+
)
107+
set_tracer_provider(tracer_provider)
108+
109+
# Create meter provider
110+
metrics_exporter = AzureMonitorMetricExporter.from_connection_string(
111+
settings.APPLICATIONINSIGHTS_CONNECTION_STRING,
112+
# credential=credential
113+
)
114+
reader = PeriodicExportingMetricReader(
115+
exporter=metrics_exporter,
116+
export_interval_millis=settings.LOGGING_SCHEDULE_DELAY,
117+
)
118+
meter_provider = MeterProvider(metric_readers=[reader], resource=resource)
119+
set_meter_provider(meter_provider)
120+
121+
# Configure custom metrics
122+
system_metrics_config = {
123+
"system.memory.usage": ["used", "free", "cached"],
124+
"system.cpu.time": ["idle", "user", "system", "irq"],
125+
"system.network.io": ["transmit", "receive"],
126+
"process.runtime.memory": ["rss", "vms"],
127+
"process.runtime.cpu.time": ["user", "system"],
128+
}
129+
130+
# Create instrumenter
131+
FastAPIInstrumentor.instrument_app(
132+
app,
133+
excluded_urls=f"{settings.API_V1_STR}/health/heartbeat",
134+
tracer_provider=tracer_provider,
135+
meter_provider=meter_provider,
136+
)
137+
HTTPXClientInstrumentor().instrument()
138+
SystemMetricsInstrumentor(config=system_metrics_config).instrument()

code/function/requirements.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
# The Python Worker is managed by Azure Functions platform
33
# Manually managing azure-functions-worker may cause unexpected issues
44

5-
# azure-identity~=1.13.0
5+
# azure-identity~=1.15.0
66
azure-functions~=1.17.0
77
fastapi~=0.106.0
88
pydantic-settings~=2.1.0
9-
aiohttp~=3.9.1
10-
opentelemetry-instrumentation-fastapi==0.43b0
11-
azure-monitor-opentelemetry-exporter==1.0.0b19
9+
httpx~=0.26.0
10+
azure-monitor-opentelemetry~=1.1.1
11+
opentelemetry-instrumentation-httpx~=0.43b0
12+
opentelemetry-instrumentation-system-metrics~=0.43b0

code/function/wrapper/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
import azure.functions as func
2-
from fastapp.main import app
2+
from fastapp.main import app, lifespan_sync
3+
from fastapp.utils import setup_tracer
4+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
5+
6+
lifespan_sync(app=app)
7+
tracer = setup_tracer(__name__)
38

49

510
async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
6-
return await func.AsgiMiddleware(app).handle_async(req, context)
11+
# Start distributed tracing
12+
functions_current_context = {
13+
"traceparent": context.trace_context.Traceparent,
14+
"tracestate": context.trace_context.Tracestate,
15+
}
16+
parent_context = TraceContextTextMapPropagator().extract(
17+
carrier=functions_current_context
18+
)
19+
20+
# Function logic
21+
with tracer.start_as_current_span("wrapper", context=parent_context) as span:
22+
response = await func.AsgiMiddleware(app).handle_async(
23+
req=req, context=parent_context
24+
)
25+
26+
return response

code/infra/function.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ resource "azapi_resource" "function" {
9393
name = "APPLICATIONINSIGHTS_CONNECTION_STRING"
9494
value = azurerm_application_insights.application_insights.connection_string
9595
},
96+
{
97+
name = "AZURE_SDK_TRACING_IMPLEMENTATION"
98+
value = "opentelemetry"
99+
},
100+
{
101+
name = "AZURE_TRACING_ENABLED"
102+
value = "true"
103+
},
96104
{
97105
name = "AZURE_FUNCTIONS_ENVIRONMENT"
98106
value = "Production"

0 commit comments

Comments
 (0)