Convey.Tracing.Jaeger

Distributed tracing integration with Jaeger providing comprehensive request tracing, performance monitoring, and debugging capabilities for microservices architectures.

Installation

dotnet add package Convey.Tracing.Jaeger

Overview

Convey.Tracing.Jaeger provides:

  • Distributed tracing - End-to-end request tracing across services
  • Jaeger integration - Native integration with Jaeger tracing system
  • OpenTelemetry support - Built on OpenTelemetry standards
  • Automatic instrumentation - HTTP, database, and framework instrumentation
  • Custom spans - Create custom spans for business operations
  • Performance insights - Detailed timing and dependency analysis
  • Error tracking - Trace error propagation across services

Configuration

Basic Setup

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddConvey()
    .AddJaeger(); // Enables Jaeger tracing

var app = builder.Build();

app.Run();

Advanced Configuration

Configure in appsettings.json:

{
  "jaeger": {
    "enabled": true,
    "serviceName": "my-service",
    "udpHost": "localhost",
    "udpPort": 6831,
    "maxPacketSize": 0,
    "sampler": "const",
    "maxTracesPerSecond": 5,
    "samplingRate": 1.0,
    "excludePaths": ["/health", "/metrics"],
    "tags": {
      "version": "1.0.0",
      "environment": "production"
    }
  }
}

Full Configuration

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddConvey()
    .AddJaeger(jaegerOptions =>
    {
        jaegerOptions.ServiceName = "order-service";
        jaegerOptions.UdpHost = "jaeger-agent";
        jaegerOptions.UdpPort = 6831;
        jaegerOptions.SamplingRate = 1.0; // Sample all traces in development
        jaegerOptions.ExcludePaths = new[] { "/health", "/metrics", "/swagger" };
    });

var app = builder.Build();

// Enable tracing middleware
app.UseTracing(); // Automatically traces HTTP requests

app.Run();

Key Features

1. Automatic HTTP Tracing

HTTP requests are automatically traced:

// HTTP requests are automatically instrumented when UseTracing() is configured
app.UseTracing(); // Tracks all HTTP requests with spans

// Automatic spans created for:
// - Incoming HTTP requests
// - Outgoing HTTP requests
// - Database operations
// - Message broker operations

2. Custom Spans

Create custom spans for business operations:

public class OrderService
{
    private readonly ITracer _tracer;
    private readonly IOrderRepository _repository;

    public OrderService(ITracer tracer, IOrderRepository repository)
    {
        _tracer = tracer;
        _repository = repository;
    }

    public async Task<OrderResult> ProcessOrderAsync(ProcessOrderCommand command)
    {
        using var span = _tracer.StartActiveSpan("order.process")
            .SetTag("order.id", command.OrderId.ToString())
            .SetTag("order.type", command.Type)
            .SetTag("customer.id", command.CustomerId.ToString());

        try
        {
            // Validate order
            using var validationSpan = _tracer.StartActiveSpan("order.validate");
            await ValidateOrderAsync(command);
            validationSpan.SetTag("validation.result", "success");

            // Process payment
            using var paymentSpan = _tracer.StartActiveSpan("order.payment")
                .SetTag("payment.amount", command.Amount)
                .SetTag("payment.method", command.PaymentMethod);

            var paymentResult = await ProcessPaymentAsync(command);
            paymentSpan.SetTag("payment.transaction_id", paymentResult.TransactionId);

            // Update inventory
            using var inventorySpan = _tracer.StartActiveSpan("order.inventory");
            await UpdateInventoryAsync(command);

            // Save order
            using var saveSpan = _tracer.StartActiveSpan("order.save");
            var order = await _repository.SaveAsync(command);

            span.SetTag("order.result", "success")
                .SetTag("order.total_amount", command.Amount);

            return new OrderResult { OrderId = order.Id, Success = true };
        }
        catch (ValidationException ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", "validation")
                .SetTag("error.message", ex.Message);
            throw;
        }
        catch (PaymentException ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", "payment")
                .SetTag("error.message", ex.Message);
            throw;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", "unknown")
                .SetTag("error.message", ex.Message);
            throw;
        }
    }
}

3. Tracing Decorators

Automatically trace handlers with decorators:

// Command handler tracing decorator
public class TracingCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand>
    where TCommand : class, ICommand
{
    private readonly ICommandHandler<TCommand> _handler;
    private readonly ITracer _tracer;

    public TracingCommandHandlerDecorator(ICommandHandler<TCommand> handler, ITracer tracer)
    {
        _handler = handler;
        _tracer = tracer;
    }

    public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default)
    {
        var commandName = typeof(TCommand).Name;

        using var span = _tracer.StartActiveSpan($"command.{commandName}")
            .SetTag("command.type", commandName)
            .SetTag("component", "command-handler");

        // Add command-specific tags
        if (command is IIdentifiable identifiable)
        {
            span.SetTag("command.id", identifiable.Id.ToString());
        }

        try
        {
            await _handler.HandleAsync(command, cancellationToken);
            span.SetTag("command.result", "success");
        }
        catch (ValidationException ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", "validation")
                .SetTag("error.message", ex.Message)
                .SetTag("command.result", "validation_error");
            throw;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", ex.GetType().Name)
                .SetTag("error.message", ex.Message)
                .SetTag("command.result", "error");
            throw;
        }
    }
}

// Query handler tracing decorator
public class TracingQueryHandlerDecorator<TQuery, TResult> : IQueryHandler<TQuery, TResult>
    where TQuery : class, IQuery<TResult>
{
    private readonly IQueryHandler<TQuery, TResult> _handler;
    private readonly ITracer _tracer;

    public TracingQueryHandlerDecorator(IQueryHandler<TQuery, TResult> handler, ITracer tracer)
    {
        _handler = handler;
        _tracer = tracer;
    }

    public async Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default)
    {
        var queryName = typeof(TQuery).Name;

        using var span = _tracer.StartActiveSpan($"query.{queryName}")
            .SetTag("query.type", queryName)
            .SetTag("component", "query-handler");

        // Add query-specific tags
        if (query is IPagedQuery pagedQuery)
        {
            span.SetTag("query.page", pagedQuery.Page)
                .SetTag("query.page_size", pagedQuery.PageSize);
        }

        try
        {
            var result = await _handler.HandleAsync(query, cancellationToken);

            span.SetTag("query.result", "success");

            // Add result information
            if (result is IPagedResult pagedResult)
            {
                span.SetTag("result.total_items", pagedResult.TotalItems)
                    .SetTag("result.total_pages", pagedResult.TotalPages);
            }

            return result;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", ex.GetType().Name)
                .SetTag("error.message", ex.Message)
                .SetTag("query.result", "error");
            throw;
        }
    }
}

// Registration
builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(TracingCommandHandlerDecorator<>));
builder.Services.TryDecorate(typeof(IQueryHandler<,>), typeof(TracingQueryHandlerDecorator<,>));

4. Database Operation Tracing

Trace database operations:

public class TracingUserRepository : IUserRepository
{
    private readonly IUserRepository _repository;
    private readonly ITracer _tracer;

    public TracingUserRepository(IUserRepository repository, ITracer tracer)
    {
        _repository = repository;
        _tracer = tracer;
    }

    public async Task<User> GetByIdAsync(Guid id)
    {
        using var span = _tracer.StartActiveSpan("db.query.users.get_by_id")
            .SetTag("db.type", "mongodb")
            .SetTag("db.collection", "users")
            .SetTag("db.operation", "find_one")
            .SetTag("user.id", id.ToString());

        try
        {
            var user = await _repository.GetByIdAsync(id);

            span.SetTag("db.result", user != null ? "found" : "not_found");

            return user;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", ex.GetType().Name)
                .SetTag("error.message", ex.Message);
            throw;
        }
    }

    public async Task<PagedResult<User>> BrowseAsync(BrowseUsersQuery query)
    {
        using var span = _tracer.StartActiveSpan("db.query.users.browse")
            .SetTag("db.type", "mongodb")
            .SetTag("db.collection", "users")
            .SetTag("db.operation", "find")
            .SetTag("query.page", query.Page)
            .SetTag("query.page_size", query.PageSize);

        try
        {
            var result = await _repository.BrowseAsync(query);

            span.SetTag("db.result_count", result.Items.Count())
                .SetTag("db.total_count", result.TotalItems);

            return result;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", ex.GetType().Name)
                .SetTag("error.message", ex.Message);
            throw;
        }
    }

