App Insights / Langfuse cross-reference
When a pod uses both Azure Application Insights (via Azure Monitor OpenTelemetry) and Langfuse, two independent observability backends receive data for every request. This document explains what cross-reference data is written to each backend, how it is written, the navigation flows that result, and what improvements can be standardised in the shared SDK versus what must remain pod-specific.
What is written where
| Field | Written to | Written from | KQL table (classic / workspace) |
|---|---|---|---|
langfuse.trace_id |
App Insights custom dimension | request_span.set_attribute(...) |
requests / AppRequests |
langfuse.trace_url |
App Insights custom dimension | request_span.set_attribute(...) |
requests / AppRequests |
app_insights.request_span_id |
Langfuse observation metadata | chain_span.update(metadata=...) |
— |
app_insights.query_url |
Langfuse observation metadata | chain_span.update(metadata=...) |
— |
App Insights has two table schemas depending on whether the workspace is classic or workspace-based: the classic schema uses lowercase table names (requests, traces, dependencies, exceptions) and column names (id, operation_Id, operation_ParentId, customDimensions); the workspace-based schema uses prefixed table names (AppRequests, AppTraces, etc.) with PascalCase columns (Id, OperationId, ParentId, Properties). Both expose the same data; the KQL examples in this document show the classic schema. Substitute the workspace equivalents as needed.
The span_id stored as app_insights.request_span_id in Langfuse is the App Insights HTTP server span's OTel span_id formatted as a 16-hex-char string (format(ctx.span_id, "016x")). This is the same value that App Insights writes to requests.id in Log Analytics.
app_insights.query_url is a pre-built Azure Portal Log Analytics URL for the query requests | where id == '<span_id>'. It is present only when the pod sets APP_INSIGHTS_RESOURCE_ID and APP_INSIGHTS_TENANT_ID in its environment.
The App Insights OperationId (the OTel trace ID formatted as a 32-char hex string) and the Langfuse trace ID are the same value. get_trace_id() returns the OTel trace ID; App Insights writes it to operation_Id (classic) / OperationId (workspace). The langfuse.trace_id custom dimension is therefore always reconstructible from the request row — it equals operation_Id. The explicit stamping is retained so that langfuse.trace_url (the clickable link to Langfuse) is available without any manual URL construction.
Architecture: dual TracerProvider
The cross-reference exists because the pod uses two separate OTel TracerProvider instances — one per backend. They cannot share a single provider because the Azure Monitor exporter and the Langfuse exporter impose conflicting configurations.
graph TD
REQ["HTTP Request"]
AI_TP["App Insights TracerProvider\ndedicated non-global provider\nFastAPIInstrumentor"]
LF_TP["Langfuse TracerProvider\nglobal OTel provider\nsetup_tracing"]
AI["App Insights\nrequests.id = request span_id\nrequests.customDimensions:\n langfuse.trace_id\n langfuse.trace_url\ntraces.operation_ParentId = request span_id"]
LF["Langfuse\npathfinder.ask observation\nmetadata.app_insights.request_span_id\nmetadata.app_insights.query_url"]
REQ --> AI_TP
REQ --> LF_TP
AI_TP --> AI
LF_TP --> LF
Because App Insights owns the OTel trace root (the HTTP server span), Langfuse's first observation (pathfinder.ask) is technically a child in the OTel context tree — but it is the root of everything Langfuse sees, since the App Insights server span never reaches the Langfuse exporter.
Engineer navigation flows
graph TD
AI_REQ["App Insights\nrequests table row"]
AI_TRACES["App Insights\ntraces table row"]
LF["Langfuse trace\n(pathfinder.ask observation)"]
AI_REQ -->|"customDimensions.langfuse.trace_url\none-click link to Langfuse"| LF
LF -->|"metadata.app_insights.query_url\none-click link to Log Analytics"| AI_REQ
LF -->|"metadata.app_insights.request_span_id\nKQL: requests where id == span_id"| AI_REQ
AI_TRACES -->|"operation_ParentId = requests.id\nKQL: requests where id == operation_ParentId"| AI_REQ
Flow A — App Insights request → Langfuse trace
Starting point: you found a specific HTTP request in App Insights and want to inspect the Langfuse trace for the AI part of that call.
- In Log Analytics:
requests | where id == "<request_span_id>"or browse to the request row directly. - Open the row and expand
customDimensions. customDimensions.langfuse.trace_urlis a direct deep link to the Langfuse trace.langfuse.trace_idholds the bare trace ID if you want to construct the URL yourself.
Flow B — App Insights trace/log entry → Langfuse trace
Starting point: you found an application log entry (Python logging module output) in the traces table and need to find the parent request and then the Langfuse trace.
All telemetry for the same HTTP request shares the same operation_Id (classic schema) / OperationId (workspace schema). Use this to join:
// Classic schema
let op_id = traces | where ... | project operation_Id | take 1;
requests
| where operation_Id == toscalar(op_id)
| project timestamp, name, duration, resultCode,
langfuse_trace_url = customDimensions["langfuse.trace_url"]
// Workspace-based schema
let op_id = AppTraces | where ... | project OperationId | take 1;
AppRequests
| where OperationId == toscalar(op_id)
| project TimeGenerated, Name, DurationMs, ResultCode,
langfuse_trace_url = Properties["langfuse.trace_url"]
traces.operation_ParentId (classic) / AppTraces.ParentId (workspace) equals the request's id / Id only for log entries that are direct children of the request span. Using OperationId is more reliable since it captures all telemetry for the request, not just direct children.
Flow C — Langfuse trace → App Insights request
Starting point: you are inspecting a Langfuse trace and want to see the full App Insights request telemetry (HTTP headers, dependencies, status code, timing).
- Open the
pathfinder.askchain observation in the Langfuse trace detail view. - In observation metadata, click
app_insights.query_urlto jump directly to the Log Analytics query for this request. The link opens the Azure Portal withrequests | where id == '<span_id>'pre-populated and scoped to the pod's App Insights resource. - Alternatively, copy
app_insights.request_span_idand runrequests | where id == "<span_id>"manually in Log Analytics. - From the request row you can drill into dependencies, traces, exceptions for the same operation via
operation_Id.
app_insights.query_url is present when the pod configures APP_INSIGHTS_RESOURCE_ID and APP_INSIGHTS_TENANT_ID. When those env vars are absent, only app_insights.request_span_id is available (step 3 above).
Quick combined KQL to see the full request context when navigating manually:
// Classic schema
let span_id = "a1b2c3d4e5f60718"; // from Langfuse metadata
requests
| where id == span_id
| project timestamp, name, duration, resultCode, operation_Id,
langfuse_url = customDimensions["langfuse.trace_url"]
// All log traces for the same request (operation_Id from the request row above)
let op_id = "00112233445566778899aabbccddeeff";
traces
| where operation_Id == op_id
| order by timestamp asc
| project timestamp, message, severityLevel, operation_ParentId
// Workspace-based schema equivalents
AppRequests | where Id == "<span_id>"
AppTraces | where OperationId == "<op_id>"
Implementation: SDK utilities
All cross-reference writes are provided by semantic_tracing as public utilities. Pods import them directly.
from semantic_tracing import (
get_app_insights_query_url,
link_app_insights_to_langfuse,
)
Per request: link the two backends
propagate() captures the currently active OTel span at entry and stores it in a ContextVar. Enter propagate() before span() so that the App Insights HTTP server span is still current — link_app_insights_to_langfuse() can then be called with no arguments:
from semantic_tracing import link_app_insights_to_langfuse, propagate, span
# propagate() wraps span() — App Insights server span is captured automatically.
with propagate(trace_name="myapp.ask", user_id=..., session_id=...):
with span("myapp.ask", as_type="chain") as s:
link_app_insights_to_langfuse() # no args; both spans read from context
with span("myapp.answer", as_type="generation"):
# ... rest of handler
link_app_insights_to_langfuse resolves the Langfuse trace-URL prefix automatically on its first call and caches the result for the process lifetime. No startup hook or app.state plumbing is needed.
TraceContext ContextVar
propagate() also populates TRACE_CONTEXT (a ContextVar[TraceContext | None]) with the cross-backend IDs for the current request:
from semantic_tracing import get_trace_context
# Anywhere inside a `with propagate():` block:
tc = get_trace_context()
if tc:
print(tc.langfuse_trace_id) # 32-hex OTel trace ID
print(tc.app_insights_operation_id) # same value, App Insights name
print(tc.app_insights_request_span_id) # 16-hex request span_id
get_trace_context() returns None outside a propagate() scope or when the App Insights span was not valid at entry time. All three IDs are the same 32/16-hex strings used by App Insights and Langfuse internally.
Streaming routes and explicit req_span
In streaming async generators the OTel context from the outer handler does not carry through. Capture req_span in the handler body and pass it explicitly:
from opentelemetry import trace as otel_trace
req_span = otel_trace.get_current_span() # captured before _gen() is iterated
async def _gen():
with span("myapp.ask", as_type="chain") as s:
with propagate(trace_name="myapp.ask", ...):
link_app_insights_to_langfuse(req_span) # chain_span still implicit
with span("myapp.answer", as_type="generation"):
...
Passing req_span explicitly bypasses the ContextVar; chain_span still defaults to the active Langfuse observation (update_current_span).
Pod configuration for Log Analytics deep-link URLs
Set two env vars in the pod's .env to enable app_insights.query_url in Langfuse observation metadata:
| Env var | Value |
|---|---|
APP_INSIGHTS_RESOURCE_ID |
App Insights component ARM resource ID: /subscriptions/{sub}/resourceGroups/{rg}/providers/microsoft.insights/components/{name} |
APP_INSIGHTS_TENANT_ID |
Azure tenant GUID (e.g. c8823c91-...) or domain |
Both must be set. When either is absent, link_app_insights_to_langfuse omits app_insights.query_url silently — no error and no change to the other cross-reference fields.
Pods that need to generate Log Analytics URLs for custom queries can call get_app_insights_query_url directly:
url = get_app_insights_query_url(
'requests | where id == "a1b2c3d4e5f60718"'
)
# → https://portal.azure.com#@{tenant}/blade/...
The resource_id and tenant params override the env vars when provided.
Note on workspace-based schema The same URL format works for Log Analytics workspace resource IDs (
microsoft.operationalinsights/workspaces/...) — setAPP_INSIGHTS_RESOURCE_IDto the workspace resource ID instead, and use workspace-based table names (AppRequests | where Id == ...) in your KQL. The default KQL generated bylink_app_insights_to_langfuseuses the classicrequeststable, so it expects the App Insights component resource ID.
Known limitation: trace-level metadata in Langfuse
app_insights.request_span_id and app_insights.query_url are written to the observation metadata of the Langfuse chain span (e.g. pathfinder.ask). In the Langfuse UI this means you must click into that observation node to see them — the trace header itself does not show them.
The App Insights HTTP server span is the OTel trace root but is never exported to Langfuse. The Langfuse 5.x Python SDK does not expose a method to update trace-level metadata from within a running trace (update_current_trace does not exist on the Langfuse facade). Trace-level metadata can only be set via propagate_attributes(metadata=...) at scope-entry time — before any observations are created — which means the App Insights identifiers (only available after the route handler starts) cannot be placed in the Langfuse trace header. Revisit if Langfuse adds this API.
SDK vs pod responsibilities
| Capability | Where it belongs | Status |
|---|---|---|
link_app_insights_to_langfuse() |
SDK (semantic_tracing) |
Implemented — all args optional (v0.3.6); prefix auto-resolved (v0.3.5) |
TraceContext / TRACE_CONTEXT / get_trace_context() |
SDK (semantic_tracing) |
Implemented (v0.3.6) — populated by propagate() |
get_app_insights_query_url() |
SDK (semantic_tracing) |
Implemented |
resolve_langfuse_trace_url_prefix() |
SDK (semantic_tracing) |
Deprecated (v0.3.5) — no longer required |
Call propagate() before span() so App Insights span is captured |
Pod convention | Implemented (Project Green v0.3.6 migration) |
propagate(log_trace_url=True) — CLI/log URL output |
SDK (propagate()) — opt-in param |
Implemented |
app_insights.request_span_id → Langfuse trace-level metadata |
Blocked — Langfuse 5.x has no update_current_trace |
Blocked; revisit if Langfuse adds this API |