Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .agents/skills/telemetry-sourcegenerator-design/.gitignore

This file was deleted.

96 changes: 96 additions & 0 deletions .agents/skills/telemetry-sourcegenerator-design/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
name: telemetry-sourcegenerator-design
description: Design best practices for Purview.Telemetry.SourceGenerator interfaces. Use when deciding between a single telemetry interface and per-area interfaces, choosing telemetry names, designing multi-target methods, or reviewing telemetry structure in a C# project.
license: ISC
compatibility: C# projects using Purview.Telemetry.SourceGenerator.
metadata:
author: Purview-Dev
version: "1.0"
---

# Design best practices for Purview Telemetry

This skill guides the design of generated telemetry interfaces.

## Single combined interface vs. per-area interfaces

The generator lets you put all telemetry types on one interface, or split them by concern.

### Combined interface

Use one interface when:

- A single service owns all telemetry and the operations naturally overlap.
- You want a single injection point and fewer constructor parameters.
- Multi-target methods emit Activity + Log + Metric from one call.

Naming convention:

```csharp
[ActivitySource]
[Logger]
[Meter]
public interface IOrderServiceTelemetry
```

### Per-area interfaces

Split into separate interfaces when:

- Different consumers need only one telemetry type (e.g., tests mock only logs).
- The team prefers clear separation between tracing, logging, and metrics.
- The service is large and each telemetry type has many operations.

Naming convention:

```csharp
[ActivitySource] public interface IOrderServiceTracing { ... }
[Logger] public interface IOrderServiceLogs { ... }
[Meter] public interface IOrderServiceMetrics { ... }
```

Default recommendation: start with one combined interface per service. Split only when a clear boundary emerges.

## Naming conventions

Default to the OpenTelemetry naming convention (`NamingConvention.OpenTelemetry`).

- ActivitySource names: preserve assembly casing (e.g., `MyApp`).
- Activity names: descriptive, sentence-case-ish, no spaces: `placing-order`.
- Log message templates: human-readable, with `{PascalCase}` placeholders.
- Metric instrument names: hierarchical, dot-separated, lowercase: `myapp.orders.placed`.
- Tag/baggage keys: `snake_case`.

Only use `NamingConvention.Legacy` for exact v3 backward compatibility.

## Multi-target method design

Combine attributes on one method when a single operation emits multiple telemetry types:

```csharp
[Activity]
[Info]
[AutoCounter]
Activity? PlacingOrder(int orderId);
```

Rules:

- Methods that return `Activity?` create an Activity.
- Methods that return `void` typically add events, log, or record metrics.
- Pass the `Activity?` from the starting method to event/context methods.

## Parameter attributes

| Attribute | Use |
|---|---|
| `[Baggage]` | Adds the parameter to Activity baggage; also included in logs. |
| `[Tag]` | Adds the parameter as an Activity tag. |
| `[ExcludeTargets(Targets.Activities)]` | Excludes the parameter from Activity generation only. |
| `[ExpandEnumerable(maximumValueCount: N)]` | Expands an array/IEnumerable into indexed log properties. |

## Testing

Inject the interface in your service and mock it in tests. The generated interface has no implementation code in the interface itself, so standard mocking frameworks (NSubstitute, Moq, TUnitMocks) work directly.

See [references/REFERENCE.md](references/REFERENCE.md) for a decision matrix and naming examples.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Design reference

## Interface shape decision matrix

| Situation | Recommendation |
|---|---|
| Small service with < 10 telemetry operations | One combined `I<ClassName>Telemetry` |
| Service only logs | `I<ClassName>Logs` |
| Service only traces | `I<ClassName>Tracing` |
| Service only emits metrics | `I<ClassName>Metrics` |
| Large service with distinct teams owning logs vs. metrics | Split into per-area interfaces |
| Need to mock only one telemetry type in many tests | Split into per-area interfaces |

## Naming examples

### OpenTelemetry mode (default)

```csharp
[ActivitySource] // name = "MyApp"
[Logger]
[Meter]
public interface IOrderServiceTelemetry
{
[Activity]
Activity? PlacingOrder(int orderId); // activity name = "placing-order"

[Info("Placing order {OrderId}")]
void PlacingOrderInfo(int orderId); // log

[AutoCounter]
void OrderPlaced(); // instrument = "myapp.orders.order-placed"
}
```

### Legacy mode

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

Use only when exact v3 names must be preserved.

## Multi-target example

```csharp
[ActivitySource]
[Logger]
[Meter]
public interface IOrderServiceTelemetry
{
// Starts Activity, logs Info, increments Counter
[Activity(ActivityKind.Internal)]
[Info("Placing order {OrderId} for {CustomerName}")]
[AutoCounter]
Activity? PlacingOrder(int orderId, string customerName, [Baggage] string region);

// Adds event to the Activity and logs Trace
[Event]
[Trace]
void OrderValidated(Activity? activity, int orderId);

// Records histogram
[Histogram]
void OrderProcessingTime(double latencyMs);
}
```

