Instana

Instana

The community for performance and observability professionals to learn, to share ideas, and to connect with others.

 View Only

Integrating Instana with OpenTelemetry in .NET 8+

By Bojana Bogicevic posted Fri February 20, 2026 06:20 AM

  

A Practical Guide to Modern Observability
Published: February 20, 2026

Introduction

In today’s cloud-native world, observability isn’t just a nice-to-have — it’s essential. As .NET developers, we need to understand what’s happening inside our applications, especially when things go wrong. This is where the powerful combination of OpenTelemetry and Instana comes into play. OpenTelemetry provides vendor-neutral instrumentation, while Instana offers enterprise-grade analysis and visualization. Together, they create a robust observability solution that gives you deep insights into your application’s behavior without vendor lock-in. In this guide, I’ll walk you through integrating Instana with OpenTelemetry in a .NET 8+ application and using both Instana native instrumentation and OpenTelemetry specific instrumentation, with examples including SignalR instrumentation for real-time communication.

image

Before diving into the implementation, let’s understand why this combination makes sense:

Vendor Neutrality

OpenTelemetry is an open standard backed by the Cloud Native Computing Foundation (CNCF). This means you’re not locked into a single vendor’s instrumentation library. If you decide to switch from Instana to another observability platform, your instrumentation code remains unchanged.

Rich Instrumentation

Instana AutoInstrumentation provides automatic instrumentation for common scenarios like HTTP requests, database calls, and message queues.

OpenTelemetry provides additional instrumentation of specific packages. You get comprehensive telemetry with minimal code changes.

Unified Observability

With a single SDK, you can collect traces, metrics, and logs. All three signals are correlated, making it easier to troubleshoot issues.

Enterprise Features

Instana brings AI-powered analysis, automatic service discovery, and beautiful visualizations to your OpenTelemetry data. It’s the best of both worlds: open standards with enterprise capabilities.

The Architecture

Understanding the data flow is crucial. Here’s how it works:

  1. Your .NET application generates telemetry (traces, metrics, logs).
  2. Instana auto instrumentation of HTTP requests.
  3. OpenTelemetry SDK collects and processes telemetry.
  4. OTLP exporters send data to Instana Agent via gRPC.
  5. Instana Agent forwards data to Instana backend.
  6. Instana backend analyzes and visualizes the data.

The key component is the Instana Agent running, which accepts OTLP data via gRPC on port 4317. This agent acts as a bridge between your application and Instana’s cloud backend.


Prerequisites

Before we begin, make sure you have:

Required Software

  • .NET 8 SDK or later (this guide uses .NET 9, but works with .NET 8+).
  • Visual Studio 2022 or VS Code with C# extension.
  • Instana Agent running locally or remotely.

Instana Agent Setup

Your Instana Agent must be configured to accept OTLP data.
Add this to your agent configuration:

com.instana.plugin.opentelemetry:
grpc:
enabled: true
# port: 4317
http:
enabled: true
# port: 4318

And enable .NET Core AutoTracing:

com.instana.plugin.netcore:
tracing:
enabled: true

After updating the configuration, restart the agent and verify port 4317 is listening.

Knowledge Requirements

  • Basic understanding of ASP.NET Core.
  • Familiarity with dependency injection.
  • Understanding of distributed tracing concepts.

Step 1: Install NuGet Packages

First, add the necessary NuGet packages to your project.
Open your .csproj file and add:

<ItemGroup>
  <!-- Core OpenTelemetry packages -->
  <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
  <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
  <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.14.0" />
  
  <!-- Instana-specific packages -->
  <PackageReference Include="Instana.Tracing.Core" Version="1.311.2" />
  <PackageReference Include="Instana.Tracing.Core.Rewriter.Windows" Version="1.311.2" />
  
  <!-- Optional: SignalR instrumentation -->
  <PackageReference Include="AspNetCore.SignalR.OpenTelemetry" Version="1.8.0" />
  <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.9" />
</ItemGroup>

