From 82f5f1a4e34c64acfbc832c6b968f5a8fb81ed63 Mon Sep 17 00:00:00 2001 From: Hanchi Wang Date: Wed, 21 Oct 2026 23:43:11 -0700 Subject: [PATCH] Add shared Foundry resource attribute to telemetry Include the microsoft.foundry string marker in Python telemetry resources and existing .NET telemetry samples. Share resource configuration across logs, traces, and metrics, preserve Python overrides, and document consumer-managed resources. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: be969812-8beb-4f72-ac73-3007f1f58671 --- .../02-agents/AgentOpenTelemetry/Program.cs | 14 ++-- .../02-agents/AgentOpenTelemetry/README.md | 11 +++ .../Agent_Step07_Observability/Program.cs | 2 + .../Agent_Step05_Observability/Program.cs | 2 + .../ClawAgent.Console/Program.cs | 4 +- .../FileSpanExporter.cs | 6 ++ .../Harness_Shared_Console/HarnessTracing.cs | 2 + .../ApplicationInsights/Program.cs | 3 +- .../Observability/AspireDashboard/Program.cs | 3 +- .../WorkflowAsAnAgent/Program.cs | 3 +- .../responses/Hosted-Observability/Program.cs | 3 + .../ServiceDefaultsExtensions.cs | 2 + .../Extensions.cs | 2 + python/packages/core/README.md | 8 ++ .../core/agent_framework/observability.py | 6 +- .../core/tests/core/test_observability.py | 81 +++++++++++++++++++ 16 files changed, 140 insertions(+), 12 deletions(-) diff --git a/dotnet/samples/02-agents/AgentOpenTelemetry/Program.cs b/dotnet/samples/02-agents/AgentOpenTelemetry/Program.cs index d83a0cb3f54..6dded58e2cf 100644 --- a/dotnet/samples/02-agents/AgentOpenTelemetry/Program.cs +++ b/dotnet/samples/02-agents/AgentOpenTelemetry/Program.cs @@ -28,18 +28,18 @@ var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"); // Create a resource to identify this service -var resource = ResourceBuilder.CreateDefault() +var resourceBuilder = ResourceBuilder.CreateDefault() .AddService(ServiceName, serviceVersion: "1.0.0") .AddAttributes(new Dictionary { ["service.instance.id"] = Environment.MachineName, - ["deployment.environment"] = "development" - }) - .Build(); + ["deployment.environment"] = "development", + ["microsoft.foundry"] = "True" + }); // Setup tracing with resource var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() - .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0")) + .SetResourceBuilder(resourceBuilder) .AddSource(SourceName) // Our custom activity source .AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI .AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint)); @@ -53,7 +53,7 @@ // Setup metrics with resource and instrument name filtering using var meterProvider = Sdk.CreateMeterProviderBuilder() - .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0")) + .SetResourceBuilder(resourceBuilder) .AddMeter(SourceName) // Our custom meter source .AddHttpClientInstrumentation() // HTTP client metrics .AddRuntimeInstrumentation() // .NET runtime metrics @@ -66,7 +66,7 @@ .SetMinimumLevel(LogLevel.Debug) .AddOpenTelemetry(options => { - options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0")); + options.SetResourceBuilder(resourceBuilder); options.AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri(otlpEndpoint)); if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString)) { diff --git a/dotnet/samples/02-agents/AgentOpenTelemetry/README.md b/dotnet/samples/02-agents/AgentOpenTelemetry/README.md index b4852fd9fcb..085d30f1b2f 100644 --- a/dotnet/samples/02-agents/AgentOpenTelemetry/README.md +++ b/dotnet/samples/02-agents/AgentOpenTelemetry/README.md @@ -30,6 +30,17 @@ graph TD ## Configuration +### Shared telemetry resource + +Logs, traces, and metrics use a shared resource containing +`"microsoft.foundry": "True"` (a string). The attribute also applies to HTTP and +runtime instrumentation using these providers; it is not a per-span tag or metric +dimension. Agent Framework does not configure resources for consuming .NET +applications, so applications managing their own providers should add this +attribute to their shared `ResourceBuilder`, or set +`OTEL_RESOURCE_ATTRIBUTES=microsoft.foundry=True` before provider initialization +(preserving any other configured resource attributes). + ### Microsoft Foundry Setup Set the following environment variables: ```powershell diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step07_Observability/Program.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step07_Observability/Program.cs index c1066fb0a01..12cbaaf7009 100644 --- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step07_Observability/Program.cs +++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step07_Observability/Program.cs @@ -7,6 +7,7 @@ using Azure.Monitor.OpenTelemetry.Exporter; using Microsoft.Agents.AI; using OpenTelemetry; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; string? applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"); @@ -16,6 +17,7 @@ // Create TracerProvider with console exporter. string sourceName = Guid.NewGuid().ToString("N"); TracerProviderBuilder tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureResource(resource => resource.AddAttributes([new("microsoft.foundry", "True")])) .AddSource(sourceName) .AddConsoleExporter(); if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString)) diff --git a/dotnet/samples/02-agents/Agents/Agent_Step05_Observability/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step05_Observability/Program.cs index 383038191ad..54c01f75007 100644 --- a/dotnet/samples/02-agents/Agents/Agent_Step05_Observability/Program.cs +++ b/dotnet/samples/02-agents/Agents/Agent_Step05_Observability/Program.cs @@ -10,6 +10,7 @@ using Azure.Monitor.OpenTelemetry.Exporter; using Microsoft.Agents.AI; using OpenTelemetry; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); @@ -20,6 +21,7 @@ // This will output the telemetry data to the console. string sourceName = Guid.NewGuid().ToString("N"); var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureResource(resource => resource.AddAttributes([new("microsoft.foundry", "True")])) .AddSource(sourceName) .AddConsoleExporter(); if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString)) diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Console/Program.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Console/Program.cs index 4c0d4e346b9..dd5a0037754 100644 --- a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Console/Program.cs +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Console/Program.cs @@ -17,7 +17,9 @@ // Export telemetry only when an OTLP endpoint is configured. We deliberately avoid the // console exporter: this is an interactive app whose UI is rendered by // HarnessConsole.RunAgentAsync, and streaming spans/metrics to stdout corrupts that UI. -var resourceBuilder = ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"); +var resourceBuilder = ResourceBuilder.CreateDefault() + .AddService(ServiceName, serviceVersion: "1.0.0") + .AddAttributes([new("microsoft.foundry", "True")]); using var tracerProvider = telemetryEnabled ? Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(resourceBuilder) diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/FileSpanExporter.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/FileSpanExporter.cs index 4d64be8eae9..f1de352f116 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/FileSpanExporter.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/FileSpanExporter.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Globalization; using OpenTelemetry; +using OpenTelemetry.Resources; namespace Harness.Shared.Console; @@ -27,6 +28,11 @@ public override ExportResult Export(in Batch batch) lock (this._lock) { using var writer = new StreamWriter(this._filePath, append: true); + foreach (var attribute in this.ParentProvider.GetResource().Attributes) + { + writer.WriteLine($"Resource: {attribute.Key}: {attribute.Value}"); + } + foreach (var activity in batch) { WriteActivity(writer, activity); diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessTracing.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessTracing.cs index e79f79e0dd4..a87f5aa02dd 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessTracing.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessTracing.cs @@ -3,6 +3,7 @@ #pragma warning disable VSTHRD002 // Synchronous waits are required by OpenTelemetry enrichment callbacks. using OpenTelemetry; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace Harness.Shared.Console; @@ -24,6 +25,7 @@ public static class HarnessTracing var traceLogPath = Path.Combine(AppContext.BaseDirectory, $"traces_{DateTime.UtcNow:yyyyMMdd_HHmmss}_{Guid.NewGuid()}.log"); return Sdk.CreateTracerProviderBuilder() + .ConfigureResource(resource => resource.AddAttributes([new("microsoft.foundry", "True")])) .AddSource(sourceName) .AddHttpClientInstrumentation((options) => { diff --git a/dotnet/samples/03-workflows/Observability/ApplicationInsights/Program.cs b/dotnet/samples/03-workflows/Observability/ApplicationInsights/Program.cs index 3d8d61b8a4e..f1b4669c68f 100644 --- a/dotnet/samples/03-workflows/Observability/ApplicationInsights/Program.cs +++ b/dotnet/samples/03-workflows/Observability/ApplicationInsights/Program.cs @@ -31,7 +31,8 @@ private static async Task Main() var resourceBuilder = ResourceBuilder .CreateDefault() - .AddService("WorkflowSample"); + .AddService("WorkflowSample") + .AddAttributes([new("microsoft.foundry", "True")]); using var traceProvider = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(resourceBuilder) diff --git a/dotnet/samples/03-workflows/Observability/AspireDashboard/Program.cs b/dotnet/samples/03-workflows/Observability/AspireDashboard/Program.cs index 9e5a396656f..c6b19d1a2d6 100644 --- a/dotnet/samples/03-workflows/Observability/AspireDashboard/Program.cs +++ b/dotnet/samples/03-workflows/Observability/AspireDashboard/Program.cs @@ -33,7 +33,8 @@ private static async Task Main() var resourceBuilder = ResourceBuilder .CreateDefault() - .AddService("WorkflowSample"); + .AddService("WorkflowSample") + .AddAttributes([new("microsoft.foundry", "True")]); using var traceProvider = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(resourceBuilder) diff --git a/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/Program.cs b/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/Program.cs index b6b67c6cf34..6029148d63b 100644 --- a/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/Program.cs +++ b/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/Program.cs @@ -54,7 +54,8 @@ private static async Task Main() var resourceBuilder = ResourceBuilder .CreateDefault() - .AddService("WorkflowSample"); + .AddService("WorkflowSample") + .AddAttributes([new("microsoft.foundry", "True")]); var traceProviderBuilder = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(resourceBuilder) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs index 7b4f0cb947d..8b472662879 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs @@ -14,6 +14,7 @@ using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; +using OpenTelemetry.Resources; // Load a local .env file when present (local development only). In Foundry the // platform injects the required environment variables at runtime. @@ -70,6 +71,8 @@ string GetWeather( // Host the agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddAttributes([new("microsoft.foundry", "True")])); var app = builder.Build(); app.MapFoundryResponses(); diff --git a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.ServiceDefaults/ServiceDefaultsExtensions.cs b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.ServiceDefaults/ServiceDefaultsExtensions.cs index 00af0166c8f..f5764a6d140 100644 --- a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.ServiceDefaults/ServiceDefaultsExtensions.cs +++ b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.ServiceDefaults/ServiceDefaultsExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace Microsoft.Extensions.Hosting; @@ -52,6 +53,7 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w }); builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddAttributes([new("microsoft.foundry", "True")])) .WithMetrics(metrics => { metrics.AddAspNetCoreInstrumentation() diff --git a/dotnet/samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.ServiceDefaults/Extensions.cs b/dotnet/samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.ServiceDefaults/Extensions.cs index 504bc71621b..89239d81b2a 100644 --- a/dotnet/samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.ServiceDefaults/Extensions.cs +++ b/dotnet/samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.ServiceDefaults/Extensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace Microsoft.Extensions.Hosting; @@ -56,6 +57,7 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w }); builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddAttributes([new("microsoft.foundry", "True")])) .WithMetrics(metrics => { metrics.AddAspNetCoreInstrumentation() diff --git a/python/packages/core/README.md b/python/packages/core/README.md index da095301470..a758b7c89dc 100644 --- a/python/packages/core/README.md +++ b/python/packages/core/README.md @@ -69,6 +69,14 @@ carry a documented feature-usage token. - `AGENT_FRAMEWORK_USER_AGENT_DISABLED=true` disables the entire Agent Framework User-Agent contribution, including the feature token. +OpenTelemetry providers configured through `configure_otel_providers()` share a +resource containing `"microsoft.foundry": "True"` (a string) by default. This +resource attribute applies to logs, traces, and metrics, including other +instrumentation using those providers. Existing environment and explicit resource +attribute overrides retain their precedence. If you configure providers yourself, +use `agent_framework.observability.create_resource()` for each provider or add the +attribute to your own shared resource. + See the following [getting started samples](https://github.com/microsoft/agent-framework/tree/main/python/samples/01-get-started) for more information. ## 2. Create a Simple Agent diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index fe24a7d138d..3f36c2430fa 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -851,6 +851,8 @@ def create_resource( This function reads standard OpenTelemetry environment variables to configure the resource, which identifies your service in telemetry backends. + Resources include the string attribute `microsoft.foundry="True"` by default, + shared by logs, traces, and metrics configured with this resource. The following environment variables are read: - OTEL_SERVICE_NAME: The name of the service (defaults to "agent_framework") @@ -925,7 +927,7 @@ def create_resource( # Apply OTEL_RESOURCE_ATTRIBUTES first, as a base — everything applied after this # (attributes/kwargs, then service_name/service_version) takes precedence over it, so an # explicit value is never silently replaced by the environment. - resource_attributes: dict[str, Any] = {} + resource_attributes: dict[str, Any] = {"microsoft.foundry": "True"} if resource_attrs_env := os.getenv("OTEL_RESOURCE_ATTRIBUTES"): resource_attributes.update(_parse_headers(resource_attrs_env)) @@ -1624,6 +1626,8 @@ def configure_otel_providers( This method creates the exporters and providers for the application based on the provided values and environment variables and enables instrumentation. + All three signal providers share a resource that includes the string attribute + `microsoft.foundry="True"` by default. Call this method once during application startup, before any telemetry is captured. DO NOT call this method multiple times, as it may lead to unexpected behavior. diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index 1403b374a98..a935fab2216 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -1478,6 +1478,33 @@ def test_get_exporters_from_env_env_only_headers_unaffected_by_origin_guard(monk # region Test create_resource +def test_create_resource_includes_foundry_attribute(monkeypatch: pytest.MonkeyPatch) -> None: + """Resources include the Foundry marker as a string, not a boolean.""" + from agent_framework.observability import create_resource + + monkeypatch.delenv("OTEL_RESOURCE_ATTRIBUTES", raising=False) + + resource = create_resource() + + assert resource.attributes["microsoft.foundry"] == "True" + + +def test_create_resource_foundry_attribute_preserves_override_precedence(monkeypatch: pytest.MonkeyPatch) -> None: + """The default marker follows existing environment and explicit attribute precedence.""" + from agent_framework.observability import create_resource + + monkeypatch.setenv("OTEL_RESOURCE_ATTRIBUTES", "microsoft.foundry=environment") + + assert create_resource().attributes["microsoft.foundry"] == "environment" + assert create_resource(attributes={"microsoft.foundry": "explicit"}).attributes["microsoft.foundry"] == "explicit" + assert ( + create_resource(attributes={"microsoft.foundry": "explicit"}, **{"microsoft.foundry": "keyword"}).attributes[ + "microsoft.foundry" + ] + == "keyword" + ) + + def test_create_resource_from_env(monkeypatch): """Test create_resource reads OTEL environment variables.""" from agent_framework.observability import create_resource @@ -4418,6 +4445,60 @@ class TestAgent(AgentTelemetryLayer, _TestAgent): # type: ignore[misc] # pyref # region Test _configure_providers +def test_configure_providers_share_foundry_resource() -> None: + """Exported logs, spans, and metrics carry the same marked resource.""" + from opentelemetry.sdk._logs.export import InMemoryLogRecordExporter + from opentelemetry.sdk.metrics.export import InMemoryMetricReader, MetricExporter + + from agent_framework.observability import ObservabilitySettings + + span_exporter = InMemorySpanExporter() + log_exporter = InMemoryLogRecordExporter() + metric_reader = InMemoryMetricReader() + metric_exporter = Mock(spec=MetricExporter) + settings = ObservabilitySettings( + service_name="foundry-test", + resource_attributes={"deployment.environment": "test"}, + ) + + with ( + patch("opentelemetry.trace.set_tracer_provider") as set_tracer, + patch("opentelemetry._logs.set_logger_provider") as set_logger, + patch("opentelemetry.metrics.set_meter_provider") as set_meter, + patch("opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader", return_value=metric_reader), + patch("agent_framework.observability.logger.addHandler"), + ): + settings._configure_providers([span_exporter, log_exporter, metric_exporter]) + + tracer_provider = set_tracer.call_args.args[0] + logger_provider = set_logger.call_args.args[0] + meter_provider = set_meter.call_args.args[0] + try: + with tracer_provider.get_tracer("test").start_as_current_span("test"): + pass + logger_provider.get_logger("test").emit(body="test log") + meter_provider.get_meter("test").create_counter("test_counter").add(1) + tracer_provider.force_flush() + logger_provider.force_flush() + + metrics_data = metric_reader.get_metrics_data() + assert metrics_data is not None + resources = [ + span_exporter.get_finished_spans()[0].resource, + log_exporter.get_finished_logs()[0].resource, + metrics_data.resource_metrics[0].resource, + ] + for resource in resources: + assert resource is resources[0] + assert resource.attributes["microsoft.foundry"] == "True" + assert resource.attributes["service.name"] == "foundry-test" + assert resource.attributes["deployment.environment"] == "test" + finally: + tracer_provider.shutdown() + logger_provider.shutdown() + meter_provider.shutdown() + + def test_configure_providers_with_span_exporters(monkeypatch): """Test _configure_providers correctly handles span exporters.""" from unittest.mock import Mock, patch