diff --git a/.generated.NoMobile.slnx b/.generated.NoMobile.slnx index cf655a4335..bbf67bf776 100644 --- a/.generated.NoMobile.slnx +++ b/.generated.NoMobile.slnx @@ -114,6 +114,7 @@ + diff --git a/Sentry-CI-Build-Linux-NoMobile.slnf b/Sentry-CI-Build-Linux-NoMobile.slnf index dd3b365807..c8f5aecc53 100644 --- a/Sentry-CI-Build-Linux-NoMobile.slnf +++ b/Sentry-CI-Build-Linux-NoMobile.slnf @@ -31,6 +31,7 @@ "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj", "samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj", + "samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj", "samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj", "src\\Sentry.Analyzers\\Sentry.Analyzers.csproj", "src\\Sentry.AspNetCore.Blazor.WebAssembly\\Sentry.AspNetCore.Blazor.WebAssembly.csproj", diff --git a/Sentry-CI-Build-Linux.slnf b/Sentry-CI-Build-Linux.slnf index c22e97061b..3025600da5 100644 --- a/Sentry-CI-Build-Linux.slnf +++ b/Sentry-CI-Build-Linux.slnf @@ -33,6 +33,7 @@ "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj", "samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj", + "samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj", "samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj", "src\\Sentry.Analyzers\\Sentry.Analyzers.csproj", "src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj", diff --git a/Sentry-CI-Build-Windows-arm64.slnf b/Sentry-CI-Build-Windows-arm64.slnf index 10ce77aa88..b2ecf8df01 100644 --- a/Sentry-CI-Build-Windows-arm64.slnf +++ b/Sentry-CI-Build-Windows-arm64.slnf @@ -34,6 +34,7 @@ "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj", "samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj", + "samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj", "samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj", "src\\Sentry.Analyzers\\Sentry.Analyzers.csproj", "src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj", diff --git a/Sentry-CI-Build-Windows.slnf b/Sentry-CI-Build-Windows.slnf index 0b8219a53e..528d7817dd 100644 --- a/Sentry-CI-Build-Windows.slnf +++ b/Sentry-CI-Build-Windows.slnf @@ -34,6 +34,7 @@ "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj", "samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj", + "samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj", "samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj", "src\\Sentry.Analyzers\\Sentry.Analyzers.csproj", "src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj", diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf index 4b21b127b3..f8d2b3ca69 100644 --- a/Sentry-CI-Build-macOS.slnf +++ b/Sentry-CI-Build-macOS.slnf @@ -38,6 +38,7 @@ "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj", "samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj", + "samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj", "samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj", "src\\Sentry.Analyzers\\Sentry.Analyzers.csproj", "src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj", diff --git a/Sentry.slnx b/Sentry.slnx index cf655a4335..bbf67bf776 100644 --- a/Sentry.slnx +++ b/Sentry.slnx @@ -114,6 +114,7 @@ + diff --git a/SentryNoMobile.slnf b/SentryNoMobile.slnf index 3917453156..a6ef473815 100644 --- a/SentryNoMobile.slnf +++ b/SentryNoMobile.slnf @@ -32,6 +32,7 @@ "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj", "samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj", + "samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj", "samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj", "src\\Sentry.Analyzers\\Sentry.Analyzers.csproj", "src\\Sentry.AspNet\\Sentry.AspNet.csproj", diff --git a/samples/Sentry.Samples.OpenTelemetry.MongoDB/Program.cs b/samples/Sentry.Samples.OpenTelemetry.MongoDB/Program.cs new file mode 100644 index 0000000000..08ddf78727 --- /dev/null +++ b/samples/Sentry.Samples.OpenTelemetry.MongoDB/Program.cs @@ -0,0 +1,98 @@ +/* + * This sample demonstrates how MongoDB queries can be captured as Sentry spans using the + * MongoDB.Driver's built-in OpenTelemetry instrumentation (available since driver version 3.7.0). + * + * The sample requires a MongoDB server. See the README.md alongside this sample for instructions + * on how to start one using Docker. + */ + +using System.Diagnostics; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Core.Configuration; +using OpenTelemetry; +using OpenTelemetry.Trace; +using Sentry.OpenTelemetry; +using Sentry.OpenTelemetry.Exporter; + +#if SENTRY_DSN_DEFINED_IN_ENV +var dsn = Environment.GetEnvironmentVariable("SENTRY_DSN") + ?? throw new InvalidOperationException("SENTRY_DSN environment variable is not set"); +#else +// A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable. +// See https://docs.sentry.io/product/sentry-basics/dsn-explainer/ +var dsn = SamplesShared.Dsn; +#endif + +var activitySource = new ActivitySource("Sentry.Samples.OpenTelemetry.MongoDB"); + +SentrySdk.Init(options => +{ + options.Dsn = dsn; + options.Debug = true; + options.TracesSampleRate = 1.0; + options.UseOtlp(); // <-- Configure Sentry to use OpenTelemetry trace information +}); + +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySource.Name) + .AddSource(MongoTelemetry.ActivitySourceName) // <-- Subscribe to the MongoDB driver's built-in instrumentation + .AddProcessor(new Sentry.Samples.OpenTelemetry.MongoDB.RedactSensitiveMongoData()) // <-- Redact PII from query text BEFORE it's exported (see below) + .AddSentryOtlpExporter(dsn) // <-- Configure OpenTelemetry to send traces to Sentry over OTLP + .Build(); + +// MongoDB.Driver 3.7.0+ creates OpenTelemetry activities for every command out of the box. +// TracingOptions is only needed to tweak that behaviour - here we opt in to capturing the query +// text on each span (off by default), which shows up in Sentry as the `db.query.text` span data. +// NOTE: The captured query text includes the actual field values sent to MongoDB, which may contain +// PII. See the RedactSensitiveMongoData span processor at the bottom of this file for how to scrub it. +var mongoUri = Environment.GetEnvironmentVariable("MONGODB_URI") ?? "mongodb://localhost:27017"; +var clientSettings = MongoClientSettings.FromConnectionString(mongoUri); +clientSettings.ServerSelectionTimeout = TimeSpan.FromSeconds(3); // <-- Fail fast if MongoDB isn't running +clientSettings.TracingOptions = new TracingOptions +{ + QueryTextMaxLength = 4096 +}; +var mongoClient = new MongoClient(clientSettings); + +var db = mongoClient.GetDatabase("sentry_mongo_sample"); +var fruit = db.GetCollection("fruit"); + +try +{ + // Everything within this activity gets sent to Sentry as a single transaction, containing one + // span for each MongoDB command. + using (activitySource.StartActivity("Fruit Salad")) + { + // Each recipe records the name of the person who contributed it. That's PII, and because we + // capture query text (above) it would otherwise be sent to Sentry verbatim in the insert + // command - the RedactSensitiveMongoData processor strips it out before export. + await fruit.InsertManyAsync([ + new BsonDocument { { "name", "Apple" }, { "color", "Red" }, { "contributor", "Alice Johnson" } }, + new BsonDocument { { "name", "Banana" }, { "color", "Yellow" }, { "contributor", "Bob Nguyen" } }, + new BsonDocument { { "name", "Cherry" }, { "color", "Red" }, { "contributor", "Carla Méndez" } }, + new BsonDocument { { "name", "Kiwi" }, { "color", "Green" }, { "contributor", "David O'Brien" } } + ]); + + var redFruit = await fruit.Find(Builders.Filter.Eq("color", "Red")).ToListAsync(); + Console.WriteLine($"Found {redFruit.Count} red fruit: " + + string.Join(", ", redFruit.Select(f => f["name"].AsString))); + + await fruit.UpdateOneAsync( + Builders.Filter.Eq("name", "Apple"), + Builders.Update.Set("color", "Green")); + + var count = await fruit.CountDocumentsAsync(Builders.Filter.Eq("color", "Green")); + Console.WriteLine($"There are now {count} green fruit."); + + // Drop the database so the sample can be run repeatedly + await mongoClient.DropDatabaseAsync(db.DatabaseNamespace.DatabaseName); + } +} +catch (TimeoutException) +{ + Console.WriteLine($""" + Could not connect to MongoDB at {mongoUri} + Is MongoDB running? See the README.md for this sample for instructions on starting MongoDB using Docker. + """); +} diff --git a/samples/Sentry.Samples.OpenTelemetry.MongoDB/README.md b/samples/Sentry.Samples.OpenTelemetry.MongoDB/README.md new file mode 100644 index 0000000000..2cebac7ef4 --- /dev/null +++ b/samples/Sentry.Samples.OpenTelemetry.MongoDB/README.md @@ -0,0 +1,92 @@ +# Sentry.Samples.OpenTelemetry.MongoDB + +This sample demonstrates how MongoDB queries can be captured as Sentry spans using OpenTelemetry. + +[MongoDB.Driver](https://www.nuget.org/packages/MongoDB.Driver) 3.7.0 and later has +[built-in OpenTelemetry instrumentation](https://www.mongodb.com/docs/drivers/csharp/current/logging-and-monitoring/) +— no additional instrumentation package is required. The driver emits one activity per MongoDB +command from an `ActivitySource` named `MongoDB.Driver`, which this sample subscribes to and +exports to Sentry: + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(MongoTelemetry.ActivitySourceName) // "MongoDB.Driver" + .AddSentryOtlpExporter(dsn) + .Build(); +``` + +By default the driver does not record the query text on spans. This sample opts in via +`TracingOptions` on the client settings, so each span includes the (truncated) command as +`db.query.text`: + +```csharp +clientSettings.TracingOptions = new TracingOptions +{ + QueryTextMaxLength = 4096 +}; +``` + +## A note on PII + +When you capture query text, the MongoDB driver records the **actual field values** sent in each +command (for example `{ "name": "Apple", "contributor": "Alice Johnson" }`) in the `db.query.text` +span attribute. If those values can contain PII, you need to scrub them. + +Importantly, the Sentry OTLP exporter used by this sample sends spans **straight to Sentry**, without +running them through the SDK's `BeforeSend`/`BeforeSendTransaction` hooks — so you can't rely on those +to redact data here. + +Your options are: +- **Don't capture query text at all** — leave `QueryTextMaxLength` at its default of `0`. The Queries + module still works using the operation and collection name. +- **Redact client-side with an OpenTelemetry span processor** — registered *before* + `AddSentryOtlpExporter` so it runs before spans are exported. This sample includes an example + `RedactSensitiveMongoData`, which strips the `contributor` field out of `db.query.text` + before it leaves the process. Adapt the field names to your own data. +- **Configure [server-side data scrubbing](https://docs.sentry.io/security-legal-pii/scrubbing/server-side-scrubbing/)** + — note the default rules only catch known-sensitive patterns (passwords, tokens, card numbers), so + for arbitrary field values you'll need to add explicit scrubbing rules. + +## Prerequisites + +### MongoDB + +The sample expects a MongoDB server at `mongodb://localhost:27017`. The easiest way to get one +running is with Docker: + +```shell +docker run --rm -d -p 27017:27017 --name sentry-mongo mongo:8 +``` + +To check that it's up: + +```shell +docker ps --filter name=sentry-mongo +``` + +And to stop it again once you're done (the `--rm` flag means the container is removed +automatically when stopped): + +```shell +docker stop sentry-mongo +``` + +If your MongoDB server is running somewhere else, set the `MONGODB_URI` environment variable to +the appropriate connection string before running the sample. + +### Sentry DSN + +As with the other samples in this repository, you need a Sentry DSN. Either set the `SENTRY_DSN` +environment variable, or replace the placeholder in [samples/SamplesShared.cs](../SamplesShared.cs). +See https://docs.sentry.io/product/sentry-basics/dsn-explainer/ for details. + +## Running the sample + +```shell +dotnet run --project samples/Sentry.Samples.OpenTelemetry.MongoDB +``` + +The sample performs a few operations against a `sentry_mongo_sample` database (insert, find, +update, count and drop) inside a single transaction. In Sentry, look for the `Fruit Salad` +transaction in **Performance** — each MongoDB command shows up as a child span, with the query +text attached as `db.query.text`. diff --git a/samples/Sentry.Samples.OpenTelemetry.MongoDB/RedactSensitiveMongoData.cs b/samples/Sentry.Samples.OpenTelemetry.MongoDB/RedactSensitiveMongoData.cs new file mode 100644 index 0000000000..01a944366d --- /dev/null +++ b/samples/Sentry.Samples.OpenTelemetry.MongoDB/RedactSensitiveMongoData.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; +using OpenTelemetry; + +namespace Sentry.Samples.OpenTelemetry.MongoDB; + +/// +/// An example OpenTelemetry span processor that redacts sensitive values from the MongoDB +/// db.query.text attribute before spans are exported to Sentry. +/// +/// +/// When query-text capture is enabled (see TracingOptions.QueryTextMaxLength above), the +/// MongoDB driver records the full command - including field values - on the span. If those values +/// can contain PII, you should redact them before they leave your process. +/// +/// A span processor is the right place to do this: the Sentry OTLP exporter sends spans straight to +/// Sentry and does NOT run them through the SDK's BeforeSend/BeforeSendTransaction +/// hooks, so the processor has to be registered before AddSentryOtlpExporter in the pipeline. +/// +/// +/// This example redacts the "contributor" field (a person's name). Adapt the field names and logic +/// to match the sensitive data in your own queries. +/// +/// +internal sealed partial class RedactSensitiveMongoData : BaseProcessor +{ + // Matches "contributor": "" in MongoDB's extended JSON (spacing may vary). + [GeneratedRegex("(\"contributor\"\\s*:\\s*)\"[^\"]*\"")] + private static partial Regex SensitiveField(); + + public override void OnEnd(Activity activity) + { + if (activity.GetTagItem("db.query.text") is string queryText) + { + activity.SetTag("db.query.text", SensitiveField().Replace(queryText, "$1\"[Filtered]\"")); + } + } +} diff --git a/samples/Sentry.Samples.OpenTelemetry.MongoDB/Sentry.Samples.OpenTelemetry.MongoDB.csproj b/samples/Sentry.Samples.OpenTelemetry.MongoDB/Sentry.Samples.OpenTelemetry.MongoDB.csproj new file mode 100644 index 0000000000..dfa5adedcd --- /dev/null +++ b/samples/Sentry.Samples.OpenTelemetry.MongoDB/Sentry.Samples.OpenTelemetry.MongoDB.csproj @@ -0,0 +1,29 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + sentry-sdks + sentry-dotnet + true + true + + + + + + + + +