Skip to content
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
1 change: 1 addition & 0 deletions .generated.NoMobile.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
<Project Path="samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj" />
<Project Path="samples/Sentry.Samples.OpenTelemetry.AzureFunctions/Sentry.Samples.OpenTelemetry.AzureFunctions.csproj" />
<Project Path="samples/Sentry.Samples.OpenTelemetry.Console/Sentry.Samples.OpenTelemetry.Console.csproj" />
<Project Path="samples/Sentry.Samples.OpenTelemetry.MongoDB/Sentry.Samples.OpenTelemetry.MongoDB.csproj" />
<Project Path="samples/Sentry.Samples.Serilog/Sentry.Samples.Serilog.csproj" />
</Folder>
<Folder Name="/scripts/">
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Linux-NoMobile.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Linux.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Windows-arm64.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Windows.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-macOS.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions Sentry.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
<Project Path="samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj" />
<Project Path="samples/Sentry.Samples.OpenTelemetry.AzureFunctions/Sentry.Samples.OpenTelemetry.AzureFunctions.csproj" />
<Project Path="samples/Sentry.Samples.OpenTelemetry.Console/Sentry.Samples.OpenTelemetry.Console.csproj" />
<Project Path="samples/Sentry.Samples.OpenTelemetry.MongoDB/Sentry.Samples.OpenTelemetry.MongoDB.csproj" />
<Project Path="samples/Sentry.Samples.Serilog/Sentry.Samples.Serilog.csproj" />
</Folder>
<Folder Name="/scripts/">
Expand Down
1 change: 1 addition & 0 deletions SentryNoMobile.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
98 changes: 98 additions & 0 deletions samples/Sentry.Samples.OpenTelemetry.MongoDB/Program.cs
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
jamescrosswell marked this conversation as resolved.
// 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<BsonDocument>("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<BsonDocument>.Filter.Eq("color", "Red")).ToListAsync();
Console.WriteLine($"Found {redFruit.Count} red fruit: " +
string.Join(", ", redFruit.Select(f => f["name"].AsString)));

await fruit.UpdateOneAsync(
Builders<BsonDocument>.Filter.Eq("name", "Apple"),
Builders<BsonDocument>.Update.Set("color", "Green"));

var count = await fruit.CountDocumentsAsync(Builders<BsonDocument>.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.
""");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong exception type caught

Medium Severity

When MongoDB is unreachable, the driver throws a MongoConnectionException (or another MongoException) after ServerSelectionTimeout, not TimeoutException. The handler meant to print the README Docker hint never runs, so users see an unhandled exception instead of the friendly message.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9403424. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a false positive — catch (TimeoutException) is correct here.

When the server can't be selected within ServerSelectionTimeout, the driver propagates a plain System.TimeoutException, not a MongoException. The MongoConnectionException mentioned above is real, but it's only embedded as the HeartbeatException inside the cluster-state diagnostics of the timeout message — it's the inner reason a server couldn't be selected, not the type that reaches the caller.

Source: Cluster.HandleServerSelectionException (driver v3.9.0) wraps and throws a new TimeoutException(...) — that's the message with the embedded MongoConnectionException that this finding was pattern-matching on.

Verified empirically with driver 3.9.0 and this sample's exact settings (ServerSelectionTimeout = 3s), catching Exception and printing the runtime type:

Scenario Thrown type is TimeoutException is MongoException
Connection refused (closed port) System.TimeoutException
Unreachable host System.TimeoutException

Broadening the catch would actually be a regression: the intent is to show the "Is MongoDB running?" hint only for the connection-timeout case. A genuine MongoException (auth failure, a real query bug, etc.) should surface as itself rather than be masked behind a misleading "is MongoDB running?" message.

92 changes: 92 additions & 0 deletions samples/Sentry.Samples.OpenTelemetry.MongoDB/README.md
Original file line number Diff line number Diff line change
@@ -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
};
```

Comment thread
jamescrosswell marked this conversation as resolved.
## 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`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using OpenTelemetry;

namespace Sentry.Samples.OpenTelemetry.MongoDB;

/// <summary>
/// An example OpenTelemetry span processor that redacts sensitive values from the MongoDB
/// <c>db.query.text</c> attribute before spans are exported to Sentry.
/// </summary>
/// <remarks>
/// When query-text capture is enabled (see <c>TracingOptions.QueryTextMaxLength</c> 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.
/// <para>
/// 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 <c>BeforeSend</c>/<c>BeforeSendTransaction</c>
/// hooks, so the processor has to be registered before <c>AddSentryOtlpExporter</c> in the pipeline.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
internal sealed partial class RedactSensitiveMongoData : BaseProcessor<Activity>
{
// Matches "contributor": "<value>" 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]\""));
}
Comment on lines +31 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The RedactSensitiveMongoData processor calls activity.SetTag() in OnEnd, which is too late in the activity lifecycle. The tag modifications are not exported, causing PII redaction to fail.
Severity: HIGH

Suggested Fix

Move the redaction logic to a point in the OpenTelemetry pipeline before the Activity is stopped. Instead of using BaseProcessor<Activity>.OnEnd, consider creating a custom processor or using a different mechanism that allows modification of tags before the activity is finalized and exported. The goal is to ensure SetTag() is called while the activity's IsRecording property is still true.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
samples/Sentry.Samples.OpenTelemetry.MongoDB/RedactSensitiveMongoData.cs#L31-L36

Potential issue: The `RedactSensitiveMongoData` processor attempts to modify an
OpenTelemetry `Activity` by calling `activity.SetTag()` within the `OnEnd` method.
According to OpenTelemetry .NET specifications, the `OnEnd` method is invoked after the
activity has already been stopped. Any tags set on an activity after it has been stopped
are not included in the exported trace data. As a result, the redaction of sensitive
information, such as contributor names in MongoDB queries, will fail silently. This
leads to Personally Identifiable Information (PII) being sent to Sentry unredacted,
defeating the purpose of the processor.

Did we get this right? 👍 / 👎 to inform future reviews.

@jamescrosswell jamescrosswell Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not true - the redacted spans are hitting Sentry just fine. See this trace, for example:
image

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<!-- MongoDB.Driver 3.7.0 and later has built-in OpenTelemetry instrumentation -->
<PackageReference Include="MongoDB.Driver" Version="3.9.0" />
<PackageReference Include="OpenTelemetry" Version="1.15.0" />
</ItemGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<SentryOrg>sentry-sdks</SentryOrg>
<SentryProject>sentry-dotnet</SentryProject>
<SentryUploadSymbols>true</SentryUploadSymbols>
<SentryUploadSources>true</SentryUploadSources>
</PropertyGroup>

<!-- In your own project, this would be PackageReferences to the latest versions of these Sentry packages. -->
<ItemGroup>
<ProjectReference Include="..\..\src\Sentry\Sentry.csproj" />
<ProjectReference Include="..\..\src\Sentry.OpenTelemetry.Exporter\Sentry.OpenTelemetry.Exporter.csproj" />
</ItemGroup>

</Project>
Loading