Observability

December 12, 2025 ยท View on GitHub

OpenTelemetry integration for comprehensive application observability including tracing, metrics, and logging.

OpenTelemetry

Complete OpenTelemetry observability platform with support for Zipkin, Prometheus, and OTLP exporters.

Installation

# Core OpenTelemetry
dotnet add package SharpAbp.Abp.OpenTelemetry
dotnet add package SharpAbp.Abp.OpenTelemetry.Abstractions

# Exporters (choose one or more):
dotnet add package SharpAbp.Abp.OpenTelemetry.Exporter.Console
dotnet add package SharpAbp.Abp.OpenTelemetry.Exporter.Zipkin
dotnet add package SharpAbp.Abp.OpenTelemetry.Exporter.Otlp
dotnet add package SharpAbp.Abp.OpenTelemetry.Exporter.Prometheus.AspNetCore
dotnet add package SharpAbp.Abp.OpenTelemetry.Exporter.Prometheus.HttpListener

Configuration

Configure in appsettings.json:

{
  "OpenTelemetry": {
    "ServiceName": "MyApplication",
    "ServiceVersion": "1.0.0",
    "Tracing": {
      "Enabled": true,
      "Zipkin": {
        "Endpoint": "http://localhost:9411/api/v2/spans"
      },
      "Otlp": {
        "Endpoint": "http://localhost:4317"
      }
    },
    "Metrics": {
      "Enabled": true,
      "Prometheus": {
        "Port": 9090,
        "Path": "/metrics"
      }
    }
  }
}

Add the module dependency:

[DependsOn(
    typeof(AbpOpenTelemetryModule),
    typeof(AbpOpenTelemetryExporterZipkinModule),
    typeof(AbpOpenTelemetryExporterPrometheusAspNetCoreModule)
)]
public class YourModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();
        var serviceName = configuration["OpenTelemetry:ServiceName"];

        Configure<AbpOpenTelemetryOptions>(options =>
        {
            options.ServiceName = serviceName;
            options.ServiceVersion = "1.0.0";
        });

        // Configure tracing
        context.Services.AddOpenTelemetryTracing(builder =>
        {
            builder
                .SetResourceBuilder(ResourceBuilder.CreateDefault()
                    .AddService(serviceName))
                .AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddSqlClientInstrumentation()
                .AddZipkinExporter(options =>
                {
                    options.Endpoint = new Uri(
                        configuration["OpenTelemetry:Tracing:Zipkin:Endpoint"]
                    );
                });
        });

        // Configure metrics
        context.Services.AddOpenTelemetryMetrics(builder =>
        {
            builder
                .SetResourceBuilder(ResourceBuilder.CreateDefault()
                    .AddService(serviceName))
                .AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddPrometheusExporter();
        });
    }

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();

        // Add Prometheus scraping endpoint
        app.UseOpenTelemetryPrometheusScrapingEndpoint();
    }
}

Usage Example

Custom Tracing

public class OrderService : ApplicationService
{
    private readonly ActivitySource _activitySource;

    public OrderService()
    {
        _activitySource = new ActivitySource("OrderService");
    }

    public async Task<OrderDto> CreateOrderAsync(CreateOrderDto input)
    {
        using (var activity = _activitySource.StartActivity("CreateOrder"))
        {
            activity?.SetTag("order.customerId", input.CustomerId);
            activity?.SetTag("order.totalAmount", input.TotalAmount);

            try
            {
                // Validate order
                using (var validateActivity = _activitySource.StartActivity("ValidateOrder"))
                {
                    await ValidateOrderAsync(input);
                    validateActivity?.SetTag("validation.result", "success");
                }

                // Create order
                var order = await CreateOrderInternalAsync(input);

                activity?.SetTag("order.id", order.Id);
                activity?.SetStatus(ActivityStatusCode.Ok);

                return ObjectMapper.Map<Order, OrderDto>(order);
            }
            catch (Exception ex)
            {
                activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
                activity?.RecordException(ex);
                throw;
            }
        }
    }

    private async Task<Order> CreateOrderInternalAsync(CreateOrderDto input)
    {
        using (var activity = _activitySource.StartActivity("CreateOrderInternal"))
        {
            // Implementation
            return new Order();
        }
    }
}

Custom Metrics

public class MetricsService : ITransientDependency
{
    private readonly Meter _meter;
    private readonly Counter<long> _orderCounter;
    private readonly Histogram<double> _orderAmount;
    private readonly ObservableGauge<int> _activeOrders;

    public MetricsService()
    {
        _meter = new Meter("OrderMetrics", "1.0.0");

        // Counter: tracks number of orders
        _orderCounter = _meter.CreateCounter<long>(
            "orders.created",
            description: "Number of orders created"
        );

        // Histogram: tracks distribution of order amounts
        _orderAmount = _meter.CreateHistogram<double>(
            "orders.amount",
            unit: "USD",
            description: "Distribution of order amounts"
        );

        // Gauge: tracks current active orders
        _activeOrders = _meter.CreateObservableGauge<int>(
            "orders.active",
            () => GetActiveOrderCount(),
            description: "Number of active orders"
        );
    }

