Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6be1e01
feat(metrics): Trace-connected Metrics (Implementation)
Flash0ver Jan 9, 2026
e462457
Merge branch 'main' into feat/trace-connected-metrics-implementation
Flash0ver Jan 9, 2026
6c5ca23
add CHANGELOG entry
Flash0ver Jan 9, 2026
9485ca4
more public overloads for convenience
Flash0ver Jan 9, 2026
aa46acb
more public overloads for convenience (API Approval Tests)
Flash0ver Jan 9, 2026
e9c349e
release: 6.1.0-alpha.1
getsentry-bot Jan 9, 2026
484f17f
Merge branch 'release/6.1.0-alpha.1' into feat/trace-connected-metrics
Jan 11, 2026
e65db5f
rename APIs, validate parameters, fix attributes
Flash0ver Jan 15, 2026
5228fbe
feat(metrics): add SentryMetricUnits class for supported units
Flash0ver Jan 16, 2026
32caf06
Update SentryUnits.cs
Flash0ver Jan 16, 2026
d730432
update API Approval Tests
Flash0ver Jan 16, 2026
cc43b38
release: 6.1.0-alpha.2
getsentry-bot Jan 16, 2026
c2108da
Merge branch 'release/6.1.0-alpha.2' into feat/trace-connected-metrics
Jan 18, 2026
7e6dbb9
revert: VersionPrefix to main
Flash0ver Jan 28, 2026
eebb0f5
Merge branch 'main' into feat/trace-connected-metrics
Flash0ver Jan 28, 2026
31dd7bc
docs: clean up Changelog
Flash0ver Jan 28, 2026
6f3a785
docs: fix indentation of Changelog
Flash0ver Jan 28, 2026
40a1635
test(metrics): Trace-connected Metrics (Tests) (#4839)
Flash0ver Jan 29, 2026
41e67c2
feat(metrics): Trace-connected Metrics (Samples) (#4841)
Flash0ver Jan 29, 2026
eb5d7a4
Merge branch 'main' into feat/trace-connected-metrics
Flash0ver Jan 29, 2026
cb812b0
docs: add comment to sample
Flash0ver Jan 29, 2026
7faa682
feat: add Scope-only overloads to Distribution and Gauge method groups
Flash0ver Jan 29, 2026
4a080e7
feat!: make public Metrics type non-generic
Flash0ver Jan 29, 2026
de47d35
Merge branch 'main' into feat/trace-connected-metrics
Flash0ver Feb 2, 2026
afb011b
ref: cleanup unused code
Flash0ver Feb 2, 2026
b078621
ref: remove unused code
Flash0ver Feb 2, 2026
5cfacba
Merge branch 'main' into feat/trace-connected-metrics
Flash0ver Feb 3, 2026
a9e4e9a
ref: de-duplicate internal method and unit tests
Flash0ver Feb 3, 2026
be699bf
ref!: rename "trace_metric" type
Flash0ver Feb 3, 2026
98e9919
ref: make BatchProcessor abstract and derive sealed types for Logs an…
Flash0ver Feb 3, 2026
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Add _experimental_ support for [Sentry trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834))
- Extended `SentryThread` by `Main` to allow indication whether the thread is considered the current main thread ([#4807](https://github.com/getsentry/sentry-dotnet/pull/4807))

### Fixes
Expand Down
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<!-- Ignore our own diagnostic ids - these are meant to be external warnings only -->
<NoWarn>$(NoWarn);SENTRY0001</NoWarn>
<NoWarn>$(NoWarn);SENTRYTRACECONNECTEDMETRICS</NoWarn> <!--https://github.com/getsentry/sentry-dotnet/discussions/4838-->

<!-- Allow references to unsigned assemblies (like MAUI) from signed projects -->
<NoWarn>$(NoWarn);CS8002</NoWarn>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

namespace Sentry.Benchmarks;

public class StructuredLogBatchProcessorBenchmarks
/// <summary>
/// <see cref="BatchProcessor{TItem}"/> (formerly "Sentry.Internal.StructuredLogBatchProcessor") was originally developed as Batch Processor for Logs only.
/// When adding support for Trace-connected Metrics, which are quite similar to Logs, it has been made generic to support both.
/// For comparability of results, we still benchmark with <see cref="SentryLog"/>, rather than <see cref="SentryMetric"/>.
/// </summary>
public class BatchProcessorBenchmarks
{
private Hub _hub;
private StructuredLogBatchProcessor _batchProcessor;
private BatchProcessor<SentryLog> _batchProcessor;
private SentryLog _log;

[Params(10, 100)]
Expand All @@ -29,7 +34,7 @@ public void Setup()
var clientReportRecorder = new NullClientReportRecorder();

_hub = new Hub(options, DisabledHub.Instance);
_batchProcessor = new StructuredLogBatchProcessor(_hub, BatchCount, batchInterval, clientReportRecorder, null);
_batchProcessor = new SentryLogBatchProcessor(_hub, BatchCount, batchInterval, clientReportRecorder, null);
_log = new SentryLog(DateTimeOffset.Now, SentryId.Empty, SentryLogLevel.Trace, "message");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
```

BenchmarkDotNet v0.13.12, macOS 26.1 (25B78) [Darwin 25.1.0]
Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores
.NET SDK 10.0.100
[Host] : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD


```
| Method | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Median | Gen0 | Allocated |
|------------------------- |----------- |-------------------- |-------------:|------------:|-------------:|-------------:|-------:|----------:|
| **EnqueueAndFlush** | **10** | **100** | **1,896.9 ns** | **9.94 ns** | **8.81 ns** | **1,894.2 ns** | **0.6104** | **5 KB** |
| EnqueueAndFlush_Parallel | 10 | 100 | 16,520.9 ns | 327.78 ns | 746.51 ns | 16,350.4 ns | 1.1292 | 9.29 KB |
| **EnqueueAndFlush** | **10** | **200** | **4,085.5 ns** | **80.03 ns** | **74.86 ns** | **4,087.1 ns** | **1.2207** | **10 KB** |
| EnqueueAndFlush_Parallel | 10 | 200 | 39,371.8 ns | 776.85 ns | 1,360.59 ns | 38,725.0 ns | 1.6479 | 13.6 KB |
| **EnqueueAndFlush** | **10** | **1000** | **18,829.3 ns** | **182.18 ns** | **142.24 ns** | **18,836.4 ns** | **6.1035** | **50 KB** |
| EnqueueAndFlush_Parallel | 10 | 1000 | 151,934.1 ns | 2,631.83 ns | 3,232.12 ns | 151,495.9 ns | 3.6621 | 31.31 KB |
| **EnqueueAndFlush** | **100** | **100** | **864.9 ns** | **2.16 ns** | **1.68 ns** | **865.0 ns** | **0.1469** | **1.2 KB** |
| EnqueueAndFlush_Parallel | 100 | 100 | 7,414.9 ns | 74.86 ns | 70.02 ns | 7,405.9 ns | 0.5722 | 4.61 KB |
| **EnqueueAndFlush** | **100** | **200** | **1,836.9 ns** | **15.28 ns** | **12.76 ns** | **1,834.9 ns** | **0.2937** | **2.41 KB** |
| EnqueueAndFlush_Parallel | 100 | 200 | 37,119.5 ns | 726.04 ns | 1,252.39 ns | 36,968.9 ns | 0.8545 | 7.27 KB |
| **EnqueueAndFlush** | **100** | **1000** | **8,567.2 ns** | **84.25 ns** | **74.68 ns** | **8,547.4 ns** | **1.4648** | **12.03 KB** |
| EnqueueAndFlush_Parallel | 100 | 1000 | 255,284.5 ns | 5,095.08 ns | 12,593.77 ns | 258,313.9 ns | 1.9531 | 19.02 KB |

This file was deleted.

38 changes: 38 additions & 0 deletions samples/Sentry.Samples.Console.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
* - Performance Tracing (Transactions / Spans)
* - Release Health (Sessions)
* - Logs
* - Metrics
* - MSBuild integration for Source Context (see the csproj)
*
* For more advanced features of the SDK, see Sentry.Samples.Console.Customized.
*/

using System.Diagnostics;
using System.Net;
using System.Net.Http;
using static System.Console;

Expand Down Expand Up @@ -50,6 +53,25 @@
// Drop logs with level Info
return log.Level is SentryLogLevel.Info ? null : log;
});

// Sentry (trace-connected) Metrics via SentrySdk.Experimental.Metrics are enabled by default.
options.Experimental.SetBeforeSendMetric(static metric =>
{
if (metric.TryGetValue(out int integer) && integer < 0)
{
// Return null to drop the metric
return null;
}

// A demonstration of how you can modify the metric object before sending it to Sentry
if (metric.Type is SentryMetricType.Counter)
{
metric.SetAttribute("operating_system.platform", Environment.OSVersion.Platform.ToString());
metric.SetAttribute("operating_system.version", Environment.OSVersion.Version.ToString());
}

return metric;
});
});

