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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';

export function initializeTelemetry(otlpUrl, headers, resourceAttributes) {
const otlpOptions = {
Expand All @@ -18,13 +16,15 @@ export function initializeTelemetry(otlpUrl, headers, resourceAttributes) {
};

var attributes = parseDelimitedValues(resourceAttributes);
attributes[SemanticResourceAttributes.SERVICE_NAME] = 'browser';
attributes['service.name'] = 'browser';

const provider = new WebTracerProvider({
resource: new Resource(attributes),
resource: resourceFromAttributes(attributes),
spanProcessors: [
new SimpleSpanProcessor(new ConsoleSpanExporter()),
new SimpleSpanProcessor(new OTLPTraceExporter(otlpOptions))
]
});
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.addSpanProcessor(new SimpleSpanProcessor(new OTLPTraceExporter(otlpOptions)));

provider.register({
// Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional
Expand Down
381 changes: 98 additions & 283 deletions playground/BrowserTelemetry/BrowserTelemetry.Web/package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions playground/BrowserTelemetry/BrowserTelemetry.Web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-zone": "^2.2.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.208.0",
"@opentelemetry/instrumentation-document-load": "^0.46.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
"@opentelemetry/instrumentation": "^0.208.0",
"@opentelemetry/instrumentation-document-load": "^0.54.0",
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-trace-base": "^2.2.0",
"@opentelemetry/sdk-trace-web": "^2.2.0"
},
"devDependencies": {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @license Angular v<unknown>
* (c) 2010-2024 Google LLC. https://angular.io/
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
88 changes: 70 additions & 18 deletions src/Aspire.Dashboard/Otlp/Http/OtlpHttpEndpointsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Utils;
using Google.Protobuf;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Primitives;
using OpenTelemetry.Proto.Collector.Logs.V1;
using OpenTelemetry.Proto.Collector.Metrics.V1;
Expand Down Expand Up @@ -49,31 +50,31 @@ public static void MapHttpOtlpApi(this IEndpointRouteBuilder endpoints, OtlpOpti

group.MapPost("logs", static (MessageBindable<ExportLogsServiceRequest> request, OtlpLogsService service) =>
{
if (request.Message == null)
if (request.Message is null)
{
return Results.Empty;
}
return OtlpResult.Response(service.Export(request.Message));
return OtlpResult.Response(service.Export(request.Message), request.RequestContentType);
});
group.MapPost("traces", static (MessageBindable<ExportTraceServiceRequest> request, OtlpTraceService service) =>
{
if (request.Message == null)
if (request.Message is null)
{
return Results.Empty;
}
return OtlpResult.Response(service.Export(request.Message));
return OtlpResult.Response(service.Export(request.Message), request.RequestContentType);
});
group.MapPost("metrics", (MessageBindable<ExportMetricsServiceRequest> request, OtlpMetricsService service) =>
{
if (request.Message == null)
if (request.Message is null)
{
return Results.Empty;
}
return OtlpResult.Response(service.Export(request.Message));
return OtlpResult.Response(service.Export(request.Message), request.RequestContentType);
});
}

private enum KnownContentType
internal enum KnownContentType
{
None,
Protobuf,
Expand Down Expand Up @@ -101,9 +102,8 @@ private static KnownContentType GetKnownContentType(string? contentType, out Str
return KnownContentType.None;
}

private static async Task WriteUnsupportedContentTypeResponse(HttpContext httpContext)
private static async Task WriteUnsupportedContentTypeResponse(HttpContext httpContext, ILogger logger)
{
var logger = httpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("Aspire.Dashboard.Otlp.Http");
logger.LogDebug("OTLP HTTP request with unsupported content type '{ContentType}' was rejected. Only '{SupportedContentType}' is supported.", httpContext.Request.ContentType, ProtobufContentType);

httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
Expand All @@ -127,13 +127,20 @@ private static T AddOtlpHttpMetadata<T>(this T builder) where T : IEndpointConve

private sealed class MessageBindable<TMessage> : IBindableFromHttpContext<MessageBindable<TMessage>> where TMessage : IMessage<TMessage>, new()
{
public static readonly MessageBindable<TMessage> Empty = new MessageBindable<TMessage>();
public static readonly MessageBindable<TMessage> Empty = new MessageBindable<TMessage>() { Logger = NullLogger.Instance };

public TMessage? Message { get; private set; }

public KnownContentType RequestContentType { get; private set; }

public required ILogger Logger { get; init; }

public static async ValueTask<MessageBindable<TMessage>?> BindAsync(HttpContext context, ParameterInfo parameter)
{
switch (GetKnownContentType(context.Request.ContentType, out var charSet))
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("Aspire.Dashboard.Otlp.Http");

var contentType = GetKnownContentType(context.Request.ContentType, out var charSet);
switch (contentType)
{
case KnownContentType.Protobuf:
try
Expand All @@ -145,33 +152,74 @@ private static T AddOtlpHttpMetadata<T>(this T builder) where T : IEndpointConve
return message;
}).ConfigureAwait(false);

return new MessageBindable<TMessage> { Message = message };
return new MessageBindable<TMessage> { Message = message, RequestContentType = contentType, Logger = logger };
}
catch (BadHttpRequestException ex)
{
context.Response.StatusCode = ex.StatusCode;
return Empty;
}
case KnownContentType.Json:
try
{
var message = await ReadOtlpJsonData<TMessage>(context).ConfigureAwait(false);
return new MessageBindable<TMessage> { Message = message, RequestContentType = contentType, Logger = logger };
}
catch (JsonException ex)
{
logger.LogDebug(ex, "Failed to deserialize OTLP JSON request.");
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return Empty;
}
catch (BadHttpRequestException ex)
{
context.Response.StatusCode = ex.StatusCode;
return Empty;
}
default:
await WriteUnsupportedContentTypeResponse(context).ConfigureAwait(false);
await WriteUnsupportedContentTypeResponse(context, logger).ConfigureAwait(false);
return Empty;
}
}
}

private static async Task<TMessage?> ReadOtlpJsonData<TMessage>(HttpContext httpContext) where TMessage : IMessage<TMessage>, new()
{
var json = await ReadOtlpData(httpContext, data =>
{
// Convert the buffer to a string for JSON parsing
if (data.IsSingleSegment)
{
return Encoding.UTF8.GetString(data.FirstSpan);
}
else
{
var bytes = data.ToArray();
return Encoding.UTF8.GetString(bytes);
}
}).ConfigureAwait(false);

return OtlpJsonConverters.DeserializeJson<TMessage>(json);
}

private sealed class OtlpResult<T> : IResult where T : IMessage
{
private readonly T _message;
private readonly KnownContentType _requestContentType;

public OtlpResult(T message) => _message = message;
public OtlpResult(T message, KnownContentType requestContentType)
{
_message = message;
_requestContentType = requestContentType;
}

public async Task ExecuteAsync(HttpContext httpContext)
{
switch (GetKnownContentType(httpContext.Request.ContentType, out _))
var logger = httpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("Aspire.Dashboard.Otlp.Http");

switch (_requestContentType)
{
case KnownContentType.Protobuf:

// This isn't very efficient but OTLP Protobuf responses are small.
var ms = new MemoryStream();
_message.WriteTo(ms);
Expand All @@ -181,16 +229,20 @@ public async Task ExecuteAsync(HttpContext httpContext)
await ms.CopyToAsync(httpContext.Response.Body).ConfigureAwait(false);
break;
case KnownContentType.Json:
httpContext.Response.ContentType = JsonContentType;
var jsonResponse = OtlpJsonConverters.SerializeJson(_message);
await httpContext.Response.WriteAsync(jsonResponse, Encoding.UTF8).ConfigureAwait(false);
break;
default:
await WriteUnsupportedContentTypeResponse(httpContext).ConfigureAwait(false);
await WriteUnsupportedContentTypeResponse(httpContext, logger).ConfigureAwait(false);
break;
}
}
}

private sealed class OtlpResult
{
public static OtlpResult<T> Response<T>(T response) where T : IMessage => new OtlpResult<T>(response);
public static OtlpResult<T> Response<T>(T response, KnownContentType requestContentType) where T : IMessage => new OtlpResult<T>(response, requestContentType);
}

private static async Task<T> ReadOtlpData<T>(
Expand Down
73 changes: 73 additions & 0 deletions src/Aspire.Dashboard/Otlp/Http/OtlpJsonConverters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Aspire.Dashboard.Otlp.Model.Serialization;
using Google.Protobuf;
using OpenTelemetry.Proto.Collector.Logs.V1;
using OpenTelemetry.Proto.Collector.Metrics.V1;
using OpenTelemetry.Proto.Collector.Trace.V1;

namespace Aspire.Dashboard.Otlp.Http;

internal static class OtlpJsonConverters
{
private static readonly Dictionary<Type, Func<string, IMessage>> s_jsonDeserializers = new()
{
[typeof(ExportTraceServiceRequest)] = json =>
{
var jsonObj = JsonSerializer.Deserialize(json, OtlpJsonSerializerContext.Default.OtlpExportTraceServiceRequestJson);
return jsonObj is null ? null! : OtlpJsonToProtobufConverter.ToProtobuf(jsonObj);
},
[typeof(ExportLogsServiceRequest)] = json =>
{
var jsonObj = JsonSerializer.Deserialize(json, OtlpJsonSerializerContext.Default.OtlpExportLogsServiceRequestJson);
return jsonObj is null ? null! : OtlpJsonToProtobufConverter.ToProtobuf(jsonObj);
},
[typeof(ExportMetricsServiceRequest)] = json =>
{
var jsonObj = JsonSerializer.Deserialize(json, OtlpJsonSerializerContext.Default.OtlpExportMetricsServiceRequestJson);
return jsonObj is null ? null! : OtlpJsonToProtobufConverter.ToProtobuf(jsonObj);
}
};

private static readonly Dictionary<Type, Func<IMessage, string>> s_jsonSerializers = new()
{
[typeof(ExportTraceServiceResponse)] = message =>
{
var json = OtlpProtobufToJsonConverter.ToJson((ExportTraceServiceResponse)message);
return JsonSerializer.Serialize(json, OtlpJsonSerializerContext.Default.OtlpExportTraceServiceResponseJson);
},
[typeof(ExportLogsServiceResponse)] = message =>
{
var json = OtlpProtobufToJsonConverter.ToJson((ExportLogsServiceResponse)message);
return JsonSerializer.Serialize(json, OtlpJsonSerializerContext.Default.OtlpExportLogsServiceResponseJson);
},
[typeof(ExportMetricsServiceResponse)] = message =>
{
var json = OtlpProtobufToJsonConverter.ToJson((ExportMetricsServiceResponse)message);
return JsonSerializer.Serialize(json, OtlpJsonSerializerContext.Default.OtlpExportMetricsServiceResponseJson);
}
};

public static TMessage? DeserializeJson<TMessage>(string json) where TMessage : IMessage
{
if (!s_jsonDeserializers.TryGetValue(typeof(TMessage), out var deserializer))
{
throw new NotSupportedException($"JSON deserialization for type {typeof(TMessage).Name} is not supported.");
}

var result = deserializer(json);
return result is null ? default : (TMessage)result;
}

public static string SerializeJson<TMessage>(TMessage message) where TMessage : IMessage
{
if (!s_jsonSerializers.TryGetValue(typeof(TMessage), out var serializer))
{
throw new NotSupportedException($"JSON serialization for type {typeof(TMessage).Name} is not supported.");
}

return serializer(message);
}
}
Loading