    public void RecordOrderCreated(decimal amount, string status)
    {
        _orderCounter.Add(1,
            new KeyValuePair<string, object>("status", status)
        );

        _orderAmount.Record((double)amount,
            new KeyValuePair<string, object>("status", status)
        );
    }

    private int GetActiveOrderCount()
    {
        // Implementation
        return 0;
    }
}

Distributed Tracing

public class DistributedService : ApplicationService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ActivitySource _activitySource;

    public DistributedService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
        _activitySource = new ActivitySource("DistributedService");
    }

    public async Task<Result> ProcessDistributedOperationAsync()
    {
        using (var activity = _activitySource.StartActivity("DistributedOperation"))
        {
            activity?.SetTag("operation.type", "distributed");

            // Call external service - trace context is automatically propagated
            var client = _httpClientFactory.CreateClient();
            var response = await client.GetAsync("https://api.example.com/data");

            activity?.SetTag("external.status", (int)response.StatusCode);

            // Process response
            var data = await response.Content.ReadAsStringAsync();

            return new Result { Data = data };
        }
    }
}

Logging with OpenTelemetry

public class LoggingService : ApplicationService
{
    private readonly ILogger<LoggingService> _logger;

    public async Task ProcessWithLoggingAsync()
    {
        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["OrderId"] = Guid.NewGuid(),
            ["CustomerId"] = 123
        }))
        {
            _logger.LogInformation("Processing order");

            try
            {
                await ProcessAsync();
                _logger.LogInformation("Order processed successfully");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to process order");
                throw;
            }
        }
    }
}

Exporter Configurations

Zipkin Exporter

builder.AddZipkinExporter(options =>
{
    options.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
    options.MaxPayloadSizeInBytes = 4096;
});

Jaeger Exporter (via OTLP)

builder.AddOtlpExporter(options =>
{
    options.Endpoint = new Uri("http://localhost:4317");
    options.Protocol = OtlpExportProtocol.Grpc;
});

Prometheus Exporter

// AspNetCore
builder.AddPrometheusExporter();

// Configure endpoint in OnApplicationInitialization
app.UseOpenTelemetryPrometheusScrapingEndpoint(options =>
{
    options.Path = "/metrics";
});

// HttpListener (standalone)
builder.AddPrometheusHttpListener(options =>
{
    options.Uris = new[] { "http://localhost:9090/" };
});

Console Exporter (Development)

builder.AddConsoleExporter();

Best Practices

1. Naming Conventions

Follow OpenTelemetry semantic conventions:

public class BestPracticeService
{
    private readonly ActivitySource _activitySource;

    public BestPracticeService()
    {
        // Use descriptive, hierarchical names
        _activitySource = new ActivitySource("MyApp.OrderService");
    }

    public async Task ProcessOrderAsync(Guid orderId)
    {
        using (var activity = _activitySource.StartActivity(
            "ProcessOrder",
            ActivityKind.Internal))
        {
            // Use semantic convention tags
            activity?.SetTag("order.id", orderId);
            activity?.SetTag("order.status", "processing");

            // Add events for significant moments
            activity?.AddEvent(new ActivityEvent("order.validation.started"));

            await ValidateOrderAsync(orderId);

            activity?.AddEvent(new ActivityEvent("order.validation.completed"));
        }
    }
}

2. Sampling

Configure sampling to control data volume:

context.Services.AddOpenTelemetryTracing(builder =>
{
    builder
        .SetSampler(new TraceIdRatioBasedSampler(0.1)) // Sample 10% of traces
        .AddAspNetCoreInstrumentation();
});

3. Resource Attributes

Add resource attributes for better identification:

builder.SetResourceBuilder(
    ResourceBuilder.CreateDefault()
        .AddService(
            serviceName: "MyService",
            serviceVersion: "1.0.0",
            serviceInstanceId: Environment.MachineName)
        .AddAttributes(new[]
        {
            new KeyValuePair<string, object>("environment", "production"),
            new KeyValuePair<string, object>("region", "us-east-1")
        })
);

4. Error Handling

Always record exceptions:

public async Task SafeOperationAsync()
{
    using (var activity = _activitySource.StartActivity("SafeOperation"))
    {
        try
        {
            await RiskyOperationAsync();
            activity?.SetStatus(ActivityStatusCode.Ok);
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
}

5. Performance Considerations

Be mindful of performance overhead:

// Don't create too many spans for simple operations
public async Task<int> GetCountAsync()
{
    // NO - too granular
    // using (var activity = _activitySource.StartActivity("GetCount"))
    // {
    //     return await _repository.CountAsync();
    // }

    // YES - appropriate granularity
    return await _repository.CountAsync();
}

// DO create spans for significant operations
public async Task<ComplexResult> ComplexOperationAsync()
{
    using (var activity = _activitySource.StartActivity("ComplexOperation"))
    {
        // This is worth tracing
        return await PerformComplexCalculationAsync();
    }
}