AutoInstrument 0.1.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package AutoInstrument --version 0.1.1
                    
NuGet\Install-Package AutoInstrument -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AutoInstrument" Version="0.1.1">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AutoInstrument" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="AutoInstrument">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AutoInstrument --version 0.1.1
                    
#r "nuget: AutoInstrument, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AutoInstrument@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AutoInstrument&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=AutoInstrument&version=0.1.1
                    
Install as a Cake Tool

AutoInstrument

Rust-style #[instrument] for .NET. Add [Instrument] to a method, get OpenTelemetry spans at every call-site.

Usage

public class YakService
{
    [Instrument]
    public async Task<string> ShaveYak(int yakId, string style)
    {
        await Task.Delay(50);
        return $"Yak #{yakId} shaved with {style}";
    }
}

Every call to ShaveYak is intercepted at compile time with a wrapper that starts an Activity, tags it with the method parameters, and records exceptions.

How it works

flowchart TD
    A["Your code<br/><code>[Instrument]<br/>public Task DoWork()</code>"] --> B["Source Generator<br/>(compile time)"]
    B --> C["ActivitySources holder<br/>one ActivitySource per source name"]
    B --> D["Interceptors<br/>one wrapper per target method"]
    D --> E["At each call-site:<br/>start Activity, tag params,<br/>call original, record exceptions"]

The generator uses C# interceptors to rewrite call-sites at compile time. Your original method is never modified.

Before & after

Before (manual OpenTelemetry):

private static readonly ActivitySource _source = new("MyApp");

public async Task<string> ShaveYak(int yakId, string style)
{
    using var activity = _source.StartActivity("YakService.ShaveYak");
    activity?.SetTag("shaveyak.yakid", yakId.ToString());
    activity?.SetTag("shaveyak.style", style);
    try
    {
        await Task.Delay(50);
        return $"Yak #{yakId} shaved";
    }
    catch (Exception ex)
    {
        activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
        throw;
    }
}

After:

[Instrument]
public async Task<string> ShaveYak(int yakId, string style)
{
    await Task.Delay(50);
    return $"Yak #{yakId} shaved";
}

Getting started

Enable interceptors in your .csproj:

<PropertyGroup>
  <InterceptorsNamespaces>$(InterceptorsNamespaces);AutoInstrument.Generated</InterceptorsNamespaces>
</PropertyGroup>

Then annotate your methods:

using AutoInstrument;

public class OrderService
{
    [Instrument]
    public async Task<Order> ProcessOrder(int orderId, string customer) { ... }

    [Instrument(Skip = new[] { "creditCard" })]
    public async Task ChargeCustomer(int orderId, string creditCard) { ... }

    [Instrument(Name = "orders.compute_total", RecordReturnValue = true)]
    public decimal ComputeTotal(List<LineItem> items) => items.Sum(i => i.Price * i.Quantity);
}

Attribute options

Name

Override the span name. Defaults to ClassName.MethodName.

[Instrument(Name = "orders.process")]
public async Task ProcessOrder(int orderId) { ... }
// Span name: "orders.process" instead of "OrderService.ProcessOrder"

Skip

Parameter names to exclude from span tags. Use for sensitive data. Supports dot-notation for complex type properties.

// Skip a simple parameter
[Instrument(Skip = new[] { "password" })]
public void Login(string username, string password) { ... }
// Tags: login.username ✓  login.password ✗

// Skip a specific property of a complex type
[Instrument(Skip = new[] { "order.CreditCard" })]
public void Process(Order order) { ... }
// Tags: process.order.id ✓  process.order.name ✓  process.order.creditcard ✗

// Skip an entire complex parameter
[Instrument(Skip = new[] { "credentials" })]
public void Auth(Credentials credentials, int userId) { ... }
// Tags: auth.userid ✓  (nothing from credentials)

Fields

Whitelist of parameters to tag. When set, only these parameters are captured. Takes precedence over Skip. Supports dot-notation.

// Only tag specific parameters
[Instrument(Fields = new[] { "orderId" })]
public void Process(int orderId, string name, string secret) { ... }
// Tags: process.orderid ✓  process.name ✗  process.secret ✗

// Only tag specific properties of a complex type
[Instrument(Fields = new[] { "order.Id", "order.Name" })]
public void Process(Order order) { ... }
// Tags: process.order.id ✓  process.order.name ✓  process.order.secret ✗

