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
http:
enabled: true
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:
- RPC request triggers SignalR notification
- SignalR hub method processes the request
- Multiple messages are sent to clients
- 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:
dotnet publish
sc create YourServiceName `
binPath= "C:\path-to-your-application\bin\Release\net8.0\publish\YourServiceName.exe" `
DisplayName= "YourServiceName"
sc start YourServiceName
Windows Process:
dotnet publish
cd .\bin\Release\net8.0\publish
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
- Open Instana UI
- Navigate to Applications → All Services
- Find your service (e.g., “YourServiceName”)
- Click on the service and go to Calls or Traces tab
- 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)
Picture 1. Visual representation of the trace using Instana
Any other HTTP request is instrumented by Instana AutoTrace.
Picture 2. Instana HTTP AutoInstrumentation