Run dotnet restore to install all packages.

Step 2: Configure OpenTelemetry Tracing

Now let’s configure OpenTelemetry in your Program.cs file. This is where the magic happens.

Define Service Information

Start by defining your service metadata:

var serviceName = "YourServiceName";
var serviceVersion = "1.0.0";
var builder = WebApplication.CreateBuilder(args);

Configure the OpenTelemetry SDK

Add OpenTelemetry to your service collection:

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource =>
    {
        resource
            .AddService(
                serviceName: serviceName,
                serviceVersion: serviceVersion,
                serviceInstanceId: Environment.ProcessId.ToString())
            .AddAttributes(new Dictionary<string, object>
            {
                ["process.pid"] = Environment.ProcessId,
                ["host.name"] = Environment.MachineName
            });
    })
    .WithTracing(tracing =>
    {
        tracing
            .AddSource(serviceName)
            .AddConsoleExporter()
            .AddOtlpExporter(options =>
            {
                options.Endpoint = new Uri("<http://localhost:4317>");
            });
    });

What’s Happening Here?

  • ConfigureResource: This identifies your service in Instana. The service name, version, and instance ID help you distinguish between different services and instances.
  • AddSource: Registers your custom ActivitySource for manual instrumentation (we’ll create this later).
  • AddConsoleExporter (Optional): Outputs traces to the console during development. Very useful for debugging.
  • AddOtlpExporter: Sends traces to Instana Agent via OTLP protocol on port 4317.

Step 3: Configure Metrics

Metrics help you understand your application’s performance over time.
Add metrics configuration:

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => { /* same as above */ })
    .WithTracing(tracing => { /* same as above */ })
    .WithMetrics(metrics =>
    {
        metrics
            .AddMeter(serviceName)
            .AddConsoleExporter()
            .AddOtlpExporter(options =>
            {
                options.Endpoint = new Uri("<http://localhost:4317>");
            });
    });

Metrics Breakdown

  • AddMeter: Registers your custom meter for application-specific metrics.
  • AddConsoleExporter (Optional): Outputs metrics to the console during development. Very useful for debugging.
  • AddOtlpExporter: Sends metrics to Instana Agent via OTLP protocol on port 4317.

Step 4: Configure Logging

OpenTelemetry can also handle your logs, correlating them with traces:

builder.Logging.AddOpenTelemetry(options =>
{
    options.SetResourceBuilder(
        ResourceBuilder.CreateDefault()
            .AddService(
                serviceName: serviceName,
                serviceVersion: serviceVersion,
                serviceInstanceId: Environment.ProcessId.ToString())
            .AddAttributes(new Dictionary<string, object>
            {
                ["process.pid"] = Environment.ProcessId,
                ["host.name"] = Environment.MachineName,
                ["process.runtime.name"] = ".NET",
                ["process.runtime.version"] = Environment.Version.ToString()
            })
    );
    options.AddConsoleExporter();
    options.AddOtlpExporter(o =>
    {
        o.Endpoint = new Uri("<http://localhost:4317>");
    });
});

Why OpenTelemetry Logging?

The key benefit is automatic correlation. When you log something during a traced operation, the log entry includes the trace ID. In Instana, you can see logs alongside traces, making debugging much easier. Use structured logging for best results:

_logger.LogInformation(
    "Processing order {OrderId} for customer {CustomerId}",
    orderId,
    customerId);

Step 5: Create Custom Instrumentation

Automatic instrumentation is great, but sometimes you need fine-grained control. That’s where custom instrumentation comes in. Both Instana tracer and OpenTelemetry support custom spans, in this case we will have a closer look at custom instrumentation using OpenTelemetry.

Create an Instrumentation Class

First, create a class to hold your ActivitySource:

