Skip to content

OpenTelemetry follow up PR #613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions doc/features/opentelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ The driver provides support for session and node level [traces](https://opentele

## Including OpenTelemetry instrumentation in your code

Add the package `Cassandra.OpenTelemetry` to the project and add the extension method `AddOpenTelemetryInstrumentation()` when building your cluster:
Add the package `Cassandra.OpenTelemetry` to the project and add the extension method `WithOpenTelemetryInstrumentation()` when building your cluster:

```csharp
var cluster = Cluster.Builder()
.AddContactPoint(Program.ContactPoint)
.WithSessionName(Program.SessionName)
.AddOpenTelemetryInstrumentation()
.WithOpenTelemetryInstrumentation()
.Build();
```

Expand Down
3 changes: 2 additions & 1 deletion doc/features/request-tracker/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Request Tracker

The driver provides the `IRequestTracker` interface that tracks the requests at Session and Node levels. It contains *start*, *finish*, and *error* events that can be subscribed by implementing the interface, and should be used by passing the implementation as an argument of the method `WithRequestTracker` that is available in the `Builder`.\
An example of an `IRequestTracker` implementation is the extension package `Cassandra.OpenTelemetry` that can be checked in the [documentation](/doc/features/opentelemetry/README.md).
An example of an `IRequestTracker` implementation is the extension package `Cassandra.OpenTelemetry` that can be checked in the [documentation](../opentelemetry/README.md).

## Available events

Expand All @@ -13,3 +13,4 @@ The full API doc is available [here](https://docs.datastax.com/en/drivers/csharp
- **OnNodeStartAsync** - that is triggered when the node request starts
- **OnNodeSuccessAsync** - that is triggered when the node level request finishes successfully.
- **OnNodeErrorAsync** - that is triggered when the node request finishes unsuccessfully.
- **OnNodeAbortedAsync** - that is triggered when the node request is aborted (e.g. pending speculative execution).
22 changes: 22 additions & 0 deletions examples/OpenTelemetry/DistributedTracing/Api/Api.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.7.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.7.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Cassandra\Cassandra.csproj" />
<ProjectReference Include="..\..\..\..\src\Extensions\Cassandra.OpenTelemetry\Cassandra.OpenTelemetry.csproj" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions examples/OpenTelemetry/DistributedTracing/Api/Api.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@Api_HostAddress = http://localhost:5284

GET {{Api_HostAddress}}/weatherforecast/
Accept: application/json

###
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Cassandra.Mapping;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IMapper _mapper;

public WeatherForecastController(ILogger<WeatherForecastController> logger, IMapper mapper)
{
_logger = logger;
_mapper = mapper;
}

[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> GetAsync()
{
var results = await _mapper.FetchAsync<WeatherForecast>().ConfigureAwait(false);
return results.ToArray();
}
}
}
149 changes: 149 additions & 0 deletions examples/OpenTelemetry/DistributedTracing/Api/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@

using Cassandra;
using Cassandra.Mapping;
using Cassandra.OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using ISession = Cassandra.ISession;

