Skip to main content

OpenTelemetry Backend Guide

Quick Reference for RFC 2002: OpenTelemetry Backend Observability​

This guide provides practical examples for implementing OpenTelemetry tracing in the .NET backend using the Activity API and Azure Application Insights integration.

Quick Start​

1. Import Activity Generators​

using arolariu.Backend.Common.Telemetry.Tracing;
using System.Diagnostics;

2. Create Activities​

public async Task ProcessInvoice(Guid invoiceId)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(ProcessInvoice));
// Your code here
}

Core Patterns​

Basic Activity Creation​

using arolariu.Backend.Common.Telemetry.Tracing;
using static arolariu.Backend.Common.Telemetry.Tracing.ActivityGenerators;

public class InvoiceService
{
public async Task CreateInvoice(Invoice invoice)
{
// Activity name convention: Use method name with nameof()
using var activity = InvoicePackageTracing.StartActivity(nameof(CreateInvoice));

// Activity is automatically disposed and completed when exiting the using block
await SaveInvoice(invoice).ConfigureAwait(false);
}
}

Specifying Activity Kind​

public async Task<IResult> CreateNewInvoiceAsync(InvoiceDto invoiceDto)
{
try
{
// Specify ActivityKind for HTTP server endpoints
using var activity = InvoicePackageTracing.StartActivity(
nameof(CreateNewInvoiceAsync),
ActivityKind.Server
);

var invoice = invoiceDto.ToInvoice();
await invoiceProcessingService.CreateInvoice(invoice).ConfigureAwait(false);

return TypedResults.Created($"/rest/v1/invoices/{invoice.Id}", invoice);
}
catch (Exception exception)
{
// Exceptions are automatically captured by Application Insights
return TypedResults.Problem(
detail: exception.Message,
statusCode: StatusCodes.Status500InternalServerError
);
}
}

Adding Tags (Attributes)​

public async Task ProcessPayment(Guid invoiceId, decimal amount)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(ProcessPayment));

// Add custom tags to provide context
activity?.SetTag("invoice.id", invoiceId.ToString());
activity?.SetTag("payment.amount", amount);
activity?.SetTag("payment.currency", "USD");

var result = await ChargeCard(amount).ConfigureAwait(false);

// Add result tags
activity?.SetTag("payment.status", result.Success ? "success" : "failed");
activity?.SetTag("payment.transaction_id", result.TransactionId);
}

Adding Events​

public async Task AnalyzeInvoice(Guid invoiceId)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(AnalyzeInvoice));

activity?.AddEvent(new ActivityEvent("validation.start"));
var isValid = await ValidateInvoice(invoiceId).ConfigureAwait(false);
activity?.AddEvent(new ActivityEvent("validation.complete"));

if (!isValid)
{
activity?.AddEvent(new ActivityEvent(
"validation.failed",
tags: new ActivityTagsCollection
{
{"invoice.id", invoiceId.ToString()},
{"validation.error", "Invalid invoice format"}
}
));
throw new ValidationException("Invalid invoice");
}

activity?.AddEvent(new ActivityEvent("analysis.start"));
await PerformAnalysis(invoiceId).ConfigureAwait(false);
activity?.AddEvent(new ActivityEvent("analysis.complete"));
}

Setting Activity Status​

public async Task DeleteInvoice(Guid invoiceId)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(DeleteInvoice));

try
{
activity?.SetTag("invoice.id", invoiceId.ToString());

await RemoveInvoice(invoiceId).ConfigureAwait(false);

// Explicitly set success status
activity?.SetStatus(ActivityStatusCode.Ok);
activity?.SetTag("invoice.deleted", true);
}
catch (Exception ex)
{
// Set error status
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.SetTag("error.type", ex.GetType().Name);
throw;
}
}

Activity Source Organization​

The backend uses domain-specific activity sources defined in ActivityGenerators.cs:

Common Package Tracing​

using static arolariu.Backend.Common.Telemetry.Tracing.ActivityGenerators;

