Skip to content

OpenTelemetry

El OpenTelemetryContainer es un componente de la librería CodeDesignPlus.Net.xUnit que proporciona un contenedor Docker para ejecutar el OpenTelemetry Collector. El OpenTelemetry Collector es una herramienta que permite recibir, procesar y exportar datos de telemetría (trazas, métricas y logs). Este componente facilita la creación de pruebas unitarias que requieren la funcionalidad de OpenTelemetry, ofreciendo un entorno de pruebas aislado y reproducible.

Además de proporcionar el contenedor Docker, esta librería incluye exportadores en memoria para facilitar la validación de los datos de telemetría generados por los servicios probados.

¿Cómo Funciona?


El OpenTelemetryContainer utiliza Docker Compose para definir y gestionar el contenedor del OpenTelemetry Collector. Al inicializar una instancia de esta clase, se genera un archivo docker-compose.yml temporal con la configuración necesaria para ejecutar el contenedor. Luego, el contenedor se inicia y se detiene automáticamente al final de la ejecución de las pruebas.

El proceso general de funcionamiento es el siguiente:

  1. Configuración del Contenedor: El método Build() define la configuración específica del contenedor OpenTelemetry Collector, incluyendo la ruta al archivo docker-compose.yml, opciones de reinicio forzado, eliminación de contenedores huérfanos, parada al finalizar y un nombre de servicio alternativo generado dinámicamente para evitar conflictos. También establece el puerto interno para el colector (4317).
  2. Creación del Contenedor: Se crea una instancia de DockerComposeCompositeService usando la configuración definida.
  3. Inicio y Detención del Contenedor: El contenedor se inicia cuando se crea una instancia de la clase OpenTelemetryContainer y se detiene automáticamente cuando la instancia se elimina, asegurando la limpieza del entorno.
  4. Exportadores en memoria: Se proporcionan tres exportadores en memoria para tracing, metrics y logs, que permiten inspeccionar los datos de telemetria generados durante las pruebas.

Container


El OpenTelemetryContainer hereda de la clase DockerCompose, la cual proporciona la lógica base para interactuar con Docker Compose. La clase utiliza la clase DockerComposeCompositeService de la librería Testcontainers para iniciar y detener contenedores docker. La configuración del contenedor se define en el método Build() que crea un archivo docker-compose.yml con los parámetros necesarios para la ejecución del contenedor OpenTelemetry Collector. El archivo docker-compose.yml se encuentra dentro del directorio Containers/OpenTelemetry.

namespace CodeDesignPlus.Net.xUnit.Containers.OpenTelemetry;
/// <summary>
/// Represents a Docker container for OpenTelemetry Collector, managed using Docker Compose.
/// </summary>
public class OpenTelemetryContainer : DockerCompose
{
/// <summary>
/// Builds the Docker Compose service configuration for the OpenTelemetry Collector container.
/// </summary>
/// <returns>An <see cref="ICompositeService"/> representing the Docker Compose service.</returns>
protected override ICompositeService Build()
{
// Define the path to the Docker Compose file.
var file = Path.Combine(Directory.GetCurrentDirectory(), "Containers", "OpenTelemetry", "docker-compose.yml");
// Configure the Docker Compose settings.
var dockerCompose = new DockerComposeConfig
{
ComposeFilePath = new[] { file },
ForceRecreate = true,
RemoveOrphans = true,
StopOnDispose = true,
AlternativeServiceName = "otel_" + Guid.NewGuid().ToString("N"),
};
// Enable port retrieval and set the internal port and container name.
this.EnableGetPort = true;
this.InternalPort = 4317;
this.ContainerName = $"{dockerCompose.AlternativeServiceName}-otel-collector";
// Create and return the Docker Compose service.
var compose = new DockerComposeCompositeService(base.DockerHost, dockerCompose);
return compose;
}
}

Además, se incluyen las clases InMemoryTraceExporter, InMemoryLogExporter e InMemoryMetricsExporter que permiten exportar los datos de telemetria a una coleccion en memoria para la validación.

