Instrumenting .NET Applications with net_profiler_rust


net_profiler_rust is a side project I created to make monitoring everyday .NET applications easier. The profiler is implemented in Rust.

Feel free to try it and let me know what could be improved.

net_profiler_rust adds OpenTelemetry tracing and metrics to .NET applications without requiring source-code changes or additional NuGet packages. It uses the .NET profiling API, starts the OpenTelemetry SDK inside the application, and sends telemetry over OTLP.

This short guide uses Dynatrace first and then shows how to send the same telemetry to an OpenTelemetry Collector and Jaeger.

Download the agent

Download the package for your operating system and CPU architecture from the net_profiler_rust download page:

Extract the archive. The package contains the native profiler, managed agent assemblies, example settings, and scripts for starting an application with the profiler enabled.

macOS: remove the quarantine attribute

macOS adds a quarantine attribute to files downloaded from the internet. This can prevent .NET from loading the native profiler. After verifying that the archive came from the download page, remove the attribute from the extracted agent directory:

xattr -lr ./profiler-deployment-osx-arm64
xattr -dr com.apple.quarantine ./profiler-deployment-osx-arm64

Complete macOS example

The following example creates a small .NET 10 console application on an Apple Silicon Mac and sends a custom method span to Dynatrace.

Create the application:

mkdir otel-agent-demo
cd otel-agent-demo
dotnet new console -n DemoApp --framework net10.0 --use-program-main
cd DemoApp

Replace Program.cs with:

namespace DemoApp;

internal static class Program
{
    private static void Main()
    {
        var result = Calculator.Calculate(21);
        Console.WriteLine($"Result: {result}");
    }
}

internal static class Calculator
{
    public static int Calculate(int input)
    {
        Thread.Sleep(250);
        return input * 2;
    }
}

Publish it for Apple Silicon:

dotnet publish -c Release -r osx-arm64 --self-contained false

Create settings/settings.yaml inside the extracted agent directory:

General:
  Enabled: true
  Provider: dynatrace
  Type: custom

Connection:
  EndpointUrl: https://<your-environment>.live.dynatrace.com/api/v2/otlp
  Type: http
  BearerToken: <your-api-token>

Monitoring:
  ServiceName: macos-demo
  Enabled: true
  Metrics:
    ServiceName: macos-demo
    Enabled: true
    CollectionInterval: 2
    Cpu: true
    Memory: true

EntryPoint:
  Assembly: DemoApp
  Type: DemoApp.Program
  Method: Main

Sources:
  - Name: Demo.Calculate
    Type: DemoApp.Calculator
    Method: Calculate
    Kind: Internal
    Capture:
      Parameters:
        - Index: 0
          Name: input

Run the application from the extracted agent directory:

cd /path/to/profiler-deployment-osx-arm64

AGENT_SETTINGS_PATH="$PWD/settings" \
  ./bin/run-with-profiler.sh \
  dotnet /path/to/otel-agent-demo/DemoApp/bin/Release/net10.0/osx-arm64/publish/DemoApp.dll

The application prints Result: 42. In Dynatrace, look for the macos-demo service and a Demo.Calculate span containing the input=21 attribute. Short-lived applications may exit before the first periodic metric export; use a longer-running application when testing metrics.

ASP.NET Core with Dynatrace

Create a directory containing a settings.yaml file:

General:
  Enabled: true
  Provider: dynatrace
  Type: aspnetcore

Connection:
  EndpointUrl: https://<your-environment>.live.dynatrace.com/api/v2/otlp
  Type: http
  BearerToken: <your-api-token>

Monitoring:
  ServiceName: demo-api
  Enabled: true
  Special:
    - HttpClientInstrumentation
  Metrics:
    ServiceName: demo-api
    Enabled: true
    CollectionInterval: 10
    Cpu: true
    Memory: true

The Dynatrace API token needs openTelemetryTrace.ingest and metrics.ingest. See the current Dynatrace OTLP API documentation when creating the token.

Start a published ASP.NET Core application:

cd profiler-deployment-<platform>

AGENT_SETTINGS_PATH=/absolute/path/to/settings \
  ./bin/run-with-profiler.sh dotnet /absolute/path/to/DemoApi.dll

Incoming ASP.NET Core requests and outgoing HttpClient calls now produce traces. The agent also exports ASP.NET Core, HTTP client, runtime, and process metrics. Runtime and process metrics require .NET 9 or newer.

