-
Notifications
You must be signed in to change notification settings - Fork 2
Migration From Activities
This guide shows you how to migrate from traditional manual ActivitySource and Activity management to the Purview Telemetry Source Generator's distributed tracing generation.
Benefits of Using the Source Generator:
- ✅ Compile-time safety - activity names and tags verified at build time
- ✅ Zero boilerplate - no manual
ActivitySourceinitialization 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
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
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
ActivityEventorActivityTagsCollectioncreation - ✅ Type-safe event parameters
- ✅ Cleaner, more readable code
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.
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)
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 totrue, or use[Escape]attribute to control
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.
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.
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 returnActivity? - 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}Telemetryinstead of creatingActivitySource - Replace activity creation - Change
_activitySource.StartActivity()to_telemetry.MethodName() - Replace events - Change
activity.AddEvent()to_telemetry.EventMethod(activity) - Remove static ActivitySource - Delete manual
ActivitySourcefield - Test - Verify traces appear with correct structure in your observability platform
// 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);[Activity]
Activity? ProcessingRequest(
[Tag] string requestId, // Local to this activity only
[Baggage] string userId, // Propagates to downstream services
[Baggage] string traceId); // Propagates to downstream servicesUse [Baggage] for cross-service correlation data. Use [Tag] for local context.
[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();By default, the method name is used. Override with:
[Activity("custom-activity-name")]
Activity? ProcessingOrder(int orderId);[Activity]
Activity? ProcessingMessage(
DateTimeOffset startTime, // Parameter name must be 'startTime'
[Tag] string messageId);
// Usage
_telemetry.ProcessingMessage(message.Timestamp, message.Id);The source generator creates:
-
Activity creation methods - Handle
ActivitySource.StartActivitywith all parameters -
Event methods - Create
ActivityEventand attach to activities - Context methods - Add tags/baggage to existing activities
- Exception handling - Automatic OpenTelemetry exception format
-
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);
}
}After migrating to generated activities:
- Add logging - Combine with structured logging in Multi-Targeting
- Add metrics - Track performance alongside traces with Metrics
- Configure OpenTelemetry - Set up exporters for your observability platform
- Explore advanced features - See Activities for complete documentation
Make sure you have the correct using statement:
using Purview.Telemetry; // v4.xChange the return type:
// ❌ Wrong
[Activity]
void ProcessingOrder(int orderId);
// ✅ Correct
[Activity]
Activity? ProcessingOrder(int orderId);Specify the ActivitySource name:
[ActivitySource("MyServiceName")]
interface IMyTelemetry { }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.
- Activities Overview - Complete activity generation documentation
- Multi-Targeting - Combine activities with logging and metrics
- TagAttribute - Using tags and baggage
- Breaking Changes - v3 to v4 migration guide
- Generated Output - Example generated code
Important
Consider helping children around the world affected by conflict. You can donate any amount to War Child here - any amount can help save a life.
Purview Telemetry Source Generator v4.0.0-prerelease.1 | Home | Getting Started | FAQ | Breaking Changes | GitHub