## Parameter attribute examples

```csharp
[Activity]
[Info]
Activity? FetchingOrder(
int orderId,
[Baggage] string region, // carried in Activity baggage
[Tag] string tenantId); // Activity tag only

[Event]
void OrderLoaded(
Activity? activity,
int orderId,
[ExcludeTargets(Targets.Activities)] string[] rawLines); // not in Activity
```

## Testing example

```csharp
public class OrderServiceTests
{
[Test]
public void PlaceOrder_EmitsTelemetry()
{
var telemetry = Substitute.For<IOrderServiceTelemetry>();
var service = new OrderService(telemetry);

service.PlaceOrder(42, "Alice", "EMEA");

telemetry.Received().PlacingOrder(42, "Alice", "EMEA");
}
}
```
8 changes: 0 additions & 8 deletions .agents/skills/telemetry-sourcegenerator-migration/.gitignore

This file was deleted.

81 changes: 81 additions & 0 deletions .agents/skills/telemetry-sourcegenerator-migration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: telemetry-sourcegenerator-migration
description: Migrate existing ILogger, ActivitySource, or System.Diagnostics.Metrics instrument code to Purview.Telemetry.SourceGenerator interfaces. Use when converting hand-written telemetry to generated telemetry interfaces, or when a user wants to replace ILogger, ActivitySource, or Counter/Histogram/UpDownCounter usage with Purview telemetry.
license: ISC
compatibility: C# projects using Purview.Telemetry.SourceGenerator.
metadata:
author: Purview-Dev
version: "1.0"
---

# Migrate to Purview Telemetry Source Generator

Convert existing telemetry code to generated Purview telemetry interfaces.

## Primary path: use the IDE refactorings

The source generator ships with Roslyn code refactorings. Right-click a class containing hand-written telemetry and choose:

- **Convert ILogger to I<ClassName>Logs** — replaces `ILogger` fields and constructor parameters with a generated logging interface.
- **Convert ActivitySource to I<ClassName>Tracing** — replaces `ActivitySource` fields and `StartActivity` calls with a generated tracing interface.
- **Convert Metrics to I<ClassName>Metrics** — replaces `Counter<T>`, `Histogram<T>`, and `UpDownCounter<T>` fields and calls with a generated metrics interface.
- **Convert all telemetry to I<ClassName>Telemetry** — combines all three into one interface when the class uses multiple telemetry types.

Each refactoring offers scopes: **class**, **document**, **project**, and **solution**.

## What the refactorings produce

The generated interface uses attributes from the `Purview.Telemetry` namespace:

```csharp
using Purview.Telemetry;

[Logger]
public interface IOrderServiceLogs
{
[Info]
void OrderPlaced(int orderId, string customerName);
}
```

The original class is rewritten to use the interface:

```csharp
public class OrderService(IOrderServiceLogs logger)
{
public void PlaceOrder(int orderId, string customerName)
{
logger.OrderPlaced(orderId, customerName);
}
}
```

## Manual fallback

If the refactoring does not cover a call pattern, convert it manually:

1. Identify the telemetry type (Logging, Activity, Metric).
2. Create a new interface with the matching class-level attribute (`[Logger]`, `[ActivitySource]`, `[Meter]`).
3. Add a method for each distinct operation, using the method-level attribute from the mapping below.
4. Replace the hand-written call with the interface method.
5. Register the interface in DI with `services.Add{InterfaceNameWithoutI}()`.

## Attribute mapping

| Hand-written telemetry | Generated attribute |
|---|---|
| `ILogger.LogInformation(...)` | `[Info]` |
| `ILogger.LogDebug(...)` | `[Debug]` |
| `ILogger.LogTrace(...)` | `[Trace]` |
| `ILogger.LogWarning(...)` | `[Warning]` |
| `ILogger.LogError(...)` | `[Error]` |
| `ILogger.LogCritical(...)` | `[Critical]` |
| `ActivitySource.StartActivity(...)` | `[Activity]` |
| `activity.AddEvent(...)` | `[Event]` |
| `activity.AddBaggage(...)` / `SetBaggage(...)` | `[Context]` with `[Baggage]` parameter |
| `activity.SetTag(...)` | `[Context]` with `[Tag]` parameter |
| `Counter<T>.Add(...)` | `[Counter]` or `[AutoCounter]` for `Add(1)` |
| `Histogram<T>.Record(...)` | `[Histogram]` |
| `UpDownCounter<T>.Add(...)` | `[UpDownCounter]` |

See [references/REFERENCE.md](references/REFERENCE.md) for detailed before/after examples and common conversions.
Loading
Loading