Dynatrace trace showing successful calculator requests and a DivideByZeroException
Dynatrace traces with captured values and a divide-by-zero error.
Dynatrace charts showing .NET runtime and process metrics exported by net_profiler_rust
.NET runtime and process metrics in Dynatrace.

Using Jaeger instead

The agent is not tied to Dynatrace. To send OTLP to a local OpenTelemetry Collector, change the provider and connection:

General:
  Enabled: true
  Provider: otel
  Type: aspnetcore

Connection:
  EndpointUrl: http://localhost:4317
  Type: grpc

A minimal Collector pipeline can forward traces to Jaeger and expose metrics for Prometheus:

receivers:
  otlp:
    protocols:
      grpc:

exporters:
  otlp/jaeger:
    endpoint: jaeger-collector:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlp/jaeger]
    metrics:
      receivers: [otlp]
      exporters: [prometheus]

Jaeger displays the traces. Prometheus, usually together with Grafana, handles the exported metrics.

Jaeger search results containing two successful calculator traces and one failed trace
Two successful Jaeger traces and one error.
Jaeger trace details showing captured calculator inputs and a DivideByZeroException
Captured inputs and the DivideByZeroException.

Custom entry points and method rules

For a console application, worker, or other application without the ASP.NET Core host, use Type: custom and tell the agent where to initialize OpenTelemetry:

General:
  Enabled: true
  Provider: dynatrace
  Type: custom

EntryPoint:
  Assembly: DemoApp
  Type: DemoApp.Program
  Method: <Main>

Sources:
  - Name: Demo.Calculate
    Type: DemoApp.Calculator
    Method: Calculate
    Kind: Internal
    Capture:
      Parameters:
        - Index: 0
          Name: input
      Exceptions: true

EntryPoint identifies the method where the agent starts and stops the OpenTelemetry SDK. Each item under Sources creates spans for a selected method. In this example, the first argument of Calculate is recorded as the input span attribute and thrown exceptions are attached to the span.

Only capture parameters that are safe to export. They may contain passwords, personal information, or other sensitive data.

Kubernetes installation

On Kubernetes, use an init container to copy the agent into an emptyDir volume before the application starts:

spec:
  initContainers:
    - name: profiler-installer
      image: <your-profiler-installer-image>
      command: ["/bin/sh", "-c"]
      args:
        - |
          case "$(uname -m)" in
            x86_64) runtime=linux-x64 ;;
            aarch64) runtime=linux-arm64 ;;
            *) exit 1 ;;
          esac
          mkdir -p /shared-profiler/native /shared-profiler/agent
          cp /installer/profiler/native/$runtime/libclr_profiler.so \
             /shared-profiler/native/libclr_profiler.so
          cp -a /installer/profiler/agent/net /shared-profiler/agent/          
      volumeMounts:
        - name: profiler
          mountPath: /shared-profiler

  containers:
    - name: app
      image: <your-application-image>
      env:
        - name: CORECLR_ENABLE_PROFILING
          value: "1"
        - name: CORECLR_PROFILER
          value: "{CF0D821E-299B-5307-A3D8-B283B7B0F2FD}"
        - name: CORECLR_PROFILER_PATH
          value: /opt/profiler/native/libclr_profiler.so
        - name: AGENT_HOME
          value: /opt/profiler
        - name: AGENT_SETTINGS_PATH
          value: /opt/profiler/settings
      volumeMounts:
        - name: profiler
          mountPath: /opt/profiler

  volumes:
    - name: profiler
      emptyDir: {}

Mount settings.yaml from a ConfigMap into /opt/profiler/settings/settings.yaml. The installer image must copy the correct linux-x64 or linux-arm64 native library. Using an init container prevents a race between installing the profiler and starting .NET.

Current metrics and outlook

The agent currently supports:

The next planned areas are:

OpAMP is intended to deliver configuration updates centrally. ReJIT will then allow affected methods to be instrumented again without restarting the application. Log ingestion is not available yet; the goal is to export logs together with traces and metrics while preserving their trace and span context.

Troubleshooting

If no telemetry appears, check the package architecture, OTLP endpoint and protocol, Dynatrace token permissions, and AGENT_SETTINGS_PATH. Set RUST_LOG=debug before starting the application for more profiler diagnostics.