Open sandboxFocusImprove this doc

Example: auditing and monitoring calls to external APIs

A team must often audit every call to a sensitive API, such as File.Delete or Process.Start, or produce telemetry for every call to a third-party SDK. The rule applies to the calls that exist today and to the calls that developers add later. Adding audit or telemetry code at each call site is repetitive, and a developer who adds a call can forget it.

With interceptors, the rule is written once, in a project fabric. Metalama rewrites every matching call site of the project at build time, including the call sites added later. The calling code does not change in the source code, and the intercepted methods can be in another assembly, such as the .NET libraries or a NuGet package.

This series uses PaymentClient, a class of a separate class library, to represent the third-party SDK. Each call to the SDK is rewritten to call a generated interceptor, which creates a trace span and records metrics:

Source Code
1using Metalama.Samples.CallTelemetry.Payments;
2
3namespace Metalama.Samples.CallTelemetry3;
4
5public class CheckoutService
6{
7    private readonly PaymentClient _payments = new();
8
9    public async Task<string> CheckoutAsync(
10        string cardNumber,
11        decimal amount,
12        CancellationToken cancellationToken = default )
13    {
14        var fee = this._payments.GetFee( amount );
15        var authorizationId = await this._payments.AuthorizeAsync( cardNumber, amount + fee, cancellationToken );
16        await this._payments.CaptureAsync( authorizationId, cancellationToken );
17
18        return authorizationId;
19    }
20}
21
Transformed Code
1using Metalama.Samples.CallTelemetry.Payments;
2
3namespace Metalama.Samples.CallTelemetry3;
4
5public class CheckoutService
6{
7    private readonly PaymentClient _payments = new();
8
9    public async Task<string> CheckoutAsync(
10        string cardNumber,
11        decimal amount,
12        CancellationToken cancellationToken = default )
13    {
14        var fee = PaymentsTelemetry.PaymentClient_GetFee_Interceptor( this._payments,amount:amount ,operation:"CheckoutService.CheckoutAsync",callingType:"CheckoutService");
15        var authorizationId = await PaymentsTelemetry.PaymentClient_AuthorizeAsync_Interceptor( this._payments,cardNumber:cardNumber,amount:amount + fee,cancellationToken:cancellationToken ,operation:"CheckoutService.CheckoutAsync",callingType:"CheckoutService");
16        await PaymentsTelemetry.PaymentClient_CaptureAsync_Interceptor( this._payments,authorizationId:authorizationId,cancellationToken:cancellationToken ,operation:"CheckoutService.CheckoutAsync",callingType:"CheckoutService");
17
18        return authorizationId;
19    }
20}
21

In this series

Article Description
Call telemetry example, step 1: Auditing calls to sensitive APIs Static methods that you write audit the calls to File.Delete and Process.Start in the whole project.
Call telemetry example, step 2: Producing trace spans for SDK calls A T# template creates an ActivitySource span around each call to the SDK.
Call telemetry example, step 3: Recording metrics The template also records a counter and a duration histogram with System.Diagnostics.Metrics.

When to use a hook of the library instead

Some libraries provide a hook that sees every call, including the calls made by the library itself and by the framework. When such a hook exists, prefer it to interceptors:

  • For HttpClient, add a DelegatingHandler to the handler pipeline, or use the telemetry that HttpClient already emits.
  • For Entity Framework Core, register an EF Core interceptor, such as a DbCommandInterceptor.
  • For libraries that publish events with DiagnosticSource, ActivitySource or Meter, subscribe to these events, for instance with the OpenTelemetry instrumentation packages.
Warning

Interceptors only rewrite the calls that are in the source code of the current project. They do not see the calls made inside the library, by the framework, or through reflection.

Use interceptors when the library has no such hook, when the API is static, such as File.Delete, or when the telemetry must know the calling code.