public class ConfigurationService
{
public async Task LoadConfiguration()
{
// Use CommonPackageTracing for infrastructure operations
using var activity = CommonPackageTracing.StartActivity(nameof(LoadConfiguration));

activity?.SetTag("config.source", "KeyVault");
var config = await keyVaultClient.GetSecretsAsync().ConfigureAwait(false);

activity?.SetTag("config.secrets_loaded", config.Count);
activity?.SetStatus(ActivityStatusCode.Ok);
}
}

Core Package Tracing​

using static arolariu.Backend.Common.Telemetry.Tracing.ActivityGenerators;

public class StartupService
{
public void ConfigureMiddleware(WebApplicationBuilder builder)
{
// Use CorePackageTracing for application startup and middleware
using var activity = CorePackageTracing.StartActivity(nameof(ConfigureMiddleware));

activity?.SetTag("middleware.count", 10);
activity?.SetTag("environment", builder.Environment.EnvironmentName);

// Configure middleware...

activity?.SetStatus(ActivityStatusCode.Ok);
}
}

Auth Package Tracing​

using static arolariu.Backend.Common.Telemetry.Tracing.ActivityGenerators;

public class AuthenticationService
{
public async Task<ClaimsPrincipal> ValidateToken(string token)
{
// Use AuthPackageTracing for authentication operations
using var activity = AuthPackageTracing.StartActivity(nameof(ValidateToken));

activity?.SetTag("auth.method", "jwt");

try
{
var principal = await jwtHandler.ValidateAsync(token).ConfigureAwait(false);

activity?.SetTag("auth.user_id", principal.FindFirst("sub")?.Value);
activity?.SetTag("auth.success", true);
activity?.SetStatus(ActivityStatusCode.Ok);

return principal;
}
catch (SecurityTokenException ex)
{
activity?.SetTag("auth.success", false);
activity?.SetTag("auth.error", ex.Message);
activity?.SetStatus(ActivityStatusCode.Error, "Token validation failed");
throw;
}
}
}

Invoice Package Tracing​

using static arolariu.Backend.Common.Telemetry.Tracing.ActivityGenerators;

public class InvoiceProcessingService
{
public async Task AnalyzeInvoice(Guid invoiceId)
{
// Use InvoicePackageTracing for invoice domain operations
using var activity = InvoicePackageTracing.StartActivity(nameof(AnalyzeInvoice));

activity?.SetTag("invoice.id", invoiceId.ToString());

await invoiceOrchestrationService
.AnalyzeInvoiceWithOptions(options, identifier, userIdentifier)
.ConfigureAwait(false);

activity?.SetStatus(ActivityStatusCode.Ok);
}
}

HTTP Endpoint Patterns​

Minimal API Endpoint​

using static arolariu.Backend.Common.Telemetry.Tracing.ActivityGenerators;

internal static async partial Task<IResult> CreateNewInvoiceAsync(
[FromServices] IInvoiceProcessingService invoiceProcessingService,
[FromBody] InvoiceDto invoiceDto)
{
try
{
using var activity = InvoicePackageTracing.StartActivity(
nameof(CreateNewInvoiceAsync),
ActivityKind.Server
);

activity?.SetTag("http.method", "POST");
activity?.SetTag("http.route", "/rest/v1/invoices");

var invoice = invoiceDto.ToInvoice();

activity?.SetTag("invoice.merchant_id", invoice.MerchantId.ToString());
activity?.SetTag("invoice.item_count", invoice.Products.Count);

await invoiceProcessingService.CreateInvoice(invoice).ConfigureAwait(false);

activity?.SetTag("http.status_code", 201);
activity?.SetStatus(ActivityStatusCode.Ok);

return TypedResults.Created($"/rest/v1/invoices/{invoice.Id}", invoice);
}
catch (InvoiceProcessingServiceValidationException exception)
{
return TypedResults.Problem(
detail: exception.Message,
statusCode: StatusCodes.Status500InternalServerError,
title: "The service encountered a validation error."
);
}
}

GET Endpoint​

