Skip to content

Migration From Activities

Kieron Lanning edited this page Feb 8, 2026 · 1 revision

Migrating from Manual ActivitySource to Source Generator

This guide shows you how to migrate from traditional manual ActivitySource and Activity management to the Purview Telemetry Source Generator's distributed tracing generation.

Why Migrate?

Benefits of Using the Source Generator:

  • Compile-time safety - activity names and tags verified at build time
  • Zero boilerplate - no manual ActivitySource initialization or string constants
  • Consistent naming - enforced patterns across your distributed traces
  • Testability - easy to mock and verify activity creation
  • Baggage & Tags - type-safe parameter handling with automatic conversion
  • Exception tracking - built-in OpenTelemetry exception format support
  • Multi-targeting - combine with logging and metrics in a single call

Migration Examples

Example 1: Basic Activity Creation

Before (Manual ActivitySource):

using System.Diagnostics;

public class OrderService
{
    private static readonly ActivitySource _activitySource = new("OrderService", "1.0.0");

    public async Task<Order> ProcessOrderAsync(int orderId, string customerName)
    {
        using var activity = _activitySource.StartActivity("ProcessOrder");
        
        activity?.SetTag("order.id", orderId);
        activity?.SetTag("customer.name", customerName);

        try
        {
            // Process order...
            var order = await _repository.GetOrderAsync(orderId);
            
            activity?.SetTag("order.total", order.Total);
            activity?.SetStatus(ActivityStatusCode.Ok);
            
            return order;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
}

After (Source Generator):

using System.Diagnostics;
using Purview.Telemetry;

// Define telemetry interface
[ActivitySource("OrderService")]
public interface IOrderServiceTelemetry
{
    [Activity]
    Activity? ProcessingOrder([Tag] int orderId, [Baggage] string customerName);

    [Context]
    void OrderRetrieved(Activity? activity, [Tag] decimal orderTotal);

    [Event(ActivityStatusCode.Ok)]
    void OrderProcessed(Activity? activity);

    [Event(ActivityStatusCode.Error)]
    void OrderProcessingFailed(Activity? activity, Exception ex);
}

// Register in DI
services.AddOrderServiceTelemetry();

// Use in your service
public class OrderService
{
    private readonly IOrderServiceTelemetry _telemetry;

    public OrderService(IOrderServiceTelemetry telemetry)
    {
        _telemetry = telemetry;
    }

    public async Task<Order> ProcessOrderAsync(int orderId, string customerName)
    {
        using var activity = _telemetry.ProcessingOrder(orderId, customerName);

        try
        {
            // Process order...
            var order = await _repository.GetOrderAsync(orderId);
            
            _telemetry.OrderRetrieved(activity, order.Total);
            _telemetry.OrderProcessed(activity);
            
            return order;
        }
        catch (Exception ex)
        {
            _telemetry.OrderProcessingFailed(activity, ex);
            throw;
        }
    }
}

Key Improvements:

  • ✅ No manual ActivitySource initialization
  • ✅ Tags and baggage handled by attributes
  • ✅ Status codes set via Event attributes
  • ✅ Exception tracking follows OpenTelemetry spec automatically
  • ✅ Type-safe method calls instead of string-based tags

Example 2: Activity Events

Before (Manual ActivityEvent):

public async Task ShipPackageAsync(string trackingNumber)
{
    using var activity = _activitySource.StartActivity("ShipPackage");
    activity?.SetTag("tracking.number", trackingNumber);

    // Label package
    await LabelPackageAsync(trackingNumber);
    activity?.AddEvent(new ActivityEvent("PackageLabeled"));

    // Weigh package
    var weight = await WeighPackageAsync(trackingNumber);
    var tags = new ActivityTagsCollection
    {
        { "package.weight", weight }
    };
    activity?.AddEvent(new ActivityEvent("PackageWeighed", tags: tags));

    // Ship
    var carrier = await SchedulePickupAsync(trackingNumber);
    tags = new ActivityTagsCollection
    {
        { "shipping.carrier", carrier }
    };
    activity?.AddEvent(new ActivityEvent("PackageShipped", tags: tags));
}

After (Source Generator):

[ActivitySource("ShippingService")]
public interface IShippingTelemetry
{
    [Activity]
    Activity? ShippingPackage([Baggage] string trackingNumber);

    [Event]
    void PackageLabeled(Activity? activity);

    [Event]
    void PackageWeighed(Activity? activity, [Tag] decimal weight);

    [Event]
    void PackageShipped(Activity? activity, [Tag] string carrier);
}

public async Task ShipPackageAsync(string trackingNumber)
{
    using var activity = _telemetry.ShippingPackage(trackingNumber);

    // Label package
    await LabelPackageAsync(trackingNumber);
    _telemetry.PackageLabeled(activity);

    // Weigh package
    var weight = await WeighPackageAsync(trackingNumber);
    _telemetry.PackageWeighed(activity, weight);

    // Ship
    var carrier = await SchedulePickupAsync(trackingNumber);
    _telemetry.PackageShipped(activity, carrier);
}

Key Improvements:

  • ✅ No manual ActivityEvent or ActivityTagsCollection creation
  • ✅ Type-safe event parameters
  • ✅ Cleaner, more readable code

Example 3: Activity Links and Parent Context

Before (Manual Activity Links):

public async Task ProcessMessageAsync(Message message)
{
    // Extract parent context from message headers
    ActivityContext parentContext = ExtractContext(message.Headers);

    var links = new List<ActivityLink>
    {
        new ActivityLink(parentContext)
    };

    using var activity = _activitySource.StartActivity(
        "ProcessMessage",
        ActivityKind.Consumer,
        parentContext: parentContext,
        links: links
    );

    activity?.SetTag("message.id", message.Id);
    activity?.SetTag("message.queue", message.Queue);

    // Process message...
}

After (Source Generator):

[ActivitySource("MessageProcessor")]
public interface IMessageProcessorTelemetry
{
    [Activity(ActivityKind.Consumer)]
    Activity? ProcessingMessage(
        ActivityContext parentContext,
        IEnumerable<ActivityLink> links,
        [Tag] string messageId,
        [Tag] string messageQueue);
}

public async Task ProcessMessageAsync(Message message)
{
    // Extract parent context from message headers
    ActivityContext parentContext = ExtractContext(message.Headers);

    var links = new List<ActivityLink>
    {
        new ActivityLink(parentContext)
    };

    using var activity = _telemetry.ProcessingMessage(
        parentContext,
        links,
        message.Id,
        message.Queue
    );

    // Process message...
}

Tip

The source generator recognizes ActivityContext, ActivityLink[], and IEnumerable<ActivityLink> parameters automatically and passes them to the underlying ActivitySource.StartActivity call.

Example 4: Baggage Propagation

Before (Manual Baggage):

public async Task ProcessRequestAsync(string userId, string sessionId)
{
    using var activity = _activitySource.StartActivity("ProcessRequest");
    
    // Add baggage for cross-service propagation
    activity?.SetBaggage("user.id", userId);
    activity?.SetBaggage("session.id", sessionId);
    
    // Add local tags
    activity?.SetTag("request.timestamp", DateTimeOffset.UtcNow.ToString());

    // Call downstream service
    await _httpClient.GetAsync($"/api/users/{userId}");
}

After (Source Generator):

[ActivitySource("RequestProcessor")]
public interface IRequestProcessorTelemetry
{
    [Activity]
    Activity? ProcessingRequest(
        [Baggage] string userId,
        [Baggage] string sessionId,
        [Tag] DateTimeOffset requestTimestamp);
}

public async Task ProcessRequestAsync(string userId, string sessionId)
{
    using var activity = _telemetry.ProcessingRequest(userId, sessionId, DateTimeOffset.UtcNow);

    // Call downstream service - baggage automatically propagates
    await _httpClient.GetAsync($"/api/users/{userId}");
}

Key Differences:

  • [Baggage] - Propagates across service boundaries (appears in downstream traces)
  • [Tag] - Local to this activity only (not propagated)

Example 5: Exception Tracking (OpenTelemetry Format)

Before (Manual Exception Recording):

public async Task ProcessAsync(int id)
{
    using var activity = _activitySource.StartActivity("Process");
    activity?.SetTag("entity.id", id);

    try
    {
        // Process...
    }
    catch (Exception ex)
    {
        // Manually record exception in OpenTelemetry format
        var tags = new ActivityTagsCollection
        {
            { "exception.type", ex.GetType().FullName },
            { "exception.message", ex.Message },
            { "exception.stacktrace", ex.StackTrace },
            { "exception.escaped", true }
        };
        
        activity?.AddEvent(new ActivityEvent("exception", tags: tags));
        activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
        
        throw;
    }
}

After (Source Generator):

[ActivitySource("Processor")]
public interface IProcessorTelemetry
{
    [Activity]
    Activity? Processing([Tag] int entityId);

    [Event(ActivityStatusCode.Error)]
    void ProcessingFailed(Activity? activity, Exception ex);
}

public async Task ProcessAsync(int id)
{
    using var activity = _telemetry.Processing(id);

    try
    {
        // Process...
    }
    catch (Exception ex)
    {
        _telemetry.ProcessingFailed(activity, ex);
        throw;
    }
}

Generated Exception Event:

The generator automatically creates an exception event with:

  • exception.type - Exception type full name
  • exception.message - Exception message
  • exception.stacktrace - Stack trace
  • exception.escaped - Defaults to true, or use [Escape] attribute to control

Example 6: Activity Context Updates

Before (Adding Tags to Existing Activity):

public async Task<User> GetUserAsync(int userId)
{
    var user = await _repository.GetUserAsync(userId);

    // Add context to current activity
    var activity = Activity.Current;
    if (activity != null)
    {
        activity.SetTag("user.name", user.Name);
        activity.SetTag("user.role", user.Role);
        activity.SetTag("user.created", user.CreatedAt);
    }

    return user;
}

After (Source Generator with Context Method):

[ActivitySource("UserService")]
public interface IUserServiceTelemetry
{
    [Context]
    void UserRetrieved(
        Activity? activity,
        [Tag] string userName,
        [Tag] string userRole,
        [Tag] DateTimeOffset userCreated);
}

public async Task<User> GetUserAsync(int userId)
{
    var user = await _repository.GetUserAsync(userId);

    // Add context to current activity
    _telemetry.UserRetrieved(
        Activity.Current,
        user.Name,
        user.Role,
        user.CreatedAt
    );

    return user;
}

Note

[Context] methods add tags or baggage to an existing activity without creating a new one. Perfect for enriching parent activities with additional data.

Example 7: Multi-Target (Activity + Logging + Metrics)

Before (Separate Manual Implementations):

public class PaymentService
{
    private static readonly ActivitySource _activitySource = new("PaymentService");
    private readonly ILogger<PaymentService> _logger;
    private readonly Counter<int> _paymentCounter;

    public PaymentService(ILogger<PaymentService> logger, IMeterFactory meterFactory)
    {
        _logger = logger;
        var meter = meterFactory.Create("PaymentService");
        _paymentCounter = meter.CreateCounter<int>("payments_processed");
    }

    public async Task<bool> ProcessPaymentAsync(Guid paymentId, decimal amount)
    {
        using var activity = _activitySource.StartActivity("ProcessPayment");
        activity?.SetTag("payment.id", paymentId.ToString());
        activity?.SetTag("payment.amount", amount);

        _logger.LogInformation("Processing payment {PaymentId} for amount {Amount}", 
            paymentId, amount);

        try
        {
            // Process payment...
            
            activity?.SetStatus(ActivityStatusCode.Ok);
            _logger.LogInformation("Payment {PaymentId} processed successfully", paymentId);
            _paymentCounter.Add(1, new("status", "success"));
            
            return true;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error);
            activity?.RecordException(ex);
            _logger.LogError(ex, "Payment {PaymentId} failed", paymentId);
            _paymentCounter.Add(1, new("status", "failed"));
            
            return false;
        }
    }
}

After (Source Generator - Single Interface for All Three):

using Purview.Telemetry;

[ActivitySource("PaymentService")]
[Logger]
[Meter("PaymentService")]
public interface IPaymentServiceTelemetry
{
    // Multi-target: Creates Activity + Logs Info + Increments Counter - all from one call!
    [Activity]
    [Info]
    [AutoCounter]
    Activity? ProcessingPayment([Baggage] Guid paymentId, [Tag] decimal amount);