    public async Task AddAsync(User user)
    {
        using var span = _tracer.StartActiveSpan("db.command.users.insert")
            .SetTag("db.type", "mongodb")
            .SetTag("db.collection", "users")
            .SetTag("db.operation", "insert_one")
            .SetTag("user.id", user.Id.ToString());

        try
        {
            await _repository.AddAsync(user);
            span.SetTag("db.result", "success");
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", ex.GetType().Name)
                .SetTag("error.message", ex.Message);
            throw;
        }
    }
}

5. External Service Tracing

Trace calls to external services:

public class TracingPaymentService : IPaymentService
{
    private readonly IPaymentService _paymentService;
    private readonly ITracer _tracer;
    private readonly HttpClient _httpClient;

    public TracingPaymentService(IPaymentService paymentService, ITracer tracer, HttpClient httpClient)
    {
        _paymentService = paymentService;
        _tracer = tracer;
        _httpClient = httpClient;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        using var span = _tracer.StartActiveSpan("external.payment.process")
            .SetTag("service.name", "payment-service")
            .SetTag("service.type", "external")
            .SetTag("payment.amount", request.Amount)
            .SetTag("payment.currency", request.Currency)
            .SetTag("payment.method", request.PaymentMethod);

        try
        {
            // Inject trace context into HTTP headers
            var headers = new Dictionary<string, string>();
            _tracer.Inject(span.Context, Format.HttpHeaders, new TextMapInjectAdapter(headers));

            foreach (var header in headers)
            {
                _httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
            }

            var result = await _paymentService.ProcessPaymentAsync(request);

            span.SetTag("payment.transaction_id", result.TransactionId)
                .SetTag("payment.status", result.Status)
                .SetTag("external.result", "success");

            return result;
        }
        catch (PaymentException ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", "payment_error")
                .SetTag("error.code", ex.ErrorCode)
                .SetTag("error.message", ex.Message);
            throw;
        }
        catch (HttpRequestException ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", "http_error")
                .SetTag("error.message", ex.Message);
            throw;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", ex.GetType().Name)
                .SetTag("error.message", ex.Message);
            throw;
        }
    }
}

// HTTP client tracing
public class TracingHttpMessageHandler : DelegatingHandler
{
    private readonly ITracer _tracer;

    public TracingHttpMessageHandler(ITracer tracer)
    {
        _tracer = tracer;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        using var span = _tracer.StartActiveSpan("http.client.request")
            .SetTag("http.method", request.Method.ToString())
            .SetTag("http.url", request.RequestUri?.ToString())
            .SetTag("component", "http-client");

        // Inject trace context
        var headers = new Dictionary<string, string>();
        _tracer.Inject(span.Context, Format.HttpHeaders, new TextMapInjectAdapter(headers));

        foreach (var header in headers)
        {
            request.Headers.Add(header.Key, header.Value);
        }

        try
        {
            var response = await base.SendAsync(request, cancellationToken);

            span.SetTag("http.status_code", (int)response.StatusCode)
                .SetTag("http.response_size", response.Content.Headers.ContentLength ?? 0);

            if (!response.IsSuccessStatusCode)
            {
                span.SetTag("error", true);
            }

            return response;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", ex.GetType().Name)
                .SetTag("error.message", ex.Message);
            throw;
        }
    }
}

// Registration
builder.Services.AddHttpClient<IPaymentService, PaymentService>()
    .AddHttpMessageHandler<TracingHttpMessageHandler>();

Advanced Features

1. Correlation ID Propagation

Propagate correlation IDs across services:

public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ITracer _tracer;

    public CorrelationIdMiddleware(RequestDelegate next, ITracer tracer)
    {
        _next = next;
        _tracer = tracer;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
                           ?? Guid.NewGuid().ToString();

        context.Items["CorrelationId"] = correlationId;
        context.Response.Headers["X-Correlation-ID"] = correlationId;

        // Add correlation ID to current span
        var activeSpan = _tracer.ActiveSpan;
        if (activeSpan != null)
        {
            activeSpan.SetTag("correlation.id", correlationId);
        }

        await _next(context);
    }
}

// Service that uses correlation ID
public class NotificationService
{
    private readonly ITracer _tracer;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public async Task SendNotificationAsync(SendNotificationCommand command)
    {
        var correlationId = _httpContextAccessor.HttpContext?.Items["CorrelationId"]?.ToString();

        using var span = _tracer.StartActiveSpan("notification.send")
            .SetTag("correlation.id", correlationId)
            .SetTag("notification.type", command.Type)
            .SetTag("recipient.id", command.RecipientId);

        // Implementation...
    }
}