internal static async partial Task<IResult> RetrieveSpecificInvoiceAsync(
[FromServices] IInvoiceProcessingService invoiceProcessingService,
[FromRoute] Guid invoiceId)
{
using var activity = InvoicePackageTracing.StartActivity(
nameof(RetrieveSpecificInvoiceAsync),
ActivityKind.Server
);

activity?.SetTag("http.method", "GET");
activity?.SetTag("http.route", "/rest/v1/invoices/{id}");
activity?.SetTag("invoice.id", invoiceId.ToString());

var invoice = await invoiceProcessingService
.RetrieveInvoice(invoiceId)
.ConfigureAwait(false);

if (invoice is null)
{
activity?.SetTag("http.status_code", 404);
return TypedResults.NotFound();
}

activity?.SetTag("http.status_code", 200);
activity?.SetStatus(ActivityStatusCode.Ok);

return TypedResults.Ok(invoice);
}

DELETE Endpoint​

internal static async partial Task<IResult> DeleteInvoiceAsync(
[FromServices] IInvoiceProcessingService invoiceProcessingService,
[FromRoute] Guid invoiceId)
{
using var activity = InvoicePackageTracing.StartActivity(
nameof(DeleteInvoiceAsync),
ActivityKind.Server
);

activity?.SetTag("http.method", "DELETE");
activity?.SetTag("http.route", "/rest/v1/invoices/{id}");
activity?.SetTag("invoice.id", invoiceId.ToString());

activity?.AddEvent(new ActivityEvent("deletion.start"));

await invoiceProcessingService
.DeleteInvoice(invoiceId)
.ConfigureAwait(false);

activity?.AddEvent(new ActivityEvent("deletion.complete"));

activity?.SetTag("http.status_code", 204);
activity?.SetTag("invoice.deleted", true);
activity?.SetStatus(ActivityStatusCode.Ok);

return TypedResults.NoContent();
}

Service Layer Patterns​

Service with Try-Catch​

public class InvoiceProcessingService : IInvoiceProcessingService
{
public async Task CreateInvoice(Invoice invoice, Guid? userIdentifier = null) =>
await TryCatchAsync(async () =>
{
using var activity = InvoicePackageTracing.StartActivity(nameof(CreateInvoice));

activity?.SetTag("invoice.id", invoice.Id.ToString());
activity?.SetTag("user.id", userIdentifier?.ToString() ?? "anonymous");

await invoiceOrchestrationService
.CreateInvoiceObject(invoice)
.ConfigureAwait(false);

activity?.SetStatus(ActivityStatusCode.Ok);
}).ConfigureAwait(false);
}

Orchestration Service​

public class InvoiceOrchestrationService
{
public async Task ProcessInvoiceWorkflow(Guid invoiceId)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(ProcessInvoiceWorkflow));

activity?.SetTag("invoice.id", invoiceId.ToString());
activity?.SetTag("workflow.steps", 3);

// Step 1: Validate
activity?.AddEvent(new ActivityEvent("workflow.validate.start"));
await ValidateInvoice(invoiceId).ConfigureAwait(false);
activity?.AddEvent(new ActivityEvent("workflow.validate.complete"));

// Step 2: Analyze
activity?.AddEvent(new ActivityEvent("workflow.analyze.start"));
await AnalyzeInvoice(invoiceId).ConfigureAwait(false);
activity?.AddEvent(new ActivityEvent("workflow.analyze.complete"));

// Step 3: Store
activity?.AddEvent(new ActivityEvent("workflow.store.start"));
await StoreResults(invoiceId).ConfigureAwait(false);
activity?.AddEvent(new ActivityEvent("workflow.store.complete"));

activity?.SetTag("workflow.completed", true);
activity?.SetStatus(ActivityStatusCode.Ok);
}
}

Database Operations​

public class InvoiceRepository
{
public async Task<Invoice?> GetInvoiceById(Guid invoiceId)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(GetInvoiceById));

activity?.SetTag("db.system", "postgresql");
activity?.SetTag("db.operation", "SELECT");
activity?.SetTag("db.table", "invoices");
activity?.SetTag("invoice.id", invoiceId.ToString());

activity?.AddEvent(new ActivityEvent("db.query.start"));

var invoice = await dbContext.Invoices
.FirstOrDefaultAsync(i => i.Id == invoiceId)
.ConfigureAwait(false);

