|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net;
using System.Text;
using Aspire.Dashboard.Authentication.OtlpApiKey;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Otlp.Http;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Model.Serialization;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Hosting;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
namespace Aspire.Dashboard.Tests.Integration;
public class OtlpHttpJsonTests
{
/// <summary>
/// Example trace JSON from https://github.com/open-telemetry/opentelemetry-proto/blob/v1.9.0/examples/trace.json
/// </summary>
private const string ExampleTraceJson = """
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
]
},
"scopeSpans": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
]
},
"spans": [
{
"traceId": "5B8EFFF798038103D269B633813FC60C",
"spanId": "EEE19B7EC3C1B174",
"parentSpanId": "EEE19B7EC3C1B173",
"name": "I'm a server span",
"startTimeUnixNano": "1544712660000000000",
"endTimeUnixNano": "1544712661000000000",
"kind": 2,
"attributes": [
{
"key": "my.span.attr",
"value": {
"stringValue": "some value"
}
}
]
}
]
}
]
}
]
}
""";
/// <summary>
/// Example logs JSON from https://github.com/open-telemetry/opentelemetry-proto/blob/v1.9.0/examples/logs.json
/// </summary>
private const string ExampleLogsJson = """
{
"resourceLogs": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
]
},
"scopeLogs": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
]
},
"logRecords": [
{
"timeUnixNano": "1544712660300000000",
"observedTimeUnixNano": "1544712660300000000",
"severityNumber": 10,
"severityText": "Information",
"traceId": "5B8EFFF798038103D269B633813FC60C",
"spanId": "EEE19B7EC3C1B174",
"body": {
"stringValue": "Example log record"
},
"attributes": [
{
"key": "string.attribute",
"value": {
"stringValue": "some string"
}
},
{
"key": "boolean.attribute",
"value": {
"boolValue": true
}
},
{
"key": "int.attribute",
"value": {
"intValue": "10"
}
},
{
"key": "double.attribute",
"value": {
"doubleValue": 637.704
}
},
{
"key": "array.attribute",
"value": {
"arrayValue": {
"values": [
{
"stringValue": "many"
},
{
"stringValue": "values"
}
]
}
}
},
{
"key": "map.attribute",
"value": {
"kvlistValue": {
"values": [
{
"key": "some.map.key",
"value": {
"stringValue": "some value"
}
}
]
}
}
}
]
}
]
}
]
}
]
}
""";
/// <summary>
/// Example events JSON from https://github.com/open-telemetry/opentelemetry-proto/blob/v1.9.0/examples/events.json
/// Events are log records with an eventName field.
/// </summary>
private const string ExampleEventsJson = """
{
"resourceLogs": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
]
},
"scopeLogs": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
]
},
"logRecords": [
{
"eventName": "browser.page_view",
"timeUnixNano": "1544712660300000000",
"observedTimeUnixNano": "1544712660300000000",
"severityNumber": 9,
"severityText": "test severity text",
"attributes": [
{
"key": "event.attribute",
"value": {
"stringValue": "some event attribute"
}
}
],
"body": {
"kvlistValue": {
"values": [
{
"key": "type",
"value": {
"intValue": "0"
}
},
{
"key": "url",
"value": {
"stringValue": "https://www.example.com/page"
}
},
{
"key": "referrer",
"value": {
"stringValue": "https://www.google.com"
}
},
{
"key": "title",
"value": {
"stringValue": "Example Page Title"
}
}
]
}
}
}
]
}
]
}
]
}
""";
/// <summary>
/// Example metrics JSON from https://github.com/open-telemetry/opentelemetry-proto/blob/v1.9.0/examples/metrics.json
/// </summary>
private const string ExampleMetricsJson = """
{
"resourceMetrics": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
]
},
"scopeMetrics": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
]
},
"metrics": [
{
"name": "my.counter",
"unit": "1",
"description": "I am a Counter",
"sum": {
"aggregationTemporality": 1,
"isMonotonic": true,
"dataPoints": [
{
"asDouble": 5,
"startTimeUnixNano": "1544712660300000000",
"timeUnixNano": "1544712660300000000",
"attributes": [
{
"key": "my.counter.attr",
"value": {
"stringValue": "some value"
}
}
]
}
]
}
},
{
"name": "my.gauge",
"unit": "1",
"description": "I am a Gauge",
"gauge": {
"dataPoints": [
{
"asDouble": 10,
"timeUnixNano": "1544712660300000000",
"attributes": [
{
"key": "my.gauge.attr",
"value": {
"stringValue": "some value"
}
}
]
}
]
}
},
{
"name": "my.histogram",
"unit": "1",
"description": "I am a Histogram",
"histogram": {
"aggregationTemporality": 1,
"dataPoints": [
{
"startTimeUnixNano": "1544712660300000000",
"timeUnixNano": "1544712660300000000",
"count": "2",
"sum": 2,
"bucketCounts": ["1", "1"],
"explicitBounds": [1],
"min": 0,
"max": 2,
"attributes": [
{
"key": "my.histogram.attr",
"value": {
"stringValue": "some value"
}
}
]
}
]
}
}
]
}
]
}
]
}
""";
private readonly ITestOutputHelper _testOutputHelper;
public OtlpHttpJsonTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
private static OtlpResource AssertMyServiceResource(TelemetryRepository telemetryRepository)
{
var resources = telemetryRepository.GetResourcesByName("my.service");
var resource = Assert.Single(resources);
Assert.Equal("my.service", resource.ResourceName);
Assert.False(resource.UninstrumentedPeer);
return resource;
}
[Fact]
public async Task CallService_Traces_JsonContentType_Success()
{
// Arrange
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper);
await app.StartAsync().DefaultTimeout();
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}");
var content = new StringContent(ExampleTraceJson, Encoding.UTF8, OtlpHttpEndpointsBuilder.JsonContentType);
// Act
var responseMessage = await httpClient.PostAsync("/v1/traces", content).DefaultTimeout();
responseMessage.EnsureSuccessStatusCode();
// Assert
Assert.Equal(OtlpHttpEndpointsBuilder.JsonContentType, responseMessage.Content.Headers.ContentType?.MediaType);
var responseBody = await responseMessage.Content.ReadAsStringAsync().DefaultTimeout();
Assert.NotNull(responseBody);
// Verify the response is valid JSON and can be deserialized
var response = System.Text.Json.JsonSerializer.Deserialize(responseBody, OtlpJsonSerializerContext.Default.OtlpExportTraceServiceResponseJson);
Assert.NotNull(response);
// Verify data was stored in the repository
var telemetryRepository = app.Services.GetRequiredService<TelemetryRepository>();
var resource = AssertMyServiceResource(telemetryRepository);
var traces = telemetryRepository.GetTraces(new GetTracesRequest
{
ResourceKey = resource.ResourceKey,
StartIndex = 0,
Count = 10,
FilterText = string.Empty,
Filters = []
});
Assert.NotEmpty(traces.PagedResult.Items);
var trace = traces.PagedResult.Items.First();
Assert.Equal("5b8efff798038103d269b633813fc60c", trace.TraceId);
Assert.Single(trace.Spans);
var span = trace.Spans.First();
Assert.Equal("I'm a server span", span.Name);
Assert.Equal("eee19b7ec3c1b174", span.SpanId);
Assert.Equal("eee19b7ec3c1b173", span.ParentSpanId);
Assert.Equal(Aspire.Dashboard.Otlp.Model.OtlpSpanKind.Server, span.Kind);
Assert.Equal(new DateTime(2018, 12, 13, 14, 51, 0, DateTimeKind.Utc), span.StartTime);
Assert.Equal(new DateTime(2018, 12, 13, 14, 51, 1, DateTimeKind.Utc), span.EndTime);
Assert.Equal(TimeSpan.FromSeconds(1), span.Duration);
Assert.Collection(span.Attributes,
a =>
{
Assert.Equal("my.span.attr", a.Key);
Assert.Equal("some value", a.Value);
});
Assert.Equal("my.library", span.Scope.Name);
Assert.Equal("1.0.0", span.Scope.Version);
}
[Fact]
public async Task CallService_Logs_JsonContentType_Success()
{
// Arrange
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper);
await app.StartAsync().DefaultTimeout();
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}");
var content = new StringContent(ExampleLogsJson, Encoding.UTF8, OtlpHttpEndpointsBuilder.JsonContentType);
// Act
var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout();
responseMessage.EnsureSuccessStatusCode();
// Assert
Assert.Equal(OtlpHttpEndpointsBuilder.JsonContentType, responseMessage.Content.Headers.ContentType?.MediaType);
var responseBody = await responseMessage.Content.ReadAsStringAsync().DefaultTimeout();
Assert.NotNull(responseBody);
// Verify the response is valid JSON and can be deserialized
var response = System.Text.Json.JsonSerializer.Deserialize(responseBody, OtlpJsonSerializerContext.Default.OtlpExportLogsServiceResponseJson);
Assert.NotNull(response);
// Verify data was stored in the repository
var telemetryRepository = app.Services.GetRequiredService<TelemetryRepository>();
var resource = AssertMyServiceResource(telemetryRepository);
var logs = telemetryRepository.GetLogs(new GetLogsContext
{
ResourceKey = resource.ResourceKey,
StartIndex = 0,
Count = 10,
Filters = []
});
Assert.NotEmpty(logs.Items);
var log = logs.Items.First();
Assert.Equal("Example log record", log.Message);
Assert.Equal(LogLevel.Information, log.Severity);
Assert.Equal(new DateTime(2018, 12, 13, 14, 51, 0, 300, DateTimeKind.Utc), log.TimeStamp);
Assert.Equal("5b8efff798038103d269b633813fc60c", log.TraceId);
Assert.Equal("eee19b7ec3c1b174", log.SpanId);
Assert.Equal("my.library", log.Scope.Name);
Assert.Equal("1.0.0", log.Scope.Version);
Assert.Collection(log.Attributes,
a =>
{
Assert.Equal("string.attribute", a.Key);
Assert.Equal("some string", a.Value);
},
a =>
{
Assert.Equal("boolean.attribute", a.Key);
Assert.Equal("true", a.Value);
},
a =>
{
Assert.Equal("int.attribute", a.Key);
Assert.Equal("10", a.Value);
},
a =>
{
Assert.Equal("double.attribute", a.Key);
Assert.Equal("637.704", a.Value);
},
a =>
{
Assert.Equal("array.attribute", a.Key);
},
a =>
{
Assert.Equal("map.attribute", a.Key);
});
}
[Fact]
public async Task CallService_Events_JsonContentType_Success()
{
// Arrange
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper);
await app.StartAsync().DefaultTimeout();
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}");
var content = new StringContent(ExampleEventsJson, Encoding.UTF8, OtlpHttpEndpointsBuilder.JsonContentType);
// Act
var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout();
responseMessage.EnsureSuccessStatusCode();
// Assert
Assert.Equal(OtlpHttpEndpointsBuilder.JsonContentType, responseMessage.Content.Headers.ContentType?.MediaType);
var responseBody = await responseMessage.Content.ReadAsStringAsync().DefaultTimeout();
Assert.NotNull(responseBody);
// Verify the response is valid JSON and can be deserialized
var response = System.Text.Json.JsonSerializer.Deserialize(responseBody, OtlpJsonSerializerContext.Default.OtlpExportLogsServiceResponseJson);
Assert.NotNull(response);
// Verify data was stored in the repository (events are stored as logs)
var telemetryRepository = app.Services.GetRequiredService<TelemetryRepository>();
var resource = AssertMyServiceResource(telemetryRepository);
var logs = telemetryRepository.GetLogs(new GetLogsContext
{
ResourceKey = resource.ResourceKey,
StartIndex = 0,
Count = 10,
Filters = []
});
Assert.NotEmpty(logs.Items);
var log = logs.Items.First();
Assert.Equal(LogLevel.Information, log.Severity);
Assert.Equal(new DateTime(2018, 12, 13, 14, 51, 0, 300, DateTimeKind.Utc), log.TimeStamp);
Assert.Equal("my.library", log.Scope.Name);
Assert.Collection(log.Attributes,
a =>
{
Assert.Equal("event.attribute", a.Key);
Assert.Equal("some event attribute", a.Value);
});
}
[Fact]
public async Task CallService_Metrics_JsonContentType_Success()
{
// Arrange
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper);
await app.StartAsync().DefaultTimeout();
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}");
var content = new StringContent(ExampleMetricsJson, Encoding.UTF8, OtlpHttpEndpointsBuilder.JsonContentType);
// Act
var responseMessage = await httpClient.PostAsync("/v1/metrics", content).DefaultTimeout();
responseMessage.EnsureSuccessStatusCode();
// Assert
Assert.Equal(OtlpHttpEndpointsBuilder.JsonContentType, responseMessage.Content.Headers.ContentType?.MediaType);
var responseBody = await responseMessage.Content.ReadAsStringAsync().DefaultTimeout();
Assert.NotNull(responseBody);
// Verify the response is valid JSON and can be deserialized
var response = System.Text.Json.JsonSerializer.Deserialize(responseBody, OtlpJsonSerializerContext.Default.OtlpExportMetricsServiceResponseJson);
Assert.NotNull(response);
// Verify data was stored in the repository
var telemetryRepository = app.Services.GetRequiredService<TelemetryRepository>();
var resource = AssertMyServiceResource(telemetryRepository);
var instruments = resource.GetInstrumentsSummary();
Assert.Collection(instruments,
counter =>
{
Assert.Equal("my.counter", counter.Name);
Assert.Equal("I am a Counter", counter.Description);
Assert.Equal("1", counter.Unit);
Assert.Equal("my.library", counter.Parent.Name);
},
gauge =>
{
Assert.Equal("my.gauge", gauge.Name);
Assert.Equal("I am a Gauge", gauge.Description);
Assert.Equal("1", gauge.Unit);
},
histogram =>
{
Assert.Equal("my.histogram", histogram.Name);
Assert.Equal("I am a Histogram", histogram.Description);
Assert.Equal("1", histogram.Unit);
});
}
[Fact]
public async Task CallService_JsonContentType_WithApiKey_Success()
{
// Arrange
var apiKey = "TestKey123!";
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
{
config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ApiKey.ToString();
config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey;
});
await app.StartAsync().DefaultTimeout();
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}");
var content = new StringContent(ExampleLogsJson, Encoding.UTF8, OtlpHttpEndpointsBuilder.JsonContentType);
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/v1/logs");
requestMessage.Content = content;
requestMessage.Headers.TryAddWithoutValidation(OtlpApiKeyAuthenticationHandler.ApiKeyHeaderName, apiKey);
// Act
var responseMessage = await httpClient.SendAsync(requestMessage).DefaultTimeout();
responseMessage.EnsureSuccessStatusCode();
// Assert
Assert.Equal(OtlpHttpEndpointsBuilder.JsonContentType, responseMessage.Content.Headers.ContentType?.MediaType);
}
[Fact]
public async Task CallService_JsonContentType_WithoutApiKey_Failure()
{
// Arrange
var apiKey = "TestKey123!";
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
{
config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ApiKey.ToString();
config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey;
});
await app.StartAsync().DefaultTimeout();
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}");
var content = new StringContent(ExampleLogsJson, Encoding.UTF8, OtlpHttpEndpointsBuilder.JsonContentType);
// Act
var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout();
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode);
}
[Fact]
public async Task CallService_JsonContentType_InvalidJson_BadRequest()
{
// Arrange
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper);
await app.StartAsync().DefaultTimeout();
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}");
var content = new StringContent("{ invalid json }", Encoding.UTF8, OtlpHttpEndpointsBuilder.JsonContentType);
// Act
var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout();
// Assert
Assert.Equal(HttpStatusCode.BadRequest, responseMessage.StatusCode);
}
[Fact]
public async Task CallService_JsonContentType_EmptyBody_Success()
{
// Arrange
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper);
await app.StartAsync().DefaultTimeout();
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}");
var content = new StringContent("{}", Encoding.UTF8, OtlpHttpEndpointsBuilder.JsonContentType);
// Act
var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout();
responseMessage.EnsureSuccessStatusCode();
// Assert
Assert.Equal(OtlpHttpEndpointsBuilder.JsonContentType, responseMessage.Content.Headers.ContentType?.MediaType);
}
}
|