Observability
Maturity: Preview
This module is under active development. Some features may change before the stable release.
Add OpenTelemetry-based distributed tracing and metrics in under 5 minutes.
1. Install Package
dotnet add package PrimusSaaS.Logging
For full OpenTelemetry support, also add:
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
2. Setup in Program.cs
using PrimusSaaS.Logging.Extensions;
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
var builder = WebApplication.CreateBuilder(args);
// Add Primus Logging with OpenTelemetry
builder.Logging.ClearProviders();
builder.Logging.AddPrimus(opts =>
{
builder.Configuration.GetSection("PrimusLogging").Bind(opts);
opts.EnableOpenTelemetry = true;
});
// Add OpenTelemetry tracing
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddConsoleExporter();
})
.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddConsoleExporter();
});
var app = builder.Build();
// Add correlation middleware (propagates trace context)
app.UsePrimusLogging();
app.MapControllers();
app.Run();
3. Configure appsettings.json
{
"PrimusLogging": {
"ApplicationId": "my-api",
"Environment": "Production",
"MinLevel": 1,
"EnableOpenTelemetry": true,
"Targets": [
{ "Type": "console", "Pretty": true },
{
"Type": "applicationInsights",
"ConnectionString": "InstrumentationKey=your-key"
}
]
},
"OpenTelemetry": {
"ServiceName": "my-api",
"ServiceVersion": "1.0.0",
"Exporter": {
"Type": "otlp",
"Endpoint": "http://localhost:4317"
}
}
}
Exporter options
| Exporter | Use Case | Configuration |
|---|---|---|
console | Development debugging | Default, no config needed |
otlp | Jaeger, Tempo, Grafana | "Endpoint": "http://localhost:4317" |
zipkin | Zipkin collector | "Endpoint": "http://localhost:9411/api/v2/spans" |
applicationInsights | Azure Monitor | Use ConnectionString from Azure |
4. Add Custom Traces
Manual Span Creation
using System.Diagnostics;
public class PaymentService
{
private static readonly ActivitySource ActivitySource =
new("MyApi.PaymentService");
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
using var activity = ActivitySource.StartActivity("ProcessPayment");
// Add attributes to the span
activity?.SetTag("payment.amount", request.Amount);
activity?.SetTag("payment.currency", request.Currency);
activity?.SetTag("customer.id", request.CustomerId);
try
{
// Process payment
var result = await _gateway.ChargeAsync(request);
activity?.SetTag("payment.status", "success");
activity?.SetTag("payment.transaction_id", result.TransactionId);
return result;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
}
Correlation IDs Across Services
// Automatically propagated via X-Correlation-ID header
public class OrderService
{
private readonly HttpClient _httpClient;
private readonly ILogger<OrderService> _logger;
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
// Correlation ID is automatically added to outgoing requests
_logger.LogInformation("Creating order for customer {CustomerId}", request.CustomerId);
// Call payment service - trace context propagates automatically
var paymentResult = await _httpClient.PostAsJsonAsync(
"http://payment-service/api/payments",
new { Amount = request.Total, CustomerId = request.CustomerId }
);
return new Order { Id = Guid.NewGuid().ToString() };
}
}
5. Test It
# Make requests and observe traces
curl -X GET http://localhost:5001/api/orders \
-H "Authorization: Bearer $TOKEN"
# Check console output for trace information
# Or view in Jaeger/Zipkin/Application Insights
# View distributed trace:
# Service A (api-gateway) Service B (order-service) Service C (payment-service)
Metrics
Custom Metrics
using System.Diagnostics.Metrics;
public class OrderMetrics
{
private readonly Counter<long> _ordersCreated;
private readonly Histogram<double> _orderProcessingTime;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApi.Orders");
_ordersCreated = meter.CreateCounter<long>(
"orders_created_total",
description: "Total number of orders created");
_orderProcessingTime = meter.CreateHistogram<double>(
"order_processing_seconds",
description: "Order processing duration");
}
public void RecordOrderCreated(string tenantId)
{
_ordersCreated.Add(1, new KeyValuePair<string, object?>("tenant", tenantId));
}
public void RecordProcessingTime(double seconds)
{
_orderProcessingTime.Record(seconds);
}
}
Azure Application Insights Integration
// Full Application Insights setup
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
});
builder.Logging.AddPrimus(opts =>
{
opts.EnableOpenTelemetry = true;
opts.Targets = new List<TargetConfig>
{
new() { Type = "applicationInsights", ConnectionString = "..." }
};
});
Next Steps
| Want to... | See Guide |
|---|---|
| Add audit trails | Audit Logging |
| Set up authentication | Identity Validator |