activity?.AddEvent(new ActivityEvent("db.query.complete"));

activity?.SetTag("db.rows_returned", invoice is null ? 0 : 1);
activity?.SetStatus(ActivityStatusCode.Ok);

return invoice;
}

public async Task SaveInvoice(Invoice invoice)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(SaveInvoice));

activity?.SetTag("db.system", "postgresql");
activity?.SetTag("db.operation", "INSERT");
activity?.SetTag("db.table", "invoices");
activity?.SetTag("invoice.id", invoice.Id.ToString());

activity?.AddEvent(new ActivityEvent("db.transaction.start"));

await dbContext.Invoices.AddAsync(invoice).ConfigureAwait(false);
var rowsAffected = await dbContext.SaveChangesAsync().ConfigureAwait(false);

activity?.AddEvent(new ActivityEvent("db.transaction.complete"));

activity?.SetTag("db.rows_affected", rowsAffected);
activity?.SetStatus(ActivityStatusCode.Ok);
}
}

Activity Kind Reference​

// Use appropriate ActivityKind for different scenarios:

// HTTP server endpoint
using var activity = InvoicePackageTracing.StartActivity(
nameof(CreateInvoiceAsync),
ActivityKind.Server
);

// HTTP client request
using var activity = CommonPackageTracing.StartActivity(
nameof(CallExternalApi),
ActivityKind.Client
);

// Message producer
using var activity = InvoicePackageTracing.StartActivity(
nameof(PublishEvent),
ActivityKind.Producer
);

// Message consumer
using var activity = InvoicePackageTracing.StartActivity(
nameof(ConsumeEvent),
ActivityKind.Consumer
);

// Internal operation (default)
using var activity = InvoicePackageTracing.StartActivity(
nameof(ProcessData),
ActivityKind.Internal
);

Best Practices​

✅ Do: Use nameof() for Activity Names​

// ✅ Good - refactor-safe
using var activity = InvoicePackageTracing.StartActivity(nameof(CreateInvoice));

✅ Do: Null-Check Activities​

// ✅ Good - activities may be null if tracing is disabled
activity?.SetTag("key", "value");
activity?.AddEvent(new ActivityEvent("event"));
activity?.SetStatus(ActivityStatusCode.Ok);

✅ Do: Use Appropriate Activity Source​

// ✅ Good - domain-specific activity source
using var activity = InvoicePackageTracing.StartActivity(nameof(ProcessInvoice));

// ✅ Good - infrastructure activity source
using var activity = CommonPackageTracing.StartActivity(nameof(LoadConfig));

// ✅ Good - authentication activity source
using var activity = AuthPackageTracing.StartActivity(nameof(ValidateToken));

✅ Do: Add Business Context​

// ✅ Good - includes meaningful business tags
activity?.SetTag("invoice.merchant_id", merchantId.ToString());
activity?.SetTag("invoice.total_amount", totalAmount);
activity?.SetTag("invoice.currency", currency);
activity?.SetTag("invoice.item_count", items.Count);

✅ Do: Use ConfigureAwait(false)​

// ✅ Good - avoids deadlocks in synchronization contexts
await invoiceService
.CreateInvoice(invoice)
.ConfigureAwait(false);

❌ Don't: Use String Literals for Activity Names​

// ❌ Bad - prone to typos and refactoring issues
using var activity = InvoicePackageTracing.StartActivity("CreateInvoice");

// ✅ Good
using var activity = InvoicePackageTracing.StartActivity(nameof(CreateInvoice));

❌ Don't: Log Sensitive Data​

// ❌ Bad - exposes sensitive information
activity?.SetTag("user.password", password);
activity?.SetTag("credit_card.number", cardNumber);

// ✅ Good - only non-sensitive identifiers
activity?.SetTag("user.id", userId.ToString());
activity?.SetTag("payment.method", "card");

❌ Don't: Create Activities for Trivial Operations​

// ❌ Bad - too granular
public int Add(int a, int b)
{
using var activity = CommonPackageTracing.StartActivity(nameof(Add));
return a + b;
}

