mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-28 23:48:39 +02:00
feat: add Aspire dashboard OTEL workflow
Add OTLP export wiring for the sidecar and dev scripts to launch the standalone Aspire Dashboard during development. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
<PackageReference Include="Microsoft.Agents.AI" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.GitHub.Copilot" Version="1.1.0-preview.260410.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.1.0" />
|
||||
<PackageReference Include="OpenTelemetry" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Aryx.AgentHost.Services;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
if (!args.Contains("--stdio", StringComparer.Ordinal))
|
||||
{
|
||||
@@ -6,5 +7,7 @@ if (!args.Contains("--stdio", StringComparer.Ordinal))
|
||||
return;
|
||||
}
|
||||
|
||||
using TracerProvider? tracerProvider = OpenTelemetrySetup.CreateTracerProviderFromEnvironment();
|
||||
|
||||
SidecarProtocolHost host = new();
|
||||
await host.RunAsync(Console.In, Console.Out, CancellationToken.None);
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Collections;
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Exporter;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace Aryx.AgentHost.Services;
|
||||
|
||||
internal static class OpenTelemetrySetup
|
||||
{
|
||||
private const string ServiceName = "Aryx.AgentHost";
|
||||
private const string EndpointEnvironmentVariableName = "OTEL_EXPORTER_OTLP_ENDPOINT";
|
||||
private const string ProtocolEnvironmentVariableName = "OTEL_EXPORTER_OTLP_PROTOCOL";
|
||||
|
||||
private static readonly string[] ActivitySourceNames =
|
||||
[
|
||||
"Microsoft.Agents.AI",
|
||||
"Microsoft.Agents.AI.Workflows",
|
||||
];
|
||||
|
||||
public static TracerProvider? CreateTracerProviderFromEnvironment(TextWriter? diagnosticsWriter = null)
|
||||
{
|
||||
return CreateTracerProvider(ReadEnvironmentVariables(), diagnosticsWriter ?? Console.Error);
|
||||
}
|
||||
|
||||
internal static TracerProvider? CreateTracerProvider(
|
||||
IEnumerable<KeyValuePair<string, string?>> environmentVariables,
|
||||
TextWriter diagnosticsWriter)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(environmentVariables);
|
||||
ArgumentNullException.ThrowIfNull(diagnosticsWriter);
|
||||
|
||||
OpenTelemetryTracingConfiguration? configuration = ResolveTracingConfiguration(environmentVariables);
|
||||
if (configuration is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
diagnosticsWriter.WriteLine(
|
||||
$"Aryx.AgentHost OpenTelemetry tracing enabled ({configuration.ProtocolLabel}) -> {configuration.Endpoint}.");
|
||||
|
||||
return Sdk.CreateTracerProviderBuilder()
|
||||
.SetResourceBuilder(
|
||||
ResourceBuilder.CreateDefault()
|
||||
.AddService(ServiceName, serviceVersion: ResolveServiceVersion()))
|
||||
.AddSource(configuration.ActivitySourceNames.ToArray())
|
||||
.AddOtlpExporter(options =>
|
||||
{
|
||||
options.Endpoint = configuration.Endpoint;
|
||||
options.Protocol = configuration.Protocol;
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
internal static OpenTelemetryTracingConfiguration? ResolveTracingConfiguration(
|
||||
IEnumerable<KeyValuePair<string, string?>> environmentVariables)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(environmentVariables);
|
||||
|
||||
string? endpointValue = ReadSetting(environmentVariables, EndpointEnvironmentVariableName);
|
||||
if (string.IsNullOrWhiteSpace(endpointValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(endpointValue, UriKind.Absolute, out Uri? endpoint)
|
||||
|| (endpoint.Scheme != Uri.UriSchemeHttp && endpoint.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{EndpointEnvironmentVariableName} must be an absolute http or https URL. Received '{endpointValue}'.");
|
||||
}
|
||||
|
||||
OtlpExportProtocol protocol = ResolveProtocol(ReadSetting(environmentVariables, ProtocolEnvironmentVariableName));
|
||||
return new OpenTelemetryTracingConfiguration(endpoint, protocol, ActivitySourceNames);
|
||||
}
|
||||
|
||||
private static string? ResolveServiceVersion()
|
||||
{
|
||||
return typeof(OpenTelemetrySetup).Assembly.GetName().Version?.ToString();
|
||||
}
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, string?>> ReadEnvironmentVariables()
|
||||
{
|
||||
return Environment.GetEnvironmentVariables()
|
||||
.Cast<DictionaryEntry>()
|
||||
.Select(entry => new KeyValuePair<string, string?>(
|
||||
entry.Key?.ToString() ?? string.Empty,
|
||||
entry.Value?.ToString()));
|
||||
}
|
||||
|
||||
private static string? ReadSetting(
|
||||
IEnumerable<KeyValuePair<string, string?>> environmentVariables,
|
||||
string name)
|
||||
{
|
||||
foreach (KeyValuePair<string, string?> entry in environmentVariables)
|
||||
{
|
||||
if (!string.Equals(entry.Key, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(entry.Value)
|
||||
? null
|
||||
: entry.Value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static OtlpExportProtocol ResolveProtocol(string? protocolValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(protocolValue))
|
||||
{
|
||||
return OtlpExportProtocol.Grpc;
|
||||
}
|
||||
|
||||
return protocolValue.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"grpc" => OtlpExportProtocol.Grpc,
|
||||
"http/protobuf" => OtlpExportProtocol.HttpProtobuf,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"{ProtocolEnvironmentVariableName} must be 'grpc' or 'http/protobuf'. Received '{protocolValue}'."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record OpenTelemetryTracingConfiguration(
|
||||
Uri Endpoint,
|
||||
OtlpExportProtocol Protocol,
|
||||
IReadOnlyList<string> ActivitySourceNames)
|
||||
{
|
||||
public string ProtocolLabel => Protocol switch
|
||||
{
|
||||
OtlpExportProtocol.HttpProtobuf => "http/protobuf",
|
||||
_ => "grpc",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Aryx.AgentHost.Services;
|
||||
using OpenTelemetry.Exporter;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace Aryx.AgentHost.Tests;
|
||||
|
||||
public sealed class OpenTelemetrySetupTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveTracingConfiguration_ReturnsNullWhenEndpointMissing()
|
||||
{
|
||||
OpenTelemetryTracingConfiguration? configuration = OpenTelemetrySetup.ResolveTracingConfiguration([]);
|
||||
|
||||
Assert.Null(configuration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTracingConfiguration_UsesGrpcByDefault()
|
||||
{
|
||||
OpenTelemetryTracingConfiguration? configuration = OpenTelemetrySetup.ResolveTracingConfiguration(
|
||||
[
|
||||
new KeyValuePair<string, string?>("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"),
|
||||
]);
|
||||
|
||||
Assert.NotNull(configuration);
|
||||
Assert.Equal(new Uri("http://localhost:4317"), configuration.Endpoint);
|
||||
Assert.Equal(OtlpExportProtocol.Grpc, configuration.Protocol);
|
||||
Assert.Collection(
|
||||
configuration.ActivitySourceNames,
|
||||
source => Assert.Equal("Microsoft.Agents.AI", source),
|
||||
source => Assert.Equal("Microsoft.Agents.AI.Workflows", source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTracingConfiguration_UsesHttpProtobufWhenRequested()
|
||||
{
|
||||
OpenTelemetryTracingConfiguration? configuration = OpenTelemetrySetup.ResolveTracingConfiguration(
|
||||
[
|
||||
new KeyValuePair<string, string?>("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"),
|
||||
new KeyValuePair<string, string?>("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf"),
|
||||
]);
|
||||
|
||||
Assert.NotNull(configuration);
|
||||
Assert.Equal(new Uri("http://localhost:4318"), configuration.Endpoint);
|
||||
Assert.Equal(OtlpExportProtocol.HttpProtobuf, configuration.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTracingConfiguration_ThrowsWhenEndpointInvalid()
|
||||
{
|
||||
InvalidOperationException error = Assert.Throws<InvalidOperationException>(() =>
|
||||
OpenTelemetrySetup.ResolveTracingConfiguration(
|
||||
[
|
||||
new KeyValuePair<string, string?>("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317"),
|
||||
]));
|
||||
|
||||
Assert.Contains("OTEL_EXPORTER_OTLP_ENDPOINT", error.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTracingConfiguration_ThrowsWhenProtocolUnsupported()
|
||||
{
|
||||
InvalidOperationException error = Assert.Throws<InvalidOperationException>(() =>
|
||||
OpenTelemetrySetup.ResolveTracingConfiguration(
|
||||
[
|
||||
new KeyValuePair<string, string?>("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"),
|
||||
new KeyValuePair<string, string?>("OTEL_EXPORTER_OTLP_PROTOCOL", "http/json"),
|
||||
]));
|
||||
|
||||
Assert.Contains("OTEL_EXPORTER_OTLP_PROTOCOL", error.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTracerProvider_CreatesProviderAndWritesDiagnosticsWhenConfigured()
|
||||
{
|
||||
StringWriter diagnosticsWriter = new();
|
||||
|
||||
using TracerProvider? tracerProvider = OpenTelemetrySetup.CreateTracerProvider(
|
||||
[
|
||||
new KeyValuePair<string, string?>("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"),
|
||||
],
|
||||
diagnosticsWriter);
|
||||
|
||||
Assert.NotNull(tracerProvider);
|
||||
Assert.Contains("Aryx.AgentHost OpenTelemetry tracing enabled", diagnosticsWriter.ToString(), StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user