using System.Diagnostics;
public class Instrumentation : IDisposable
{
    internal const string ActivitySourceName = "YourServiceName";
    internal const string ActivitySourceVersion = "1.0.0";
    public Instrumentation()
    {
        this.ActivitySource = new ActivitySource(
            ActivitySourceName, 
            ActivitySourceVersion);
    }
    public ActivitySource ActivitySource { get; }
    public void Dispose()
    {
        this.ActivitySource.Dispose();
    }
}

Register in Dependency Injection

In Program.cs:

builder.Services.AddSingleton<Instrumentation>();

Use Custom Spans in Your Code

Now you can create custom spans in your business logic:

public class Dice
{
    private readonly ActivitySource _activitySource;
    private readonly int _min;
    private readonly int _max;
    public Dice(int min, int max, ActivitySource activitySource)
    {
        _min = min;
        _max = max;
        _activitySource = activitySource;
    }
    public List<int> RollTheDice(int rolls)
    {
        var results = new List<int>();
        using (var activity = _activitySource.StartActivity("rollTheDice"))
        {
            activity?.SetTag("dice.min", _min);
            activity?.SetTag("dice.max", _max);
            activity?.SetTag("dice.rolls", rolls);
            for (int i = 0; i < rolls; i++)
            {
                var value = RollOnce();
                results.Add(value);
                
                activity?.AddEvent(new ActivityEvent(
                    $"Roll {i + 1}",
                    tags: new ActivityTagsCollection
                    {
                        { "roll.value", value }
                    }));
            }
            activity?.SetTag("dice.total", results.Sum());
            return results;
        }
    }
    private int RollOnce()
    {
        return Random.Shared.Next(_min, _max + 1);
    }
}

Understanding Custom Spans

  • StartActivity: Creates a new span (a unit of work in a trace).
  • SetTag: Adds metadata to the span. These appear as attributes in Instana.
  • AddEvent: Records a point-in-time event within the span. Great for tracking milestones The using statement ensures the span is properly closed when the operation completes.

Step 6: Instrument SignalR (Optional)

If your application uses SignalR for real-time communication, you’ll want to trace those interactions too.

Configure SignalR with Instrumentation

In Program.cs:

builder.Services.AddSignalR().AddHubInstrumentation();
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing
            .AddSource(serviceName)
            .AddSignalRInstrumentation()  // Add this line
            .AddOtlpExporter(options =>
            {
                options.Endpoint = new Uri("<http://localhost:4317>");
            });
    });
var app = builder.Build();
app.MapHub<DiceHub>("/hubs/dice");

Create a SignalR Hub with Custom Spans

public class DiceHub : Hub<IDiceHubClient>, IDiceHub
{
    private readonly ILogger<DiceHub> _logger;
    private readonly ActivitySource _activitySource;
    public DiceHub(
        ILogger<DiceHub> logger,
        Instrumentation instrumentation)
    {
        _logger = logger;
        _activitySource = instrumentation.ActivitySource;
    }
    public async Task RollDiceLive(string player, int rolls)
    {
        // SignalR automatically creates a span for hub method calls
        
        if (rolls <= 0)
        {
            throw new HubException("Invalid rolls parameter");
        }
        player = string.IsNullOrEmpty(player) ? "anonymous" : player;
        using var activity = _activitySource.StartActivity(
            "DiceLogic",
            ActivityKind.Internal);
        activity?.SetTag("player", player);
        activity?.SetTag("requested.rolls", rolls);
        var dice = new Dice(1, 6, _activitySource);
        for (int i = 0; i < rolls; i++)
        {
            var value = dice.RollTheDice(1).First();
            // Create a span for each SignalR push
            using var sendActivity = _activitySource.StartActivity(
                "SignalR DiceRolled",
                ActivityKind.Producer);
            sendActivity?.SetTag("signalr.hub", nameof(DiceHub));
            sendActivity?.SetTag("signalr.method", "DiceRolled");
            sendActivity?.SetTag("roll.index", i + 1);
            sendActivity?.SetTag("roll.value", value);
            sendActivity?.SetTag("peer.service", "client-service");
            await Clients.All.DiceRolled(new
            {
                player,
                roll = i + 1,
                value
            });
            await Task.Delay(300);
        }
        _logger.LogInformation(
            "{Player} completed {Rolls} dice rolls",
            player,
            rolls);
    }
}