// This starts a new transaction and attaches it to the scope.
Expand All @@ -71,9 +93,25 @@ async Task FirstFunction()
// This is an example of making an HttpRequest. A trace us automatically captured by Sentry for this.
var messageHandler = new SentryHttpMessageHandler();
var httpClient = new HttpClient(messageHandler, true);

var stopwatch = Stopwatch.StartNew();
var html = await httpClient.GetStringAsync("https://example.com/");
stopwatch.Stop();

WriteLine(html);

// Info-Log filtered via "BeforeSendLog" callback
SentrySdk.Logger.LogInfo("HTTP Request completed.");

// Counter-Metric prevented from being sent to Sentry via "BeforeSendMetric" callback
SentrySdk.Experimental.Metrics.EmitCounter("sentry.samples.console.basic.ignore", -1);

// Counter-Metric modified before sending it to Sentry via "BeforeSendMetric" callback
SentrySdk.Experimental.Metrics.EmitCounter("sentry.samples.console.basic.http_requests_completed", 1);

// Distribution-Metric sent as is (see "BeforeSendMetric" callback)
SentrySdk.Experimental.Metrics.EmitDistribution("sentry.samples.console.basic.http_request_duration", stopwatch.Elapsed.TotalSeconds, SentryUnits.Duration.Second,
[new KeyValuePair<string, object>("http.request.method", HttpMethod.Get.Method), new KeyValuePair<string, object>("http.response.status_code", (int)HttpStatusCode.OK)]);
}

