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