Skip to main content

Observability

Maturity: Preview

This module is under active development. Some features may change before the stable release.

Try in Playground

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
ExporterUse CaseConfiguration
consoleDevelopment debuggingDefault, no config needed
otlpJaeger, Tempo, Grafana"Endpoint": "http://localhost:4317"
zipkinZipkin collector"Endpoint": "http://localhost:9411/api/v2/spans"
applicationInsightsAzure MonitorUse 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 trailsAudit Logging
Set up authenticationIdentity Validator