Make sure that the peer.service tag value matches your SingalR client service.

Setting up simple HTML SignalR client

In your HTML page add the script to invoke the SignalR hub:

<script>
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("http://localhost:5120/hubs/dice")
            .build();

        connection.on("DiceRolled", data => {
            const li = document.createElement("li");
            li.textContent = `Roll ${data.roll}: ${data.value} (Player: ${data.player})`;
            document.getElementById("rolls").appendChild(li);
            console.log("Roll:", data.roll, "Value:", data.value);
        });
        connection.start()
            .then(async () => {
                console.log("Connected to DiceHub");
                await connection.invoke(
                    "RollDiceLive",
                    "Alice",
                    3
                );
            })
            .catch(err => console.error(err));
</script>

SignalR Tracing Benefits

With this setup, you can see the complete flow:

  1. RPC request triggers SignalR notification
  2. SignalR hub method processes the request
  3. Multiple messages are sent to clients
  4. Each step is traced and correlated In Instana, this appears as a beautiful waterfall diagram showing the entire operation.

Step 7: Testing and Verification

Now let’s verify everything works correctly.

Publish and run Your Application

On Windows run it as a Windows Service or Windows Process.

Windows Service:

# Publish the application
dotnet publish
# Create Windows service (adjust path and service name)
sc create YourServiceName `
binPath= "C:\path-to-your-application\bin\Release\net8.0\publish\YourServiceName.exe" `
DisplayName= "YourServiceName"
# Start the service
sc start YourServiceName

Windows Process:

# Publish the application
dotnet publish
# Go to publish folder
cd .\bin\Release\net8.0\publish
# Run the service (make sure YourServiceName.dll is the same as serviceName)
dotnet YourServiceName.dll