    // Multi-target: Adds event + Logs success + Increments counter
    [Event(ActivityStatusCode.Ok)]
    [Info]
    [AutoCounter]
    void PaymentProcessedSuccessfully(Activity? activity);

    // Multi-target: Adds event + Logs error + Increments counter
    [Event(ActivityStatusCode.Error)]
    [Error]
    [AutoCounter]
    void PaymentFailed(Activity? activity, Exception ex);
}

// Register once for all three telemetry types
services.AddPaymentServiceTelemetry();

public class PaymentService
{
    private readonly IPaymentServiceTelemetry _telemetry;

    public PaymentService(IPaymentServiceTelemetry telemetry)
    {
        _telemetry = telemetry;
    }

    public async Task<bool> ProcessPaymentAsync(Guid paymentId, decimal amount)
    {
        // Single call creates Activity AND logs AND increments counter!
        using var activity = _telemetry.ProcessingPayment(paymentId, amount);

        try
        {
            // Process payment...
            
            // Single call adds event AND logs AND increments counter!
            _telemetry.PaymentProcessedSuccessfully(activity);
            
            return true;
        }
        catch (Exception ex)
        {
            // Single call adds event AND logs AND increments counter!
            _telemetry.PaymentFailed(activity, ex);
            
            return false;
        }
    }
}

Key Benefits:

  • One interface - replaces ActivitySource + ILogger + Meter
  • One DI registration - AddPaymentServiceTelemetry()
  • One method call - emits activity + log + metric simultaneously
  • Consistent data - same parameters used across all telemetry types

See Multi-Targeting for more details.

Migration Checklist

When migrating from manual ActivitySource to the source generator:

  • Create telemetry interface - Define an interface with [ActivitySource] attribute
  • Define activity methods - Add methods with [Activity] that return Activity?
  • Define event methods - Add methods with [Event] for activity events
  • Define context methods - Add methods with [Context] to add tags/baggage
  • Add parameter attributes - Use [Tag] and [Baggage] to control tag placement
  • Register with DI - Call the generated Add{InterfaceName}() extension method
  • Update constructors - Inject I{YourInterface}Telemetry instead of creating ActivitySource
  • Replace activity creation - Change _activitySource.StartActivity() to _telemetry.MethodName()
  • Replace events - Change activity.AddEvent() to _telemetry.EventMethod(activity)
  • Remove static ActivitySource - Delete manual ActivitySource field
  • Test - Verify traces appear with correct structure in your observability platform

Common Patterns

Pattern 1: Activity Return Types

// Return Activity? to get the created activity
[Activity]
Activity? ProcessingOrder(int orderId);

// Use it with 'using' for automatic disposal
using var activity = _telemetry.ProcessingOrder(orderId);

Pattern 2: Tags vs Baggage

[Activity]
Activity? ProcessingRequest(
    [Tag] string requestId,      // Local to this activity only
    [Baggage] string userId,     // Propagates to downstream services
    [Baggage] string traceId);   // Propagates to downstream services

Use [Baggage] for cross-service correlation data. Use [Tag] for local context.

Pattern 3: Activity Kinds

[Activity(ActivityKind.Server)]  // Incoming request
Activity? HandleRequest();

[Activity(ActivityKind.Client)]  // Outgoing request
Activity? CallingExternalApi();

[Activity(ActivityKind.Consumer)] // Message queue consumer
Activity? ProcessingMessage();

[Activity(ActivityKind.Producer)] // Message queue producer
Activity? PublishingMessage();

[Activity(ActivityKind.Internal)] // Internal operation (default)
Activity? ProcessingData();

Pattern 4: Custom Activity Names

By default, the method name is used. Override with:

[Activity("custom-activity-name")]
Activity? ProcessingOrder(int orderId);

Pattern 5: Start Time Control

[Activity]
Activity? ProcessingMessage(
    DateTimeOffset startTime,  // Parameter name must be 'startTime'
    [Tag] string messageId);

// Usage
_telemetry.ProcessingMessage(message.Timestamp, message.Id);

Generated Code Structure

The source generator creates:

  1. Activity creation methods - Handle ActivitySource.StartActivity with all parameters
  2. Event methods - Create ActivityEvent and attach to activities
  3. Context methods - Add tags/baggage to existing activities
  4. Exception handling - Automatic OpenTelemetry exception format
  5. DI registration - Extension method for IServiceCollection

Example generated code (simplified):

sealed partial class OrderServiceTelemetryCore : IOrderServiceTelemetry
{
    private static readonly ActivitySource _activitySource = new("OrderService");

    public Activity? ProcessingOrder(int orderId, string customerName)
    {
        if (!_activitySource.HasListeners())
            return null;

        var activity = _activitySource.StartActivity("ProcessingOrder");
        
        if (activity != null)
        {
            activity.SetTag("order_id", orderId);
            activity.SetBaggage("customer_name", customerName);
        }

        return activity;
    }

    public void OrderProcessed(Activity? activity)
    {
        if (!_activitySource.HasListeners() || activity == null)
            return;

        var tags = new ActivityTagsCollection();
        var activityEvent = new ActivityEvent("OrderProcessed", tags: tags);
        activity.AddEvent(activityEvent);
        activity.SetStatus(ActivityStatusCode.Ok);
    }
}

Next Steps

After migrating to generated activities:

  1. Add logging - Combine with structured logging in Multi-Targeting
  2. Add metrics - Track performance alongside traces with Metrics
  3. Configure OpenTelemetry - Set up exporters for your observability platform
  4. Explore advanced features - See Activities for complete documentation

Troubleshooting

"Type or namespace 'ActivitySourceAttribute' could not be found"

Make sure you have the correct using statement:

using Purview.Telemetry;  // v4.x

"Activity methods must return Activity or Activity?"

Change the return type:

// ❌ Wrong
[Activity]
void ProcessingOrder(int orderId);

// ✅ Correct
[Activity]
Activity? ProcessingOrder(int orderId);

"ActivitySource name is 'purview'"

Specify the ActivitySource name:

[ActivitySource("MyServiceName")]
interface IMyTelemetry { }

"Tag names are snake_case instead of camelCase"

In v4, tag names use snake_case by default (OpenTelemetry convention). To revert to v3 behavior:

[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]

See Breaking Changes - OpenTelemetry Naming for details.

See Also

Clone this wiki locally