// ✅ Good - trace meaningful operations
public async Task CalculateInvoiceTotal(Invoice invoice)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(CalculateInvoiceTotal));
// Complex business logic
}

Azure Application Insights Integration​

Automatic Instrumentation​

The backend automatically exports traces to Azure Application Insights:

// Configured in Program.cs or extension methods
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.AddSource("arolariu.Backend.*")
.AddAzureMonitorTraceExporter(options =>
{
options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
});
});

Viewing Traces​

  1. Azure Portal: Navigate to Application Insights → Transaction search
  2. End-to-End Transaction: View complete request flow across services
  3. Performance: Analyze slow operations and bottlenecks
  4. Failures: Investigate exceptions and error patterns

Custom Metrics​

using System.Diagnostics.Metrics;

public class InvoiceMetrics
{
private static readonly Meter Meter = new("arolariu.Backend.Invoices");
private static readonly Counter<long> InvoicesCreated = Meter.CreateCounter<long>("invoices.created");

public async Task CreateInvoice(Invoice invoice)
{
using var activity = InvoicePackageTracing.StartActivity(nameof(CreateInvoice));

await SaveInvoice(invoice).ConfigureAwait(false);

// Increment metric
InvoicesCreated.Add(1, new KeyValuePair<string, object?>("merchant.id", invoice.MerchantId));
}
}

Troubleshooting​

Issue: Activities Not Appearing in Application Insights​

Check:

  1. Application Insights connection string is configured
  2. Activity source names are registered with OpenTelemetry
  3. Activity source name matches the pattern "arolariu.Backend.*"

Solution:

// Verify activity source registration
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddSource("arolariu.Backend.Common");
tracing.AddSource("arolariu.Backend.Core");
tracing.AddSource("arolariu.Backend.Auth");
tracing.AddSource("arolariu.Backend.Domain.Invoices");
});

Issue: Tags Not Appearing​

Root Cause: Tags set after activity is disposed

Solution:

// ✅ Set tags before leaving the using block
using var activity = InvoicePackageTracing.StartActivity(nameof(CreateInvoice));
activity?.SetTag("key", "value"); // ← Before disposal
// Operation completes, activity is disposed here

Issue: Nested Activities Not Showing Hierarchy​

Root Cause: Parent activity context not propagated

Solution: Activities automatically inherit parent context when using the same thread. For async operations, ensure proper async/await usage:

public async Task ParentOperation()
{
using var parentActivity = InvoicePackageTracing.StartActivity(nameof(ParentOperation));

// Child activities automatically become children of parent
await ChildOperation().ConfigureAwait(false);
}

private async Task ChildOperation()
{
using var childActivity = InvoicePackageTracing.StartActivity(nameof(ChildOperation));
// This activity will be a child of ParentOperation
}

Configuration​

Environment Variables​

# Application Insights connection string
APPLICATIONINSIGHTS__CONNECTIONSTRING=InstrumentationKey=your-key;...

# OpenTelemetry configuration
OTEL_SERVICE_NAME=arolariu.Backend
OTEL_LOG_LEVEL=Information

appsettings.json​

{
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=your-key;..."
},
"OpenTelemetry": {
"ServiceName": "arolariu.Backend",
"TracingEnabled": true
}
}

Quick Reference​

TaskCode PatternExample
Create activityusing var activity = Source.StartActivity(nameof(Method))InvoicePackageTracing.StartActivity(nameof(CreateInvoice))
HTTP endpointStartActivity(nameof(Method), ActivityKind.Server)StartActivity(nameof(GetInvoice), ActivityKind.Server)
Add tagactivity?.SetTag(key, value)activity?.SetTag("invoice.id", id.ToString())
Add eventactivity?.AddEvent(new ActivityEvent(name))activity?.AddEvent(new ActivityEvent("validation.start"))
Set statusactivity?.SetStatus(ActivityStatusCode)activity?.SetStatus(ActivityStatusCode.Ok)
Error statusactivity?.SetStatus(ActivityStatusCode.Error, desc)activity?.SetStatus(ActivityStatusCode.Error, ex.Message)

Additional Resources​

// was this page useful?