namespace Api
{
// This API creates a keyspace "weather" and table "weather_forecast" on startup (if they don't exist).
public class Program
{
private const string CassandraContactPoint = "127.0.0.1";
private const int CassandraPort = 9042;

public static async Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("Weather Forecast API"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddSource(CassandraActivitySourceHelper.ActivitySourceName)
//.AddOtlpExporter(opt => opt.Endpoint = new Uri("http://localhost:4317")) // uncomment if you want to use an OTPL exporter like Jaeger
.AddConsoleExporter());
builder.Services.AddSingleton<ICluster>(_ =>
{
var cassandraBuilder = Cluster.Builder()
.AddContactPoint(CassandraContactPoint)
.WithPort(CassandraPort)
.WithOpenTelemetryInstrumentation(opts => opts.IncludeDatabaseStatement = true);
return cassandraBuilder.Build();
});
builder.Services.AddSingleton<ISession>(provider =>
{
var cluster = provider.GetService<ICluster>();
if (cluster == null)
{
throw new ArgumentNullException(nameof(cluster));
}
return cluster.Connect();
});
builder.Services.AddSingleton<IMapper>(provider =>
{
var session = provider.GetService<ISession>();
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}

return new Mapper(session);
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// force initialization of C* Session because if it fails then it can not be reused and the app should restart
// (or the Session should be created before registering it on the service collection with some kind of retries if needed)
var session = app.Services.GetService<ISession>();
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
var mapper = app.Services.GetService<IMapper>();
if (mapper == null)
{
throw new ArgumentNullException(nameof(mapper));
}

await SetupWeatherForecastDb(session, mapper).ConfigureAwait(false);

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();


app.MapControllers();

await app.RunAsync().ConfigureAwait(false);
}

private static async Task SetupWeatherForecastDb(ISession session, IMapper mapper)
{
await session.ExecuteAsync(
new SimpleStatement(
"CREATE KEYSPACE IF NOT EXISTS weather WITH REPLICATION = { 'class': 'SimpleStrategy', 'replication_factor': 1 }"))
.ConfigureAwait(false);

await session.ExecuteAsync(
new SimpleStatement(
"CREATE TABLE IF NOT EXISTS weather.weather_forecast ( id uuid PRIMARY KEY, date timestamp, summary text, temp_c int )"))
.ConfigureAwait(false);

var weatherForecasts = new WeatherForecast[]
{
new()
{
Date = new DateTime(2024, 9, 18),
Id = Guid.Parse("9c9fdc2c-cf59-4ebe-93ac-c26e2fd1a56a"),
Summary = "Generally clear. Areas of smoke and haze are possible, reducing visibility at times. High 30\u00b0C. Winds NE at 10 to 15 km/h.",
TemperatureC = 30
},
new()
{
Date = new DateTime(2024, 9, 19),
Id = Guid.Parse("b38b338f-56a8-4f56-a8f1-640d037ed8f6"),
Summary = "Generally clear. Areas of smoke and haze are possible, reducing visibility at times. High 28\u00b0C. Winds SSW at 10 to 15 km/h.",
TemperatureC = 28
},
new()
{
Date = new DateTime(2024, 9, 20),
Id = Guid.Parse("04b8e06a-7f59-4921-888f-1a71a52ff7bb"),
Summary = "Partly cloudy. High 24\u00b0C. Winds SW at 10 to 15 km/h.",
TemperatureC = 24
},
new()
{
Date = new DateTime(2024, 9, 21),
Id = Guid.Parse("036c25a6-e354-4613-8c27-1822ffb9e184"),
Summary = "Rain. High 23\u00b0C. Winds SSW and variable. Chance of rain 70%.",
TemperatureC = 23
},
new()
{
Date = new DateTime(2024, 9, 22),
Id = Guid.Parse("ebd16ca8-ee00-42c1-9763-bb19dbf9a8e9"),
Summary = "Morning showers. High 22\u00b0C. Winds SW and variable. Chance of rain 50%.",
TemperatureC = 22
},
};

var tasks = weatherForecasts.Select(w => mapper.InsertAsync(w));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Api": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5284",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
24 changes: 24 additions & 0 deletions examples/OpenTelemetry/DistributedTracing/Api/WeatherForecast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Cassandra.Mapping.Attributes;

namespace Api
{
[Table(Keyspace = "weather", Name = "weather_forecast")]
public class WeatherForecast
{
[PartitionKey]
[Column("id")]
public Guid Id { get; set; }

[Column("date")]
public DateTime? Date { get; set; }

[Column("temp_c")]
public int TemperatureC { get; set; }

[Ignore]
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

[Column("summary")]
public string? Summary { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.8.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Extensions\Cassandra.OpenTelemetry\Cassandra.OpenTelemetry.csproj" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
</ItemGroup>

</Project>
60 changes: 60 additions & 0 deletions examples/OpenTelemetry/DistributedTracing/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

namespace Client
{
internal class Program
{
private const string WeatherApiUri = "http://localhost:5284";

private const string WeatherForecastEndpointUri = WeatherApiUri + "/" + "WeatherForecast";

private static readonly ActivitySource ClientActivity = new ActivitySource("Weather Forecast Client Request");

static async Task Main(string[] args)
{
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(
serviceName: "Weather Forecast Client",
serviceVersion: "1.0.0"))
.AddSource(ClientActivity.Name)
//.AddOtlpExporter(opt => opt.Endpoint = new Uri("http://localhost:4317")) // uncomment if you want to use an OTPL exporter like Jaeger
.Build();
var cts = new CancellationTokenSource();
var task = Task.Run(async () =>
{
await Task.Delay(1000, cts.Token).ConfigureAwait(false);
using var httpClient = new HttpClient();
while (!cts.IsCancellationRequested)
{
try
{
using (var _ = ClientActivity.StartActivity(ActivityKind.Client))
{
await Console.Out.WriteLineAsync("TraceId: " + Activity.Current?.TraceId + Environment.NewLine + "Sending request.").ConfigureAwait(false);
var forecastResponse = await httpClient.GetAsync(WeatherForecastEndpointUri, cts.Token).ConfigureAwait(false);

if (forecastResponse.IsSuccessStatusCode)
{
var content = await forecastResponse.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
await Console.Out.WriteLineAsync("TraceId: " + Activity.Current?.TraceId + Environment.NewLine + content + Environment.NewLine).ConfigureAwait(false);
}
}

await Task.Delay(5000, cts.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
}
}
});

await Console.Out.WriteLineAsync("Press enter to shut down.").ConfigureAwait(false);
await Console.In.ReadLineAsync().ConfigureAwait(false);
await cts.CancelAsync().ConfigureAwait(false);
await task.ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Client
{
public class WeatherForecast
{
public DateOnly Date { get; set; }

public int TemperatureC { get; set; }

public int TemperatureF { get; set; }

public string? Summary { get; set; }
}
}
Loading