using System.Diagnostics;
using OpenTelemetry;
using System.Collections.Concurrent;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
namespace CodeDesignPlus.Net.xUnit.Containers.OpenTelemetry;
/// <summary>
/// An in-memory exporter for tracing activities, used for testing purposes.
/// </summary>
public class InMemoryTraceExporter : BaseExporter<Activity>
{
/// <summary>
/// Gets the collection of exported activities.
/// </summary>
public static ConcurrentDictionary<string, Activity> Exporters { get; } = new ();
/// <summary>
/// Exports a batch of activities to the in-memory collection.
/// </summary>
/// <param name="batch">The batch of activities to export.</param>
/// <returns>The result of the export operation.</returns>
public override ExportResult Export(in Batch<Activity> batch)
{
foreach (var activity in batch)
{
Exporters.TryAdd(activity.OperationName, activity);
}
return ExportResult.Success;
}
}
/// <summary>
/// An in-memory exporter for log records, used for testing purposes.
/// </summary>
public class InMemoryLogExporter : BaseExporter<LogRecord>
{
/// <summary>
/// Gets the collection of exported log records.
/// </summary>
public static ConcurrentDictionary<string, LogRecord> Exporters { get; } = new ();
/// <summary>
/// Exports a batch of log records to the in-memory collection.
/// </summary>
/// <param name="batch">The batch of log records to export.</param>
/// <returns>The result of the export operation.</returns>
public override ExportResult Export(in Batch<LogRecord> batch)
{
foreach (var log in batch)
{
Exporters.TryAdd(log.SpanId.ToString(), log);
}
return ExportResult.Success;
}
}
/// <summary>
/// An in-memory exporter for metrics, used for testing purposes.
/// </summary>
public class InMemoryMetricsExporter : BaseExporter<Metric>
{
/// <summary>
/// Gets the collection of exported metrics.
/// </summary>
public static ConcurrentDictionary<string, Metric> Exporters { get; } = new ();
/// <summary>
/// Exports a batch of metrics to the in-memory collection.
/// </summary>
/// <param name="batch">The batch of metrics to export.</param>
/// <returns>The result of the export operation.</returns>
public override ExportResult Export(in Batch<Metric> batch)
{
foreach (var metric in batch)
{
Exporters.TryAdd(metric.Name, metric);
}
return ExportResult.Success;
}
}

Ejemplo de Uso


Este ejemplo muestra cómo usar OpenTelemetryContainer junto con exportadores en memoria para realizar pruebas de integración con OpenTelemetry. Se configura un servidor de prueba que genera logs, trazas y métricas, utilizando el OpenTelemetry Collector proporcionado por OpenTelemetryContainer. Los datos de telemetría se exportan en memoria mediante exportadores personalizados, permitiendo que la prueba verifique la correcta generación y exportación de estos datos. Este ejemplo también demuestra cómo configurar el exporter de logs para la consola y como configurar los exporter para obtener la información de telemetria en memoria.

using CodeDesignPlus.Net.xUnit.Containers.OpenTelemetry;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using OpenTelemetry.Exporter;
using OpenTelemetry.Metrics;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using CodeDesignPlus.Net.xUnit.Extensions;
namespace CodeDesignPlus.Net.xUnit.Test;
public class OpenTelemetryTest : IClassFixture<OpenTelemetryContainer>
{
private readonly TestServer server;
private readonly HttpClient client;
public OpenTelemetryTest(OpenTelemetryContainer container)
{
var configuration = ConfigurationUtil.GetConfiguration(new { });
this.server = new TestServer(new WebHostBuilder()
.UseConfiguration(configuration)
.ConfigureLogging(logger =>
{
logger.AddOpenTelemetry(logging =>
{
logging.AddOtlpExporter();
logging.AddProcessor(new SimpleLogRecordExportProcessor(new InMemoryLogExporter()));
logging.AddConsoleExporter(x => x.Targets = ConsoleExporterOutputTargets.Console);
});
})
.ConfigureServices(x =>
{
x.AddOpenTelemetry()
.ConfigureResource(resource =>
{
resource.AddService("MyServiceName", "1.0.0");
})
.WithTracing(tracing => tracing
.AddProcessor(new BatchActivityExportProcessor(new InMemoryTraceExporter()))
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddOtlpExporter()
.AddConsoleExporter()
.AddReader(new PeriodicExportingMetricReader(new InMemoryMetricsExporter(), 1000)));
})
.Configure(x =>
{
x.Map("/api/test", app =>
{
app.Run(async context =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<OpenTelemetryTest>>();
logger.LogWarning("This is a warning log message");
await context.Response.WriteAsync("Hello World!");
});
});
})
);
this.client = server.CreateClient();
}
[Fact]
public async Task CheckService()
{
var response = await client.GetAsync("/api/test");
response.EnsureSuccessStatusCode();
Assert.False(InMemoryLogExporter.Exporters.IsEmpty);
}
}

Conclusiones


  • El OpenTelemetryContainer facilita la creación de pruebas unitarias que requieren un OpenTelemetry Collector.
  • Utiliza Docker Compose para la gestión del contenedor, proporcionando una forma sencilla de definir la configuración.
  • La clase OpenTelemetryCollectionFixture permite compartir un contenedor entre todas las pruebas de una misma colección.
  • El uso de exportadores en memoria permite validar la información generada por los servicios probados.
  • El uso del OpenTelemetryContainer mejora la reproducibilidad y el aislamiento de las pruebas unitarias.
  • El contenedor OpenTelemetry Collector se inicia automaticamente al comienzo de cada prueba y se detiene una vez termina, esto permite un ambiente de pruebas limpio.

Referencias Adicionales