diff --git a/samples/dotnet/README.md b/samples/dotnet/README.md index 2ca97dfa..9d8aa798 100644 --- a/samples/dotnet/README.md +++ b/samples/dotnet/README.md @@ -12,4 +12,5 @@ |RetrievalBot Sample with Semantic Kernel|A simple Retrieval Agent that is hosted on an Asp.net core web service. |[RetrievalBot](RetrievalBot/README.md)| |MultiAgent|Demonstrates multiple AgentApplication in the same host|[MultiAgent](multiagent/README.md)| |GenesysHandoff|Demonstrates how a Microsoft Copilot Studio Agent (bot) can seamlessly **hand off a conversation to a live agent** in **Genesys Cloud**.|[GenesysHandoff](genesys-handoff/README.md)| -|Proactive|Demonstrates the basics of a proactive conversation using in-code and Http triggers.|[Proactive](proactive/README.md)| \ No newline at end of file +|Proactive|Demonstrates the basics of a proactive conversation using in-code and Http triggers.|[Proactive](proactive/README.md)| +|OpenTelemetry Agent|Configures OTel tracing, metrics, and logging with OTLP export|[otel](otel/README.md)| \ No newline at end of file diff --git a/samples/dotnet/Samples.sln b/samples/dotnet/Samples.sln index dad0dfd0..3eba12e4 100644 --- a/samples/dotnet/Samples.sln +++ b/samples/dotnet/Samples.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.1.11312.151 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.37111.16 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamingMessageAgent", "azure-ai-streaming\StreamingMessageAgent.csproj", "{6547D985-7762-488D-9ADC-1BC3FD62AF66}" EndProject @@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiAgent", "multiagent\Mu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proactive", "proactive\Proactive.csproj", "{2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Otel", "otel\Otel.csproj", "{1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,6 +87,10 @@ Global {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|Any CPU.Build.0 = Release|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/dotnet/otel/AgentOtelExtension.cs b/samples/dotnet/otel/AgentOtelExtension.cs new file mode 100644 index 00000000..782b6527 --- /dev/null +++ b/samples/dotnet/otel/AgentOtelExtension.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using OpenTelemetry.Logs; +using System.Linq; +using Microsoft.Agents.Core.Telemetry; + +namespace Otel +{ + // This can be used by ASP.NET Core apps, Azure Functions, and other .NET apps using the Generic Host. + // This allows you to use the local aspire desktop and monitor Agents SDK operations. + // To learn more about using the local aspire desktop, see https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone?tabs=bash + public static class AgentOtelExtension + { + + public static TBuilder ConfigureOtelProviders(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + + builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService( + serviceName: AgentsTelemetry.SourceName, + serviceVersion: AgentsTelemetry.SourceVersion + )) + .WithTracing(tracing => tracing + .AddSource( + "Microsoft.AspNetCore", + "System.Net.Http", + AgentsTelemetry.SourceName + ) + .SetSampler(new AlwaysOnSampler()) + .AddAspNetCoreInstrumentation(tracing => + { + // Exclude health check requests from tracing + tracing.RecordException = true; + tracing.EnrichWithHttpRequest = (activity, request) => + { + activity.SetTag("http.request.body.size", request.ContentLength); + activity.SetTag("user_agent", request.Headers.UserAgent); + }; + tracing.EnrichWithHttpResponse = (activity, response) => + { + activity.SetTag("http.response.body.size", response.ContentLength); + }; + }) + .AddHttpClientInstrumentation(o => + { + o.RecordException = true; + // Enrich outgoing request/response with extra tags + o.EnrichWithHttpRequestMessage = (activity, request) => + { + activity.SetTag("http.request.method", request.Method); + activity.SetTag("http.request.host", request.RequestUri?.Host); + activity.SetTag("http.request.useragent", request.Headers?.UserAgent); + }; + o.EnrichWithHttpResponseMessage = (activity, response) => + { + activity.SetTag("http.response.status_code", (int)response.StatusCode); + //activity.SetTag("http.response.headers", response.Content.Headers); + // Convert response.Content.Headers to a string array: "HeaderName=val1,val2" + var headerList = response.Content?.Headers? + .Where(h => h.Key != "Authorization") + .Select(h => $"{h.Key}={string.Join(",", h.Value)}") + .ToArray(); + + if (headerList is { Length: > 0 }) + { + // Set as an array tag (preferred for OTEL exporters supporting array-of-primitive attributes) + activity.SetTag("http.response.headers", headerList); + + // (Optional) Also emit individual header tags (comment out if too high-cardinality) + // foreach (var h in response.Content.Headers) + // { + // activity.SetTag($"http.response.header.{h.Key.ToLowerInvariant()}", string.Join(",", h.Value)); + // } + } + + }; + }) + //.AddConsoleExporter() + .AddOtlpExporter()) + .WithMetrics(metrics => metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddMeter(AgentsTelemetry.SourceName) + //.AddConsoleExporter() + .AddOtlpExporter()); + + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + //logging.AddConsoleExporter(); + logging.AddOtlpExporter(); + }); + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + } +} \ No newline at end of file diff --git a/samples/dotnet/otel/AspNetExtension.cs b/samples/dotnet/otel/AspNetExtension.cs new file mode 100644 index 00000000..b5a747e6 --- /dev/null +++ b/samples/dotnet/otel/AspNetExtension.cs @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Authentication; +using Microsoft.Agents.Core; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Validators; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +public static class AspNetExtensions +{ + private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. + /// + /// The to add authentication services to. + /// The application configuration containing a section. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + /// + /// + /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and + /// all requests will be treated as unauthenticated. This is useful for local development only. + /// + /// + /// Minimum configuration for Azure Public cloud: + /// + /// "TokenValidation": { + /// "Audiences": [ "{{ClientId}}" ], + /// "TenantId": "{{TenantId}}" + /// } + /// + /// + /// + /// Minimum configuration for Azure Government cloud — add "IsGov": true: + /// + /// "TokenValidation": { + /// "Audiences": [ "{{ClientId}}" ], + /// "TenantId": "{{TenantId}}", + /// "IsGov": true + /// } + /// + /// Setting IsGov automatically selects the correct government-cloud issuer URLs and OpenID metadata + /// endpoints. See for the full list of defaults that are applied. + /// + /// + /// For China or other sovereign clouds, omit IsGov and set + /// , + /// , and + /// explicitly. + /// See for the full set of available settings. + /// + /// + public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation") + { + IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); + + if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + { + System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); + services.AddControllers(); // else calls to UseAuthorization will fail. + return; + } + + services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests using the supplied options. + /// + /// The to add authentication services to. + /// The fully populated to use. + public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) + { + AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); + services.AddControllers(); + + // Must have at least one Audience. + if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences requires at least one ClientId"); + } + + // Audience values must be GUID's + foreach (var audience in validationOptions.Audiences) + { + if (!Guid.TryParse(audience, out _)) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences values must be a GUID"); + } + } + + // If ValidIssuers is empty, default for ABS Public Cloud + if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) + { + if (validationOptions.IsGov) + { + validationOptions.ValidIssuers = + [ + AuthenticationConstants.GovBotFrameworkTokenIssuer, + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0" + ]; + + if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _)) + { + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId)); + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2, validationOptions.TenantId)); + } + } + else + { + validationOptions.ValidIssuers = + [ + AuthenticationConstants.BotFrameworkTokenIssuer, + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", + "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", + "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", + ]; + + if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _)) + { + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId)); + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, validationOptions.TenantId)); + } + } + } + + // If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. + if (string.IsNullOrEmpty(validationOptions.AzureBotServiceOpenIdMetadataUrl)) + { + validationOptions.AzureBotServiceOpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl; + } + + // If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens. + if (string.IsNullOrEmpty(validationOptions.OpenIdMetadataUrl)) + { + validationOptions.OpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; + } + + var openIdMetadataRefresh = validationOptions.OpenIdMetadataRefresh ?? BaseConfigurationManager.DefaultAutomaticRefreshInterval; + + _ = services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + ValidIssuers = validationOptions.ValidIssuers, + ValidAudiences = validationOptions.Audiences, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + }; + + // Using Microsoft.IdentityModel.Validators + options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + + options.Events = new JwtBearerEvents + { + // Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens. + OnMessageReceived = async context => + { + string authorizationHeader = context.Request.Headers.Authorization.ToString(); + + if (string.IsNullOrEmpty(authorizationHeader)) + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + await Task.CompletedTask.ConfigureAwait(false); + return; + } + + string[] parts = authorizationHeader?.Split(' ')!; + if (parts.Length != 2 || parts[0] != "Bearer") + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + await Task.CompletedTask.ConfigureAwait(false); + return; + } + + JwtSecurityToken token = new(parts[1]); + string issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value!; + + if (validationOptions.AzureBotServiceTokenHandling + && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + { + // Use the Azure Bot authority for this configuration manager + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => + { + return new ConfigurationManager(validationOptions.AzureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + } + else + { + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.OpenIdMetadataUrl, key => + { + return new ConfigurationManager(validationOptions.OpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + } + + await Task.CompletedTask.ConfigureAwait(false); + }, + + OnTokenValidated = context => + { + return Task.CompletedTask; + }, + OnForbidden = context => + { + return Task.CompletedTask; + }, + OnAuthenticationFailed = context => + { + return Task.CompletedTask; + } + }; + }); + } + + /// + /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// Read from the TokenValidation configuration section by . + /// + /// + /// An Enabled key may also appear in the same configuration section. When set to false, + /// authentication is disabled entirely and this class is not read. This key is not a property of + /// because it is evaluated before deserialization. + /// + public class TokenValidationOptions + { + /// + /// One or more Client IDs of the Azure Bot registration. At least one value is required. + /// + public IList? Audiences { get; set; } + + /// + /// Tenant ID of the Azure Bot. Optional but recommended. + /// When provided, tenant-specific issuer URLs are added to automatically. + /// + public string? TenantId { get; set; } + + /// + /// Override the list of trusted token issuers. Optional. + /// When omitted, default issuers are derived from and . + /// For Public cloud the defaults include the Azure Bot Service issuer and common Microsoft tenant issuers. + /// For Gov cloud the defaults include plus + /// tenant-specific issuer URLs built from + /// and . + /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// + public IList? ValidIssuers { get; set; } + + /// + /// Set to true for Azure Government (USGov) cloud deployments. Defaults to false (Public cloud). + /// When true, the following defaults are applied to any property that is not set explicitly: + /// + /// + /// → + /// + /// (https://login.botframework.azure.us/v1/.well-known/openidconfiguration) + /// + /// + /// → + /// + /// (https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration) + /// + /// + /// → + /// (https://api.botframework.us), + /// plus tenant-specific v1 and v2 issuer URLs when is provided. + /// + /// + /// For China or other sovereign clouds, leave this false and set all URLs and issuers explicitly. + /// + public bool IsGov { get; set; } = false; + + /// + /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. + /// When omitted, defaults to when + /// is false, or + /// when is true. + /// Set explicitly for China or other sovereign clouds. + /// + public string? AzureBotServiceOpenIdMetadataUrl { get; set; } + + /// + /// OpenID Connect metadata URL used to validate Entra ID (AAD) tokens. Optional. + /// When omitted, defaults to when + /// is false, or + /// when is true. + /// Set explicitly for China or other sovereign clouds. + /// + public string? OpenIdMetadataUrl { get; set; } + + /// + /// Enables special handling for tokens issued directly by Azure Bot Service (as opposed to Entra ID tokens). + /// Defaults to true and should remain true until Azure Bot Service sends Entra ID tokens exclusively. + /// When true, the endpoint is used for ABS token validation + /// and is used for all other tokens. + /// + public bool AzureBotServiceTokenHandling { get; set; } = true; + + /// + /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. + /// + public TimeSpan? OpenIdMetadataRefresh { get; set; } + } +} \ No newline at end of file diff --git a/samples/dotnet/otel/MyAgent.cs b/samples/dotnet/otel/MyAgent.cs new file mode 100644 index 00000000..18cc0547 --- /dev/null +++ b/samples/dotnet/otel/MyAgent.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Builder; +using Microsoft.Agents.Builder.App; +using Microsoft.Agents.Builder.State; +using Microsoft.Agents.Core.Models; +using System.Threading.Tasks; +using System.Threading; + +namespace Otel; + +public class MyAgent : AgentApplication +{ + public MyAgent(AgentApplicationOptions options) : base(options) + { + OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeMessageAsync); + OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last); + } + + private async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + foreach (ChannelAccount member in turnContext.Activity.MembersAdded) + { + if (member.Id != turnContext.Activity.Recipient.Id) + { + await turnContext.SendActivityAsync(MessageFactory.Text("Hello and Welcome!"), cancellationToken); + } + } + } + + private async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + await turnContext.SendActivityAsync($"You said: {turnContext.Activity.Text}", cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/samples/dotnet/otel/Otel.csproj b/samples/dotnet/otel/Otel.csproj new file mode 100644 index 00000000..e20bb999 --- /dev/null +++ b/samples/dotnet/otel/Otel.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + latest + disable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/dotnet/otel/Program.cs b/samples/dotnet/otel/Program.cs new file mode 100644 index 00000000..116b982f --- /dev/null +++ b/samples/dotnet/otel/Program.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Hosting.AspNetCore; +using Microsoft.Agents.Storage; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Otel; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// Configure defaults for Aspire dashboard +builder.ConfigureOtelProviders(); + +builder.Services.AddHttpClient(); + +// Add the AgentApplication, which contains the logic for responding to +// user messages. +builder.AddAgent(); + +// Register IStorage. For development, MemoryStorage is suitable. +// For production Agents, persisted storage should be used so +// that state survives Agent restarts, and operates correctly +// in a cluster of Agent instances. +builder.Services.AddSingleton(); + +// Add AspNet token validation for Azure Bot Service and Entra. Authentication is +// configured in the appsettings.json "TokenValidation" section. +builder.Services.AddControllers(); +builder.Services.AddAgentAspNetAuthentication(builder.Configuration); + +WebApplication app = builder.Build(); + +// Enable AspNet authentication and authorization +app.UseAuthentication(); +app.UseAuthorization(); + +// Map GET "/" +app.MapAgentRootEndpoint(); + +// Map the endpoints for all agents using the [AgentInterface] attribute. +// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). +app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); + +app.Run(); \ No newline at end of file diff --git a/samples/dotnet/otel/Properties/launchSettings.json b/samples/dotnet/otel/Properties/launchSettings.json new file mode 100644 index 00000000..846c12bc --- /dev/null +++ b/samples/dotnet/otel/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Otel": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:3979;http://localhost:3978" + } + } +} \ No newline at end of file diff --git a/samples/dotnet/otel/README.md b/samples/dotnet/otel/README.md new file mode 100644 index 00000000..f3b5267a --- /dev/null +++ b/samples/dotnet/otel/README.md @@ -0,0 +1,124 @@ +# OpenTelemetry Agent + +This is a sample of a simple Agent that is hosted on an ASP.NET Core web service. The sample demonstrates how to configure [OpenTelemetry](https://opentelemetry.io/) (OTel) for distributed tracing, metrics, and logging in a Microsoft 365 Agents SDK application. + +Telemetry is exported via OTLP to a configurable endpoint. The sample instruments ASP.NET Core, `HttpClient`, the .NET runtime, and the Agents SDK telemetry source. + +## Prerequisites + +- [.NET](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) version 8.0 +- [dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) (for local development) +- [Docker](https://www.docker.com/) (to run the Aspire Dashboard for local telemetry visualization) + +## Start the Telemetry Dashboard + +Run the [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) as a standalone OTLP collector and visualization UI: + +```bash +docker run --rm -it -p 18888:18888 -p 4317:18889 --name aspire-dashboard mcr.microsoft.com/dotnet/aspire-dashboard:latest +``` + +- **Port 18888** — Dashboard UI. Open `http://localhost:18888` in a browser to view traces, metrics, and logs. +- **Port 4317** — OTLP gRPC endpoint (default for the agent to export telemetry). + +## Local Setup + +### Configure Azure Bot Service + +1. Create an Azure Bot with one of these authentication types + - [SingleTenant, Client Secret](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-single-secret) + - [SingleTenant, Federated Credentials](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-federated-credentials) + - [User Assigned Managed Identity](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-managed-identity) + + > **IMPORTANT:** If you want to run your agent locally via devtunnels, the only support auth type is ClientSecrets and Certificates + +1. Update `appsettings.json` with your bot credentials: + ```json + "Connections": { + "ServiceConnection": { + "Settings": { + "ClientId": "{{ClientId}}", + "ClientSecret": "{{ClientSecret}}", + "AuthorityEndpoint": "https://login.microsoftonline.com/{{TenantId}}" + } + } + } + ``` + +1. Running the Agent + 1. Running the Agent locally + - Requires a tunneling tool to allow for local development and debugging should you wish to do local development whilst connected to a external client such as Microsoft Teams. + - **For ClientSecret or Certificate authentication types only.** Federated Credentials and Managed Identity will not work via a tunnel to a local agent and must be deployed to an App Service or container. + + 1. Run `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: + + ```bash + devtunnel host -p 3978 --allow-anonymous + ``` + + 1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `{tunnel-url}/api/messages` + + 1. Start the Agent in Visual Studio + + 1. Deploy Agent code to Azure + 1. VS Publish works well for this. But any tools used to deploy a web application will also work. + 1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `https://{{appServiceDomain}}/api/messages` + + +## Accessing the Agent + +### Using the Agent in WebChat + +1. Go to your Azure Bot Service resource in the Azure Portal and select **Test in WebChat** + +## OpenTelemetry Configuration + +The `AgentOtelExtension.cs` file provides the `ConfigureOtelProviders` extension method, which wires up all three OTel signals before the app starts: + +```csharp +builder.ConfigureOtelProviders(); +``` + +By default, telemetry is exported to `http://localhost:4317` via OTLP gRPC. To change the endpoint, set the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable or configure it in `appsettings.json`. + +### What is instrumented + +| Signal | Sources | +|--------|---------| +| **Traces** | ASP.NET Core requests, `HttpClient` outgoing calls, Agents SDK (`AgentsTelemetry.ActivitySource`) | +| **Metrics** | ASP.NET Core, `HttpClient`, .NET runtime, Agents SDK meter | +| **Logs** | All `ILogger` log records forwarded to the OTLP log exporter | + +### Azure Monitor (Application Insights) + +The sample includes a commented-out block for exporting to Azure Monitor. To enable it, add the `Azure.Monitor.OpenTelemetry.AspNetCore` NuGet package and uncomment the following in `AgentOtelExtension.cs`: + +```csharp +if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) +{ + builder.Services.AddOpenTelemetry() + .UseAzureMonitor(); +} +``` + +Then set `APPLICATIONINSIGHTS_CONNECTION_STRING` to your Application Insights connection string. + +## Enabling JWT Token Validation + +By default, JWT token validation is disabled to support local debugging. To enable it, update `appsettings.json`: + +```json +"TokenValidation": { + "Enabled": true, + "Audiences": [ + "{{ClientId}}" + ], + "TenantId": "{{TenantId}}" +} +``` + +## Further reading + +- [OpenTelemetry .NET](https://opentelemetry.io/docs/languages/net/) +- [.NET Aspire Dashboard (standalone)](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) +- [Microsoft 365 Agents SDK](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/) diff --git a/samples/dotnet/otel/appManifest/color.png b/samples/dotnet/otel/appManifest/color.png new file mode 100644 index 00000000..b8cf81af Binary files /dev/null and b/samples/dotnet/otel/appManifest/color.png differ diff --git a/samples/dotnet/otel/appManifest/manifest.json b/samples/dotnet/otel/appManifest/manifest.json new file mode 100644 index 00000000..c6ffa1ed --- /dev/null +++ b/samples/dotnet/otel/appManifest/manifest.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.22/MicrosoftTeams.schema.json", + "manifestVersion": "1.22", + "version": "1.0.0", + "id": "${{AAD_APP_CLIENT_ID}}", + "developer": { + "name": "Microsoft, Inc.", + "websiteUrl": "https://example.azurewebsites.net", + "privacyUrl": "https://example.azurewebsites.net/privacy", + "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "AgentSDK OpenTelemetry", + "full": "AgentSDK OpenTelemetry" + }, + "description": { + "short": "Sample demonstrating AgentSDK + Azure Bot Services for OpenTelemetry integration.", + "full": "This sample demonstrates how to export telemetry from an Agent built with the AgentSDK to an OpenTelemetry collector." + }, + "accentColor": "#FFFFFF", + "copilotAgents": { + "customEngineAgents": [ + { + "id": "${{AAD_APP_CLIENT_ID}}", + "type": "bot" + } + ] + }, + "bots": [ + { + "botId": "${{AAD_APP_CLIENT_ID}}", + "scopes": [ + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "token.botframework.com", + "<>" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://botid-${{AAD_APP_CLIENT_ID}}" + } +} \ No newline at end of file diff --git a/samples/dotnet/otel/appManifest/outline.png b/samples/dotnet/otel/appManifest/outline.png new file mode 100644 index 00000000..2c3bf6fa Binary files /dev/null and b/samples/dotnet/otel/appManifest/outline.png differ diff --git a/samples/dotnet/otel/appsettings.json b/samples/dotnet/otel/appsettings.json new file mode 100644 index 00000000..9ba681bb --- /dev/null +++ b/samples/dotnet/otel/appsettings.json @@ -0,0 +1,42 @@ +{ + "TokenValidation": { + "Enabled": true, + "Audiences": [ + "{{ClientId}}" // this is the Client ID used for the Azure Bot + ], + "TenantId": "{{TenantId}}" + }, + + "AgentApplication": { + "StartTypingTimer": false, + "RemoveRecipientMention": false, + "NormalizeMentions": false + }, + + "Connections": { + "ServiceConnection": { + "Settings": { + "AuthType": "ClientSecret", // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. The default is ClientSecret. + "AuthorityEndpoint": "https://login.microsoftonline.com/{{TenantId}}", + "ClientId": "{{ClientId}}", // this is the Client ID used for the Azure Bot + "ClientSecret": "00000000-0000-0000-0000-000000000000", // this is the Client Secret used for the connection. + "Scopes": [ + "https://api.botframework.com/.default" + ] + } + } + }, + "ConnectionsMap": [ + { + "ServiceUrl": "*", + "Connection": "ServiceConnection" + } + ], + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/samples/python/README.md b/samples/python/README.md index ab5ce7eb..8cd77d9f 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -10,6 +10,7 @@ |Copilot Studio Client|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| |Cards Agent|Agent that uses rich cards to enhance conversation design |[cards](cards/README.md)| |Copilot Studio Skill|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| +|OpenTelemetry|Instrument an agent and consume telemetry via the Aspire dashboard|[opentelemetry](otel/README.md)| ## Important Notice - Import Changes diff --git a/samples/python/otel/README.md b/samples/python/otel/README.md new file mode 100644 index 00000000..d0a16bd9 --- /dev/null +++ b/samples/python/otel/README.md @@ -0,0 +1,110 @@ +# OpenTelemetry Agent + +This is a sample of a simple Agent that is hosted on a Python web service. The sample demonstrates how to configure [OpenTelemetry](https://opentelemetry.io/) (OTel) for distributed tracing, metrics, and logging in a Microsoft 365 Agents SDK application. + +The sample exports telemetry via OTLP (gRPC) to a configurable endpoint and automatically instruments the `aiohttp` server, `aiohttp` client, and `requests` libraries. + +## Prerequisites + +- [Python](https://www.python.org/) version 3.9 or higher +- [dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) (for local development) +- [Docker](https://www.docker.com/) (to run the Aspire Dashboard for local telemetry visualization) + +## Local Setup + +### Start the Telemetry Dashboard + +This sample includes a PowerShell script to launch the [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/overview) as an OTLP collector and visualization UI. + +```powershell +./start_dashboard.ps1 +``` + +This runs the Aspire Dashboard container with: +- **Port 18888** — Dashboard UI (open in browser to view traces, metrics, and logs) +- **Port 4317** — OTLP gRPC endpoint (default for the agent to export telemetry) + +### Configure Azure Bot Service + +1. [Create an Azure Bot](https://aka.ms/AgentsSDK-CreateBot) + - Record the Application ID, the Tenant ID, and the Client Secret for use below + +1. Configuring the token connection in the Agent settings + 1. Open the `env.TEMPLATE` file in the root of the sample project, rename it to `.env` and configure the following values: + 1. Set the **CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID** to the AppId of the bot identity. + 2. Set the **CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET** to the Secret that was created for your identity. *This is the `Secret Value` shown in the AppRegistration*. + 3. Set the **CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID** to the Tenant Id where your application is registered. + +1. Run `dev tunnels`. See [Create and host a dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: + + ```bash + devtunnel host -p 3978 --allow-anonymous + ``` + +1. Take note of the url shown after `Connect via browser:` + +1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `{tunnel-url}/api/messages` + +### Running the Agent + +1. Open this folder from your IDE or Terminal of preference +1. (Optional but recommended) Set up virtual environment and activate it. +1. Install dependencies + +```sh +pip install -r requirements.txt +``` + +### Run in localhost, anonymous mode + +1. Start the application + +```sh +python -m src.main +``` + +At this point you should see the message + +```text +======== Running on http://localhost:3978 ======== +``` + +The agent is ready to accept messages. + +## Accessing the Agent + +### Using the Agent in WebChat + +1. Go to your Azure Bot Service resource in the Azure Portal and select **Test in WebChat** + +## OpenTelemetry Configuration + +The `telemetry.py` files provides the `configure_otel_providers` function, which sets up tracing, metrics, and logging before the agent starts: + +```python +from telemetry import configure_otel_providers + +configure_otel_providers(service_name="quickstart_agent") +``` + +By default, telemetry is exported to `http://localhost:4317/` via OTLP gRPC. To change the endpoint, set the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable in your `.env` file: + +``` +OTEL_EXPORTER_OTLP_ENDPOINT=http://your-collector:4317/ +``` + +### What is instrumented + +| Signal | Description | +|--------|-------------| +| **Traces** | HTTP spans for every incoming request (via `opentelemetry-instrumentation-aiohttp-server`) and outgoing requests (via `opentelemetry-instrumentation-aiohttp-client` and `opentelemetry-instrumentation-requests`) | +| **Metrics** | Exported on a periodic interval using `PeriodicExportingMetricReader` | +| **Logs** | Python `logging` records are forwarded to the OTLP log exporter via `LoggingHandler` | + +## Further reading + +- [OpenTelemetry Python](https://opentelemetry-python.readthedocs.io/) +- [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/overview) +- [Microsoft 365 Agents SDK](https://github.com/microsoft/agents) + +For more information on standard logging configuration, see the logging section in the [Quickstart Agent sample README](../quickstart/README.md). diff --git a/samples/python/otel/env.TEMPLATE b/samples/python/otel/env.TEMPLATE new file mode 100644 index 00000000..fda041d3 --- /dev/null +++ b/samples/python/otel/env.TEMPLATE @@ -0,0 +1,7 @@ +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=client-id +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=client-secret +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=tenant-id + +AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__GRAPH__SETTINGS__AZUREBOTOAUTHCONNECTIONNAME=connection-name + +LOGGING__LOGLEVEL__microsoft_agents.hosting.core=INFO \ No newline at end of file diff --git a/samples/python/otel/requirements.txt b/samples/python/otel/requirements.txt new file mode 100644 index 00000000..879687ff --- /dev/null +++ b/samples/python/otel/requirements.txt @@ -0,0 +1,14 @@ +python-dotenv +aiohttp +microsoft-agents-hosting-aiohttp +microsoft-agents-hosting-core +microsoft-agents-authentication-msal +microsoft-agents-activity +opentelemetry-instrumentation-aiohttp-server +opentelemetry-instrumentation-aiohttp-client +opentelemetry-instrumentation-requests +opentelemetry-exporter-otlp +opentelemetry-sdk +opentelemetry-api +opentelemetry-instrumentation-logging +opentelemetry-instrumentation \ No newline at end of file diff --git a/samples/python/otel/src/agent.py b/samples/python/otel/src/agent.py new file mode 100644 index 00000000..9dc9496e --- /dev/null +++ b/samples/python/otel/src/agent.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import sys +import traceback +from dotenv import load_dotenv + +from os import environ +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import ( + Authorization, + AgentApplication, + TurnState, + TurnContext, + MemoryStorage, +) +from microsoft_agents.authentication.msal import MsalConnectionManager +from microsoft_agents.activity import load_configuration_from_env + +load_dotenv() +agents_sdk_config = load_configuration_from_env(environ) + +STORAGE = MemoryStorage() +CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config) +ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER) +AUTHORIZATION = Authorization(STORAGE, CONNECTION_MANAGER, **agents_sdk_config) + + +AGENT_APP = AgentApplication[TurnState]( + storage=STORAGE, adapter=ADAPTER, authorization=AUTHORIZATION, **agents_sdk_config +) + + +@AGENT_APP.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, _state: TurnState): + await context.send_activity( + "Welcome to the empty agent! " + "This agent is designed to be a starting point for your own agent development." + ) + return True + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, _state: TurnState): + await context.send_activity(f"you said: {context.activity.text}") + +@AGENT_APP.error +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") diff --git a/samples/python/otel/src/main.py b/samples/python/otel/src/main.py new file mode 100644 index 00000000..9d7d5253 --- /dev/null +++ b/samples/python/otel/src/main.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from telemetry import configure_otel_providers + +configure_otel_providers(service_name="quickstart_agent") + +from agent import AGENT_APP, CONNECTION_MANAGER +from start_server import start_server + +start_server( + agent_application=AGENT_APP, + auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), +) diff --git a/samples/python/otel/src/start_server.py b/samples/python/otel/src/start_server.py new file mode 100644 index 00000000..d5b655ad --- /dev/null +++ b/samples/python/otel/src/start_server.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from os import environ +import logging + +from microsoft_agents.hosting.core import AgentApplication, AgentAuthConfiguration +from microsoft_agents.hosting.aiohttp import ( + start_agent_process, + CloudAdapter, + jwt_authorization_middleware, +) +from aiohttp.web import Request, Response, Application, run_app + +logger = logging.getLogger(__name__) + + +def start_server( + agent_application: AgentApplication, auth_configuration: AgentAuthConfiguration +): + async def entry_point(req: Request) -> Response: + + logger.info("Request received at /api/messages endpoint.") + agent: AgentApplication = req.app["agent_app"] + adapter: CloudAdapter = req.app["adapter"] + + return await start_agent_process( + req, + agent, + adapter, + ) + + APP = Application(middlewares=[jwt_authorization_middleware]) + APP.router.add_post("/api/messages", entry_point) + + APP["agent_configuration"] = auth_configuration + APP["agent_app"] = agent_application + APP["adapter"] = agent_application.adapter + + run_app(APP, host="localhost", port=int(environ.get("PORT", 3978))) diff --git a/samples/python/otel/src/telemetry.py b/samples/python/otel/src/telemetry.py new file mode 100644 index 00000000..d6cdbf1a --- /dev/null +++ b/samples/python/otel/src/telemetry.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging +import os +import requests + +import aiohttp +from opentelemetry import metrics, trace +from opentelemetry.trace import Span +from opentelemetry._logs import set_logger_provider +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor + +from opentelemetry.instrumentation.aiohttp_server import AioHttpServerInstrumentor +from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor + +def instrument_libraries(): + """Instrument libraries for OpenTelemetry.""" + + # ## + # # instrument aiohttp client + # ## + def aiohttp_client_request_hook( + span: Span, params: aiohttp.TraceRequestStartParams + ): + if span and span.is_recording(): + span.set_attribute("http.url", str(params.url)) + + def aiohttp_client_response_hook( + span: Span, + params: aiohttp.TraceRequestEndParams | aiohttp.TraceRequestExceptionParams, + ): + if span and span.is_recording(): + span.set_attribute("http.url", str(params.url)) + + AioHttpClientInstrumentor().instrument( + request_hook=aiohttp_client_request_hook, + response_hook=aiohttp_client_response_hook, + ) + + # + # instrument requests library + ## + def requests_request_hook(span: Span, request: requests.Request): + if span and span.is_recording(): + span.set_attribute("http.url", request.url) + + def requests_response_hook( + span: Span, request: requests.Request, response: requests.Response + ): + if span and span.is_recording(): + span.set_attribute("http.url", response.url) + + RequestsInstrumentor().instrument( + request_hook=requests_request_hook, response_hook=requests_response_hook + ) + +def configure_otel_providers(service_name: str = "app"): + """Configure OpenTelemetry for FastAPI application.""" + + # Create resource with service name + resource = Resource.create( + { + "service.name": service_name, + "service.version": "1.0.0", + "service.instance.id": os.getenv("HOSTNAME", "unknown"), + "telemetry.sdk.language": "python", + } + ) + + endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317/") + + # Configure Tracing + tracer_provider = TracerProvider(resource=resource) + tracer_provider.add_span_processor( + SimpleSpanProcessor(OTLPSpanExporter(endpoint=endpoint)) + ) + trace.set_tracer_provider(tracer_provider) + + # Configure Metrics + metric_reader = PeriodicExportingMetricReader( + OTLPMetricExporter(endpoint=endpoint) + ) + meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader]) + metrics.set_meter_provider(meter_provider) + + # Configure Logging + logger_provider = LoggerProvider(resource=resource) + logger_provider.add_log_record_processor( + BatchLogRecordProcessor(OTLPLogExporter(endpoint=endpoint)) + ) + set_logger_provider(logger_provider) + + # Add logging handler + handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider) + logging.getLogger().addHandler(handler) + + logging.getLogger().info("OpenTelemetry providers configured with endpoint: %s", endpoint) + + instrument_libraries() \ No newline at end of file diff --git a/samples/python/otel/start_dashboard.ps1 b/samples/python/otel/start_dashboard.ps1 new file mode 100644 index 00000000..de2dd386 --- /dev/null +++ b/samples/python/otel/start_dashboard.ps1 @@ -0,0 +1 @@ +docker run --rm -it -p 18888:18888 -p 4317:18889 --name aspire-dashboard mcr.microsoft.com/dotnet/aspire-dashboard:latest \ No newline at end of file