You should see output indicating the application is listening on a port (e.g., http://localhost:5120).

Test from Client application

Invoke the SignalR communication by triggering the script on your Client application, whether it’s from a button or from refreshing the page.

Expected response:

Roll 1: 1 (Player: Alice)
Roll 2: 3 (Player: Alice)
Roll 3: 3 (Player: Alice)

Verify Console Output

You should see traces in the console:

Activity.TraceId:            7f1e8e3c4d5a6b7c8d9e0f1a2b3c4d5e
Activity.SpanId: 1a2b3c4d5e6f7a8b
Activity.DisplayName: rollTheDice
Activity.Kind: Internal
Activity.Tags:
dice.min: 1
dice.max: 6
dice.rolls: 3
dice.total: 12

Verify in Instana

  1. Open Instana UI
  2. Navigate to Applications → All Services
  3. Find your service (e.g., “YourServiceName”)
  4. Click on the service and go to Calls or Traces tab
  5. You should see RPC requests with nested spans

Click on a trace to see the waterfall view:

DiceHub/RollDiceLive
└── DiceLogic
└── rollTheDice
├── Roll 1 (event)
├── Roll 2 (event)
└── Roll 3 (event)
image

Picture 1. Visual representation of the trace using Instana

Any other HTTP request is instrumented by Instana AutoTrace.

image
Picture 2. Instana HTTP AutoInstrumentation

Best Practices

Here are some best practices I’ve learned:

Resource Attributes

Always include comprehensive resource attributes:

.ConfigureResource(resource =>
{
    resource
        .AddService(serviceName, serviceVersion, instanceId)
        .AddAttributes(new Dictionary<string, object>
        {
            ["process.pid"] = Environment.ProcessId,
            ["host.name"] = Environment.MachineName,
            ["deployment.environment"] = environmentName,
            ["service.namespace"] = "your-namespace",
            ["team"] = "platform-team",
            ["region"] = "us-east-1"
        });
});

Error Handling

Always record exceptions in spans:

using var activity = _activitySource.StartActivity("YourActivity");
try
{
    // Your logic
}
catch (Exception ex)
{
    activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
    activity?.RecordException(ex);
    _logger.LogError(ex, "Error processing order");
    throw;
}

Semantic Conventions

Follow OpenTelemetry semantic conventions for tags:

// SignalR attributes
sendActivity?.SetTag("signalr.hub", nameof(DiceHub));
sendActivity?.SetTag("signalr.method", "DiceRolled");

// Target service attributes
sendActivity?.SetTag("peer.service", "TargetServiceName");

Configuration Management

If you prefer configuring your service in configuration files for environment-specific settings rather than in code option:

appsettings.json:

{
"OpenTelemetry": {
"ServiceName": "YourService",
"ServiceVersion": "1.0.0",
"OtlpEndpoint": "<http://localhost:4317>",
"ConsoleExporterEnabled": false
}
}

appsettings.Development.json:

{
"OpenTelemetry": {
"ConsoleExporterEnabled": true
}
}

appsettings.Production.json:

{
"OpenTelemetry": {
"OtlpEndpoint": "<http://instana-agent.production:4317>",
"ConsoleExporterEnabled": false
}
}

Common Issues and Solutions

Issue: Traces Not Appearing in Instana

Symptoms: Console exporter shows traces, but nothing in Instana UI
Solutions:

  1. Verify Instana Agent is running and OTLP receiver is enabled
  2. Check network connectivity to port 4317
  3. Verify firewall rules allow traffic on port 4317
  4. Enable debug logging to see export attempts
  5. Check Instana Agent logs for errors

Issue: High Memory Usage

Symptoms: Application memory grows over time, OutOfMemoryException
Solutions:

  1. Reduce batch size in OTLP exporter configuration
  2. Ensure ActivitySource is properly disposed
  3. Monitor span creation rate

Issue: Missing Span Attributes

Symptoms: Spans appear but lack expected tags
Solutions:

  1. Verify ActivitySource name matches the one registered in tracing configuration
  2. Check if activity is null before setting tags

Issue: Performance Degradation

Symptoms: Application slower after adding OpenTelemetry
Solutions:

  1. Reduce instrumentation scope (filter out health checks, metrics endpoints)
  2. Only create spans for significant operations
  3. Use batch processor with appropriate delays

Conclusion

You’ve now successfully integrated Instana with OpenTelemetry in your .NET application, combining automated intelligence with flexible instrumentation for complete system visibility. By bringing together Instana’s smart monitoring and OpenTelemetry’s open standard framework, you’ve created a modern observability setup that helps your team detect issues faster, optimize performance, and ensure reliability across your services. This powerful combination gives you:

✅ Automatic and manual instrumentation capabilities
✅ Instana Auto Instrumentation
✅ OpenTelemetry package specific instrumentation
✅ Enterprise-grade analysis with Instana
✅ Real-time communication tracing with SignalR
✅ Comprehensive observability (traces, metrics, logs)

Key Takeaways

  1. OpenTelemetry provides a unified SDK for all telemetry signals
  2. OTLP is the standard protocol for exporting telemetry data
  3. Automatic instrumentation handles most common scenarios
  4. Manual instrumentation gives fine-grained control
  5. Resource attributes help organize services in Instana
  6. Sampling is essential for production environments

Next Steps

Now that you have the basics working, consider:

  1. Adding database instrumentation (Entity Framework, Dapper)
  2. Instrumenting HTTP client calls
  3. Adding message queue instrumentation
  4. Setting up custom dashboards in Instana
  5. Creating reusable instrumentation libraries for your team

Resources

Thank you for reading! If you found this guide helpful, please share it with your team. For questions or feedback, feel free to reach out. Happy observing! 🎉

Compatible with: .NET 8+, OpenTelemetry 1.14+, Instana Agent 1.311+


#BusinessObservability
#OpenTelemetry
#Tracing
#Demo
#Tutorial

0 comments
25 views

Permalink