2. Baggage Propagation

Use baggage to propagate additional context:

public class BaggageMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ITracer _tracer;

    public BaggageMiddleware(RequestDelegate next, ITracer tracer)
    {
        _next = next;
        _tracer = tracer;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var activeSpan = _tracer.ActiveSpan;
        if (activeSpan != null)
        {
            // Set baggage from headers or context
            var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            if (!string.IsNullOrEmpty(userId))
            {
                activeSpan.SetBaggageItem("user.id", userId);
            }

            var tenantId = context.Request.Headers["X-Tenant-ID"].FirstOrDefault();
            if (!string.IsNullOrEmpty(tenantId))
            {
                activeSpan.SetBaggageItem("tenant.id", tenantId);
            }
        }

        await _next(context);
    }
}

// Service that uses baggage
public class AuditService
{
    private readonly ITracer _tracer;

    public async Task LogActionAsync(string action, object data)
    {
        var activeSpan = _tracer.ActiveSpan;
        var userId = activeSpan?.GetBaggageItem("user.id");
        var tenantId = activeSpan?.GetBaggageItem("tenant.id");

        using var span = _tracer.StartActiveSpan("audit.log")
            .SetTag("audit.action", action)
            .SetTag("audit.user_id", userId)
            .SetTag("audit.tenant_id", tenantId);

        // Log audit event with context
        await LogAuditEvent(action, data, userId, tenantId);
    }
}

3. Sampling Strategies

Implement custom sampling strategies:

public class CustomSampler : ISampler
{
    private readonly double _defaultSamplingRate;
    private readonly Dictionary<string, double> _operationSamplingRates;

    public CustomSampler(double defaultSamplingRate, Dictionary<string, double> operationSamplingRates = null)
    {
        _defaultSamplingRate = defaultSamplingRate;
        _operationSamplingRates = operationSamplingRates ?? new Dictionary<string, double>();
    }

    public SamplingResult Sample(string operationName, TraceId traceId)
    {
        var samplingRate = _operationSamplingRates.GetValueOrDefault(operationName, _defaultSamplingRate);

        // Special sampling rules
        if (operationName.Contains("health") || operationName.Contains("metrics"))
        {
            return SamplingResult.Create(false); // Don't sample health/metrics endpoints
        }

        if (operationName.Contains("error") || operationName.Contains("exception"))
        {
            return SamplingResult.Create(true); // Always sample errors
        }

        // Use trace ID for deterministic sampling
        var traceIdBytes = traceId.ToByteArray();
        var hash = BitConverter.ToUInt64(traceIdBytes, 0);
        var shouldSample = (hash % 10000) < (samplingRate * 10000);

        return SamplingResult.Create(shouldSample);
    }
}

// Registration
builder.Services.AddJaeger(options =>
{
    options.Sampler = new CustomSampler(0.1, new Dictionary<string, double>
    {
        ["order.process"] = 1.0,    // Sample all order processing
        ["payment.process"] = 1.0,  // Sample all payment processing
        ["user.login"] = 0.5,       // Sample 50% of logins
        ["search.query"] = 0.01     // Sample 1% of searches
    });
});

Integration Patterns

1. Message Broker Integration

Trace message processing:

public class TracingEventHandler<TEvent> : IEventHandler<TEvent>
    where TEvent : class, IEvent
{
    private readonly IEventHandler<TEvent> _handler;
    private readonly ITracer _tracer;

    public TracingEventHandler(IEventHandler<TEvent> handler, ITracer tracer)
    {
        _handler = handler;
        _tracer = tracer;
    }

    public async Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default)
    {
        var eventName = typeof(TEvent).Name;

        // Extract trace context from event metadata
        ISpanContext parentContext = null;
        if (@event is ITraceable traceable && traceable.TraceContext != null)
        {
            parentContext = ExtractTraceContext(traceable.TraceContext);
        }

        var spanBuilder = _tracer.BuildSpan($"event.{eventName}")
            .WithTag("event.type", eventName)
            .WithTag("component", "event-handler");

        if (parentContext != null)
        {
            spanBuilder = spanBuilder.AsChildOf(parentContext);
        }

        using var span = spanBuilder.Start();

        try
        {
            await _handler.HandleAsync(@event, cancellationToken);
            span.SetTag("event.result", "success");
        }
        catch (Exception ex)
        {
            span.SetTag("error", true)
                .SetTag("error.type", ex.GetType().Name)
                .SetTag("error.message", ex.Message);
            throw;
        }
    }

    private ISpanContext ExtractTraceContext(Dictionary<string, string> traceContext)
    {
        try
        {
            return _tracer.Extract(Format.TextMap, new TextMapExtractAdapter(traceContext));
        }
        catch
        {
            return null;
        }
    }
}