ActivitySourceName

Override the ActivitySource name for this specific method. See Configuration for project-wide defaults.

[Instrument(ActivitySourceName = "MyApp.Orders")]
public void ProcessOrder(int orderId) { ... }

RecordReturnValue

Tag the return value on the span. Default: false.

[Instrument(RecordReturnValue = true)]
public string GetStatus(int id) { return "active"; }
// Tags: getstatus.id, getstatus.return_value

RecordException

Whether to record exceptions on the span with status, type, message, and stacktrace. Default: true.

// Disable exception recording
[Instrument(RecordException = false)]
public void DoWork() { ... }
// No try/catch wrapper — exceptions propagate without being recorded on the span

Kind

The ActivityKind for the span. Default: 0 (Internal).

Value Kind
0 Internal
1 Server
2 Client
3 Producer
4 Consumer
[Instrument(Kind = 2)] // Client
public async Task<string> CallExternalApi(string endpoint) { ... }

Summary

Property Default Description
Name Class.Method Span name
Skip null Parameters/properties to exclude from tags
Fields null Only these parameters/properties become tags
ActivitySourceName Assembly name Custom ActivitySource name
RecordReturnValue false Tag the return value
RecordException true Record exceptions on span
Kind 0 (Internal) ActivityKind (0-4)

Complex types

Parameters that are classes or structs are automatically expanded into their public properties as individual span tags.

public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; }
    public string CreditCard { get; set; }
}

[Instrument]
public void Process(Order order, int priority) { ... }
// Tags: process.order.id, process.order.customer, process.order.creditcard, process.priority

Primitive types (int, string, bool, decimal, DateTime, Guid, enums, etc.) are tagged directly with their value. Complex types are expanded 1 level deep.

Use Skip and Fields with dot-notation to control which properties are tagged:

[Instrument(Skip = new[] { "order.CreditCard" })]
public void Process(Order order) { ... }
// Tags: process.order.id ✓  process.order.customer ✓  process.order.creditcard ✗

[Instrument(Fields = new[] { "order.Id" })]
public void Process(Order order) { ... }
// Tags: process.order.id ✓  (nothing else)

Configuration

ActivitySource name

By default, the ActivitySource name is the assembly name. Override it at three levels (highest priority first):

  1. Per-method -- [Instrument(ActivitySourceName = "X")]
  2. MSBuild property -- <AutoInstrumentSourceName>X</AutoInstrumentSourceName>
  3. Assembly attribute -- [assembly: AutoInstrumentSource("X")]
  4. Assembly name -- fallback
MSBuild property
<PropertyGroup>
  <AutoInstrumentSourceName>MyApp</AutoInstrumentSourceName>
</PropertyGroup>
Assembly attribute
using AutoInstrument;

[assembly: AutoInstrumentSource("MyApp")]

Both set the default for all [Instrument] methods in the project. The per-method ActivitySourceName always wins.

Diagnostics

The analyzer validates Skip and Fields at compile time:

ID Message
AUTOINST001 '{name}' in Skip is not a parameter of '{method}'
AUTOINST002 '{name}' in Fields is not a parameter of '{method}'
AUTOINST003 '{property}' is not a public property of parameter '{param}' (type '{type}') in '{method}'
[Instrument(Skip = new[] { "pwd" })]           // AUTOINST001 — no parameter named 'pwd'
public void Login(string username, string password) { }

[Instrument(Skip = new[] { "order.Foo" })]      // AUTOINST003 — 'Foo' is not a property of Order
public void Process(Order order) { }

Rust comparison

Rust #[instrument] C# [Instrument]
Auto-capture params yes yes
Complex type expansion no (uses Debug) yes (public properties)
Skip params skip(pwd) Skip = ["pwd"]
Custom span name name = "x" Name = "x"
Record return ret RecordReturnValue = true
Zero runtime cost proc macro source gen + interceptor
Modifies user code no no

Limitations

  • Same compilation only -- interceptors can only intercept call-sites within the same project.
  • Recursive calls are intercepted -- creates nested spans.
  • 1-level expansion -- complex types expand public properties one level deep, not recursively.

Requirements

  • .NET 10

Building

dotnet build
dotnet run --project samples/SampleApp
dotnet test

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 115 2/24/2026
1.1.0 99 2/24/2026
1.0.0 107 2/24/2026
0.1.1 101 2/24/2026
0.1.0 102 2/23/2026