async Task SecondFunction()
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry.Extensions.Logging/SentryStructuredLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
}

var timestamp = _clock.GetUtcNow();
SentryLog.GetTraceIdAndSpanId(_hub, out var traceId, out var spanId);
_hub.GetTraceIdAndSpanId(out var traceId, out var spanId);

var level = logLevel.ToSentryLogLevel();
Debug.Assert(level != default);
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry.Serilog/SentrySink.Structured.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ internal sealed partial class SentrySink
{
private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEvent logEvent, string formatted, string? template)
{
SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId);
hub.GetTraceIdAndSpanId(out var traceId, out var spanId);
GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes);

SentryLog log = new(logEvent.Timestamp, traceId, logEvent.Level.ToSentryLogLevel(), formatted)
Expand Down
17 changes: 16 additions & 1 deletion src/Sentry/BindableSentryOptions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Sentry;

/// <summary>
/// Contains representations of the subset of properties in SentryOptions that can be set from ConfigurationBindings.
/// Contains representations of the subset of properties in <see cref="SentryOptions"/> that can be set from ConfigurationBindings.
/// Note that all of these properties are nullable, so that if they are not present in configuration, the values from
/// the type being bound to will be preserved.
/// </summary>
Expand Down Expand Up @@ -56,6 +56,8 @@ internal partial class BindableSentryOptions
public bool? EnableSpotlight { get; set; }
public string? SpotlightUrl { get; set; }

public ExperimentalSentryOptions? Experimental { get; set; }

public void ApplyTo(SentryOptions options)
{
options.IsGlobalModeEnabled = IsGlobalModeEnabled ?? options.IsGlobalModeEnabled;
Expand Down Expand Up @@ -106,11 +108,24 @@ public void ApplyTo(SentryOptions options)
options.EnableSpotlight = EnableSpotlight ?? options.EnableSpotlight;
options.SpotlightUrl = SpotlightUrl ?? options.SpotlightUrl;

if (Experimental is { } experimental)
{
options.Experimental.EnableMetrics = experimental.EnableMetrics ?? options.Experimental.EnableMetrics;
}

#if ANDROID
Android.ApplyTo(options.Android);
Native.ApplyTo(options.Native);
#elif __IOS__
Native.ApplyTo(options.Native);
#endif
}

/// <summary>
/// Bindable Options for <see cref="SentryOptions.ExperimentalSentryOptions"/>.
/// </summary>
internal class ExperimentalSentryOptions
{
public bool? EnableMetrics { get; set; }
}
}
7 changes: 6 additions & 1 deletion src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Sentry.Internal;
using Sentry.Protocol.Envelopes;
using Sentry.Protocol.Metrics;

namespace Sentry.Extensibility;

Expand Down Expand Up @@ -267,4 +266,10 @@ public void Dispose()
/// Disabled Logger.
/// </summary>
public SentryStructuredLogger Logger => DisabledSentryStructuredLogger.Instance;

/// <summary>
/// Disabled Metrics.
/// </summary>
[Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")]
public SentryMetricEmitter Metrics => DisabledSentryMetricEmitter.Instance;
}
7 changes: 6 additions & 1 deletion src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Sentry.Infrastructure;
using Sentry.Protocol.Envelopes;
using Sentry.Protocol.Metrics;

namespace Sentry.Extensibility;

Expand Down Expand Up @@ -37,6 +36,12 @@ private HubAdapter() { }
/// </summary>
public SentryStructuredLogger Logger { [DebuggerStepThrough] get => SentrySdk.Logger; }

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")]
public SentryMetricEmitter Metrics { [DebuggerStepThrough] get => SentrySdk.Experimental.Metrics; }

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
Expand Down
30 changes: 30 additions & 0 deletions src/Sentry/HubExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,34 @@ internal static ITransactionTracer StartTransaction(
hub.ConfigureScope(scope => current = scope);
return current;
}

/// <summary>
/// Get <paramref name="traceId"/> of either the currently active Span, or the current Scope.
/// Get <paramref name="spanId"/> only if there is a currently active Span.
/// </summary>
/// <remarks>
/// Intended for use by <see cref="SentryLog"/> and <see cref="SentryMetric{T}"/>.
/// </remarks>
internal static void GetTraceIdAndSpanId(this IHub hub, out SentryId traceId, out SpanId? spanId)
{
var activeSpan = hub.GetSpan();
if (activeSpan is not null)
{
traceId = activeSpan.TraceId;
spanId = activeSpan.SpanId;
return;
}

var scope = hub.GetScope();
if (scope is not null)
{
traceId = scope.PropagationContext.TraceId;
spanId = null;
return;
}

Debug.Assert(hub is not Hub, "In case of a 'full' Hub, there is always a Scope. Otherwise (disabled) there is no Scope, but this branch should be unreachable.");
traceId = SentryId.Empty;
spanId = null;
}
}
13 changes: 13 additions & 0 deletions src/Sentry/IHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ public interface IHub : ISentryClient, ISentryScopeManager
/// </remarks>
public SentryStructuredLogger Logger { get; }

/// <summary>
/// Generates and sends metrics to Sentry.
/// </summary>
/// <remarks>
/// Available options:
/// <list type="bullet">
/// <item><see cref="Sentry.SentryOptions.ExperimentalSentryOptions.EnableMetrics"/></item>
/// <item><see cref="Sentry.SentryOptions.ExperimentalSentryOptions.SetBeforeSendMetric(System.Func{SentryMetric, SentryMetric})"/></item>
/// </list>
/// </remarks>
[Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")]
public SentryMetricEmitter Metrics { get; }

/// <summary>
/// Starts a transaction.
/// </summary>
Expand Down
Loading
Loading