// Event with trace context
public interface ITraceable
{
    Dictionary<string, string> TraceContext { get; set; }
}

public class OrderCreatedEvent : IEvent, ITraceable
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public decimal Amount { get; set; }
    public Dictionary<string, string> TraceContext { get; set; }
}

// Publish event with trace context
public class EventPublisher
{
    private readonly ITracer _tracer;
    private readonly IEventDispatcher _dispatcher;

    public async Task PublishAsync<TEvent>(TEvent @event) where TEvent : class, IEvent
    {
        if (@event is ITraceable traceable)
        {
            var traceContext = new Dictionary<string, string>();
            _tracer.Inject(_tracer.ActiveSpan.Context, Format.TextMap, new TextMapInjectAdapter(traceContext));
            traceable.TraceContext = traceContext;
        }

        await _dispatcher.PublishAsync(@event);
    }
}

Configuration Options

Jaeger Settings

public class JaegerOptions
{
    public bool Enabled { get; set; } = true;
    public string ServiceName { get; set; }
    public string UdpHost { get; set; } = "localhost";
    public int UdpPort { get; set; } = 6831;
    public int MaxPacketSize { get; set; } = 0;
    public string Sampler { get; set; } = "const";
    public double SamplingRate { get; set; } = 1.0;
    public int MaxTracesPerSecond { get; set; } = 5;
    public string[] ExcludePaths { get; set; } = Array.Empty<string>();
    public Dictionary<string, object> Tags { get; set; } = new();
}

API Reference

ITracer Interface

public interface ITracer
{
    ISpan ActiveSpan { get; }
    ISpanBuilder BuildSpan(string operationName);
    ISpan StartActiveSpan(string operationName);
    void Inject<TCarrier>(ISpanContext spanContext, IFormat<TCarrier> format, TCarrier carrier);
    ISpanContext Extract<TCarrier>(IFormat<TCarrier> format, TCarrier carrier);
}

ISpan Interface

public interface ISpan : IDisposable
{
    ISpanContext Context { get; }
    ISpan SetTag(string key, string value);
    ISpan SetTag(string key, bool value);
    ISpan SetTag(string key, int value);
    ISpan SetTag(string key, double value);
    ISpan SetBaggageItem(string key, string value);
    string GetBaggageItem(string key);
    ISpan Log(string message);
    ISpan Log(Dictionary<string, object> fields);
    void Finish();
}

Extension Methods

public static class ConveyExtensions
{
    public static IConveyBuilder AddJaeger(this IConveyBuilder builder, string sectionName = "jaeger");
    public static IConveyBuilder AddJaeger(this IConveyBuilder builder, Action<JaegerOptions> configure);
    public static IApplicationBuilder UseTracing(this IApplicationBuilder app);
}

Best Practices

  1. Use meaningful span names - Use clear, hierarchical naming conventions
  2. Add relevant tags - Include business context and technical details
  3. Handle errors properly - Mark spans with error tags and log exceptions
  4. Use appropriate sampling - Balance observability with performance
  5. Propagate context - Ensure trace context flows across service boundaries
  6. Monitor trace performance - Keep tracing overhead minimal
  7. Use child spans - Break down complex operations into smaller spans
  8. Include business metrics - Add business-relevant tags and logs

Troubleshooting

Common Issues

  1. Traces not appearing in Jaeger
    • Check Jaeger agent connectivity
    • Verify service name configuration
    • Ensure sampling is not too restrictive
  2. Missing trace context
    • Verify context propagation in HTTP headers
    • Check async/await patterns for context flow
    • Ensure proper decorator registration
  3. Performance impact
    • Adjust sampling rates for high-volume endpoints
    • Use asynchronous reporting
    • Monitor memory usage of trace buffers
  4. Incomplete traces
    • Check for exceptions that terminate spans early
    • Verify proper span disposal with using statements
    • Ensure child spans are properly linked to parents

Copyright © 2025 Convey. Distributed by an [MIT license](https://github.com/mehyaa/Convey/blob/master/LICENSE).