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();
}
}