From d0c8a5f7352c5a072de7444ecab57a30436ec9e4 Mon Sep 17 00:00:00 2001 From: Jeel Mehta Date: Fri, 28 Feb 2025 14:42:18 -0800 Subject: [PATCH 1/4] Rough work for packaging udp exporter and then using it in a sample app --- rough-work/README.md | 10 + .../Controllers/AppController.cs | 129 ++++++ .../dotnet-sample-app/Controllers/Config.cs | 45 ++ .../Controllers/MetricEmitter.cs | 185 ++++++++ rough-work/dotnet-sample-app/Dockerfile | 13 + rough-work/dotnet-sample-app/Program.cs | 26 ++ .../Properties/launchSettings.json | 27 ++ rough-work/dotnet-sample-app/README.md | 50 +++ rough-work/dotnet-sample-app/Startup.cs | 115 +++++ .../appsettings.Development.json | 9 + rough-work/dotnet-sample-app/appsettings.json | 10 + .../collector-config-local.yml | 28 ++ rough-work/dotnet-sample-app/config.yaml | 9 + .../dotnet-sample-app/docker-compose.yml | 28 ++ .../dotnet-sample-app.csproj | 20 + ...AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj | 20 + .../OtlpUdpExporter.cs | 395 ++++++++++++++++++ 17 files changed, 1119 insertions(+) create mode 100644 rough-work/README.md create mode 100644 rough-work/dotnet-sample-app/Controllers/AppController.cs create mode 100644 rough-work/dotnet-sample-app/Controllers/Config.cs create mode 100644 rough-work/dotnet-sample-app/Controllers/MetricEmitter.cs create mode 100644 rough-work/dotnet-sample-app/Dockerfile create mode 100644 rough-work/dotnet-sample-app/Program.cs create mode 100644 rough-work/dotnet-sample-app/Properties/launchSettings.json create mode 100644 rough-work/dotnet-sample-app/README.md create mode 100644 rough-work/dotnet-sample-app/Startup.cs create mode 100644 rough-work/dotnet-sample-app/appsettings.Development.json create mode 100644 rough-work/dotnet-sample-app/appsettings.json create mode 100644 rough-work/dotnet-sample-app/collector-config-local.yml create mode 100644 rough-work/dotnet-sample-app/config.yaml create mode 100644 rough-work/dotnet-sample-app/docker-compose.yml create mode 100644 rough-work/dotnet-sample-app/dotnet-sample-app.csproj create mode 100644 rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj create mode 100644 rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/OtlpUdpExporter.cs diff --git a/rough-work/README.md b/rough-work/README.md new file mode 100644 index 00000000..8775572c --- /dev/null +++ b/rough-work/README.md @@ -0,0 +1,10 @@ +After packaging the udp-exporter, +```sh +cp bin/Release/*.nupkg ~/nuget-local/ +``` + +Then add the local nuget source +```sh +dotnet nuget add source ~/nuget-local -n local +``` + diff --git a/rough-work/dotnet-sample-app/Controllers/AppController.cs b/rough-work/dotnet-sample-app/Controllers/AppController.cs new file mode 100644 index 00000000..35c01d96 --- /dev/null +++ b/rough-work/dotnet-sample-app/Controllers/AppController.cs @@ -0,0 +1,129 @@ +using System; +using Amazon.S3; +using Microsoft.AspNetCore.Mvc; +using System.Diagnostics; +using System.Net.Http; +using Microsoft.AspNetCore.Http.Extensions; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Instrumentation; +using System.Diagnostics.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace dotnet_sample_app.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AppController : ControllerBase + { + private readonly AmazonS3Client s3Client = new AmazonS3Client(); + private readonly HttpClient httpClient = new HttpClient(); + private static Random rand = new Random(DateTime.Now.Millisecond); + + public static readonly ActivitySource tracer = new( + "dotnet-sample-app"); + + public AppController() {} + + [HttpGet] + [Route("/outgoing-http-call")] + public string OutgoingHttp() + { + using var activity = tracer.StartActivity("outgoing-http-call"); + activity?.SetTag("language", "dotnet"); + activity?.SetTag("signal", "trace"); + + var res = httpClient.GetAsync("https://aws.amazon.com/").Result; + string statusCode = res.StatusCode.ToString(); + + // Request Based Metrics + Startup.metricEmitter.emitReturnTimeMetric(MimicLatency()); + int loadSize = MimicPayLoadSize(); + Startup.metricEmitter.apiRequestSentMetric(); + Startup.metricEmitter.updateTotalBytesSentMetric(loadSize); + + return GetTraceId(); + } + + [HttpGet] + [Route("/aws-sdk-call")] + public string AWSSDKCall() + { + using var activity = tracer.StartActivity("aws-sdk-call"); + activity?.SetTag("language", "dotnet"); + activity?.SetTag("signal", "trace"); + + var res = s3Client.ListBucketsAsync().Result; + string statusCode = res.HttpStatusCode.ToString(); + + // Request Based Metrics + Startup.metricEmitter.emitReturnTimeMetric(MimicLatency()); + int loadSize = MimicPayLoadSize(); + Startup.metricEmitter.apiRequestSentMetric(); + Startup.metricEmitter.updateTotalBytesSentMetric(loadSize); + Startup.metricEmitter.totalTimeSentObserver.Add(3); + + return GetTraceId(); + } + + [HttpGet] + [Route("/")] + public string Default() + { + return "Application started!"; + } + + [HttpGet] + [Route("/outgoing-sampleapp")] + public string OutgoingSampleApp() + { + using var activity = tracer.StartActivity("outgoing-sampleapp"); + activity?.SetTag("language", "dotnet"); + activity?.SetTag("signal", "trace"); + string statusCode = ""; + + if (Program.cfg.SampleAppPorts.Length == 0) { + var res = httpClient.GetAsync("https://aws.amazon.com/").Result; + statusCode = res.StatusCode.ToString(); + } + else { + foreach (string port in Program.cfg.SampleAppPorts) { + if (!String.IsNullOrEmpty(port)) { + string uri = string.Format("http://127.0.0.1:{0}/outgoing-sampleapp", port); + var res = httpClient.GetAsync(uri).Result; + statusCode = res.StatusCode.ToString(); + } + } + } + + // Request Based Metrics + Startup.metricEmitter.emitReturnTimeMetric(MimicLatency()); + int loadSize = MimicPayLoadSize(); + Startup.metricEmitter.apiRequestSentMetric(); + Startup.metricEmitter.updateTotalBytesSentMetric(loadSize); + + return GetTraceId(); + } + + private string GetTraceId() + { + var traceId = Activity.Current.TraceId.ToHexString(); + var version = "1"; + var epoch = traceId.Substring(0, 8); + var random = traceId.Substring(8); + return "{" + "\"traceId\"" + ": " + "\"" + version + "-" + epoch + "-" + random + "\"" + "}"; + } + + private static int MimicPayLoadSize() + { + return rand.Next(101); + } + + private static int MimicLatency() + { + return rand.Next(100,500); + } + } + +} diff --git a/rough-work/dotnet-sample-app/Controllers/Config.cs b/rough-work/dotnet-sample-app/Controllers/Config.cs new file mode 100644 index 00000000..0903c6c6 --- /dev/null +++ b/rough-work/dotnet-sample-app/Controllers/Config.cs @@ -0,0 +1,45 @@ +using System.IO; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace dotnet_sample_app.Controllers +{ + public class Config + { + public string Host; + public string Port; + public int TimeInterval; + public int RandomTimeAliveIncrementer; + public int RandomTotalHeapSizeUpperBound; + public int RandomThreadsActiveUpperBound; + public int RandomCpuUsageUpperBound; + public string[] SampleAppPorts; + + public Config() { + this.Host = "0.0.0.0"; + this.Port = "8080"; + this.TimeInterval = 1; + this.RandomTimeAliveIncrementer = 1; + this.RandomTotalHeapSizeUpperBound = 100; + this.RandomThreadsActiveUpperBound = 10; + this.RandomCpuUsageUpperBound = 100; + this.SampleAppPorts = new string[0]; + } + + public static Config ReadInFile(string file) { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .Build(); + + Config returnConfig = null; + try { + returnConfig = deserializer.Deserialize(File.ReadAllText(file)); + } + catch { + returnConfig = new Config(); + } + return returnConfig; + + } + } +} diff --git a/rough-work/dotnet-sample-app/Controllers/MetricEmitter.cs b/rough-work/dotnet-sample-app/Controllers/MetricEmitter.cs new file mode 100644 index 00000000..c8569754 --- /dev/null +++ b/rough-work/dotnet-sample-app/Controllers/MetricEmitter.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenTelemetry; +using System.Diagnostics.Metrics; +using OpenTelemetry.Metrics; +using OpenTelemetry.Instrumentation; + +namespace dotnet_sample_app.Controllers +{ + public class MetricEmitter + { + const string DIMENSION_API_NAME = "apiName"; + const string DIMENSION_STATUS_CODE = "statusCode"; + + static string API_COUNTER_METRIC = "total_api_requests"; + static string API_LATENCY_METRIC = "latency_time"; + static string API_SUM_METRIC = "total_bytes_sent"; + static string API_TOTAL_TIME_METRIC = "time_alive"; + static string API_TOTAL_HEAP_SIZE = "total_heap_size"; + static string API_TOTAL_THREAD_SIZE = "threads_active"; + static string API_CPU_USAGE = "cpu_usage"; + + public Histogram apiLatencyRecorder; + public Counter totalTimeSentObserver; + public ObservableUpDownCounter totalHeapSizeObserver; + public UpDownCounter totalThreadsObserver; + + private long apiRequestSent = 0; + private long totalBytesSent = 0; + private long totalHeapSize = 0; + private int cpuUsage = 0; + private int totalTime = 1; + private int totalThreads = 0; + private bool threadsBool = true; + private int returnTime = 100; + + private static Random rand = new Random(DateTime.Now.Millisecond); + + private KeyValuePair[] requestAttributes = new KeyValuePair[] { + new KeyValuePair("signal", "metric"), + new KeyValuePair("language", "dotnet"), + new KeyValuePair("metricType", "request")}; + + private KeyValuePair[] randomAttributes = new KeyValuePair[] { + new KeyValuePair("signal", "metric"), + new KeyValuePair("language", "dotnet"), + new KeyValuePair("metricType", "random")}; + + public MetricEmitter() + { + Meter meter = new Meter("adot", "1.0"); + + string latencyMetricName = API_LATENCY_METRIC; + string totalApiRequestSent = API_COUNTER_METRIC; + string totalApiBytesSentMetricName = API_SUM_METRIC; + string totaltimealiveMetricName = API_TOTAL_TIME_METRIC; + string totalHeapSizeMetricName = API_TOTAL_HEAP_SIZE; + string totalThreadsMetricName = API_TOTAL_THREAD_SIZE; + string cpuUsageMetricName = API_CPU_USAGE; + + string instanceId = Environment.GetEnvironmentVariable("INSTANCE_ID"); + if (instanceId != null && !instanceId.Trim().Equals("")) + { + latencyMetricName = API_LATENCY_METRIC + "_" + instanceId; + totalApiRequestSent = API_COUNTER_METRIC + "_" + instanceId; + totalApiBytesSentMetricName = API_SUM_METRIC + "_" + instanceId; + totaltimealiveMetricName = API_TOTAL_TIME_METRIC + "_" + instanceId; + totalHeapSizeMetricName = API_TOTAL_HEAP_SIZE + "_" + instanceId; + totalThreadsMetricName = API_TOTAL_THREAD_SIZE + "_" + instanceId; + cpuUsageMetricName = API_CPU_USAGE + "_" + instanceId; + + } + + + meter.CreateObservableCounter(totalApiRequestSent,() => { + return new Measurement(apiRequestSent, requestAttributes); + }, + "1", + "Increments by one every time a sampleapp endpoint is used"); + + meter.CreateObservableCounter(totalApiBytesSentMetricName, () => { + return new Measurement(totalBytesSent, requestAttributes); + }, + "By", + "Keeps a sum of the total amount of bytes sent while the application is alive"); + + meter.CreateObservableGauge(cpuUsageMetricName, () => { + return new Measurement(cpuUsage, randomAttributes); + }, + "1", + "Cpu usage percent"); + + meter.CreateObservableUpDownCounter(totalHeapSizeMetricName, () => { + return new Measurement(totalHeapSize, randomAttributes); + }, + "1", + "The current total heap size”"); + + apiLatencyRecorder = meter.CreateHistogram(latencyMetricName, + "ms", + "Measures latency time in buckets of 100 300 and 500"); + + totalThreadsObserver = meter.CreateUpDownCounter(totalThreadsMetricName, + "1", + "The total number of threads active”"); + + totalTimeSentObserver = meter.CreateCounter(totaltimealiveMetricName, + "ms", + "Measures the total time the application has been alive"); + + + totalTimeSentObserver.Add(totalTime, randomAttributes); + totalThreadsObserver.Add(totalThreads++, randomAttributes); + apiLatencyRecorder.Record(returnTime, requestAttributes); + } + + public void emitReturnTimeMetric(int returnTime) { + apiLatencyRecorder.Record( + returnTime, requestAttributes); + } + + public void apiRequestSentMetric() { + this.apiRequestSent += 1; + Console.WriteLine("apiBs: "+ this.apiRequestSent); + } + + public void updateTotalBytesSentMetric(int bytes) { + totalBytesSent += bytes; + Console.WriteLine("Total amount of bytes sent while the application is alive:"+ totalBytesSent); + } + + public void updateTotalHeapSizeMetric() { + this.totalHeapSize += rand.Next(0,1) * Program.cfg.RandomTotalHeapSizeUpperBound; + } + + public void updateTotalThreadSizeMetric() { + if (threadsBool) { + if (totalThreads < Program.cfg.RandomThreadsActiveUpperBound) { + totalThreadsObserver.Add(1, randomAttributes); + totalThreads += 1; + } + else { + threadsBool = false; + totalThreads -= 1; + } + } + else { + if (totalThreads > 0) { + totalThreadsObserver.Add(-1, randomAttributes); + totalThreads -= 1; + } + else { + threadsBool = true; + totalThreads += 1; + } + } + } + + public void updateCpuUsageMetric() { + this.cpuUsage = rand.Next(0,1) * Program.cfg.RandomCpuUsageUpperBound; + } + + public void updateTotalTimeMetric() { + totalTimeSentObserver.Add(Program.cfg.RandomTimeAliveIncrementer, randomAttributes); + } + + public async Task UpdateRandomMetrics(CancellationToken cancellationToken = default) { + void update() { + updateTotalTimeMetric(); + updateTotalHeapSizeMetric(); + updateTotalThreadSizeMetric(); + updateCpuUsageMetric(); + } + + while (true) { + var delayTask = Task.Delay(Program.cfg.TimeInterval * 1000, cancellationToken); + await Task.Run(() => update()); + await delayTask; + } + } + + } +} diff --git a/rough-work/dotnet-sample-app/Dockerfile b/rough-work/dotnet-sample-app/Dockerfile new file mode 100644 index 00000000..3627e713 --- /dev/null +++ b/rough-work/dotnet-sample-app/Dockerfile @@ -0,0 +1,13 @@ +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env +WORKDIR /app +COPY . ./ +RUN dotnet publish *.csproj -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY . ./ +ENV AWS_REGION=us-west-2 +ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://otel:4317 +COPY --from=build-env /app/out . +ENTRYPOINT ["dotnet", "dotnet-sample-app.dll"] + diff --git a/rough-work/dotnet-sample-app/Program.cs b/rough-work/dotnet-sample-app/Program.cs new file mode 100644 index 00000000..162f9ceb --- /dev/null +++ b/rough-work/dotnet-sample-app/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using dotnet_sample_app.Controllers; +using System; + +namespace dotnet_sample_app +{ + public class Program + { + public static Config cfg = Config.ReadInFile("config.yaml"); + + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + string listenAddress = "http://"+cfg.Host+":"+cfg.Port; + webBuilder.UseUrls(listenAddress); + }); + } +} diff --git a/rough-work/dotnet-sample-app/Properties/launchSettings.json b/rough-work/dotnet-sample-app/Properties/launchSettings.json new file mode 100644 index 00000000..119da20d --- /dev/null +++ b/rough-work/dotnet-sample-app/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "dotnet-sample-app": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", + "OTEL_EXPORTER_OTLP_INSECURE": "True" + } + } + } +} diff --git a/rough-work/dotnet-sample-app/README.md b/rough-work/dotnet-sample-app/README.md new file mode 100644 index 00000000..201a2c76 --- /dev/null +++ b/rough-work/dotnet-sample-app/README.md @@ -0,0 +1,50 @@ +## .NET Opentelemetry Sample App + +### Description + +This .NET Sample App will emit Traces and Metrics. There are two types of metrics emitted; +Request Based and Random Based. +Metrics are generated as soon as the application is ran or deployed without any additional effort. These are considered the random based metrics which track a mock of TimeAlive, TotalHeapSize, ThreadsActive and CpuUsage. The boundaries for these metrics are standard and can be found in the configuration file (YAML) called config.yaml. + +Additionally, you can generate Traces and request based Metrics by making requests to the following exposed endpoints: + +1. / + 1. Ensures the application is running +2. /outgoing-http-call + 1. Makes a HTTP request to aws.amazon.com (http://aws.amazon.com/) +3. /aws-sdk-call + 1. Makes a call to AWS S3 to list buckets for the account corresponding to the provided AWS credentials +4. /outgoing-sampleapp + 1. Makes a call to all other sample app ports configured at `:/outgoing-sampleapp`. If none available, makes a HTTP request to www.amazon.com (http://www.amazon.com/) + +[Sample App Spec](../SampleAppSpec.md) + +* Non-conformance: This SDK language is not missing any features or extensions required other than Resource Detectors +* Workarounds: No workarounds are being used in this application + +### Getting Started: + +#### Running the application (local) + +In order to run the application + +- Clone the repository +`git clone https://github.com/aws-observability/aws-otel-community.git` +- Switch into the directory +`cd sample-apps/dotnet-sample-app` +- Install dependencies +`dotnet build` +- Run the .NET server +`dotnet run` +Now the application is ran and the endpoints can be called at `0.0.0.0:8080/`. + +#### Docker + +In order to build the Docker image and run it in a container + +- Build the image +`docker build -t dotnet-sample-app .` +- Run the image in a container +`docker run -p 8080:8080 dotnet-sample-app` + + diff --git a/rough-work/dotnet-sample-app/Startup.cs b/rough-work/dotnet-sample-app/Startup.cs new file mode 100644 index 00000000..fbbd0b5c --- /dev/null +++ b/rough-work/dotnet-sample-app/Startup.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry; +using OpenTelemetry.Contrib.Extensions.AWSXRay.Trace; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; +using System; +using System.Diagnostics; +using dotnet_sample_app.Controllers; +using AWS.OpenTelemetry.Exporter.Otlp.Udp; + + +namespace dotnet_sample_app +{ + public class Startup + { + public static MetricEmitter metricEmitter = new MetricEmitter(); + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + if(!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("OTEL_RESOURCE_ATTRIBUTES"))) { + var resourceBuilder = ResourceBuilder.CreateDefault().AddTelemetrySdk(); + Sdk.CreateTracerProviderBuilder() + .AddSource("dotnet-sample-app") + .SetResourceBuilder(resourceBuilder) + .AddAWSInstrumentation() + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(options => + { + options.Endpoint = new Uri(Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT")); + }) + .AddOtlpUdpExporter(resourceBuilder.Build(), "localhost:2000") + .Build(); + } + else { + var resourceBuilder = ResourceBuilder.CreateDefault() + .AddService(serviceName: "dotnet-sample-app") // Added first + .AddTelemetrySdk(); + Sdk.CreateTracerProviderBuilder() + .AddSource("dotnet-sample-app") + .SetResourceBuilder(resourceBuilder) + .AddAWSInstrumentation() + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(options => + { + options.Endpoint = new Uri(Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT")); + }) + .AddOtlpUdpExporter(resourceBuilder.Build(), "localhost:2000") + .Build(); + } + + Sdk.CreateMeterProviderBuilder() + .AddMeter("adot") + .AddOtlpExporter() + .Build(); + + Sdk.SetDefaultTextMapPropagator(new AWSXRayPropagator()); + + + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + metricEmitter.UpdateRandomMetrics(); + } + } + + public static class TracerProviderBuilderExtensions + { + public static TracerProviderBuilder AddOtlpUdpExporter( + this TracerProviderBuilder builder, + Resource resource, + string endpoint) + { + return builder.AddProcessor( + new BatchActivityExportProcessor( + new OtlpUdpExporter(resource, endpoint) + ) + ); + } + } +} \ No newline at end of file diff --git a/rough-work/dotnet-sample-app/appsettings.Development.json b/rough-work/dotnet-sample-app/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/rough-work/dotnet-sample-app/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/rough-work/dotnet-sample-app/appsettings.json b/rough-work/dotnet-sample-app/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/rough-work/dotnet-sample-app/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/rough-work/dotnet-sample-app/collector-config-local.yml b/rough-work/dotnet-sample-app/collector-config-local.yml new file mode 100644 index 00000000..0dbbeae0 --- /dev/null +++ b/rough-work/dotnet-sample-app/collector-config-local.yml @@ -0,0 +1,28 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + +exporters: + debug: + verbosity: detailed + awsxray: + region: us-west-2 + awsemf: + region: us-west-2 + +service: + pipelines: + traces: + receivers: + - otlp + exporters: + - debug + - awsxray + metrics: + receivers: + - otlp + exporters: + - debug + - awsemf diff --git a/rough-work/dotnet-sample-app/config.yaml b/rough-work/dotnet-sample-app/config.yaml new file mode 100644 index 00000000..674baf1a --- /dev/null +++ b/rough-work/dotnet-sample-app/config.yaml @@ -0,0 +1,9 @@ +--- +Host: "0.0.0.0" # Host - String Address +Port: "8080" # Port - String Port +TimeInterval: 1 # Interval - Time in seconds to generate new metrics +RandomTimeAliveIncrementer: 1 # Metric - Amount to incremement metric by every TimeInterval +RandomTotalHeapSizeUpperBound: 100 # Metric - UpperBound for TotalHeapSize for random metric value every TimeInterval +RandomThreadsActiveUpperBound: 10 # Metric - UpperBound for ThreadsActive for random metric value every TimeInterval +RandomCpuUsageUpperBound: 100 # Metric - UppperBound for CpuUsage for random metric value every TimeInterval +SampleAppPorts: [] # Sampleapp ports to make calls to diff --git a/rough-work/dotnet-sample-app/docker-compose.yml b/rough-work/dotnet-sample-app/docker-compose.yml new file mode 100644 index 00000000..e566dbd3 --- /dev/null +++ b/rough-work/dotnet-sample-app/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3.7" +services: + otel: + image: amazon/aws-otel-collector:latest + command: --config /config/collector-config-local.yml + volumes: + - ~/.aws:/home/aoc/.aws:ro + - .:/config + environment: + - AWS_REGION=us-west-2 + ports: + - '4317:4317' + + app: + build: + context: . + dockerfile: Dockerfile + environment: + - AWS_REGION=us-west-2 + - INSTANCE_ID + - LISTEN_ADDRESS=0.0.0.0:8080 + - OTEL_RESOURCE_ATTRIBUTES=service.name=adot-integ-test + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel:4317 + - ASPNETCORE_URLS=http://+:8080 + ports: + - '8080:8080' + volumes: + - ~/.aws:/root/.aws:ro diff --git a/rough-work/dotnet-sample-app/dotnet-sample-app.csproj b/rough-work/dotnet-sample-app/dotnet-sample-app.csproj new file mode 100644 index 00000000..2dc385e2 --- /dev/null +++ b/rough-work/dotnet-sample-app/dotnet-sample-app.csproj @@ -0,0 +1,20 @@ + + + net8.0 + dotnet_sample_app + + + + + + + + + + + + + + + + diff --git a/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj b/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj new file mode 100644 index 00000000..3eee5113 --- /dev/null +++ b/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj @@ -0,0 +1,20 @@ + + + net8.0 + enable + enable + AWS.OpenTelemetry.Exporter.Otlp.Udp + 1.0.1 + Amazon.com Inc. or its affiliates + Amazon.com Inc. or its affiliates + AWS OpenTelemetry UDP Exporter + + + + + + + + + + diff --git a/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/OtlpUdpExporter.cs b/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/OtlpUdpExporter.cs new file mode 100644 index 00000000..e2318c4c --- /dev/null +++ b/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/OtlpUdpExporter.cs @@ -0,0 +1,395 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using AWS.Distro.OpenTelemetry.AutoInstrumentation.Logging; +using Google.Protobuf; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OpenTelemetry; +using OpenTelemetry.Proto.Collector.Trace.V1; +using OpenTelemetry.Proto.Trace.V1; +using OpenTelemetry.Resources; +using OtlpResource = OpenTelemetry.Proto.Resource.V1; + +/// +/// OTLP UDP Exporter class. This class is used to build an OtlpUdpExporter to registered as in exporter +/// during the instrumentation initialization phase +/// +namespace AWS.OpenTelemetry.Exporter.Otlp.Udp; + +public class OtlpUdpExporter : BaseExporter +{ + private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider())); + private static readonly ILogger Logger = Factory.CreateLogger(); + + private UdpExporter udpExporter; + private string signalPrefix; + private Resource processResource; + + /// + /// Initializes a new instance of the class. + /// + /// Endpoint to export requests to + /// Sampled vs UnSampled signal prefix + /// Otel Resource object + public OtlpUdpExporter(Resource processResource, string? endpoint = null, string? signalPrefix = null) + { + endpoint = endpoint ?? UdpExporter.DefaultEndpoint; + this.udpExporter = new UdpExporter(endpoint); + this.signalPrefix = signalPrefix ?? UdpExporter.DefaultFormatOtelTracesBinaryPrefix; + this.processResource = processResource; + } + + /// + public override ExportResult Export(in Batch batch) + { + byte[]? serializedData = this.SerializeSpans(batch); + if (serializedData == null) + { + return ExportResult.Failure; + } + + try + { + this.udpExporter.SendData(serializedData, this.signalPrefix); + return ExportResult.Success; + } + catch (Exception ex) + { + Logger.LogError($"Error exporting spans: {ex.Message}"); + return ExportResult.Failure; + } + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + try + { + this.udpExporter.Shutdown(); + return true; + } + catch (Exception ex) + { + Logger.LogError($"Error shutting down exporter: {ex.Message}"); + return false; + } + } + + // Function that uses reflection to call ResourceExtensions.ToOtlpResource function. + // This functions converts from an OpenTelemetry.Resources.Resource to + // OpenTelemetry.Proto.Resource.V1.Resource (protobuf resource to be exported) + private OtlpResource.Resource? ToOtlpResource(Resource processResource) + { + Type? resourceExtensionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ResourceExtensions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); + + if (resourceExtensionsType == null) + { + Logger.LogTrace("ResourceExtensions Type was not found"); + return null; + } + + MethodInfo? toOtlpResourceMethod = resourceExtensionsType.GetMethod( + "ToOtlpResource", + BindingFlags.Static | BindingFlags.Public, + null, + new[] { typeof(Resource) }, + null); + + if (toOtlpResourceMethod == null) + { + Logger.LogTrace("ResourceExtensions.ToOtlpResource Method was not found"); + return null; + } + + var otlpResource = toOtlpResourceMethod.Invoke(null, new object[] { processResource }); + + if (otlpResource == null) + { + Logger.LogTrace("OtlpResource object cannot be converted from OpenTelemetry.Resources"); + return null; + } + + // Below is a workaround to casting and works by converting an object into JSON then converting the + // JSON string back into the required object type. The reason casting isn't working is because of different + // assemblies being used. To use the protobuf library, we need to have a local copy of the protobuf assembly. + // Since upstream also has their own copy of the protobuf library, casting is not possible since the complier + // is recognizing them as two different types. + try + { + // ToString method from OpenTelemetry.Proto.Resource.V1.Resource already converts the object into + // Json using the proper converters. + string? otlpResourceJson = otlpResource.ToString(); + if (otlpResourceJson == null) + { + Logger.LogTrace("OtlpResource object cannot be converted to JSON"); + return null; + } + + var otlpResourceConverted = JsonConvert.DeserializeObject(otlpResourceJson); + return otlpResourceConverted; + } + catch (Exception ex) + { + Logger.LogError($"Error converting OtlpResource to/from JSON: {ex.Message}"); + return null; + } + } + + // Uses reflection to the get the SdkLimitOptions required to invoke the ToOtlpSpan function used in the + // SerializeSpans function below. More information about SdkLimitOptions can be found in this link: + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/SdkLimitOptions.cs#L24 + private object? GetSdkLimitOptions() + { + Type? sdkLimitOptionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.SdkLimitOptions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); + + if (sdkLimitOptionsType == null) + { + Logger.LogTrace("SdkLimitOptions Type was not found"); + return null; + } + + // Create an instance of SdkLimitOptions using the default parameterless constructor + object? sdkLimitOptionsInstance = Activator.CreateInstance(sdkLimitOptionsType); + return sdkLimitOptionsInstance; + } + + // The SerializeSpans function builds a ExportTraceServiceRequest object by calling private "ToOtlpSpan" function + // using reflection. "ToOtlpSpan" converts an Activity object into an OpenTelemetry.Proto.Trace.V1.Span object. + // With the conversion above, the Activity object is converted to an Otel span object to be exported using the + // UDP exporter. The "ToOtlpSpan" function can be found here: + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs#L136 + private byte[]? SerializeSpans(Batch batch) + { + Type? activityExtensionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ActivityExtensions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); + + Type? sdkLimitOptionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.SdkLimitOptions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); + + if (sdkLimitOptionsType == null) + { + Logger.LogTrace("SdkLimitOptions Type was not found"); + return null; + } + + MethodInfo? toOtlpSpanMethod = activityExtensionsType?.GetMethod( + "ToOtlpSpan", + BindingFlags.Static | BindingFlags.NonPublic, + null, + new[] { typeof(Activity), sdkLimitOptionsType }, + null); + + var request = new ExportTraceServiceRequest(); + var sdkLimitOptions = this.GetSdkLimitOptions(); + + if (sdkLimitOptions == null) + { + Logger.LogTrace("SdkLimitOptions Object was not found/created properly using the default parameterless constructor"); + return null; + } + + OtlpResource.Resource? otlpResource = this.ToOtlpResource(this.processResource); + + // Create a ResourceSpans instance to hold the span and the otlpResource + ResourceSpans resourceSpans = new ResourceSpans + { + Resource = otlpResource, + }; + var scopeSpans = new ScopeSpans(); + + if (toOtlpSpanMethod != null) + { + foreach (var activity in batch) + { + var otlpSpan = toOtlpSpanMethod.Invoke(null, new object[] { activity, sdkLimitOptions }); + + // The converters below are required since the the JsonConvert.DeserializeObject doesn't + // know how to deserialize a BytesString or SpanKinds from otlp proto json object. + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new ByteStringConverter()); + settings.Converters.Add(new SpanKindConverter()); + settings.Converters.Add(new StatusCodeConverter()); + + // Below is a workaround to casting and works by converting an object into JSON then converting the + // JSON string back into the required object type. The reason casting isn't working is because of different + // assemblies being used. To use the protobuf library, we need to have a local copy of the protobuf assembly. + // Since upstream also has their own copy of the protobuf library, casting is not possible since the complier + // is recognizing them as two different types. + try + { + var otlpSpanJson = otlpSpan?.ToString(); + if (otlpSpanJson == null) + { + continue; + } + + var otlpSpanConverted = JsonConvert.DeserializeObject(otlpSpanJson, settings); + scopeSpans.Spans.Add(otlpSpanConverted); + } + catch (Exception ex) + { + Logger.LogError($"Error converting OtlpSpan to/from JSON: {ex.Message}"); + } + } + + resourceSpans.ScopeSpans.Add(scopeSpans); + request.ResourceSpans.Add(resourceSpans); + } + else + { + Logger.LogTrace("ActivityExtensions.ToOtlpSpan method is not found"); + } + + return request.ToByteArray(); + } +} + +internal class UdpExporter +{ + internal const string DefaultEndpoint = "127.0.0.1:2000"; + internal const string ProtocolHeader = "{\"format\":\"json\",\"version\":1}\n"; + internal const string DefaultFormatOtelTracesBinaryPrefix = "T1S"; + + private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider())); + private static readonly ILogger Logger = Factory.CreateLogger(); + + private string endpoint; + private string host; + private int port; + private UdpClient udpClient; + + /// + /// Initializes a new instance of the class. + /// + /// Endpoint to send udp request to + internal UdpExporter(string? endpoint = null) + { + this.endpoint = endpoint ?? DefaultEndpoint; + (this.host, this.port) = this.ParseEndpoint(this.endpoint); + this.udpClient = new UdpClient(); + this.udpClient.Client.ReceiveTimeout = 1000; // Optional: Set timeout + } + + internal void SendData(byte[] data, string signalFormatPrefix) + { + string base64EncodedString = Convert.ToBase64String(data); + string message = $"{ProtocolHeader}{signalFormatPrefix}{base64EncodedString}"; + + try + { + byte[] messageBytes = Encoding.UTF8.GetBytes(message); + this.udpClient.Send(messageBytes, messageBytes.Length, this.host, this.port); + } + catch (Exception ex) + { + Logger.LogError($"Error sending UDP data: {ex.Message}"); + throw; + } + } + + internal void Shutdown() + { + this.udpClient.Close(); + } + + private (string, int) ParseEndpoint(string endpoint) + { + try + { + var parts = endpoint.Split(':'); + if (parts.Length != 2 || !int.TryParse(parts[1], out int port)) + { + throw new ArgumentException($"Invalid endpoint: {endpoint}"); + } + + return (parts[0], port); + } + catch (Exception ex) + { + throw new ArgumentException($"Invalid endpoint: {endpoint}", ex); + } + } +} + +internal class ByteStringConverter : JsonConverter +{ + /// + public override ByteString? ReadJson(JsonReader reader, Type objectType, ByteString? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var base64String = (string?)reader.Value; + return ByteString.FromBase64(base64String); + } + + /// + public override void WriteJson(JsonWriter writer, ByteString? value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToBase64()); + } +} + +internal class SpanKindConverter : JsonConverter +{ + /// + public override Span.Types.SpanKind ReadJson(JsonReader reader, Type objectType, Span.Types.SpanKind existingValue, bool hasExistingValue, JsonSerializer serializer) + { + // Handle the string to enum conversion + string? enumString = reader.Value?.ToString(); + + // Convert the string representation to the corresponding enum value + switch (enumString) + { + case "SPAN_KIND_CLIENT": + return Span.Types.SpanKind.Client; + case "SPAN_KIND_SERVER": + return Span.Types.SpanKind.Server; + case "SPAN_KIND_INTERNAL": + return Span.Types.SpanKind.Internal; + case "SPAN_KIND_PRODUCER": + return Span.Types.SpanKind.Producer; + case "SPAN_KIND_CONSUMER": + return Span.Types.SpanKind.Consumer; + default: + throw new JsonSerializationException($"Unknown SpanKind: {enumString}"); + } + } + + /// + public override void WriteJson(JsonWriter writer, Span.Types.SpanKind value, JsonSerializer serializer) + { + // Write the string representation of the enum + writer.WriteValue(value.ToString()); + } +} + +internal class StatusCodeConverter : JsonConverter +{ + /// + public override Status.Types.StatusCode ReadJson(JsonReader reader, Type objectType, Status.Types.StatusCode existingValue, bool hasExistingValue, JsonSerializer serializer) + { + // Handle the string to enum conversion + string? enumString = reader.Value?.ToString(); + + // Convert the string representation to the corresponding enum value + switch (enumString) + { + case "STATUS_CODE_UNSET": + return Status.Types.StatusCode.Unset; + case "STATUS_CODE_OK": + return Status.Types.StatusCode.Ok; + case "STATUS_CODE_ERROR": + return Status.Types.StatusCode.Error; + default: + throw new JsonSerializationException($"Unknown StatusCode: {enumString}"); + } + } + + /// + public override void WriteJson(JsonWriter writer, Status.Types.StatusCode value, JsonSerializer serializer) + { + // Write the string representation of the enum + writer.WriteValue(value.ToString()); + } +} \ No newline at end of file From 7210c8888f1de3e99613b7d1447bc747f2160768 Mon Sep 17 00:00:00 2001 From: Jeel Mehta Date: Mon, 16 Jun 2025 11:01:01 -0700 Subject: [PATCH 2/4] Cleaning up --- rough-work/README.md | 10 - .../Controllers/AppController.cs | 129 ------ .../dotnet-sample-app/Controllers/Config.cs | 45 -- .../Controllers/MetricEmitter.cs | 185 -------- rough-work/dotnet-sample-app/Dockerfile | 13 - rough-work/dotnet-sample-app/Program.cs | 26 -- .../Properties/launchSettings.json | 27 -- rough-work/dotnet-sample-app/README.md | 50 --- rough-work/dotnet-sample-app/Startup.cs | 115 ----- .../appsettings.Development.json | 9 - rough-work/dotnet-sample-app/appsettings.json | 10 - .../collector-config-local.yml | 28 -- rough-work/dotnet-sample-app/config.yaml | 9 - .../dotnet-sample-app/docker-compose.yml | 28 -- .../dotnet-sample-app.csproj | 20 - ...AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj | 20 - .../OtlpUdpExporter.cs | 395 ------------------ 17 files changed, 1119 deletions(-) delete mode 100644 rough-work/README.md delete mode 100644 rough-work/dotnet-sample-app/Controllers/AppController.cs delete mode 100644 rough-work/dotnet-sample-app/Controllers/Config.cs delete mode 100644 rough-work/dotnet-sample-app/Controllers/MetricEmitter.cs delete mode 100644 rough-work/dotnet-sample-app/Dockerfile delete mode 100644 rough-work/dotnet-sample-app/Program.cs delete mode 100644 rough-work/dotnet-sample-app/Properties/launchSettings.json delete mode 100644 rough-work/dotnet-sample-app/README.md delete mode 100644 rough-work/dotnet-sample-app/Startup.cs delete mode 100644 rough-work/dotnet-sample-app/appsettings.Development.json delete mode 100644 rough-work/dotnet-sample-app/appsettings.json delete mode 100644 rough-work/dotnet-sample-app/collector-config-local.yml delete mode 100644 rough-work/dotnet-sample-app/config.yaml delete mode 100644 rough-work/dotnet-sample-app/docker-compose.yml delete mode 100644 rough-work/dotnet-sample-app/dotnet-sample-app.csproj delete mode 100644 rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj delete mode 100644 rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/OtlpUdpExporter.cs diff --git a/rough-work/README.md b/rough-work/README.md deleted file mode 100644 index 8775572c..00000000 --- a/rough-work/README.md +++ /dev/null @@ -1,10 +0,0 @@ -After packaging the udp-exporter, -```sh -cp bin/Release/*.nupkg ~/nuget-local/ -``` - -Then add the local nuget source -```sh -dotnet nuget add source ~/nuget-local -n local -``` - diff --git a/rough-work/dotnet-sample-app/Controllers/AppController.cs b/rough-work/dotnet-sample-app/Controllers/AppController.cs deleted file mode 100644 index 35c01d96..00000000 --- a/rough-work/dotnet-sample-app/Controllers/AppController.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using Amazon.S3; -using Microsoft.AspNetCore.Mvc; -using System.Diagnostics; -using System.Net.Http; -using Microsoft.AspNetCore.Http.Extensions; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Instrumentation; -using System.Diagnostics.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; - -namespace dotnet_sample_app.Controllers -{ - [ApiController] - [Route("[controller]")] - public class AppController : ControllerBase - { - private readonly AmazonS3Client s3Client = new AmazonS3Client(); - private readonly HttpClient httpClient = new HttpClient(); - private static Random rand = new Random(DateTime.Now.Millisecond); - - public static readonly ActivitySource tracer = new( - "dotnet-sample-app"); - - public AppController() {} - - [HttpGet] - [Route("/outgoing-http-call")] - public string OutgoingHttp() - { - using var activity = tracer.StartActivity("outgoing-http-call"); - activity?.SetTag("language", "dotnet"); - activity?.SetTag("signal", "trace"); - - var res = httpClient.GetAsync("https://aws.amazon.com/").Result; - string statusCode = res.StatusCode.ToString(); - - // Request Based Metrics - Startup.metricEmitter.emitReturnTimeMetric(MimicLatency()); - int loadSize = MimicPayLoadSize(); - Startup.metricEmitter.apiRequestSentMetric(); - Startup.metricEmitter.updateTotalBytesSentMetric(loadSize); - - return GetTraceId(); - } - - [HttpGet] - [Route("/aws-sdk-call")] - public string AWSSDKCall() - { - using var activity = tracer.StartActivity("aws-sdk-call"); - activity?.SetTag("language", "dotnet"); - activity?.SetTag("signal", "trace"); - - var res = s3Client.ListBucketsAsync().Result; - string statusCode = res.HttpStatusCode.ToString(); - - // Request Based Metrics - Startup.metricEmitter.emitReturnTimeMetric(MimicLatency()); - int loadSize = MimicPayLoadSize(); - Startup.metricEmitter.apiRequestSentMetric(); - Startup.metricEmitter.updateTotalBytesSentMetric(loadSize); - Startup.metricEmitter.totalTimeSentObserver.Add(3); - - return GetTraceId(); - } - - [HttpGet] - [Route("/")] - public string Default() - { - return "Application started!"; - } - - [HttpGet] - [Route("/outgoing-sampleapp")] - public string OutgoingSampleApp() - { - using var activity = tracer.StartActivity("outgoing-sampleapp"); - activity?.SetTag("language", "dotnet"); - activity?.SetTag("signal", "trace"); - string statusCode = ""; - - if (Program.cfg.SampleAppPorts.Length == 0) { - var res = httpClient.GetAsync("https://aws.amazon.com/").Result; - statusCode = res.StatusCode.ToString(); - } - else { - foreach (string port in Program.cfg.SampleAppPorts) { - if (!String.IsNullOrEmpty(port)) { - string uri = string.Format("http://127.0.0.1:{0}/outgoing-sampleapp", port); - var res = httpClient.GetAsync(uri).Result; - statusCode = res.StatusCode.ToString(); - } - } - } - - // Request Based Metrics - Startup.metricEmitter.emitReturnTimeMetric(MimicLatency()); - int loadSize = MimicPayLoadSize(); - Startup.metricEmitter.apiRequestSentMetric(); - Startup.metricEmitter.updateTotalBytesSentMetric(loadSize); - - return GetTraceId(); - } - - private string GetTraceId() - { - var traceId = Activity.Current.TraceId.ToHexString(); - var version = "1"; - var epoch = traceId.Substring(0, 8); - var random = traceId.Substring(8); - return "{" + "\"traceId\"" + ": " + "\"" + version + "-" + epoch + "-" + random + "\"" + "}"; - } - - private static int MimicPayLoadSize() - { - return rand.Next(101); - } - - private static int MimicLatency() - { - return rand.Next(100,500); - } - } - -} diff --git a/rough-work/dotnet-sample-app/Controllers/Config.cs b/rough-work/dotnet-sample-app/Controllers/Config.cs deleted file mode 100644 index 0903c6c6..00000000 --- a/rough-work/dotnet-sample-app/Controllers/Config.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.IO; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace dotnet_sample_app.Controllers -{ - public class Config - { - public string Host; - public string Port; - public int TimeInterval; - public int RandomTimeAliveIncrementer; - public int RandomTotalHeapSizeUpperBound; - public int RandomThreadsActiveUpperBound; - public int RandomCpuUsageUpperBound; - public string[] SampleAppPorts; - - public Config() { - this.Host = "0.0.0.0"; - this.Port = "8080"; - this.TimeInterval = 1; - this.RandomTimeAliveIncrementer = 1; - this.RandomTotalHeapSizeUpperBound = 100; - this.RandomThreadsActiveUpperBound = 10; - this.RandomCpuUsageUpperBound = 100; - this.SampleAppPorts = new string[0]; - } - - public static Config ReadInFile(string file) { - var deserializer = new DeserializerBuilder() - .WithNamingConvention(PascalCaseNamingConvention.Instance) - .Build(); - - Config returnConfig = null; - try { - returnConfig = deserializer.Deserialize(File.ReadAllText(file)); - } - catch { - returnConfig = new Config(); - } - return returnConfig; - - } - } -} diff --git a/rough-work/dotnet-sample-app/Controllers/MetricEmitter.cs b/rough-work/dotnet-sample-app/Controllers/MetricEmitter.cs deleted file mode 100644 index c8569754..00000000 --- a/rough-work/dotnet-sample-app/Controllers/MetricEmitter.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using OpenTelemetry; -using System.Diagnostics.Metrics; -using OpenTelemetry.Metrics; -using OpenTelemetry.Instrumentation; - -namespace dotnet_sample_app.Controllers -{ - public class MetricEmitter - { - const string DIMENSION_API_NAME = "apiName"; - const string DIMENSION_STATUS_CODE = "statusCode"; - - static string API_COUNTER_METRIC = "total_api_requests"; - static string API_LATENCY_METRIC = "latency_time"; - static string API_SUM_METRIC = "total_bytes_sent"; - static string API_TOTAL_TIME_METRIC = "time_alive"; - static string API_TOTAL_HEAP_SIZE = "total_heap_size"; - static string API_TOTAL_THREAD_SIZE = "threads_active"; - static string API_CPU_USAGE = "cpu_usage"; - - public Histogram apiLatencyRecorder; - public Counter totalTimeSentObserver; - public ObservableUpDownCounter totalHeapSizeObserver; - public UpDownCounter totalThreadsObserver; - - private long apiRequestSent = 0; - private long totalBytesSent = 0; - private long totalHeapSize = 0; - private int cpuUsage = 0; - private int totalTime = 1; - private int totalThreads = 0; - private bool threadsBool = true; - private int returnTime = 100; - - private static Random rand = new Random(DateTime.Now.Millisecond); - - private KeyValuePair[] requestAttributes = new KeyValuePair[] { - new KeyValuePair("signal", "metric"), - new KeyValuePair("language", "dotnet"), - new KeyValuePair("metricType", "request")}; - - private KeyValuePair[] randomAttributes = new KeyValuePair[] { - new KeyValuePair("signal", "metric"), - new KeyValuePair("language", "dotnet"), - new KeyValuePair("metricType", "random")}; - - public MetricEmitter() - { - Meter meter = new Meter("adot", "1.0"); - - string latencyMetricName = API_LATENCY_METRIC; - string totalApiRequestSent = API_COUNTER_METRIC; - string totalApiBytesSentMetricName = API_SUM_METRIC; - string totaltimealiveMetricName = API_TOTAL_TIME_METRIC; - string totalHeapSizeMetricName = API_TOTAL_HEAP_SIZE; - string totalThreadsMetricName = API_TOTAL_THREAD_SIZE; - string cpuUsageMetricName = API_CPU_USAGE; - - string instanceId = Environment.GetEnvironmentVariable("INSTANCE_ID"); - if (instanceId != null && !instanceId.Trim().Equals("")) - { - latencyMetricName = API_LATENCY_METRIC + "_" + instanceId; - totalApiRequestSent = API_COUNTER_METRIC + "_" + instanceId; - totalApiBytesSentMetricName = API_SUM_METRIC + "_" + instanceId; - totaltimealiveMetricName = API_TOTAL_TIME_METRIC + "_" + instanceId; - totalHeapSizeMetricName = API_TOTAL_HEAP_SIZE + "_" + instanceId; - totalThreadsMetricName = API_TOTAL_THREAD_SIZE + "_" + instanceId; - cpuUsageMetricName = API_CPU_USAGE + "_" + instanceId; - - } - - - meter.CreateObservableCounter(totalApiRequestSent,() => { - return new Measurement(apiRequestSent, requestAttributes); - }, - "1", - "Increments by one every time a sampleapp endpoint is used"); - - meter.CreateObservableCounter(totalApiBytesSentMetricName, () => { - return new Measurement(totalBytesSent, requestAttributes); - }, - "By", - "Keeps a sum of the total amount of bytes sent while the application is alive"); - - meter.CreateObservableGauge(cpuUsageMetricName, () => { - return new Measurement(cpuUsage, randomAttributes); - }, - "1", - "Cpu usage percent"); - - meter.CreateObservableUpDownCounter(totalHeapSizeMetricName, () => { - return new Measurement(totalHeapSize, randomAttributes); - }, - "1", - "The current total heap size”"); - - apiLatencyRecorder = meter.CreateHistogram(latencyMetricName, - "ms", - "Measures latency time in buckets of 100 300 and 500"); - - totalThreadsObserver = meter.CreateUpDownCounter(totalThreadsMetricName, - "1", - "The total number of threads active”"); - - totalTimeSentObserver = meter.CreateCounter(totaltimealiveMetricName, - "ms", - "Measures the total time the application has been alive"); - - - totalTimeSentObserver.Add(totalTime, randomAttributes); - totalThreadsObserver.Add(totalThreads++, randomAttributes); - apiLatencyRecorder.Record(returnTime, requestAttributes); - } - - public void emitReturnTimeMetric(int returnTime) { - apiLatencyRecorder.Record( - returnTime, requestAttributes); - } - - public void apiRequestSentMetric() { - this.apiRequestSent += 1; - Console.WriteLine("apiBs: "+ this.apiRequestSent); - } - - public void updateTotalBytesSentMetric(int bytes) { - totalBytesSent += bytes; - Console.WriteLine("Total amount of bytes sent while the application is alive:"+ totalBytesSent); - } - - public void updateTotalHeapSizeMetric() { - this.totalHeapSize += rand.Next(0,1) * Program.cfg.RandomTotalHeapSizeUpperBound; - } - - public void updateTotalThreadSizeMetric() { - if (threadsBool) { - if (totalThreads < Program.cfg.RandomThreadsActiveUpperBound) { - totalThreadsObserver.Add(1, randomAttributes); - totalThreads += 1; - } - else { - threadsBool = false; - totalThreads -= 1; - } - } - else { - if (totalThreads > 0) { - totalThreadsObserver.Add(-1, randomAttributes); - totalThreads -= 1; - } - else { - threadsBool = true; - totalThreads += 1; - } - } - } - - public void updateCpuUsageMetric() { - this.cpuUsage = rand.Next(0,1) * Program.cfg.RandomCpuUsageUpperBound; - } - - public void updateTotalTimeMetric() { - totalTimeSentObserver.Add(Program.cfg.RandomTimeAliveIncrementer, randomAttributes); - } - - public async Task UpdateRandomMetrics(CancellationToken cancellationToken = default) { - void update() { - updateTotalTimeMetric(); - updateTotalHeapSizeMetric(); - updateTotalThreadSizeMetric(); - updateCpuUsageMetric(); - } - - while (true) { - var delayTask = Task.Delay(Program.cfg.TimeInterval * 1000, cancellationToken); - await Task.Run(() => update()); - await delayTask; - } - } - - } -} diff --git a/rough-work/dotnet-sample-app/Dockerfile b/rough-work/dotnet-sample-app/Dockerfile deleted file mode 100644 index 3627e713..00000000 --- a/rough-work/dotnet-sample-app/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env -WORKDIR /app -COPY . ./ -RUN dotnet publish *.csproj -c Release -o out - -FROM mcr.microsoft.com/dotnet/aspnet:7.0 -WORKDIR /app -COPY . ./ -ENV AWS_REGION=us-west-2 -ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://otel:4317 -COPY --from=build-env /app/out . -ENTRYPOINT ["dotnet", "dotnet-sample-app.dll"] - diff --git a/rough-work/dotnet-sample-app/Program.cs b/rough-work/dotnet-sample-app/Program.cs deleted file mode 100644 index 162f9ceb..00000000 --- a/rough-work/dotnet-sample-app/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using dotnet_sample_app.Controllers; -using System; - -namespace dotnet_sample_app -{ - public class Program - { - public static Config cfg = Config.ReadInFile("config.yaml"); - - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - string listenAddress = "http://"+cfg.Host+":"+cfg.Port; - webBuilder.UseUrls(listenAddress); - }); - } -} diff --git a/rough-work/dotnet-sample-app/Properties/launchSettings.json b/rough-work/dotnet-sample-app/Properties/launchSettings.json deleted file mode 100644 index 119da20d..00000000 --- a/rough-work/dotnet-sample-app/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "dotnet-sample-app": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", - "OTEL_EXPORTER_OTLP_INSECURE": "True" - } - } - } -} diff --git a/rough-work/dotnet-sample-app/README.md b/rough-work/dotnet-sample-app/README.md deleted file mode 100644 index 201a2c76..00000000 --- a/rough-work/dotnet-sample-app/README.md +++ /dev/null @@ -1,50 +0,0 @@ -## .NET Opentelemetry Sample App - -### Description - -This .NET Sample App will emit Traces and Metrics. There are two types of metrics emitted; -Request Based and Random Based. -Metrics are generated as soon as the application is ran or deployed without any additional effort. These are considered the random based metrics which track a mock of TimeAlive, TotalHeapSize, ThreadsActive and CpuUsage. The boundaries for these metrics are standard and can be found in the configuration file (YAML) called config.yaml. - -Additionally, you can generate Traces and request based Metrics by making requests to the following exposed endpoints: - -1. / - 1. Ensures the application is running -2. /outgoing-http-call - 1. Makes a HTTP request to aws.amazon.com (http://aws.amazon.com/) -3. /aws-sdk-call - 1. Makes a call to AWS S3 to list buckets for the account corresponding to the provided AWS credentials -4. /outgoing-sampleapp - 1. Makes a call to all other sample app ports configured at `:/outgoing-sampleapp`. If none available, makes a HTTP request to www.amazon.com (http://www.amazon.com/) - -[Sample App Spec](../SampleAppSpec.md) - -* Non-conformance: This SDK language is not missing any features or extensions required other than Resource Detectors -* Workarounds: No workarounds are being used in this application - -### Getting Started: - -#### Running the application (local) - -In order to run the application - -- Clone the repository -`git clone https://github.com/aws-observability/aws-otel-community.git` -- Switch into the directory -`cd sample-apps/dotnet-sample-app` -- Install dependencies -`dotnet build` -- Run the .NET server -`dotnet run` -Now the application is ran and the endpoints can be called at `0.0.0.0:8080/`. - -#### Docker - -In order to build the Docker image and run it in a container - -- Build the image -`docker build -t dotnet-sample-app .` -- Run the image in a container -`docker run -p 8080:8080 dotnet-sample-app` - - diff --git a/rough-work/dotnet-sample-app/Startup.cs b/rough-work/dotnet-sample-app/Startup.cs deleted file mode 100644 index fbbd0b5c..00000000 --- a/rough-work/dotnet-sample-app/Startup.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using OpenTelemetry; -using OpenTelemetry.Contrib.Extensions.AWSXRay.Trace; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using OpenTelemetry.Metrics; -using System; -using System.Diagnostics; -using dotnet_sample_app.Controllers; -using AWS.OpenTelemetry.Exporter.Otlp.Udp; - - -namespace dotnet_sample_app -{ - public class Startup - { - public static MetricEmitter metricEmitter = new MetricEmitter(); - - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - - if(!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("OTEL_RESOURCE_ATTRIBUTES"))) { - var resourceBuilder = ResourceBuilder.CreateDefault().AddTelemetrySdk(); - Sdk.CreateTracerProviderBuilder() - .AddSource("dotnet-sample-app") - .SetResourceBuilder(resourceBuilder) - .AddAWSInstrumentation() - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddOtlpExporter(options => - { - options.Endpoint = new Uri(Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT")); - }) - .AddOtlpUdpExporter(resourceBuilder.Build(), "localhost:2000") - .Build(); - } - else { - var resourceBuilder = ResourceBuilder.CreateDefault() - .AddService(serviceName: "dotnet-sample-app") // Added first - .AddTelemetrySdk(); - Sdk.CreateTracerProviderBuilder() - .AddSource("dotnet-sample-app") - .SetResourceBuilder(resourceBuilder) - .AddAWSInstrumentation() - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddOtlpExporter(options => - { - options.Endpoint = new Uri(Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT")); - }) - .AddOtlpUdpExporter(resourceBuilder.Build(), "localhost:2000") - .Build(); - } - - Sdk.CreateMeterProviderBuilder() - .AddMeter("adot") - .AddOtlpExporter() - .Build(); - - Sdk.SetDefaultTextMapPropagator(new AWSXRayPropagator()); - - - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - - metricEmitter.UpdateRandomMetrics(); - } - } - - public static class TracerProviderBuilderExtensions - { - public static TracerProviderBuilder AddOtlpUdpExporter( - this TracerProviderBuilder builder, - Resource resource, - string endpoint) - { - return builder.AddProcessor( - new BatchActivityExportProcessor( - new OtlpUdpExporter(resource, endpoint) - ) - ); - } - } -} \ No newline at end of file diff --git a/rough-work/dotnet-sample-app/appsettings.Development.json b/rough-work/dotnet-sample-app/appsettings.Development.json deleted file mode 100644 index 8983e0fc..00000000 --- a/rough-work/dotnet-sample-app/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/rough-work/dotnet-sample-app/appsettings.json b/rough-work/dotnet-sample-app/appsettings.json deleted file mode 100644 index d9d9a9bf..00000000 --- a/rough-work/dotnet-sample-app/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/rough-work/dotnet-sample-app/collector-config-local.yml b/rough-work/dotnet-sample-app/collector-config-local.yml deleted file mode 100644 index 0dbbeae0..00000000 --- a/rough-work/dotnet-sample-app/collector-config-local.yml +++ /dev/null @@ -1,28 +0,0 @@ -receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - -exporters: - debug: - verbosity: detailed - awsxray: - region: us-west-2 - awsemf: - region: us-west-2 - -service: - pipelines: - traces: - receivers: - - otlp - exporters: - - debug - - awsxray - metrics: - receivers: - - otlp - exporters: - - debug - - awsemf diff --git a/rough-work/dotnet-sample-app/config.yaml b/rough-work/dotnet-sample-app/config.yaml deleted file mode 100644 index 674baf1a..00000000 --- a/rough-work/dotnet-sample-app/config.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -Host: "0.0.0.0" # Host - String Address -Port: "8080" # Port - String Port -TimeInterval: 1 # Interval - Time in seconds to generate new metrics -RandomTimeAliveIncrementer: 1 # Metric - Amount to incremement metric by every TimeInterval -RandomTotalHeapSizeUpperBound: 100 # Metric - UpperBound for TotalHeapSize for random metric value every TimeInterval -RandomThreadsActiveUpperBound: 10 # Metric - UpperBound for ThreadsActive for random metric value every TimeInterval -RandomCpuUsageUpperBound: 100 # Metric - UppperBound for CpuUsage for random metric value every TimeInterval -SampleAppPorts: [] # Sampleapp ports to make calls to diff --git a/rough-work/dotnet-sample-app/docker-compose.yml b/rough-work/dotnet-sample-app/docker-compose.yml deleted file mode 100644 index e566dbd3..00000000 --- a/rough-work/dotnet-sample-app/docker-compose.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: "3.7" -services: - otel: - image: amazon/aws-otel-collector:latest - command: --config /config/collector-config-local.yml - volumes: - - ~/.aws:/home/aoc/.aws:ro - - .:/config - environment: - - AWS_REGION=us-west-2 - ports: - - '4317:4317' - - app: - build: - context: . - dockerfile: Dockerfile - environment: - - AWS_REGION=us-west-2 - - INSTANCE_ID - - LISTEN_ADDRESS=0.0.0.0:8080 - - OTEL_RESOURCE_ATTRIBUTES=service.name=adot-integ-test - - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel:4317 - - ASPNETCORE_URLS=http://+:8080 - ports: - - '8080:8080' - volumes: - - ~/.aws:/root/.aws:ro diff --git a/rough-work/dotnet-sample-app/dotnet-sample-app.csproj b/rough-work/dotnet-sample-app/dotnet-sample-app.csproj deleted file mode 100644 index 2dc385e2..00000000 --- a/rough-work/dotnet-sample-app/dotnet-sample-app.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - net8.0 - dotnet_sample_app - - - - - - - - - - - - - - - - diff --git a/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj b/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj deleted file mode 100644 index 3eee5113..00000000 --- a/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/AWS.OpenTelemetry.Exporter.Otlp.Udp.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - net8.0 - enable - enable - AWS.OpenTelemetry.Exporter.Otlp.Udp - 1.0.1 - Amazon.com Inc. or its affiliates - Amazon.com Inc. or its affiliates - AWS OpenTelemetry UDP Exporter - - - - - - - - - - diff --git a/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/OtlpUdpExporter.cs b/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/OtlpUdpExporter.cs deleted file mode 100644 index e2318c4c..00000000 --- a/rough-work/udp-exporter/src/AWS.Opentelemetry.Exporter.Otlp.Udp/OtlpUdpExporter.cs +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using System.Net.Sockets; -using System.Reflection; -using System.Text; -using AWS.Distro.OpenTelemetry.AutoInstrumentation.Logging; -using Google.Protobuf; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OpenTelemetry; -using OpenTelemetry.Proto.Collector.Trace.V1; -using OpenTelemetry.Proto.Trace.V1; -using OpenTelemetry.Resources; -using OtlpResource = OpenTelemetry.Proto.Resource.V1; - -/// -/// OTLP UDP Exporter class. This class is used to build an OtlpUdpExporter to registered as in exporter -/// during the instrumentation initialization phase -/// -namespace AWS.OpenTelemetry.Exporter.Otlp.Udp; - -public class OtlpUdpExporter : BaseExporter -{ - private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider())); - private static readonly ILogger Logger = Factory.CreateLogger(); - - private UdpExporter udpExporter; - private string signalPrefix; - private Resource processResource; - - /// - /// Initializes a new instance of the class. - /// - /// Endpoint to export requests to - /// Sampled vs UnSampled signal prefix - /// Otel Resource object - public OtlpUdpExporter(Resource processResource, string? endpoint = null, string? signalPrefix = null) - { - endpoint = endpoint ?? UdpExporter.DefaultEndpoint; - this.udpExporter = new UdpExporter(endpoint); - this.signalPrefix = signalPrefix ?? UdpExporter.DefaultFormatOtelTracesBinaryPrefix; - this.processResource = processResource; - } - - /// - public override ExportResult Export(in Batch batch) - { - byte[]? serializedData = this.SerializeSpans(batch); - if (serializedData == null) - { - return ExportResult.Failure; - } - - try - { - this.udpExporter.SendData(serializedData, this.signalPrefix); - return ExportResult.Success; - } - catch (Exception ex) - { - Logger.LogError($"Error exporting spans: {ex.Message}"); - return ExportResult.Failure; - } - } - - /// - protected override bool OnShutdown(int timeoutMilliseconds) - { - try - { - this.udpExporter.Shutdown(); - return true; - } - catch (Exception ex) - { - Logger.LogError($"Error shutting down exporter: {ex.Message}"); - return false; - } - } - - // Function that uses reflection to call ResourceExtensions.ToOtlpResource function. - // This functions converts from an OpenTelemetry.Resources.Resource to - // OpenTelemetry.Proto.Resource.V1.Resource (protobuf resource to be exported) - private OtlpResource.Resource? ToOtlpResource(Resource processResource) - { - Type? resourceExtensionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ResourceExtensions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); - - if (resourceExtensionsType == null) - { - Logger.LogTrace("ResourceExtensions Type was not found"); - return null; - } - - MethodInfo? toOtlpResourceMethod = resourceExtensionsType.GetMethod( - "ToOtlpResource", - BindingFlags.Static | BindingFlags.Public, - null, - new[] { typeof(Resource) }, - null); - - if (toOtlpResourceMethod == null) - { - Logger.LogTrace("ResourceExtensions.ToOtlpResource Method was not found"); - return null; - } - - var otlpResource = toOtlpResourceMethod.Invoke(null, new object[] { processResource }); - - if (otlpResource == null) - { - Logger.LogTrace("OtlpResource object cannot be converted from OpenTelemetry.Resources"); - return null; - } - - // Below is a workaround to casting and works by converting an object into JSON then converting the - // JSON string back into the required object type. The reason casting isn't working is because of different - // assemblies being used. To use the protobuf library, we need to have a local copy of the protobuf assembly. - // Since upstream also has their own copy of the protobuf library, casting is not possible since the complier - // is recognizing them as two different types. - try - { - // ToString method from OpenTelemetry.Proto.Resource.V1.Resource already converts the object into - // Json using the proper converters. - string? otlpResourceJson = otlpResource.ToString(); - if (otlpResourceJson == null) - { - Logger.LogTrace("OtlpResource object cannot be converted to JSON"); - return null; - } - - var otlpResourceConverted = JsonConvert.DeserializeObject(otlpResourceJson); - return otlpResourceConverted; - } - catch (Exception ex) - { - Logger.LogError($"Error converting OtlpResource to/from JSON: {ex.Message}"); - return null; - } - } - - // Uses reflection to the get the SdkLimitOptions required to invoke the ToOtlpSpan function used in the - // SerializeSpans function below. More information about SdkLimitOptions can be found in this link: - // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/SdkLimitOptions.cs#L24 - private object? GetSdkLimitOptions() - { - Type? sdkLimitOptionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.SdkLimitOptions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); - - if (sdkLimitOptionsType == null) - { - Logger.LogTrace("SdkLimitOptions Type was not found"); - return null; - } - - // Create an instance of SdkLimitOptions using the default parameterless constructor - object? sdkLimitOptionsInstance = Activator.CreateInstance(sdkLimitOptionsType); - return sdkLimitOptionsInstance; - } - - // The SerializeSpans function builds a ExportTraceServiceRequest object by calling private "ToOtlpSpan" function - // using reflection. "ToOtlpSpan" converts an Activity object into an OpenTelemetry.Proto.Trace.V1.Span object. - // With the conversion above, the Activity object is converted to an Otel span object to be exported using the - // UDP exporter. The "ToOtlpSpan" function can be found here: - // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs#L136 - private byte[]? SerializeSpans(Batch batch) - { - Type? activityExtensionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ActivityExtensions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); - - Type? sdkLimitOptionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.SdkLimitOptions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); - - if (sdkLimitOptionsType == null) - { - Logger.LogTrace("SdkLimitOptions Type was not found"); - return null; - } - - MethodInfo? toOtlpSpanMethod = activityExtensionsType?.GetMethod( - "ToOtlpSpan", - BindingFlags.Static | BindingFlags.NonPublic, - null, - new[] { typeof(Activity), sdkLimitOptionsType }, - null); - - var request = new ExportTraceServiceRequest(); - var sdkLimitOptions = this.GetSdkLimitOptions(); - - if (sdkLimitOptions == null) - { - Logger.LogTrace("SdkLimitOptions Object was not found/created properly using the default parameterless constructor"); - return null; - } - - OtlpResource.Resource? otlpResource = this.ToOtlpResource(this.processResource); - - // Create a ResourceSpans instance to hold the span and the otlpResource - ResourceSpans resourceSpans = new ResourceSpans - { - Resource = otlpResource, - }; - var scopeSpans = new ScopeSpans(); - - if (toOtlpSpanMethod != null) - { - foreach (var activity in batch) - { - var otlpSpan = toOtlpSpanMethod.Invoke(null, new object[] { activity, sdkLimitOptions }); - - // The converters below are required since the the JsonConvert.DeserializeObject doesn't - // know how to deserialize a BytesString or SpanKinds from otlp proto json object. - var settings = new JsonSerializerSettings(); - settings.Converters.Add(new ByteStringConverter()); - settings.Converters.Add(new SpanKindConverter()); - settings.Converters.Add(new StatusCodeConverter()); - - // Below is a workaround to casting and works by converting an object into JSON then converting the - // JSON string back into the required object type. The reason casting isn't working is because of different - // assemblies being used. To use the protobuf library, we need to have a local copy of the protobuf assembly. - // Since upstream also has their own copy of the protobuf library, casting is not possible since the complier - // is recognizing them as two different types. - try - { - var otlpSpanJson = otlpSpan?.ToString(); - if (otlpSpanJson == null) - { - continue; - } - - var otlpSpanConverted = JsonConvert.DeserializeObject(otlpSpanJson, settings); - scopeSpans.Spans.Add(otlpSpanConverted); - } - catch (Exception ex) - { - Logger.LogError($"Error converting OtlpSpan to/from JSON: {ex.Message}"); - } - } - - resourceSpans.ScopeSpans.Add(scopeSpans); - request.ResourceSpans.Add(resourceSpans); - } - else - { - Logger.LogTrace("ActivityExtensions.ToOtlpSpan method is not found"); - } - - return request.ToByteArray(); - } -} - -internal class UdpExporter -{ - internal const string DefaultEndpoint = "127.0.0.1:2000"; - internal const string ProtocolHeader = "{\"format\":\"json\",\"version\":1}\n"; - internal const string DefaultFormatOtelTracesBinaryPrefix = "T1S"; - - private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider())); - private static readonly ILogger Logger = Factory.CreateLogger(); - - private string endpoint; - private string host; - private int port; - private UdpClient udpClient; - - /// - /// Initializes a new instance of the class. - /// - /// Endpoint to send udp request to - internal UdpExporter(string? endpoint = null) - { - this.endpoint = endpoint ?? DefaultEndpoint; - (this.host, this.port) = this.ParseEndpoint(this.endpoint); - this.udpClient = new UdpClient(); - this.udpClient.Client.ReceiveTimeout = 1000; // Optional: Set timeout - } - - internal void SendData(byte[] data, string signalFormatPrefix) - { - string base64EncodedString = Convert.ToBase64String(data); - string message = $"{ProtocolHeader}{signalFormatPrefix}{base64EncodedString}"; - - try - { - byte[] messageBytes = Encoding.UTF8.GetBytes(message); - this.udpClient.Send(messageBytes, messageBytes.Length, this.host, this.port); - } - catch (Exception ex) - { - Logger.LogError($"Error sending UDP data: {ex.Message}"); - throw; - } - } - - internal void Shutdown() - { - this.udpClient.Close(); - } - - private (string, int) ParseEndpoint(string endpoint) - { - try - { - var parts = endpoint.Split(':'); - if (parts.Length != 2 || !int.TryParse(parts[1], out int port)) - { - throw new ArgumentException($"Invalid endpoint: {endpoint}"); - } - - return (parts[0], port); - } - catch (Exception ex) - { - throw new ArgumentException($"Invalid endpoint: {endpoint}", ex); - } - } -} - -internal class ByteStringConverter : JsonConverter -{ - /// - public override ByteString? ReadJson(JsonReader reader, Type objectType, ByteString? existingValue, bool hasExistingValue, JsonSerializer serializer) - { - var base64String = (string?)reader.Value; - return ByteString.FromBase64(base64String); - } - - /// - public override void WriteJson(JsonWriter writer, ByteString? value, JsonSerializer serializer) - { - writer.WriteValue(value?.ToBase64()); - } -} - -internal class SpanKindConverter : JsonConverter -{ - /// - public override Span.Types.SpanKind ReadJson(JsonReader reader, Type objectType, Span.Types.SpanKind existingValue, bool hasExistingValue, JsonSerializer serializer) - { - // Handle the string to enum conversion - string? enumString = reader.Value?.ToString(); - - // Convert the string representation to the corresponding enum value - switch (enumString) - { - case "SPAN_KIND_CLIENT": - return Span.Types.SpanKind.Client; - case "SPAN_KIND_SERVER": - return Span.Types.SpanKind.Server; - case "SPAN_KIND_INTERNAL": - return Span.Types.SpanKind.Internal; - case "SPAN_KIND_PRODUCER": - return Span.Types.SpanKind.Producer; - case "SPAN_KIND_CONSUMER": - return Span.Types.SpanKind.Consumer; - default: - throw new JsonSerializationException($"Unknown SpanKind: {enumString}"); - } - } - - /// - public override void WriteJson(JsonWriter writer, Span.Types.SpanKind value, JsonSerializer serializer) - { - // Write the string representation of the enum - writer.WriteValue(value.ToString()); - } -} - -internal class StatusCodeConverter : JsonConverter -{ - /// - public override Status.Types.StatusCode ReadJson(JsonReader reader, Type objectType, Status.Types.StatusCode existingValue, bool hasExistingValue, JsonSerializer serializer) - { - // Handle the string to enum conversion - string? enumString = reader.Value?.ToString(); - - // Convert the string representation to the corresponding enum value - switch (enumString) - { - case "STATUS_CODE_UNSET": - return Status.Types.StatusCode.Unset; - case "STATUS_CODE_OK": - return Status.Types.StatusCode.Ok; - case "STATUS_CODE_ERROR": - return Status.Types.StatusCode.Error; - default: - throw new JsonSerializationException($"Unknown StatusCode: {enumString}"); - } - } - - /// - public override void WriteJson(JsonWriter writer, Status.Types.StatusCode value, JsonSerializer serializer) - { - // Write the string representation of the enum - writer.WriteValue(value.ToString()); - } -} \ No newline at end of file From 3dcabb7ae81048a722542bf5177926e94b832611 Mon Sep 17 00:00:00 2001 From: Jeel Mehta Date: Mon, 14 Jul 2025 14:02:33 -0700 Subject: [PATCH 3/4] Initial work on SigV4 Log Exporter --- build/Build.cs | 4 +- .../OtlpExporterUtils.cs | 100 ++++- .../integration-test-app/Dockerfile | 25 +- .../integration-test-app/docker-compose.yml | 27 +- .../Controllers/AppController.cs | 42 +- .../integration-test-app.csproj | 1 + .../OtlpAwsLogExporter/OtlpAwsLogExporter.cs | 400 ++++++++++++++++++ .../Plugin.cs | 175 +++++++- 8 files changed, 723 insertions(+), 51 deletions(-) create mode 100644 src/AWS.Distro.OpenTelemetry.AutoInstrumentation/OtlpAwsLogExporter/OtlpAwsLogExporter.cs diff --git a/build/Build.cs b/build/Build.cs index 6fbbc8dc..6a27fa5c 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -84,6 +84,7 @@ private static string GetOTelAutoInstrumentationFileName() case PlatformFamily.Windows: fileName = "opentelemetry-dotnet-instrumentation-windows.zip"; break; + case PlatformFamily.OSX: case PlatformFamily.Linux: var architecture = RuntimeInformation.ProcessArchitecture; string architectureSuffix; @@ -103,9 +104,6 @@ private static string GetOTelAutoInstrumentationFileName() ? $"opentelemetry-dotnet-instrumentation-linux-musl-{architectureSuffix}.zip" : $"opentelemetry-dotnet-instrumentation-linux-glibc-{architectureSuffix}.zip"; break; - case PlatformFamily.OSX: - fileName = "opentelemetry-dotnet-instrumentation-macos.zip"; - break; case PlatformFamily.Unknown: throw new NotSupportedException(); default: diff --git a/exporters/AWS.Distro.OpenTelemetry.Exporter.Xray.Udp/OtlpExporterUtils.cs b/exporters/AWS.Distro.OpenTelemetry.Exporter.Xray.Udp/OtlpExporterUtils.cs index 363f9803..236ae1a4 100644 --- a/exporters/AWS.Distro.OpenTelemetry.Exporter.Xray.Udp/OtlpExporterUtils.cs +++ b/exporters/AWS.Distro.OpenTelemetry.Exporter.Xray.Udp/OtlpExporterUtils.cs @@ -7,6 +7,8 @@ using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Resources; +using OpenTelemetry.Logs; +using OpenTelemetry.Exporter; namespace AWS.Distro.OpenTelemetry.Exporter.Xray.Udp; @@ -16,11 +18,15 @@ public class OtlpExporterUtils private static readonly ILogger Logger = Factory.CreateLogger(); private static readonly MethodInfo? WriteTraceDataMethod; + private static readonly MethodInfo? WriteLogsDataMethod; private static readonly object? SdkLimitOptions; + private static readonly object? ExperimentalOptions; static OtlpExporterUtils() { - Type? otlpSerializerType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer.ProtobufOtlpTraceSerializer, OpenTelemetry.Exporter.OpenTelemetryProtocol"); + Type? otlpTraceSerializerType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer.ProtobufOtlpTraceSerializer, OpenTelemetry.Exporter.OpenTelemetryProtocol"); + Type? otlpLogSerializerType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer.ProtobufOtlpLogSerializer, OpenTelemetry.Exporter.OpenTelemetryProtocol"); Type? sdkLimitOptionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.SdkLimitOptions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); + Type? experimentalOptionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExperimentalOptions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); if (sdkLimitOptionsType == null) { @@ -28,13 +34,25 @@ static OtlpExporterUtils() { return; } - if (otlpSerializerType == null) + if (experimentalOptionsType == null) { - Logger.LogTrace("OtlpSerializer Type was not found"); + Logger.LogTrace("ExperimentalOptions Type was not found"); return; } - WriteTraceDataMethod = otlpSerializerType.GetMethod( + if (otlpTraceSerializerType == null) + { + Logger.LogTrace("OtlpTraceSerializer Type was not found"); + return; + } + + if (otlpLogSerializerType == null) + { + Logger.LogTrace("OtlpLogSerializer Type was not found"); + return; + } + + WriteTraceDataMethod = otlpTraceSerializerType.GetMethod( "WriteTraceData", BindingFlags.NonPublic | BindingFlags.Static, null, @@ -42,14 +60,34 @@ static OtlpExporterUtils() { { typeof(byte[]).MakeByRefType(), // ref byte[] buffer typeof(int), // int writePosition - sdkLimitOptionsType, // SdkLimitOptions + sdkLimitOptionsType, // SdkLimitOptions typeof(Resource), // Resource? typeof(Batch).MakeByRefType() // in Batch }, null) - ?? throw new MissingMethodException("WriteTraceData not found"); // :contentReference[oaicite:1]{index=1} + ?? throw new MissingMethodException("WriteTraceData not found"); + + // Get the WriteLogsData method from the ProtobufOtlpLogSerializer using reflection. "WriteLogsData" is based on the + // OpenTelemetry.Exporter.OpenTelemetryProtocol dependency found at + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs + WriteLogsDataMethod = otlpLogSerializerType.GetMethod( + "WriteLogsData", + BindingFlags.NonPublic | BindingFlags.Static, + null, + new[] + { + typeof(byte[]).MakeByRefType(), // ref byte[] buffer + typeof(int), // int writePosition + sdkLimitOptionsType, // SdkLimitOptions + experimentalOptionsType, // ExperimentalOptions + typeof(Resource), // Resource? + typeof(Batch).MakeByRefType() // in Batch + }, + null) + ?? throw new MissingMethodException("WriteLogsData not found"); SdkLimitOptions = GetSdkLimitOptions(); + ExperimentalOptions = GetExperimentalOptions(); } // The WriteTraceData function builds writes data to the buffer byte[] object by calling private "WriteTraceData" function @@ -71,7 +109,7 @@ public static int WriteTraceData( // Pack arguments (ref/in remain by-ref in the args array) object[] args = { buffer, writePosition, SdkLimitOptions, resource!, batch! }; - // Invoke static method (null target) :contentReference[oaicite:2]{index=2} + // Invoke static method (null target) var result = (int)WriteTraceDataMethod?.Invoke(obj: null, parameters: args)!; // Unpack ref-buffer @@ -80,8 +118,35 @@ public static int WriteTraceData( return result; } - // Uses reflection to the get the SdkLimitOptions required to invoke the ToOtlpSpan function used in the - // SerializeSpans function below. More information about SdkLimitOptions can be found in this link: + // The WriteLogsData function writes log data to the buffer byte[] object by calling private "WriteLogsData" function + // using reflection. "WriteLogsData" is based on the OpenTelemetry.Exporter.OpenTelemetryProtocol dependency found at + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs + public static int WriteLogsData( + ref byte[] buffer, + int writePosition, + Resource? resource, + in Batch batch) + { + if (SdkLimitOptions == null || ExperimentalOptions == null) + { + Logger.LogTrace("SdkLimitOptions or ExperimentalOptions Object was not found/created properly"); + return -1; + } + + // Pack arguments (ref/in remain by-ref in the args array) + object[] args = { buffer, writePosition, SdkLimitOptions, ExperimentalOptions, resource!, batch! }; + + // Invoke static method (null target) + var result = (int)WriteLogsDataMethod?.Invoke(obj: null, parameters: args)!; + + // Unpack ref-buffer + buffer = (byte[])args[0]; + + return result; + } + + // Uses reflection to get the SdkLimitOptions required to invoke the serialization functions. + // More information about SdkLimitOptions can be found in this link: // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/SdkLimitOptions.cs#L24 private static object? GetSdkLimitOptions() { @@ -97,4 +162,21 @@ public static int WriteTraceData( object? sdkLimitOptionsInstance = Activator.CreateInstance(sdkLimitOptionsType); return sdkLimitOptionsInstance; } + + // Uses reflection to get the ExperimentalOptions required for log serialization. + // More information about ExperimentalOptions can be found in the OpenTelemetry implementation: + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs + private static object? GetExperimentalOptions() + { + Type? experimentalOptionsType = Type.GetType("OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExperimentalOptions, OpenTelemetry.Exporter.OpenTelemetryProtocol"); + if (experimentalOptionsType == null) + { + Logger.LogTrace("ExperimentalOptions Type was not found"); + return null; + } + + // Create an instance of ExperimentalOptions using the default parameterless constructor + object? experimentalOptionsInstance = Activator.CreateInstance(experimentalOptionsType); + return experimentalOptionsInstance; + } } \ No newline at end of file diff --git a/sample-applications/integration-test-app/Dockerfile b/sample-applications/integration-test-app/Dockerfile index 2d55066d..dbe841c6 100644 --- a/sample-applications/integration-test-app/Dockerfile +++ b/sample-applications/integration-test-app/Dockerfile @@ -1,3 +1,4 @@ +# Base configuration FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env WORKDIR /app COPY . ./ @@ -9,21 +10,43 @@ EXPOSE 8080 COPY --from=build-env /app/out . ENTRYPOINT ["dotnet", "integration-test-app.dll"] +# ADOT Setup ARG INSTALL_DIR=/opt/aws/otel/dotnet RUN mkdir -p ${INSTALL_DIR} RUN ls ../../ COPY ./OpenTelemetryDistribution/ ${INSTALL_DIR} RUN mkdir -p /var/log/opentelemetry/dotnet +# Auto-instrumentation configuration ENV CORECLR_ENABLE_PROFILING=1 ENV CORECLR_PROFILER={918728DD-259F-4A6A-AC2B-B85E1B658318} -ENV CORECLR_PROFILER_PATH=${INSTALL_DIR}/linux-x64/OpenTelemetry.AutoInstrumentation.Native.so +ENV CORECLR_PROFILER_PATH=${INSTALL_DIR}/linux-arm64/OpenTelemetry.AutoInstrumentation.Native.so ENV DOTNET_ADDITIONAL_DEPS=${INSTALL_DIR}/AdditionalDeps ENV DOTNET_SHARED_STORE=${INSTALL_DIR}/store ENV DOTNET_STARTUP_HOOKS=${INSTALL_DIR}/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll ENV OTEL_DOTNET_AUTO_HOME=${INSTALL_DIR} ENV OTEL_DOTNET_AUTO_PLUGINS="AWS.Distro.OpenTelemetry.AutoInstrumentation.Plugin, AWS.Distro.OpenTelemetry.AutoInstrumentation" + +# OpenTelemetry configuration ENV OTEL_AWS_APPLICATION_SIGNALS_ENABLED="true" ENV OTEL_TRACES_SAMPLER="always_on" ENV OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +ENV OTEL_RESOURCE_ATTRIBUTES=service.name=dotnet-sample-application +ENV OTEL_AWS_SIG_V4_ENABLED=true + +# Exporter endpoints ENV OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT="http://otel:4318/v1/metrics" +ENV OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://xray.us-east-1.amazonaws.com/v1/traces +ENV OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.us-east-1.amazonaws.com/v1/logs + +# Exporter configuration +ENV OTEL_METRICS_EXPORTER=none +ENV OTEL_TRACES_EXPORTER=none +ENV OTEL_LOGS_EXPORTER=otlp + +# CloudWatch Logs specific configuration +ENV OTEL_EXPORTER_OTLP_LOGS_HEADERS="x-aws-log-group=otlp_logs,x-aws-log-stream=default" +ENV OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="http/protobuf" + +# App configuration +ENV ASPNETCORE_URLS=http://0.0.0.0:8080 \ No newline at end of file diff --git a/sample-applications/integration-test-app/docker-compose.yml b/sample-applications/integration-test-app/docker-compose.yml index 8c56f577..25398116 100644 --- a/sample-applications/integration-test-app/docker-compose.yml +++ b/sample-applications/integration-test-app/docker-compose.yml @@ -1,34 +1,25 @@ version: "3.7" services: - cwagent: - image: amazon/cloudwatch-agent:latest - volumes: - - ./amazon-cloudwatch-agent.json:/opt/aws/amazon-cloudwatch-agent/bin/default_linux_config.json # agent config - - ./aws:/root/.aws # required for authentication - environment: - - AWS_REGION=us-west-2 - ports: - - '4316:4316' - - '4317:4317' - - '2000:2000' - app: image: aspnetapp:latest environment: - - AWS_REGION=us-west-2 + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN + - AWS_REGION=us-east-1 - INSTANCE_ID - LISTEN_ADDRESS=0.0.0.0:8080 - OTEL_RESOURCE_ATTRIBUTES=service.name=aws-otel-integ-test - - OTEL_EXPORTER_OTLP_ENDPOINT=http://cwagent:4316 # TODO: workaround for trace exporter endpoint - - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://cwagent:4316/v1/traces - - OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT=http://cwagent:4316/v1/metrics + # - OTEL_EXPORTER_OTLP_ENDPOINT=http://cwagent:4316 # TODO: workaround for trace exporter endpoint + # - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://cwagent:4316/v1/traces + # - OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT=http://cwagent:4316/v1/metrics - ASPNETCORE_URLS=http://+:8080 - OTEL_METRICS_EXPORTER=none - OTEL_AWS_APPLICATION_SIGNALS_ENABLED=true - OTEL_TRACES_SAMPLER=xray - - OTEL_TRACES_SAMPLER_ARG=endpoint=http://cwagent:2000,polling_interval=1 + - RESOURCE_DETECTORS_ENABLED=false ports: - '8080:8080' volumes: - - ~/.aws:/root/.aws:ro + - ./logs:/var/log/opentelemetry/dotnet diff --git a/sample-applications/integration-test-app/integration-test-app/Controllers/AppController.cs b/sample-applications/integration-test-app/integration-test-app/Controllers/AppController.cs index b9b3cd7f..aac8288b 100644 --- a/sample-applications/integration-test-app/integration-test-app/Controllers/AppController.cs +++ b/sample-applications/integration-test-app/integration-test-app/Controllers/AppController.cs @@ -1,11 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - +using System; using System.Diagnostics; using System.Net.Http; using Amazon.S3; using Microsoft.AspNetCore.Mvc; - +using Microsoft.Extensions.Logging; + namespace integration_test_app.Controllers; [ApiController] @@ -14,12 +15,25 @@ public class AppController : ControllerBase { private readonly AmazonS3Client s3Client = new AmazonS3Client(); private readonly HttpClient httpClient = new HttpClient(); + private readonly ILogger _logger; // Add this + + // Add constructor + public AppController(ILogger logger) + { + _logger = logger; + _logger.LogInformation("AppController initialized"); + _logger.LogInformation($"OTLP Logs Endpoint: {Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT")}"); + _logger.LogInformation($"AWS SigV4 Enabled: {Environment.GetEnvironmentVariable("OTEL_AWS_SIG_V4_ENABLED")}"); + _logger.LogInformation($"Log Headers: {Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_LOGS_HEADERS")}"); + } [HttpGet] [Route("/outgoing-http-call")] public string OutgoingHttp() { + _logger.LogInformation("Making outgoing HTTP call to aws.amazon.com"); _ = this.httpClient.GetAsync("https://aws.amazon.com").Result; + _logger.LogInformation("HTTP call completed"); return this.GetTraceId(); } @@ -28,7 +42,9 @@ public string OutgoingHttp() [Route("/aws-sdk-call")] public string AWSSDKCall() { + _logger.LogInformation("Making AWS SDK call to list buckets"); _ = this.s3Client.ListBucketsAsync().Result; + _logger.LogInformation("AWS SDK call completed"); return this.GetTraceId(); } @@ -37,15 +53,35 @@ public string AWSSDKCall() [Route("/")] public string Default() { + _logger.LogInformation("Default endpoint called"); return "Application started!"; } + [HttpGet] + [Route("/test-logging")] + public IActionResult TestLogging() + { + try + { + _logger.LogInformation("Test log message with timestamp: {Time}", DateTimeOffset.UtcNow); + _logger.LogWarning("Test warning message"); + _logger.LogError("Test error message"); + return Ok("Logging test completed"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during logging test"); + return StatusCode(500, "Logging test failed"); + } + } + private string GetTraceId() { var traceId = Activity.Current.TraceId.ToHexString(); var version = "1"; var epoch = traceId.Substring(0, 8); var random = traceId.Substring(8); + _logger.LogInformation($"Generated trace ID: {version}-{epoch}-{random}"); return "{" + "\"traceId\"" + ": " + "\"" + version + "-" + epoch + "-" + random + "\"" + "}"; } -} +} \ No newline at end of file diff --git a/sample-applications/integration-test-app/integration-test-app/integration-test-app.csproj b/sample-applications/integration-test-app/integration-test-app/integration-test-app.csproj index 57556212..d5959ea3 100644 --- a/sample-applications/integration-test-app/integration-test-app/integration-test-app.csproj +++ b/sample-applications/integration-test-app/integration-test-app/integration-test-app.csproj @@ -11,6 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/OtlpAwsLogExporter/OtlpAwsLogExporter.cs b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/OtlpAwsLogExporter/OtlpAwsLogExporter.cs new file mode 100644 index 00000000..e78c0f45 --- /dev/null +++ b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/OtlpAwsLogExporter/OtlpAwsLogExporter.cs @@ -0,0 +1,400 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. + +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using Amazon; +using Amazon.Runtime; +using Amazon.Runtime.Internal; +using Amazon.Runtime.Internal.Auth; +using Amazon.XRay; +using AWS.Distro.OpenTelemetry.AutoInstrumentation.Logging; +using AWS.Distro.OpenTelemetry.Exporter.Xray.Udp; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; + +#pragma warning disable CS1700 // Assembly reference is invalid and cannot be resolved +[assembly: InternalsVisibleTo("AWS.Distro.OpenTelemetry.AutoInstrumentation.Tests, PublicKey=6ba7de5ce46d6af3")] + +namespace AWS.Distro.OpenTelemetry.AutoInstrumentation; + +/// +/// This exporter OVERRIDES the Export functionality of the http/protobuf OtlpLogExporter to allow logs to be exported +/// to the CloudWatch OTLP endpoint https://logs.[AWSRegion].amazonaws.com/v1/logs. Utilizes the AWSSDK +/// library to sign and directly inject SigV4 Authentication to the exported request's headers. +/// +/// NOTE: In order to properly configure the usage of this exporter. Please make sure you have the +/// following environment variables: +/// +/// export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.[AWSRegion].amazonaws.com/v1/logs +/// export OTEL_AWS_SIG_V4_ENABLED=true +/// export OTEL_EXPORTER_OTLP_LOGS_HEADERS=x-aws-log-group=your-log-group,x-aws-log-stream=your-log-stream +/// +/// +/// +/// For more information, see AWS documentation on CloudWatch OTLP Endpoint. +/// +public class OtlpAwsLogExporter : BaseExporter +#pragma warning restore CS1700 // Assembly reference is invalid and cannot be resolved +{ + private static readonly string ServiceName = "logs"; + private static readonly string ContentType = "application/x-protobuf"; +#pragma warning disable CS0436 // Type conflicts with imported type + private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider())); +#pragma warning restore CS0436 // Type conflicts with imported type + private static readonly ILogger Logger = Factory.CreateLogger(); + private static readonly string OtelExporterOtlpLogsHeadersConfig = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"; + private readonly HttpClient client = new HttpClient(); + private readonly Uri endpoint; + private readonly string region; + private readonly int timeout; + private readonly Resource processResource; + private readonly Dictionary headers; + private IAwsAuthenticator authenticator; + + /// + /// Initializes a new instance of the class. + /// + /// OpenTelemetry Protocol (OTLP) exporter options. + /// Otel Resource Object + public OtlpAwsLogExporter(OtlpExporterOptions options, Resource processResource) + : this(options, processResource, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// OpenTelemetry Protocol (OTLP) exporter options. + /// Otel Resource Object + /// The authentication used to sign the request with SigV4 + internal OtlpAwsLogExporter(OtlpExporterOptions options, Resource processResource, IAwsAuthenticator? authenticator = null) + { + this.endpoint = options.Endpoint; + this.timeout = options.TimeoutMilliseconds; + + // Verified in Plugin.cs that the endpoint matches the CloudWatch endpoint format. + this.region = this.endpoint.AbsoluteUri.Split('.')[1]; + this.processResource = processResource; + this.authenticator = authenticator == null ? new DefaultAwsAuthenticator() : authenticator; + this.headers = ParseHeaders(System.Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_LOGS_HEADERS")); + } + + /// + public override ExportResult Export(in Batch batch) + { + using IDisposable scope = SuppressInstrumentationScope.Begin(); + + // Inheriting the size from upstream: https://github.com/open-telemetry/opentelemetry-dotnet/blob/24a13ab91c9c152d03fd0871bbb94e8f6ef08698/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs#L28-L31 + byte[] serializedData = new byte[750000]; + int serializedDataLength = OtlpExporterUtils.WriteLogsData(ref serializedData, 0, this.processResource, batch); + + if (serializedDataLength == -1) + { + Logger.LogError("Logs cannot be serialized"); + return ExportResult.Failure; + } + + try + { + HttpResponseMessage? message = Task.Run(() => + { + // The retry delay cannot exceed the configured timeout period for otlp exporter. + // If the backend responds with `RetryAfter` duration that would result in exceeding the configured timeout period + // we would fail and drop the data. + return RetryHelper.ExecuteWithRetryAsync(() => this.InjectSigV4AndSendAsync(serializedData, 0, serializedDataLength), TimeSpan.FromMilliseconds(this.timeout)); + }).GetAwaiter().GetResult(); + + if (message == null || message.StatusCode != HttpStatusCode.OK) + { + return ExportResult.Failure; + } + } + catch (Exception) + { + return ExportResult.Failure; + } + + return ExportResult.Success; + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + return base.OnShutdown(timeoutMilliseconds); + } + + // Creates the UserAgent for the headers. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs#L223 + private static string GetUserAgentString() + { + var assembly = typeof(OtlpExporterOptions).Assembly; + return $"OTel-OTLP-Exporter-Dotnet/{GetPackageVersion(assembly)}"; + } + + // Creates the DotNet instrumentation version for UserAgent header. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/Shared/AssemblyVersionExtensions.cs#L49 + private static string GetPackageVersion(Assembly assembly) + { + var informationalVersion = assembly.GetCustomAttribute()?.InformationalVersion; + Debug.Assert(!string.IsNullOrEmpty(informationalVersion), "AssemblyInformationalVersionAttribute was not found in assembly"); + + var indexOfPlusSign = informationalVersion!.IndexOf('+'); + return indexOfPlusSign > 0 + ? informationalVersion.Substring(0, indexOfPlusSign) + : informationalVersion; + } + + private static Dictionary ParseHeaders(string? headersString) + { + var headers = new Dictionary(); + if (!string.IsNullOrEmpty(headersString)) + { + var headerPairs = headersString.Split(','); + foreach (var pair in headerPairs) + { + var keyValue = pair.Split('='); + if (keyValue.Length == 2) + { + headers[keyValue[0].Trim()] = keyValue[1].Trim(); + } + } + } + return headers; + } + + private async Task InjectSigV4AndSendAsync(byte[] serializedLogs, int offset, int serializedDataLength) + { + Logger.LogInformation("Attempting to send logs"); + if (!this.headers.TryGetValue("x-aws-log-group", out var logGroup) || + !this.headers.TryGetValue("x-aws-log-stream", out var logStream)) + { + Logger.LogError("Log group and stream must be specified in OTEL_EXPORTER_OTLP_LOGS_HEADERS"); + throw new InvalidOperationException("Missing required log group or stream headers"); + } + + Logger.LogInformation($"Using log group: {logGroup}, stream: {logStream}"); + + HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, this.endpoint.AbsoluteUri); + IRequest sigV4Request = await this.GetSignedSigV4Request(serializedLogs, offset, serializedDataLength); + + sigV4Request.Headers.Remove("content-type"); + sigV4Request.Headers.Add("User-Agent", GetUserAgentString()); + + // Add headers from environment variable + foreach (var header in this.headers) + { + sigV4Request.Headers.Add(header.Key, header.Value); + } + + foreach (var header in sigV4Request.Headers) + { + httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + var content = new ByteArrayContent(serializedLogs, offset, serializedDataLength); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(ContentType); + + httpRequest.Method = HttpMethod.Post; + httpRequest.Content = content; + + return await this.client.SendAsync(httpRequest); + } + + private async Task GetSignedSigV4Request(byte[] content, int offset, int serializedDataLength) + { + IRequest request = new DefaultRequest(new EmptyAmazonWebServiceRequest(), ServiceName) + { + HttpMethod = "POST", + ContentStream = new MemoryStream(content, offset, serializedDataLength), + Endpoint = this.endpoint, + SignatureVersion = SignatureVersion.SigV4, + }; + + AmazonXRayConfig config = new AmazonXRayConfig() + { + AuthenticationRegion = this.region, + UseHttp = false, + ServiceURL = this.endpoint.AbsoluteUri, + RegionEndpoint = RegionEndpoint.GetBySystemName(this.region), + }; + + ImmutableCredentials credentials = await this.authenticator.GetCredentialsAsync(); + + // Need to explicitly add this for using temporary security credentials from AWS STS. + // SigV4 signing library does not automatically add this header. + if (credentials.UseToken && credentials.Token != null) + { + request.Headers.Add("x-amz-security-token", credentials.Token); + } + + request.Headers.Add("Host", this.endpoint.Host); + request.Headers.Add("content-type", ContentType); + + this.authenticator.Sign(request, config, credentials); + + return request; + } + + private class EmptyAmazonWebServiceRequest : AmazonWebServiceRequest + { + } +} + +// Implementation based on: +// https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs#L41 +internal class RetryHelper +{ + private const int InitialBackoffMilliseconds = 1000; + private const int MaxBackoffMilliseconds = 5000; + private const double BackoffMultiplier = 1.5; + + // This is to ensure there is no flakiness with the number of times logs are exported in the retry window. Not part of the upstream's implementation + private const int BufferWindow = 20; +#pragma warning disable CS0436 // Type conflicts with imported type + private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider())); +#pragma warning restore CS0436 // Type conflicts with imported type + private static readonly ILogger Logger = Factory.CreateLogger(); + +#if !NET6_0_OR_GREATER + private static readonly Random Randomizer = new Random(); +#endif + + public static async Task ExecuteWithRetryAsync( + Func> sendRequestFunc, + TimeSpan timeout) + { + var deadline = DateTime.UtcNow + timeout; + int currentDelay = InitialBackoffMilliseconds; + HttpResponseMessage? response = null; + while (true) + { + try + { + if (HasDeadlinePassed(deadline, 0)) + { + Logger.LogDebug("Timeout of {Deadline}ms reached, stopping retries", deadline.Millisecond); + return response; + } + + // Attempt to send the http request + response = await sendRequestFunc(); + + // Stop and return the response if the status code is success or there is an unretryable status code. + if (response.IsSuccessStatusCode || !IsRetryableStatusCode(response.StatusCode)) + { + string loggingMessage = response.IsSuccessStatusCode ? $"Logs successfully exported with status code {response.StatusCode}" : $"Logs were not exported with unretryable status code: {response.StatusCode}"; + Logger.LogInformation(loggingMessage); + return response; + } + + // First check if the backend responds with a retry delay + TimeSpan? retryAfterDelay = response.Headers.RetryAfter != null ? response.Headers.RetryAfter.Delta : null; + + TimeSpan delayDuration; + + if (retryAfterDelay.HasValue) + { + delayDuration = retryAfterDelay.Value; + + try + { + currentDelay = Convert.ToInt32(retryAfterDelay.Value.TotalMilliseconds); + } + catch (OverflowException) + { + currentDelay = MaxBackoffMilliseconds; + } + } + else + { + // If no response for delay from backend we add our own jitter delay + delayDuration = TimeSpan.FromMilliseconds(GetRandomNumber(0, currentDelay)); + } + + Logger.LogDebug("Logs were not exported with status code: {StatusCode}. Checking to see if retryable again after: {DelayMilliseconds} ms", response.StatusCode, delayDuration.Milliseconds); + + // If delay exceeds deadline. We drop the http request completely. + if (HasDeadlinePassed(deadline, delayDuration.Milliseconds)) + { + Logger.LogDebug("Timeout will be reached after {Delay}ms delay. Dropping logs with status code {StatusCode}.", delayDuration.Milliseconds, response.StatusCode); + return response; + } + + currentDelay = CalculateNextRetryDelay(currentDelay); + await Task.Delay(delayDuration); + } + catch (Exception e) + { + string exceptionName = e.GetType().Name; + var delayDuration = TimeSpan.FromMilliseconds(GetRandomNumber(0, currentDelay)); + + // Handling exceptions. Same logic, we retry with custom jitter delay until it succeeds. If it fails by the time deadline is reached we drop the request completely. + if (!HasDeadlinePassed(deadline, 0)) + { + currentDelay = CalculateNextRetryDelay(currentDelay); + if (!HasDeadlinePassed(deadline, delayDuration.Milliseconds)) + { + Logger.LogDebug("{@ExceptionMessage}. Retrying again after {@Delay}ms", exceptionName, delayDuration.Milliseconds); + + await Task.Delay(delayDuration); + continue; + } + } + + Logger.LogDebug("Timeout will be reached after {Delay}ms delay. Dropping logs with exception: {@ExceptionMessage}", delayDuration.Milliseconds, e); + throw; + } + } + } + + private static bool HasDeadlinePassed(DateTime deadline, double delayDuration) + { + return DateTime.UtcNow.AddMilliseconds(delayDuration) >= + deadline.Subtract(TimeSpan.FromMilliseconds(BufferWindow)); + } + + private static int GetRandomNumber(int min, int max) + { +#if NET6_0_OR_GREATER + return Random.Shared.Next(min, max); +#else + lock (Randomizer) + { + return Randomizer.Next(min, max); + } +#endif + } + + private static bool IsRetryableStatusCode(HttpStatusCode statusCode) + { + switch (statusCode) + { +#if NETSTANDARD2_1_OR_GREATER || NET + case HttpStatusCode.TooManyRequests: +#else + case (HttpStatusCode)429: +#endif + case HttpStatusCode.BadGateway: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.GatewayTimeout: + return true; + default: + return false; + } + } + + private static int CalculateNextRetryDelay(int currentDelayMs) + { + var nextDelay = currentDelayMs * BackoffMultiplier; + return Convert.ToInt32(Math.Min(nextDelay, MaxBackoffMilliseconds)); + } +} \ No newline at end of file diff --git a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs index caacb082..c2f2492e 100644 --- a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs +++ b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs @@ -26,6 +26,7 @@ using OpenTelemetry.Resources; using OpenTelemetry.Sampler.AWS; using OpenTelemetry.Trace; +using OpenTelemetry.Logs; using B3Propagator = OpenTelemetry.Extensions.Propagators.B3Propagator; namespace AWS.Distro.OpenTelemetry.AutoInstrumentation; @@ -40,13 +41,19 @@ public class Plugin /// public static readonly string ApplicationSignalsEnabledConfig = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"; private static readonly string XRayOtlpEndpointPattern = "^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$"; + private static readonly string CloudWatchLogsEndpointPattern = "^https://logs\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/logs$"; + private static readonly string LogsExporterConfig = "OTEL_LOGS_EXPORTER"; + private static readonly string OtelExporterOtlpLogsProtocolConfig = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"; + private static readonly string OtelExporterOtlpLogsHeadersConfig = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"; + private static readonly string OtelExporterOtlpLogsTimeout = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT"; + private static readonly int DefaultOtlpLogsTimeoutMilli = 10000; private static readonly string SigV4EnabledConfig = "OTEL_AWS_SIG_V4_ENABLED"; private static readonly string TracesExporterConfig = "OTEL_TRACES_EXPORTER"; private static readonly string OtelExporterOtlpTracesTimeout = "OTEL_EXPORTER_OTLP_TIMEOUT"; private static readonly int DefaultOtlpTracesTimeoutMilli = 10000; -#pragma warning disable CS0436 // Type conflicts with imported type + #pragma warning disable CS0436 // Type conflicts with imported type private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider())); -#pragma warning restore CS0436 // Type conflicts with imported type + #pragma warning restore CS0436 // Type conflicts with imported type private static readonly ILogger Logger = Factory.CreateLogger(); private static readonly string ApplicationSignalsExporterEndpointConfig = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"; private static readonly string ApplicationSignalsRuntimeEnabledConfig = "OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED"; @@ -64,6 +71,9 @@ public class Plugin private static readonly string OtelExporterOtlpTracesEndpointConfig = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"; private static readonly string? OtelExporterOtlpTracesEndpoint = System.Environment.GetEnvironmentVariable(OtelExporterOtlpTracesEndpointConfig); + private static readonly string OtelExporterOtlpLogsEndpointConfig = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"; + private static readonly string? OtelExporterOtlpLogsEndpoint = System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsEndpointConfig); + private static readonly string OtelExporterOtlpEndpointConfig = "OTEL_EXPORTER_OTLP_ENDPOINT"; private static readonly string? OtelExporterOtlpEndpoint = System.Environment.GetEnvironmentVariable(OtelExporterOtlpEndpointConfig); @@ -169,14 +179,14 @@ public void TracerProviderInitialized(TracerProvider tracerProvider) } } - if (this.IsSigV4AuthEnabled()) + if (this.IsSigV4AuthEnabled(OtelExporterOtlpTracesEndpoint, XRayOtlpEndpointPattern, "traces")) { OtlpExporterOptions options = new OtlpExporterOptions(); -#pragma warning disable CS8604 // Possible null reference argument. + #pragma warning disable CS8604 // Possible null reference argument. // This is already checked in isSigV4Enabled predicate options.Endpoint = new Uri(OtelExporterOtlpTracesEndpoint); -#pragma warning restore CS8604 // Possible null reference argument. + #pragma warning restore CS8604 // Possible null reference argument. options.TimeoutMilliseconds = this.GetTracesOtlpTimeout(); var otlpAwsSpanExporter = new OtlpAwsSpanExporter(options, tracerProvider.GetResource()); @@ -184,6 +194,66 @@ public void TracerProviderInitialized(TracerProvider tracerProvider) } } + /// + /// To access LoggerProvider right after LoggerProviderBuilder.Build() is executed. + /// + /// Provider to configure + public void LoggerProviderInitialized(global::OpenTelemetry.Logs.LoggerProvider loggerProvider) + { + Console.WriteLine($"Checking logs configuration:"); + Console.WriteLine($"Logs Endpoint: {OtelExporterOtlpLogsEndpoint}"); + Console.WriteLine($"Logs Headers: {System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsHeadersConfig)}"); + Console.WriteLine($"Logs Exporter: {System.Environment.GetEnvironmentVariable(LogsExporterConfig)}"); + Console.WriteLine($"SigV4 Enabled: {System.Environment.GetEnvironmentVariable(SigV4EnabledConfig)}"); + if (this.IsSigV4AuthEnabled(OtelExporterOtlpLogsEndpoint, CloudWatchLogsEndpointPattern, "logs")) + { + try + { + OtlpExporterOptions options = new OtlpExporterOptions(); + options.Endpoint = new Uri(OtelExporterOtlpLogsEndpoint!); + options.TimeoutMilliseconds = this.GetLogsOtlpTimeout(); + + var resource = this.GetResourceFromProvider(loggerProvider); + var otlpAwsLogExporter = new OtlpAwsLogExporter(options, resource); + var processor = new BatchLogRecordExportProcessor(otlpAwsLogExporter); + + // Use reflection to add processor to LoggerProviderSdk + var loggerProviderSdkType = Type.GetType("OpenTelemetry.Logs.LoggerProviderSdk, OpenTelemetry"); + if (loggerProviderSdkType != null && loggerProviderSdkType.IsInstanceOfType(loggerProvider)) + { + var addProcessorMethod = loggerProviderSdkType.GetMethod("AddProcessor", BindingFlags.NonPublic | BindingFlags.Instance); + addProcessorMethod?.Invoke(loggerProvider, new object[] { processor }); + } + + Logger.Log(LogLevel.Information, "CloudWatch Logs exporter configured successfully"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to configure CloudWatch Logs exporter"); + } + } + } + + /// + /// To configure logging SDK after Auto Instrumentation configured SDK + /// + /// The logger provider builder + /// The configured logger provider builder + public global::OpenTelemetry.Logs.LoggerProviderBuilder AfterConfigureLoggerProvider(global::OpenTelemetry.Logs.LoggerProviderBuilder builder) + { + Console.WriteLine("AfterConfigureLoggerProvider called"); + // Configure resource + builder.ConfigureResource(resourceBuilder => this.ResourceBuilderCustomizer(resourceBuilder)); + + // If CloudWatch Logs is configured, we need to ensure the OTLP exporter is added + if (IsSigV4AuthEnabled(OtelExporterOtlpLogsEndpoint, CloudWatchLogsEndpointPattern, "logs")) + { + Logger.Log(LogLevel.Information, "CloudWatch Logs OTLP endpoint detected, SigV4 authentication will be configured"); + } + + return builder; + } + /// /// To configure tracing SDK before Auto Instrumentation configured SDK /// @@ -575,33 +645,66 @@ private bool HasCustomTracesEndpoint() // The setup here requires OTEL_TRACES_EXPORTER to be set to none in order to avoid exporting the spans twice. // However that introduces the problem of overriding the default behavior of when OTEL_TRACES_EXPORTER is set to none which is // why we introduce a new environment variable that confirms traces are exported to the OTLP XRay endpoint. - private bool IsSigV4AuthEnabled() + private bool IsSigV4AuthEnabled(string? endpoint = null, string pattern = "", string exporterType = "traces") { - bool isXrayOtlpEndpoint = OtelExporterOtlpTracesEndpoint != null && new Regex(XRayOtlpEndpointPattern, RegexOptions.Compiled).IsMatch(OtelExporterOtlpTracesEndpoint); + if (endpoint == null) + { + endpoint = OtelExporterOtlpTracesEndpoint; + pattern = XRayOtlpEndpointPattern; + } + + bool isAwsOtlpEndpoint = endpoint != null && new Regex(pattern, RegexOptions.Compiled).IsMatch(endpoint); - if (isXrayOtlpEndpoint) + if (isAwsOtlpEndpoint) { - Logger.Log(LogLevel.Information, "Detected using AWS OTLP XRay Endpoint."); - string? sigV4EnabledConfig = System.Environment.GetEnvironmentVariable(Plugin.SigV4EnabledConfig); + string endpointType = exporterType == "traces" ? "XRay" : "CloudWatch Logs"; + Logger.Log(LogLevel.Information, $"Detected using AWS OTLP {endpointType} Endpoint."); + string? sigV4EnabledConfig = System.Environment.GetEnvironmentVariable(SigV4EnabledConfig); if (sigV4EnabledConfig == null || !sigV4EnabledConfig.Equals("true")) { - Logger.Log(LogLevel.Information, $"Please enable SigV4 authentication when exporting traces to OTLP XRay Endpoint by setting {SigV4EnabledConfig}=true"); + Logger.Log(LogLevel.Information, $"Please enable SigV4 authentication when exporting {exporterType} to OTLP {endpointType} Endpoint by setting {SigV4EnabledConfig}=true"); return false; } Logger.Log(LogLevel.Information, $"SigV4 authentication is enabled"); - string? tracesExporter = System.Environment.GetEnvironmentVariable(Plugin.TracesExporterConfig); - - if (tracesExporter == null || tracesExporter != "none") + if (exporterType == "traces") { - Logger.Log(LogLevel.Information, $"Please disable other tracing exporters by setting {TracesExporterConfig}=none"); - return false; + string? tracesExporter = System.Environment.GetEnvironmentVariable(TracesExporterConfig); + if (tracesExporter == null || tracesExporter != "none") + { + Logger.Log(LogLevel.Information, $"Please disable other tracing exporters by setting {TracesExporterConfig}=none"); + return false; + } } + else if (exporterType == "logs") + { + string? logsHeaders = System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsHeadersConfig); + if (string.IsNullOrEmpty(logsHeaders) || + !logsHeaders.Contains("x-aws-log-group=") || + !logsHeaders.Contains("x-aws-log-stream=")) + { + Logger.Log(LogLevel.Information, $"Please set {OtelExporterOtlpLogsHeadersConfig} with x-aws-log-group and x-aws-log-stream headers"); + return false; + } + + string? logsExporter = System.Environment.GetEnvironmentVariable(LogsExporterConfig); + if (!string.IsNullOrEmpty(logsExporter) && logsExporter != "otlp") + { + Logger.Log(LogLevel.Information, $"Please set {LogsExporterConfig}=otlp or leave it unset to use the default value"); + return false; + } - Logger.Log(LogLevel.Information, $"Proper configuration has been detected, now exporting spans to {OtelExporterOtlpTracesEndpoint}"); + string? protocol = System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsProtocolConfig); + if (!string.IsNullOrEmpty(protocol) && protocol != "http/protobuf") + { + Logger.Log(LogLevel.Information, $"Please set {OtelExporterOtlpLogsProtocolConfig}=http/protobuf or leave it unset to use the default value"); + return false; + } + } + Logger.Log(LogLevel.Information, $"Proper configuration has been detected, now exporting {exporterType} to {endpoint}"); return true; } @@ -627,4 +730,42 @@ private int GetTracesOtlpTimeout() return DefaultOtlpTracesTimeoutMilli; } + + private Resource GetResourceFromProvider(global::OpenTelemetry.Logs.LoggerProvider loggerProvider) + { + try + { + var resourceProperty = loggerProvider.GetType().GetProperty("Resource", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); + return (Resource)(resourceProperty?.GetValue(loggerProvider) ?? Resource.Empty); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to get resource from LoggerProvider, using empty resource"); + return Resource.Empty; + } + } + + private int GetLogsOtlpTimeout() + { + string? timeout = System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsTimeout); + + if (string.IsNullOrEmpty(timeout)) + { + timeout = System.Environment.GetEnvironmentVariable(OtelExporterOtlpTracesTimeout); + } + + if (!string.IsNullOrEmpty(timeout)) + { + try + { + return int.Parse(timeout); + } + catch (Exception) + { + return DefaultOtlpLogsTimeoutMilli; + } + } + + return DefaultOtlpLogsTimeoutMilli; + } } From 1e78f2d6cf678ae61a82205b5d34e881119eb0b7 Mon Sep 17 00:00:00 2001 From: Jeel Mehta Date: Fri, 1 Aug 2025 14:34:13 -0700 Subject: [PATCH 4/4] Cleaning up files to be commited so that log exporter can be used in the future --- build/Build.cs | 6 +- .../integration-test-app/Dockerfile | 27 +-- .../integration-test-app/docker-compose.yml | 27 ++- .../Controllers/AppController.cs | 38 +--- .../integration-test-app.csproj | 3 +- .../Plugin.cs | 178 ++---------------- 6 files changed, 45 insertions(+), 234 deletions(-) diff --git a/build/Build.cs b/build/Build.cs index 6a27fa5c..e5005c83 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -84,7 +84,6 @@ private static string GetOTelAutoInstrumentationFileName() case PlatformFamily.Windows: fileName = "opentelemetry-dotnet-instrumentation-windows.zip"; break; - case PlatformFamily.OSX: case PlatformFamily.Linux: var architecture = RuntimeInformation.ProcessArchitecture; string architectureSuffix; @@ -104,6 +103,9 @@ private static string GetOTelAutoInstrumentationFileName() ? $"opentelemetry-dotnet-instrumentation-linux-musl-{architectureSuffix}.zip" : $"opentelemetry-dotnet-instrumentation-linux-glibc-{architectureSuffix}.zip"; break; + case PlatformFamily.OSX: + fileName = "opentelemetry-dotnet-instrumentation-macos.zip"; + break; case PlatformFamily.Unknown: throw new NotSupportedException(); default: @@ -252,4 +254,4 @@ Copyright The OpenTelemetry Authors under Apache License Version 2.0 // .DependsOn(RunUnitTests) // .DependsOn(RunIntegrationTests) .DependsOn(this.PackAWSDistribution); -} +} \ No newline at end of file diff --git a/sample-applications/integration-test-app/Dockerfile b/sample-applications/integration-test-app/Dockerfile index dbe841c6..1dce7cb0 100644 --- a/sample-applications/integration-test-app/Dockerfile +++ b/sample-applications/integration-test-app/Dockerfile @@ -1,4 +1,3 @@ -# Base configuration FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env WORKDIR /app COPY . ./ @@ -10,43 +9,21 @@ EXPOSE 8080 COPY --from=build-env /app/out . ENTRYPOINT ["dotnet", "integration-test-app.dll"] -# ADOT Setup ARG INSTALL_DIR=/opt/aws/otel/dotnet RUN mkdir -p ${INSTALL_DIR} RUN ls ../../ COPY ./OpenTelemetryDistribution/ ${INSTALL_DIR} RUN mkdir -p /var/log/opentelemetry/dotnet -# Auto-instrumentation configuration ENV CORECLR_ENABLE_PROFILING=1 ENV CORECLR_PROFILER={918728DD-259F-4A6A-AC2B-B85E1B658318} -ENV CORECLR_PROFILER_PATH=${INSTALL_DIR}/linux-arm64/OpenTelemetry.AutoInstrumentation.Native.so +ENV CORECLR_PROFILER_PATH=${INSTALL_DIR}/linux-x64/OpenTelemetry.AutoInstrumentation.Native.so ENV DOTNET_ADDITIONAL_DEPS=${INSTALL_DIR}/AdditionalDeps ENV DOTNET_SHARED_STORE=${INSTALL_DIR}/store ENV DOTNET_STARTUP_HOOKS=${INSTALL_DIR}/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll ENV OTEL_DOTNET_AUTO_HOME=${INSTALL_DIR} ENV OTEL_DOTNET_AUTO_PLUGINS="AWS.Distro.OpenTelemetry.AutoInstrumentation.Plugin, AWS.Distro.OpenTelemetry.AutoInstrumentation" - -# OpenTelemetry configuration ENV OTEL_AWS_APPLICATION_SIGNALS_ENABLED="true" ENV OTEL_TRACES_SAMPLER="always_on" ENV OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" -ENV OTEL_RESOURCE_ATTRIBUTES=service.name=dotnet-sample-application -ENV OTEL_AWS_SIG_V4_ENABLED=true - -# Exporter endpoints -ENV OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT="http://otel:4318/v1/metrics" -ENV OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://xray.us-east-1.amazonaws.com/v1/traces -ENV OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.us-east-1.amazonaws.com/v1/logs - -# Exporter configuration -ENV OTEL_METRICS_EXPORTER=none -ENV OTEL_TRACES_EXPORTER=none -ENV OTEL_LOGS_EXPORTER=otlp - -# CloudWatch Logs specific configuration -ENV OTEL_EXPORTER_OTLP_LOGS_HEADERS="x-aws-log-group=otlp_logs,x-aws-log-stream=default" -ENV OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="http/protobuf" - -# App configuration -ENV ASPNETCORE_URLS=http://0.0.0.0:8080 \ No newline at end of file +ENV OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT="http://otel:4318/v1/metrics" \ No newline at end of file diff --git a/sample-applications/integration-test-app/docker-compose.yml b/sample-applications/integration-test-app/docker-compose.yml index 25398116..d6bb64ae 100644 --- a/sample-applications/integration-test-app/docker-compose.yml +++ b/sample-applications/integration-test-app/docker-compose.yml @@ -1,25 +1,34 @@ version: "3.7" services: + cwagent: + image: amazon/cloudwatch-agent:latest + volumes: + - ./amazon-cloudwatch-agent.json:/opt/aws/amazon-cloudwatch-agent/bin/default_linux_config.json # agent config + - ./aws:/root/.aws # required for authentication + environment: + - AWS_REGION=us-west-2 + ports: + - '4316:4316' + - '4317:4317' + - '2000:2000' + app: image: aspnetapp:latest environment: - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - - AWS_SESSION_TOKEN - - AWS_REGION=us-east-1 + - AWS_REGION=us-west-2 - INSTANCE_ID - LISTEN_ADDRESS=0.0.0.0:8080 - OTEL_RESOURCE_ATTRIBUTES=service.name=aws-otel-integ-test - # - OTEL_EXPORTER_OTLP_ENDPOINT=http://cwagent:4316 # TODO: workaround for trace exporter endpoint - # - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://cwagent:4316/v1/traces - # - OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT=http://cwagent:4316/v1/metrics + - OTEL_EXPORTER_OTLP_ENDPOINT=http://cwagent:4316 # TODO: workaround for trace exporter endpoint + - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://cwagent:4316/v1/traces + - OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT=http://cwagent:4316/v1/metrics - ASPNETCORE_URLS=http://+:8080 - OTEL_METRICS_EXPORTER=none - OTEL_AWS_APPLICATION_SIGNALS_ENABLED=true - OTEL_TRACES_SAMPLER=xray - - RESOURCE_DETECTORS_ENABLED=false + - OTEL_TRACES_SAMPLER_ARG=endpoint=http://cwagent:2000,polling_interval=1 ports: - '8080:8080' volumes: - - ./logs:/var/log/opentelemetry/dotnet + - ~/.aws:/root/.aws:ro \ No newline at end of file diff --git a/sample-applications/integration-test-app/integration-test-app/Controllers/AppController.cs b/sample-applications/integration-test-app/integration-test-app/Controllers/AppController.cs index aac8288b..9699c195 100644 --- a/sample-applications/integration-test-app/integration-test-app/Controllers/AppController.cs +++ b/sample-applications/integration-test-app/integration-test-app/Controllers/AppController.cs @@ -1,11 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -using System; + using System.Diagnostics; using System.Net.Http; using Amazon.S3; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace integration_test_app.Controllers; @@ -15,25 +14,12 @@ public class AppController : ControllerBase { private readonly AmazonS3Client s3Client = new AmazonS3Client(); private readonly HttpClient httpClient = new HttpClient(); - private readonly ILogger _logger; // Add this - - // Add constructor - public AppController(ILogger logger) - { - _logger = logger; - _logger.LogInformation("AppController initialized"); - _logger.LogInformation($"OTLP Logs Endpoint: {Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT")}"); - _logger.LogInformation($"AWS SigV4 Enabled: {Environment.GetEnvironmentVariable("OTEL_AWS_SIG_V4_ENABLED")}"); - _logger.LogInformation($"Log Headers: {Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_LOGS_HEADERS")}"); - } [HttpGet] [Route("/outgoing-http-call")] public string OutgoingHttp() { - _logger.LogInformation("Making outgoing HTTP call to aws.amazon.com"); _ = this.httpClient.GetAsync("https://aws.amazon.com").Result; - _logger.LogInformation("HTTP call completed"); return this.GetTraceId(); } @@ -42,9 +28,7 @@ public string OutgoingHttp() [Route("/aws-sdk-call")] public string AWSSDKCall() { - _logger.LogInformation("Making AWS SDK call to list buckets"); _ = this.s3Client.ListBucketsAsync().Result; - _logger.LogInformation("AWS SDK call completed"); return this.GetTraceId(); } @@ -53,35 +37,15 @@ public string AWSSDKCall() [Route("/")] public string Default() { - _logger.LogInformation("Default endpoint called"); return "Application started!"; } - [HttpGet] - [Route("/test-logging")] - public IActionResult TestLogging() - { - try - { - _logger.LogInformation("Test log message with timestamp: {Time}", DateTimeOffset.UtcNow); - _logger.LogWarning("Test warning message"); - _logger.LogError("Test error message"); - return Ok("Logging test completed"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during logging test"); - return StatusCode(500, "Logging test failed"); - } - } - private string GetTraceId() { var traceId = Activity.Current.TraceId.ToHexString(); var version = "1"; var epoch = traceId.Substring(0, 8); var random = traceId.Substring(8); - _logger.LogInformation($"Generated trace ID: {version}-{epoch}-{random}"); return "{" + "\"traceId\"" + ": " + "\"" + version + "-" + epoch + "-" + random + "\"" + "}"; } } \ No newline at end of file diff --git a/sample-applications/integration-test-app/integration-test-app/integration-test-app.csproj b/sample-applications/integration-test-app/integration-test-app/integration-test-app.csproj index d5959ea3..1cf4fb51 100644 --- a/sample-applications/integration-test-app/integration-test-app/integration-test-app.csproj +++ b/sample-applications/integration-test-app/integration-test-app/integration-test-app.csproj @@ -11,8 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - + \ No newline at end of file diff --git a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs index c2f2492e..f8cf3a2f 100644 --- a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs +++ b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs @@ -26,7 +26,6 @@ using OpenTelemetry.Resources; using OpenTelemetry.Sampler.AWS; using OpenTelemetry.Trace; -using OpenTelemetry.Logs; using B3Propagator = OpenTelemetry.Extensions.Propagators.B3Propagator; namespace AWS.Distro.OpenTelemetry.AutoInstrumentation; @@ -40,20 +39,15 @@ public class Plugin /// OTEL_AWS_APPLICATION_SIGNALS_ENABLED /// public static readonly string ApplicationSignalsEnabledConfig = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"; + internal static readonly string LambdaApplicationSignalsRemoteEnvironment = "LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT"; private static readonly string XRayOtlpEndpointPattern = "^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$"; - private static readonly string CloudWatchLogsEndpointPattern = "^https://logs\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/logs$"; - private static readonly string LogsExporterConfig = "OTEL_LOGS_EXPORTER"; - private static readonly string OtelExporterOtlpLogsProtocolConfig = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"; - private static readonly string OtelExporterOtlpLogsHeadersConfig = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"; - private static readonly string OtelExporterOtlpLogsTimeout = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT"; - private static readonly int DefaultOtlpLogsTimeoutMilli = 10000; private static readonly string SigV4EnabledConfig = "OTEL_AWS_SIG_V4_ENABLED"; private static readonly string TracesExporterConfig = "OTEL_TRACES_EXPORTER"; private static readonly string OtelExporterOtlpTracesTimeout = "OTEL_EXPORTER_OTLP_TIMEOUT"; private static readonly int DefaultOtlpTracesTimeoutMilli = 10000; - #pragma warning disable CS0436 // Type conflicts with imported type +#pragma warning disable CS0436 // Type conflicts with imported type private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider())); - #pragma warning restore CS0436 // Type conflicts with imported type +#pragma warning restore CS0436 // Type conflicts with imported type private static readonly ILogger Logger = Factory.CreateLogger(); private static readonly string ApplicationSignalsExporterEndpointConfig = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"; private static readonly string ApplicationSignalsRuntimeEnabledConfig = "OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED"; @@ -71,9 +65,6 @@ public class Plugin private static readonly string OtelExporterOtlpTracesEndpointConfig = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"; private static readonly string? OtelExporterOtlpTracesEndpoint = System.Environment.GetEnvironmentVariable(OtelExporterOtlpTracesEndpointConfig); - private static readonly string OtelExporterOtlpLogsEndpointConfig = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"; - private static readonly string? OtelExporterOtlpLogsEndpoint = System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsEndpointConfig); - private static readonly string OtelExporterOtlpEndpointConfig = "OTEL_EXPORTER_OTLP_ENDPOINT"; private static readonly string? OtelExporterOtlpEndpoint = System.Environment.GetEnvironmentVariable(OtelExporterOtlpEndpointConfig); @@ -179,14 +170,14 @@ public void TracerProviderInitialized(TracerProvider tracerProvider) } } - if (this.IsSigV4AuthEnabled(OtelExporterOtlpTracesEndpoint, XRayOtlpEndpointPattern, "traces")) + if (this.IsSigV4AuthEnabled()) { OtlpExporterOptions options = new OtlpExporterOptions(); - #pragma warning disable CS8604 // Possible null reference argument. +#pragma warning disable CS8604 // Possible null reference argument. // This is already checked in isSigV4Enabled predicate options.Endpoint = new Uri(OtelExporterOtlpTracesEndpoint); - #pragma warning restore CS8604 // Possible null reference argument. +#pragma warning restore CS8604 // Possible null reference argument. options.TimeoutMilliseconds = this.GetTracesOtlpTimeout(); var otlpAwsSpanExporter = new OtlpAwsSpanExporter(options, tracerProvider.GetResource()); @@ -194,66 +185,6 @@ public void TracerProviderInitialized(TracerProvider tracerProvider) } } - /// - /// To access LoggerProvider right after LoggerProviderBuilder.Build() is executed. - /// - /// Provider to configure - public void LoggerProviderInitialized(global::OpenTelemetry.Logs.LoggerProvider loggerProvider) - { - Console.WriteLine($"Checking logs configuration:"); - Console.WriteLine($"Logs Endpoint: {OtelExporterOtlpLogsEndpoint}"); - Console.WriteLine($"Logs Headers: {System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsHeadersConfig)}"); - Console.WriteLine($"Logs Exporter: {System.Environment.GetEnvironmentVariable(LogsExporterConfig)}"); - Console.WriteLine($"SigV4 Enabled: {System.Environment.GetEnvironmentVariable(SigV4EnabledConfig)}"); - if (this.IsSigV4AuthEnabled(OtelExporterOtlpLogsEndpoint, CloudWatchLogsEndpointPattern, "logs")) - { - try - { - OtlpExporterOptions options = new OtlpExporterOptions(); - options.Endpoint = new Uri(OtelExporterOtlpLogsEndpoint!); - options.TimeoutMilliseconds = this.GetLogsOtlpTimeout(); - - var resource = this.GetResourceFromProvider(loggerProvider); - var otlpAwsLogExporter = new OtlpAwsLogExporter(options, resource); - var processor = new BatchLogRecordExportProcessor(otlpAwsLogExporter); - - // Use reflection to add processor to LoggerProviderSdk - var loggerProviderSdkType = Type.GetType("OpenTelemetry.Logs.LoggerProviderSdk, OpenTelemetry"); - if (loggerProviderSdkType != null && loggerProviderSdkType.IsInstanceOfType(loggerProvider)) - { - var addProcessorMethod = loggerProviderSdkType.GetMethod("AddProcessor", BindingFlags.NonPublic | BindingFlags.Instance); - addProcessorMethod?.Invoke(loggerProvider, new object[] { processor }); - } - - Logger.Log(LogLevel.Information, "CloudWatch Logs exporter configured successfully"); - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to configure CloudWatch Logs exporter"); - } - } - } - - /// - /// To configure logging SDK after Auto Instrumentation configured SDK - /// - /// The logger provider builder - /// The configured logger provider builder - public global::OpenTelemetry.Logs.LoggerProviderBuilder AfterConfigureLoggerProvider(global::OpenTelemetry.Logs.LoggerProviderBuilder builder) - { - Console.WriteLine("AfterConfigureLoggerProvider called"); - // Configure resource - builder.ConfigureResource(resourceBuilder => this.ResourceBuilderCustomizer(resourceBuilder)); - - // If CloudWatch Logs is configured, we need to ensure the OTLP exporter is added - if (IsSigV4AuthEnabled(OtelExporterOtlpLogsEndpoint, CloudWatchLogsEndpointPattern, "logs")) - { - Logger.Log(LogLevel.Information, "CloudWatch Logs OTLP endpoint detected, SigV4 authentication will be configured"); - } - - return builder; - } - /// /// To configure tracing SDK before Auto Instrumentation configured SDK /// @@ -645,66 +576,33 @@ private bool HasCustomTracesEndpoint() // The setup here requires OTEL_TRACES_EXPORTER to be set to none in order to avoid exporting the spans twice. // However that introduces the problem of overriding the default behavior of when OTEL_TRACES_EXPORTER is set to none which is // why we introduce a new environment variable that confirms traces are exported to the OTLP XRay endpoint. - private bool IsSigV4AuthEnabled(string? endpoint = null, string pattern = "", string exporterType = "traces") + private bool IsSigV4AuthEnabled() { - if (endpoint == null) - { - endpoint = OtelExporterOtlpTracesEndpoint; - pattern = XRayOtlpEndpointPattern; - } - - bool isAwsOtlpEndpoint = endpoint != null && new Regex(pattern, RegexOptions.Compiled).IsMatch(endpoint); + bool isXrayOtlpEndpoint = OtelExporterOtlpTracesEndpoint != null && new Regex(XRayOtlpEndpointPattern, RegexOptions.Compiled).IsMatch(OtelExporterOtlpTracesEndpoint); - if (isAwsOtlpEndpoint) + if (isXrayOtlpEndpoint) { - string endpointType = exporterType == "traces" ? "XRay" : "CloudWatch Logs"; - Logger.Log(LogLevel.Information, $"Detected using AWS OTLP {endpointType} Endpoint."); - string? sigV4EnabledConfig = System.Environment.GetEnvironmentVariable(SigV4EnabledConfig); + Logger.Log(LogLevel.Information, "Detected using AWS OTLP XRay Endpoint."); + string? sigV4EnabledConfig = System.Environment.GetEnvironmentVariable(Plugin.SigV4EnabledConfig); if (sigV4EnabledConfig == null || !sigV4EnabledConfig.Equals("true")) { - Logger.Log(LogLevel.Information, $"Please enable SigV4 authentication when exporting {exporterType} to OTLP {endpointType} Endpoint by setting {SigV4EnabledConfig}=true"); + Logger.Log(LogLevel.Information, $"Please enable SigV4 authentication when exporting traces to OTLP XRay Endpoint by setting {SigV4EnabledConfig}=true"); return false; } Logger.Log(LogLevel.Information, $"SigV4 authentication is enabled"); - if (exporterType == "traces") + string? tracesExporter = System.Environment.GetEnvironmentVariable(Plugin.TracesExporterConfig); + + if (tracesExporter == null || tracesExporter != "none") { - string? tracesExporter = System.Environment.GetEnvironmentVariable(TracesExporterConfig); - if (tracesExporter == null || tracesExporter != "none") - { - Logger.Log(LogLevel.Information, $"Please disable other tracing exporters by setting {TracesExporterConfig}=none"); - return false; - } + Logger.Log(LogLevel.Information, $"Please disable other tracing exporters by setting {TracesExporterConfig}=none"); + return false; } - else if (exporterType == "logs") - { - string? logsHeaders = System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsHeadersConfig); - if (string.IsNullOrEmpty(logsHeaders) || - !logsHeaders.Contains("x-aws-log-group=") || - !logsHeaders.Contains("x-aws-log-stream=")) - { - Logger.Log(LogLevel.Information, $"Please set {OtelExporterOtlpLogsHeadersConfig} with x-aws-log-group and x-aws-log-stream headers"); - return false; - } - - string? logsExporter = System.Environment.GetEnvironmentVariable(LogsExporterConfig); - if (!string.IsNullOrEmpty(logsExporter) && logsExporter != "otlp") - { - Logger.Log(LogLevel.Information, $"Please set {LogsExporterConfig}=otlp or leave it unset to use the default value"); - return false; - } - string? protocol = System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsProtocolConfig); - if (!string.IsNullOrEmpty(protocol) && protocol != "http/protobuf") - { - Logger.Log(LogLevel.Information, $"Please set {OtelExporterOtlpLogsProtocolConfig}=http/protobuf or leave it unset to use the default value"); - return false; - } - } + Logger.Log(LogLevel.Information, $"Proper configuration has been detected, now exporting spans to {OtelExporterOtlpTracesEndpoint}"); - Logger.Log(LogLevel.Information, $"Proper configuration has been detected, now exporting {exporterType} to {endpoint}"); return true; } @@ -730,42 +628,4 @@ private int GetTracesOtlpTimeout() return DefaultOtlpTracesTimeoutMilli; } - - private Resource GetResourceFromProvider(global::OpenTelemetry.Logs.LoggerProvider loggerProvider) - { - try - { - var resourceProperty = loggerProvider.GetType().GetProperty("Resource", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); - return (Resource)(resourceProperty?.GetValue(loggerProvider) ?? Resource.Empty); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to get resource from LoggerProvider, using empty resource"); - return Resource.Empty; - } - } - - private int GetLogsOtlpTimeout() - { - string? timeout = System.Environment.GetEnvironmentVariable(OtelExporterOtlpLogsTimeout); - - if (string.IsNullOrEmpty(timeout)) - { - timeout = System.Environment.GetEnvironmentVariable(OtelExporterOtlpTracesTimeout); - } - - if (!string.IsNullOrEmpty(timeout)) - { - try - { - return int.Parse(timeout); - } - catch (Exception) - { - return DefaultOtlpLogsTimeoutMilli; - } - } - - return DefaultOtlpLogsTimeoutMilli; - } -} +} \ No newline at end of file