Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions dotnet/samples/02-agents/AgentOpenTelemetry/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object>
{
["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));
Expand All @@ -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
Expand All @@ -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))
{
Expand Down
11 changes: 11 additions & 0 deletions dotnet/samples/02-agents/AgentOpenTelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Globalization;
using OpenTelemetry;
using OpenTelemetry.Resources;

namespace Harness.Shared.Console;

Expand All @@ -27,6 +28,11 @@ public override ExportResult Export(in Batch<Activity> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

namespace Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -52,6 +53,7 @@ public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) w
});

builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddAttributes([new("microsoft.foundry", "True")]))
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

namespace Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -56,6 +57,7 @@ public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) w
});

builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddAttributes([new("microsoft.foundry", "True")]))
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
Expand Down
8 changes: 8 additions & 0 deletions python/packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion python/packages/core/agent_framework/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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.
Expand Down
81 changes: 81 additions & 0 deletions python/packages/core/tests/core/test_observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading