diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 06b2b68..54c8cd0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,8 +11,9 @@ permissions:
contents: write
jobs:
- build-and-test:
- name: Build, Test, and Benchmark
+ # 1. Первая работа: только сборка проекта
+ build:
+ name: Build Solution
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false || github.event_name == 'push'
@@ -38,10 +39,62 @@ jobs:
- name: Build solution
run: dotnet build --configuration Release --no-restore /p:TreatWarningsAsErrors=true
+ # Сохраняем скомпилированные файлы, чтобы не собирать их заново в других jobs
+ - name: Upload Build Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: dotnet-build-artifacts
+ path: |
+ **/bin/Release/
+ **/obj/Release/
+ retention-days: 1
+
+ # 2. Вторая работа: запуск тестов (запустится только после успешной сборки)
+ test:
+ name: Run Tests
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ # Скачиваем скомпилированные файлы из шага Build
+ - name: Download Build Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: dotnet-build-artifacts
+
- name: Run Tests
run: dotnet test --configuration Release --no-build --verbosity normal
+ # 3. Третья работа: запуск бенчмарков (запустится параллельно с тестами после сборки)
+ benchmark:
+ name: Run Benchmarks
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ # Скачиваем скомпилированные файлы из шага Build
+ - name: Download Build Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: dotnet-build-artifacts
+
+ - name: Grant execute permissions
+ run: chmod +x src/EasyTrace.Benchmarks/bin/Release/net9.0/EasyTrace.Benchmarks
+
- name: Run Benchmarks
run: |
- dotnet run --project src/EasyTrace.Benchmarks/EasyTrace.Benchmarks.csproj --configuration Release -- --filter '*'
-
+ dotnet run --project src/EasyTrace.Benchmarks/EasyTrace.Benchmarks.csproj --configuration Release --no-build -- --filter * --anyCategories AllowOnCI
diff --git a/README.md b/README.md
index e423233..2642f71 100644
--- a/README.md
+++ b/README.md
@@ -3,40 +3,64 @@
[](https://github.com/bugRanger/EasyTrace/actions/workflows/ci.yml)
# EasyTrace
+
A lightweight project for tracing high-load systems.
## Description
-The goal of the repository is to offer an alternative [System.Diagnostics.Activity](https://learn.microsoft.com/ru-ru/dotnet/api/system.diagnostics.activity?view=net-9.0) that is less expensive in terms of RAM and garbage collection load.
+
+The goal of the repository is to offer an
+alternative [System.Diagnostics.Activity](https://learn.microsoft.com/ru-ru/dotnet/api/system.diagnostics.activity?view=net-9.0)
+that is less expensive in terms of RAM and garbage collection load.
### Benchmarks
+
```
+
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6456/22H2/2022Update)
-AMD Ryzen 7 5700G with Radeon Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
-.NET SDK 10.0.103
- [Host] : .NET 9.0.13 (9.0.13, 9.0.1326.6317), X64 RyuJIT x86-64-v3
- DefaultJob : .NET 9.0.13 (9.0.13, 9.0.1326.6317), X64 RyuJIT x86-64-v3
-
-
-| Method | Iterations | ParallelLimit | IsExporter | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
-|------------------- |----------- |-------------- |----------- |-------------:|-----------:|-----------:|-------------:|----------:|--------:|------------:|
-| ActivitySource | 1000 | 4 | False | 45.394 us | 0.9058 us | 2.3054 us | 45.285 us | 0.2441 | - | 2.46 KB |
-| TraceActivityScope | 1000 | 4 | False | 5.169 us | 0.0343 us | 0.0320 us | 5.181 us | 0.3052 | - | 2.5 KB |
-| ActivitySource | 1000 | 4 | True | 698.665 us | 6.8543 us | 6.4115 us | 698.552 us | 588.8672 | 8.7891 | 4752.48 KB |
-| TraceActivityScope | 1000 | 4 | True | 349.706 us | 6.9106 us | 15.8783 us | 341.561 us | - | - | 2.46 KB |
-| ActivitySource | 10000 | 4 | False | 377.078 us | 7.3748 us | 9.8451 us | 374.799 us | - | - | 2.46 KB |
-| TraceActivityScope | 10000 | 4 | False | 25.074 us | 0.4772 us | 0.4230 us | 25.096 us | 0.2747 | - | 2.45 KB |
-| ActivitySource | 10000 | 4 | True | 7,855.249 us | 74.1372 us | 69.3480 us | 7,867.755 us | 5882.8125 | 46.8750 | 47502.54 KB |
-| TraceActivityScope | 10000 | 4 | True | 3,491.807 us | 18.0259 us | 16.8614 us | 3,494.504 us | - | - | 2.52 KB |
+AMD Ryzen 3 2200G with Radeon Vega Graphics 3.50GHz, 1 CPU, 4 logical and 4 physical cores
+.NET SDK 10.0.302
+ [Host] : .NET 9.0.14 (9.0.14, 9.0.1426.11910), X64 RyuJIT x86-64-v3
+ DefaultJob : .NET 9.0.14 (9.0.14, 9.0.1426.11910), X64 RyuJIT x86-64-v3
+
```
+| Method | Iterations | ParallelLimit | IsExporter | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
+|--------------------|------------|---------------|------------|----------------:|---------------:|---------------:|----------------:|--------:|--------------:|----------------:|----------------:|
+| **ActivitySource** | **1000** | **4** | **False** | **1,780.34 μs** | **31.973 μs** | **28.343 μs** | **baseline** | **** | **2345.7031** | **4752.46 KB** | **** |
+| TraceActivityScope | 1000 | 4 | False | 11.83 μs | 0.208 μs | 0.194 μs | 150.554x faster | 3.31x | 1.0681 | 2.16 KB | 2,205.036x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **4** | **True** | **1,899.03 μs** | **37.965 μs** | **43.720 μs** | **baseline** | **** | **2343.7500** | **4752.46 KB** | **** |
+| TraceActivityScope | 1000 | 4 | True | 1,839.84 μs | 35.923 μs | 53.769 μs | 1.03x faster | 0.04x | - | 3.73 KB | 1,273.290x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **8** | **False** | **3,714.61 μs** | **72.337 μs** | **96.568 μs** | **baseline** | **** | **4683.5938** | **9502.8 KB** | **** |
+| TraceActivityScope | 1000 | 8 | False | 17.41 μs | 0.246 μs | 0.205 μs | 213.402x faster | 5.95x | 1.1597 | 2.35 KB | 4,041.059x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **8** | **True** | **4,131.53 μs** | **81.874 μs** | **114.776 μs** | **baseline** | **** | **4679.6875** | **9502.7 KB** | **** |
+| TraceActivityScope | 1000 | 8 | True | 3,327.28 μs | 64.202 μs | 81.195 μs | 1.24x faster | 0.05x | - | 4.53 KB | 2,098.958x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **16** | **False** | **7,640.13 μs** | **115.598 μs** | **102.475 μs** | **baseline** | **** | **9367.1875** | **19003.47 KB** | **** |
+| TraceActivityScope | 1000 | 16 | False | 26.51 μs | 0.521 μs | 0.913 μs | 288.534x faster | 10.53x | 1.2817 | 2.64 KB | 7,209.911x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **16** | **True** | **8,312.79 μs** | **161.504 μs** | **198.342 μs** | **baseline** | **** | **9359.3750** | **19003.05 KB** | **** |
+| TraceActivityScope | 1000 | 16 | True | 6,747.51 μs | 114.312 μs | 106.928 μs | 1.23x faster | 0.03x | - | 6.88 KB | 2,762.510x less |
+
## 💡 Usage
Provide a quick example of how to use your code or run the application:
```csharp
var source = new TraceActivitySourceBuilder()
+ // set your resources.
+ .SetResources(new MyDefaultResources())
+ // add your resources.
+ .AddResources(new MyAdditionalResources())
+ // add intercept in your impl.
+ .AddInterceptor(new MyInterceptor())
+ // set batch processing for all exporters.
.SetBatchExportOptions(new BatchExportOptions())
- // your impl for export.
+ // add export in OTLP (HTTP/1.1 + Protobuf)
+ .AddOtlpExporter(new HttpExportParameters())
+ // add export in your impl.
.AddExporter(new MyExport())
.Build("MySource");
@@ -47,14 +71,22 @@ using (var scope = source.Start())
```
## Authors
+
Contributors names and contact info
[@bugRanger](https://github.com/bugRanger)
## 🤝 Contributing
+
Contributions are welcome. Please fork the repository, create a feature branch, and submit a pull request.
+## 💖 Acknowledgments
+
+* A huge thanks to **[NetCoreServer](https://github.com/chronoxor/NetCoreServer)**, their library helped me achieve
+ maximum export efficiency 👍
+
## 📄 License
-This project is licensed under the [MIT License](LICENSE-MIT)
+
+This project is licensed under the [MIT License](LICENSE)
diff --git a/docs/http2.pcapng b/docs/http2.pcapng
new file mode 100644
index 0000000..f7c5298
Binary files /dev/null and b/docs/http2.pcapng differ
diff --git a/src/EasyTrace.Benchmarks/EasyTrace.Benchmarks.csproj b/src/EasyTrace.Benchmarks/EasyTrace.Benchmarks.csproj
index 94309d7..b2764f3 100644
--- a/src/EasyTrace.Benchmarks/EasyTrace.Benchmarks.csproj
+++ b/src/EasyTrace.Benchmarks/EasyTrace.Benchmarks.csproj
@@ -16,10 +16,11 @@
+
+
-
diff --git a/src/EasyTrace.Benchmarks/MemoryBenchmark.cs b/src/EasyTrace.Benchmarks/MemoryBenchmark.cs
index 0c08092..b469cbf 100644
--- a/src/EasyTrace.Benchmarks/MemoryBenchmark.cs
+++ b/src/EasyTrace.Benchmarks/MemoryBenchmark.cs
@@ -1,12 +1,16 @@
-using System.Diagnostics;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
-using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using EasyTrace.Benchmarks.TestData;
+using OpenTelemetry;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
namespace EasyTrace.Benchmarks;
+[BenchmarkCategory("AllowOnCI")]
[MemoryDiagnoser]
[JsonExporterAttribute.Full]
[JsonExporterAttribute.FullCompressed]
@@ -15,53 +19,27 @@ public class MemoryBenchmark
public static void Run() => BenchmarkRunner.Run();
private static ActivitySource? _activitySource;
- private static TraceActivitySource? _traceActivitySource;
+ private static FakeProcessor? _activityProcessor;
- private ActivityListener? _listener;
- private ulong _activityCounter;
+ private static TraceActivitySource? _traceActivitySource;
+ private static FakeInterceptor? _traceActivityInterceptor;
- [Params(1_000, 10_000)] public int Iterations { get; set; }
+ [Params(1_000)] public int Iterations { get; set; }
- [Params(4)] public int ParallelLimit { get; set; }
+ [Params(4, 8, 16)] public int ParallelLimit { get; set; }
[Params(true, false)] public bool IsExporter { get; set; }
[GlobalSetup]
public void Setup()
{
- _activitySource = new ActivitySource(nameof(MemoryBenchmark));
- if (IsExporter)
- {
- _listener = new ActivityListener
- {
- ShouldListenTo = s => s.Name == nameof(MemoryBenchmark),
- Sample = (ref ActivityCreationOptions _) =>
- ActivitySamplingResult.AllData
- };
- _listener.ActivityStopped += _ => Interlocked.Increment(ref _activityCounter);
- System.Diagnostics.ActivitySource.AddActivityListener(_listener);
- }
-
- var traceActivitySourceBuilder = new TraceActivitySourceBuilder();
- if (IsExporter)
- {
- traceActivitySourceBuilder.AddExporter(new FakeExporter(() => Interlocked.Increment(ref _activityCounter)));
- }
-
- _traceActivitySource = traceActivitySourceBuilder.Build(nameof(MemoryBenchmark));
- }
-
- [GlobalCleanup]
- public void Cleanup()
- {
- _listener?.Dispose();
+ SetupActivitySource();
+ SetupTraceActivitySource();
}
- [Benchmark]
+ [Benchmark(Baseline = true)]
public ulong ActivitySource()
{
- _activityCounter = 0;
-
Parallel.For(0,
ParallelLimit,
i =>
@@ -74,14 +52,12 @@ public ulong ActivitySource()
}
});
- return Interlocked.Read(ref _activityCounter);
+ return _activityProcessor!.TotalEvents;
}
[Benchmark]
public ulong TraceActivityScope()
{
- _activityCounter = 0;
-
Parallel.For(0,
ParallelLimit,
i =>
@@ -94,6 +70,38 @@ public ulong TraceActivityScope()
}
});
- return Interlocked.Read(ref _activityCounter);
+ return _traceActivityInterceptor!.TotalEvents;
+ }
+
+ private void SetupActivitySource()
+ {
+ _activityProcessor = new FakeProcessor();
+ _activitySource = new ActivitySource(nameof(OtlpExportBenchmark));
+ var builder = Sdk.CreateTracerProviderBuilder()
+ .SetResourceBuilder(ResourceBuilder.CreateDefault())
+ .AddSource(_activitySource.Name);
+
+ if (IsExporter)
+ {
+ // In their implementation, the processor acts as an exporter that listens to activity states.
+ builder.AddProcessor(_activityProcessor);
+ }
+
+ _ = builder.Build();
+ }
+
+ private void SetupTraceActivitySource()
+ {
+ _traceActivityInterceptor = new FakeInterceptor();
+
+ var builder = new TraceActivitySourceBuilder()
+ .AddInterceptor(_traceActivityInterceptor);
+
+ if (IsExporter)
+ {
+ builder.AddExporter(new FakeExporter());
+ }
+
+ _traceActivitySource = builder.Build(nameof(OtlpExportBenchmark));
}
}
\ No newline at end of file
diff --git a/src/EasyTrace.Benchmarks/OtlpExportBenchmark.cs b/src/EasyTrace.Benchmarks/OtlpExportBenchmark.cs
new file mode 100644
index 0000000..35deca5
--- /dev/null
+++ b/src/EasyTrace.Benchmarks/OtlpExportBenchmark.cs
@@ -0,0 +1,106 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using BenchmarkDotNet.Attributes;
+using EasyTrace.Benchmarks.TestData;
+using EasyTrace.Export.Batch;
+using EasyTrace.Export.Otlp.Http;
+using OpenTelemetry;
+using OpenTelemetry.Exporter;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+namespace EasyTrace.Benchmarks;
+
+///
+/// Comparative analysis of export to a running process with export via OTLP.
+///
+///
+/// This benchmark does NOT start the process that will receive data via OTLP.
+/// This process must be running before running the benchmark.
+///
+[MemoryDiagnoser]
+[JsonExporterAttribute.Full]
+[JsonExporterAttribute.FullCompressed]
+public class OtlpExportBenchmark
+{
+ public static void Run() => BenchmarkRunner.Run();
+
+ private static ActivitySource? _activitySource;
+ private static FakeProcessor? _activityProcessor;
+
+ private static TraceActivitySource? _traceActivitySource;
+ private static FakeInterceptor? _traceActivityInterceptor;
+
+ private const int ExportBatchSize = 3;
+ private const int ExportDelayInMs = 50;
+
+ [Params(100)] public int Iterations { get; set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ SetupActivitySource();
+ SetupTraceActivity();
+ }
+
+ [Benchmark(Baseline = true)]
+ public ulong ActivitySource()
+ {
+ foreach (var _ in Enumerable.Repeat(0, Iterations))
+ {
+ using var activity1 = _activitySource!.StartActivity();
+ using var activity2 = _activitySource!.StartActivity();
+ using var activity3 = _activitySource!.StartActivity();
+ Thread.Sleep(ExportDelayInMs);
+ }
+
+ return _activityProcessor!.TotalEvents;
+ }
+
+ [Benchmark]
+ public ulong TraceActivity()
+ {
+ foreach (var _ in Enumerable.Repeat(0, Iterations))
+ {
+ using var activity1 = _traceActivitySource!.Start();
+ using var activity2 = _traceActivitySource!.Start();
+ using var activity3 = _traceActivitySource!.Start();
+ Thread.Sleep(ExportDelayInMs);
+ }
+
+ return _traceActivityInterceptor!.TotalEvents;
+ }
+
+ private static void SetupActivitySource()
+ {
+ _activityProcessor = new FakeProcessor();
+ _activitySource = new ActivitySource(nameof(OtlpExportBenchmark));
+ _ = Sdk.CreateTracerProviderBuilder()
+ .SetResourceBuilder(ResourceBuilder.CreateDefault())
+ .AddSource(_activitySource.Name)
+ .AddProcessor(_activityProcessor)
+ .AddOtlpExporter(options =>
+ {
+ options.Protocol = OtlpExportProtocol.HttpProtobuf;
+ options.BatchExportProcessorOptions.MaxQueueSize = ExportBatchSize * 2;
+ options.BatchExportProcessorOptions.MaxExportBatchSize = ExportBatchSize;
+ })
+ .Build();
+ }
+
+ private static void SetupTraceActivity()
+ {
+ _traceActivityInterceptor = new FakeInterceptor();
+ _traceActivitySource = new TraceActivitySourceBuilder()
+ .SetBatchExportOptions(new BatchExportOptions
+ {
+ MaxQueueSize = ExportBatchSize * 2,
+ MaxExportBatchSize = ExportBatchSize,
+ })
+ .AddInterceptor(_traceActivityInterceptor)
+ .AddOtlpExporter(new HttpExportParameters())
+ .Build(nameof(OtlpExportBenchmark));
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace.Benchmarks/TestData/FakeExporter.cs b/src/EasyTrace.Benchmarks/TestData/FakeExporter.cs
index f393db3..d3be97c 100644
--- a/src/EasyTrace.Benchmarks/TestData/FakeExporter.cs
+++ b/src/EasyTrace.Benchmarks/TestData/FakeExporter.cs
@@ -1,13 +1,16 @@
-using System;
+using System.Threading;
using EasyTrace.Activity;
using EasyTrace.Export;
namespace EasyTrace.Benchmarks.TestData;
-internal sealed class FakeExporter(Action action) : ITraceActivityExporter
+internal sealed class FakeExporter : ITraceActivityExporter
{
- public void Export(scoped in TraceActivityRef activityRef)
+ private int _exportedCounter;
+
+ public void Export(scoped in TraceActivityRef activityRef) => Interlocked.Increment(ref _exportedCounter);
+
+ public void Flush()
{
- action();
}
}
\ No newline at end of file
diff --git a/src/EasyTrace.Benchmarks/TestData/FakeIdentGenerator.cs b/src/EasyTrace.Benchmarks/TestData/FakeIdentGenerator.cs
new file mode 100644
index 0000000..f511b8c
--- /dev/null
+++ b/src/EasyTrace.Benchmarks/TestData/FakeIdentGenerator.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections;
+using System.Diagnostics;
+using EasyTrace.Identifier;
+
+namespace EasyTrace.Benchmarks.TestData;
+
+internal class FakeIdentGenerator(IEnumerator traceIds, IEnumerator spanIds) : ITraceIdentifierGenerator
+{
+ public static FakeIdentGenerator Infinity()
+ {
+ return new FakeIdentGenerator(Cycle(ActivityTraceId.CreateRandom()), Cycle(ActivitySpanId.CreateRandom()));
+ }
+
+ public void Generate(Span bytes)
+ {
+ switch (bytes.Length)
+ {
+ case 8:
+ spanIds.MoveNext();
+ ((ActivitySpanId)spanIds.Current).CopyTo(bytes);
+ break;
+ case 16:
+ traceIds.MoveNext();
+ ((ActivityTraceId)traceIds.Current).CopyTo(bytes);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(bytes.Length));
+ }
+ }
+
+ private static IEnumerator Cycle(T item)
+ {
+ while (true)
+ {
+ yield return item;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace.Benchmarks/TestData/FakeInterceptor.cs b/src/EasyTrace.Benchmarks/TestData/FakeInterceptor.cs
new file mode 100644
index 0000000..5998227
--- /dev/null
+++ b/src/EasyTrace.Benchmarks/TestData/FakeInterceptor.cs
@@ -0,0 +1,17 @@
+using System.Threading;
+using EasyTrace.Activity;
+using EasyTrace.Interceptor;
+
+namespace EasyTrace.Benchmarks.TestData;
+
+internal sealed class FakeInterceptor : ITraceActivityInterceptor
+{
+ private uint _startedCounter;
+ private uint _stoppedCounter;
+
+ public uint TotalEvents => _startedCounter + _stoppedCounter;
+
+ public void Start(scoped in TraceActivityRef activityRef) => Interlocked.Increment(ref _startedCounter);
+
+ public void Stop(scoped in TraceActivityRef activityRef) => Interlocked.Increment(ref _stoppedCounter);
+}
\ No newline at end of file
diff --git a/src/EasyTrace.Benchmarks/TestData/FakeProcessor.cs b/src/EasyTrace.Benchmarks/TestData/FakeProcessor.cs
new file mode 100644
index 0000000..d59414f
--- /dev/null
+++ b/src/EasyTrace.Benchmarks/TestData/FakeProcessor.cs
@@ -0,0 +1,20 @@
+using System.Threading;
+using OpenTelemetry;
+
+namespace EasyTrace.Benchmarks.TestData;
+
+internal sealed class FakeProcessor : BaseProcessor
+{
+ private uint _startedCounter;
+ private uint _stoppedCounter;
+
+ public uint TotalEvents => _startedCounter + _stoppedCounter;
+
+ public override void OnStart(System.Diagnostics.Activity activity) => Interlocked.Increment(ref _startedCounter);
+
+ public override void OnEnd(System.Diagnostics.Activity activity) => Interlocked.Increment(ref _stoppedCounter);
+
+ protected override bool OnForceFlush(int timeoutMilliseconds) => true;
+
+ protected override bool OnShutdown(int timeoutMilliseconds) => true;
+}
\ No newline at end of file
diff --git a/src/EasyTrace.Benchmarks/bin/Release/net9.0/results/EasyTrace.Benchmarks.ExportBenchmark-report-github.md b/src/EasyTrace.Benchmarks/bin/Release/net9.0/results/EasyTrace.Benchmarks.ExportBenchmark-report-github.md
new file mode 100644
index 0000000..3374a56
--- /dev/null
+++ b/src/EasyTrace.Benchmarks/bin/Release/net9.0/results/EasyTrace.Benchmarks.ExportBenchmark-report-github.md
@@ -0,0 +1,14 @@
+```
+
+BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6456/22H2/2022Update)
+AMD Ryzen 7 5700G with Radeon Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 10.0.302
+ [Host] : .NET 9.0.18 (9.0.18, 9.0.1826.31522), X64 RyuJIT x86-64-v3
+ DefaultJob : .NET 9.0.18 (9.0.18, 9.0.1826.31522), X64 RyuJIT x86-64-v3
+
+
+```
+| Method | Iterations | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
+|--------------- |----------- |--------:|---------:|---------:|-------------:|--------:|----------:|------------:|
+| ActivitySource | 100 | 6.273 s | 0.0159 s | 0.0149 s | baseline | | 319.57 KB | |
+| TraceActivity | 100 | 6.278 s | 0.0069 s | 0.0065 s | 1.00x slower | 0.00x | 10.09 KB | 31.68x less |
diff --git a/src/EasyTrace.Benchmarks/bin/Release/net9.0/results/EasyTrace.Benchmarks.MemoryBenchmark-report-github.md b/src/EasyTrace.Benchmarks/bin/Release/net9.0/results/EasyTrace.Benchmarks.MemoryBenchmark-report-github.md
index 9fa09e4..5f6f683 100644
--- a/src/EasyTrace.Benchmarks/bin/Release/net9.0/results/EasyTrace.Benchmarks.MemoryBenchmark-report-github.md
+++ b/src/EasyTrace.Benchmarks/bin/Release/net9.0/results/EasyTrace.Benchmarks.MemoryBenchmark-report-github.md
@@ -1,20 +1,29 @@
```
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6456/22H2/2022Update)
-AMD Ryzen 7 5700G with Radeon Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
-.NET SDK 10.0.103
- [Host] : .NET 9.0.13 (9.0.13, 9.0.1326.6317), X64 RyuJIT x86-64-v3
- DefaultJob : .NET 9.0.13 (9.0.13, 9.0.1326.6317), X64 RyuJIT x86-64-v3
+AMD Ryzen 3 2200G with Radeon Vega Graphics 3.50GHz, 1 CPU, 4 logical and 4 physical cores
+.NET SDK 10.0.302
+ [Host] : .NET 9.0.14 (9.0.14, 9.0.1426.11910), X64 RyuJIT x86-64-v3
+ DefaultJob : .NET 9.0.14 (9.0.14, 9.0.1426.11910), X64 RyuJIT x86-64-v3
```
-| Method | Iterations | ParallelLimit | IsExporter | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
-|----------------- |----------- |-------------- |----------- |------------:|----------:|-----------:|------------:|----------:|--------:|------------:|
-| **ActivitySource** | **1000** | **4** | **False** | **43.42 μs** | **0.866 μs** | **2.252 μs** | **43.36 μs** | **0.2441** | **-** | **2.48 KB** |
-| TraceActivityRef | 1000 | 4 | False | 16.15 μs | 0.251 μs | 0.222 μs | 16.07 μs | 0.3052 | - | 2.48 KB |
-| **ActivitySource** | **1000** | **4** | **True** | **754.08 μs** | **14.908 μs** | **28.364 μs** | **754.12 μs** | **588.8672** | **9.7656** | **4752.54 KB** |
-| TraceActivityRef | 1000 | 4 | True | 341.85 μs | 2.581 μs | 2.015 μs | 341.62 μs | - | - | 2.51 KB |
-| **ActivitySource** | **10000** | **4** | **False** | **394.35 μs** | **7.847 μs** | **13.744 μs** | **392.32 μs** | **-** | **-** | **2.5 KB** |
-| TraceActivityRef | 10000 | 4 | False | 139.95 μs | 2.774 μs | 4.401 μs | 139.84 μs | 0.2441 | - | 2.49 KB |
-| **ActivitySource** | **10000** | **4** | **True** | **6,722.27 μs** | **93.437 μs** | **87.401 μs** | **6,726.45 μs** | **5882.8125** | **54.6875** | **47502.54 KB** |
-| TraceActivityRef | 10000 | 4 | True | 3,500.19 μs | 69.626 μs | 129.055 μs | 3,564.43 μs | - | - | 2.53 KB |
+| Method | Iterations | ParallelLimit | IsExporter | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
+|------------------- |----------- |-------------- |----------- |------------:|-----------:|-----------:|----------------:|--------:|----------:|------------:|----------------:|
+| **ActivitySource** | **1000** | **4** | **False** | **1,780.34 μs** | **31.973 μs** | **28.343 μs** | **baseline** | **** | **2345.7031** | **4752.46 KB** | **** |
+| TraceActivityScope | 1000 | 4 | False | 11.83 μs | 0.208 μs | 0.194 μs | 150.554x faster | 3.31x | 1.0681 | 2.16 KB | 2,205.036x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **4** | **True** | **1,899.03 μs** | **37.965 μs** | **43.720 μs** | **baseline** | **** | **2343.7500** | **4752.46 KB** | **** |
+| TraceActivityScope | 1000 | 4 | True | 1,839.84 μs | 35.923 μs | 53.769 μs | 1.03x faster | 0.04x | - | 3.73 KB | 1,273.290x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **8** | **False** | **3,714.61 μs** | **72.337 μs** | **96.568 μs** | **baseline** | **** | **4683.5938** | **9502.8 KB** | **** |
+| TraceActivityScope | 1000 | 8 | False | 17.41 μs | 0.246 μs | 0.205 μs | 213.402x faster | 5.95x | 1.1597 | 2.35 KB | 4,041.059x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **8** | **True** | **4,131.53 μs** | **81.874 μs** | **114.776 μs** | **baseline** | **** | **4679.6875** | **9502.7 KB** | **** |
+| TraceActivityScope | 1000 | 8 | True | 3,327.28 μs | 64.202 μs | 81.195 μs | 1.24x faster | 0.05x | - | 4.53 KB | 2,098.958x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **16** | **False** | **7,640.13 μs** | **115.598 μs** | **102.475 μs** | **baseline** | **** | **9367.1875** | **19003.47 KB** | **** |
+| TraceActivityScope | 1000 | 16 | False | 26.51 μs | 0.521 μs | 0.913 μs | 288.534x faster | 10.53x | 1.2817 | 2.64 KB | 7,209.911x less |
+| | | | | | | | | | | | |
+| **ActivitySource** | **1000** | **16** | **True** | **8,312.79 μs** | **161.504 μs** | **198.342 μs** | **baseline** | **** | **9359.3750** | **19003.05 KB** | **** |
+| TraceActivityScope | 1000 | 16 | True | 6,747.51 μs | 114.312 μs | 106.928 μs | 1.23x faster | 0.03x | - | 6.88 KB | 2,762.510x less |
diff --git a/src/EasyTrace.Benchmarks/packages.lock.json b/src/EasyTrace.Benchmarks/packages.lock.json
index 8028925..8a83d44 100644
--- a/src/EasyTrace.Benchmarks/packages.lock.json
+++ b/src/EasyTrace.Benchmarks/packages.lock.json
@@ -30,6 +30,27 @@
"Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21"
}
},
+ "OpenTelemetry": {
+ "type": "Direct",
+ "requested": "[1.18.0, )",
+ "resolved": "1.18.0",
+ "contentHash": "13AKxycK+olOLe061MjliPTNBR/DUkjyRr0b9zwUhKyMjFS8lXS7G31R85rkbM55up2YFmdykWdAw2giBDJpiQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.0",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Logging.Configuration": "9.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "1.18.0"
+ }
+ },
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
+ "type": "Direct",
+ "requested": "[1.18.0, )",
+ "resolved": "1.18.0",
+ "contentHash": "Et3WAviooKGObZjUZN8d6eTsdJs1YM3ZoN+KlWSAHksTmHG1nyGi9XGo2wb0fkFMWK0g80FOPdR1njg52Dm+aw==",
+ "dependencies": {
+ "OpenTelemetry": "1.18.0"
+ }
+ },
"BenchmarkDotNet.Annotations": {
"type": "Transitive",
"resolved": "0.15.8",
@@ -111,54 +132,121 @@
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "YIMO9T3JL8MeEXgVozKt2v79hquo/EFtnY0vgxmLnUvk1Rei/halI7kOWZL2RBeV9FMGzgM9LZA8CVaNwFMaNA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Primitives": "9.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "lqvd7W3FGKUO1+ZoUEMaZ5XDJeWvjpy2/M/ptCGz3tXLD4HWVaSzjufsAsjemasBEg+2SxXVtYVvGt5r2nKDlg==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "v5R638eNMxksfXb7MFnkPwLPp+Ym4W/SIGNuoe8qFVVyvygQD5DdLusybmYSJEr9zc1UzWzim/ATKeIOVvOFDg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.0",
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
+ }
+ },
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==",
+ "resolved": "9.0.0",
+ "contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg=="
+ "resolved": "9.0.0",
+ "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg=="
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Options": "9.0.0"
+ }
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==",
+ "resolved": "9.0.0",
+ "contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection": "6.0.0",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
- "Microsoft.Extensions.Logging.Abstractions": "6.0.0",
- "Microsoft.Extensions.Options": "6.0.0",
- "System.Diagnostics.DiagnosticSource": "6.0.0"
+ "Microsoft.Extensions.DependencyInjection": "9.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Options": "9.0.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA=="
+ "resolved": "9.0.0",
+ "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0"
+ }
+ },
+ "Microsoft.Extensions.Logging.Configuration": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "H05HiqaNmg6GjH34ocYE9Wm1twm3Oz2aXZko8GTwGBzM7op2brpAA8pJ5yyD1OpS1mXUtModBYOlcZ/wXeWsSg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.0",
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "9.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Logging": "9.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Options": "9.0.0",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0"
+ }
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==",
+ "resolved": "9.0.0",
+ "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
- "Microsoft.Extensions.Primitives": "6.0.0"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Primitives": "9.0.0"
}
},
- "Microsoft.Extensions.Primitives": {
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==",
+ "resolved": "9.0.0",
+ "contentHash": "Ob3FXsXkcSMQmGZi7qP07EQ39kZpSBlTcAZLbJLdI4FIf0Jug8biv2HTavWmnTirchctPlq9bl/26CXtQRguzA==",
"dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "9.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Options": "9.0.0",
+ "Microsoft.Extensions.Primitives": "9.0.0"
}
},
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg=="
+ },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "5.0.0",
@@ -173,6 +261,28 @@
"System.Security.Principal.Windows": "5.0.0"
}
},
+ "NetCoreServer": {
+ "type": "Transitive",
+ "resolved": "8.0.7",
+ "contentHash": "BhNiJ6EVxKS1eKqlHINy7BruLDc+xCH59oZxu7d2Hve8CzFki69PumVlf6vyAQvIVfxbZiHw/FI3UHNnMajuVg=="
+ },
+ "OpenTelemetry.Api": {
+ "type": "Transitive",
+ "resolved": "1.18.0",
+ "contentHash": "Dhzm5oughIY9EWckt2J1yXcEbXpki5WCL2q7prJoYxzH6C7jdkfGtPMTkUVZZ4AhjJKzQnlC+kLSIlmIVwC4Vw==",
+ "dependencies": {
+ "System.Diagnostics.DiagnosticSource": "10.0.0"
+ }
+ },
+ "OpenTelemetry.Api.ProviderBuilderExtensions": {
+ "type": "Transitive",
+ "resolved": "1.18.0",
+ "contentHash": "io6A6PJFp3vIObRGf6oPjdHsdglkinXt/0SpTDJHrVnsLxUtOzwh/QwTsnhBJ43wJ6QcGMkyNzsSSY8XEB1Q5Q==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
+ "OpenTelemetry.Api": "1.18.0"
+ }
+ },
"Perfolizer": {
"type": "Transitive",
"resolved": "0.6.1",
@@ -198,11 +308,8 @@
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==",
- "dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
+ "resolved": "10.0.0",
+ "contentHash": "0KdBK+h7G13PuOSC2R/DalAoFMvdYMznvGRuICtkdcUMXgl/gYXsG6z4yUvTxHSMACorWgHCU1Faq0KUHU6yAQ=="
},
"System.Management": {
"type": "Transitive",
@@ -247,7 +354,10 @@
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg=="
},
"easytrace": {
- "type": "Project"
+ "type": "Project",
+ "dependencies": {
+ "NetCoreServer": "[8.0.7, )"
+ }
}
}
}
diff --git a/src/EasyTrace.Tests/Export/ActivityExportTests.cs b/src/EasyTrace.Tests/Export/ActivityExportTests.cs
index 8a05b13..6690757 100644
--- a/src/EasyTrace.Tests/Export/ActivityExportTests.cs
+++ b/src/EasyTrace.Tests/Export/ActivityExportTests.cs
@@ -4,6 +4,7 @@
namespace EasyTrace.Tests.Export;
+[TestFixture]
public class ActivityExportTests
{
private static readonly VerifySettings Settings;
@@ -29,6 +30,11 @@ public Task SingleActivity(ActivityKind kind)
.SetIdentifierGenerator(MoqIdentGenerator.Set(
ActivityTraceId.CreateFromString("0af7651916cd43dd8448eb211c80319c"),
ActivitySpanId.CreateFromString("b7ad6b7169203331")))
+ .SetBatchExportOptions(new BatchExportOptions
+ {
+ MaxExportBatchSize = 1,
+ ScheduledDelayMilliseconds = uint.MaxValue,
+ })
.AddExporter(inMemoryExporter)
.Build(nameof(ActivityExportTests));
@@ -38,6 +44,7 @@ public Task SingleActivity(ActivityKind kind)
}
// Assert
+ Task.Delay(500).Wait();
return Verify(inMemoryExporter.Items, Settings);
}
@@ -54,6 +61,11 @@ public Task GroupActivity()
ActivitySpanId.CreateFromString("b8ad6b7169203331"),
ActivitySpanId.CreateFromString("b9ad6b7169203331")
))
+ .SetBatchExportOptions(new BatchExportOptions
+ {
+ MaxExportBatchSize = 3,
+ ScheduledDelayMilliseconds = uint.MaxValue,
+ })
.AddExporter(inMemoryExporter)
.Build(nameof(ActivityExportTests));
@@ -65,6 +77,7 @@ public Task GroupActivity()
}
// Assert
+ Task.Delay(500).Wait();
return Verify(inMemoryExporter.Items, Settings);
}
diff --git a/src/EasyTrace.Tests/Export/OtlpExportTests.cs b/src/EasyTrace.Tests/Export/OtlpExportTests.cs
new file mode 100644
index 0000000..f035c3b
--- /dev/null
+++ b/src/EasyTrace.Tests/Export/OtlpExportTests.cs
@@ -0,0 +1,162 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using EasyTrace.Export.Batch;
+using EasyTrace.Export.Otlp.Http;
+using EasyTrace.Tests.TestData;
+using NetCoreServer;
+
+namespace EasyTrace.Tests.Export;
+
+public class OtlpExportTests
+{
+ private static readonly VerifySettings Settings;
+
+ static OtlpExportTests()
+ {
+ Settings = new VerifySettings();
+ Settings.UseDirectory("snapshots");
+ }
+
+ [Test]
+ public Task HttpExport()
+ {
+ var endPoint = new Uri("http://127.0.0.1:4318");
+
+ // Start local HTTP-server.
+ var server = new TestHttpServer(IPAddress.Any, endPoint.Port);
+ Assert.True(server.Start());
+ while (!server.IsStarted)
+ {
+ Thread.Yield();
+ }
+
+ try
+ {
+ // Make activity source.
+ var source = new TraceActivitySourceBuilder()
+ .SetTimeProvider(new MoqTimeProvider())
+ .SetIdentifierGenerator(MoqIdentGenerator.Set(
+ ActivityTraceId.CreateFromString("0af7651916cd43dd8448eb211c80319c"),
+ ActivitySpanId.CreateFromString("b7ad6b7169203331"),
+ ActivitySpanId.CreateFromString("b9ad6b7169203331")
+ ))
+ .SetResources(new Dictionary
+ {
+ ["telemetry.sdk.name"] = "easytrace",
+ ["telemetry.sdk.language"] = "dotnet",
+ ["telemetry.sdk.version"] = "1.0.0",
+ ["service.name"] = "unknown",
+ })
+ .SetBatchExportOptions(new BatchExportOptions
+ {
+ MaxExportBatchSize = 2,
+ ScheduledDelayMilliseconds = uint.MaxValue,
+ })
+ .AddOtlpExporter(new HttpExportParameters
+ {
+ EndPoint = endPoint,
+ BufferSize = 1024 * 4,
+ })
+ .Build(nameof(ActivityExportTests));
+
+ // TODO: Add more actions (x3 MaxExportBatchSize) to test splitting into multiple messages.
+ // Make activity for batch export.
+ {
+ using var _ = source.Start();
+ using var __ = source.Start();
+ Task.Delay(100).Wait();
+ }
+
+ // Wait sending.
+ Task.Delay(500).Wait();
+ // Check activity export in batch.
+ return Verify(TestRequestCache.GetInstanceCache(), Settings);
+ }
+ finally
+ {
+ // Stop HTTP-server.
+ Assert.True(server.Stop());
+ while (server.IsStarted)
+ {
+ Thread.Yield();
+ }
+ }
+ }
+}
+
+class TestHttpSession(HttpServer server) : HttpSession(server)
+{
+ protected override void OnReceivedRequest(HttpRequest request)
+ {
+ if (request.Method == "POST")
+ {
+ var key = request.Url;
+ var value = request.BodyBytes;
+
+ // Decode the key value
+ key = Uri.UnescapeDataString(key);
+
+ TestRequestCache.GetInstance().Set(key, value);
+ SendResponseAsync(Response.MakeOkResponse());
+ }
+ else
+ {
+ SendResponseAsync(Response.MakeErrorResponse("Unsupported HTTP method: " + request.Method));
+ }
+ }
+
+ protected override void OnReceivedRequestError(HttpRequest request, string error)
+ {
+ Console.WriteLine($"Request error: {error}");
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Console.WriteLine($"HTTP session caught an error: {error}");
+ }
+}
+
+class TestHttpServer(IPAddress address, int port) : HttpServer(address, port)
+{
+ protected override TcpSession CreateSession() => new TestHttpSession(this);
+
+ protected override void OnError(SocketError error) => Console.WriteLine($"HTTP session caught an error: {error}");
+}
+
+public class TestRequestCache
+{
+ public static TestRequestCache GetInstance()
+ {
+ _instance ??= new TestRequestCache();
+ return _instance;
+ }
+
+ public static string GetInstanceCache() => GetInstance().GetAllCache();
+
+ public void Set(string key, byte[] value)
+ {
+ _cache[key] = value;
+ }
+
+ public string GetAllCache()
+ {
+ var result = new StringBuilder();
+ result.Append("[\n");
+ foreach (var item in _cache)
+ {
+ result.Append(" {\n");
+ result.AppendFormat($" \"key\": \"{item.Key}\",\n");
+ result.AppendFormat($" \"value\": \"{string.Join(", ", item.Value)}\",\n");
+ result.Append(" },\n");
+ }
+
+ result.Append("]\n");
+ return result.ToString();
+ }
+
+ private readonly ConcurrentDictionary _cache = new();
+ private static TestRequestCache? _instance;
+}
\ No newline at end of file
diff --git a/src/EasyTrace.Tests/Export/ProtobufSerializeTests.cs b/src/EasyTrace.Tests/Export/ProtobufSerializeTests.cs
new file mode 100644
index 0000000..cbc32bd
--- /dev/null
+++ b/src/EasyTrace.Tests/Export/ProtobufSerializeTests.cs
@@ -0,0 +1,86 @@
+using System.Diagnostics;
+using EasyTrace.Activity;
+using EasyTrace.Export.Otlp.Protobuf;
+using EasyTrace.Tests.TestData;
+
+namespace EasyTrace.Tests.Export;
+
+[TestFixture]
+public class ProtobufSerializeTests
+{
+ private static readonly VerifySettings Settings;
+
+ static ProtobufSerializeTests()
+ {
+ Settings = new VerifySettings();
+ Settings.UseDirectory("snapshots");
+ }
+
+ [Test]
+ public Task SourceWithResources()
+ {
+ // Arrange
+ var moqIdentGenerator = MoqIdentGenerator.Set(
+ ActivityTraceId.CreateFromString("0af7651916cd43dd8448eb211c80319c"),
+ ActivitySpanId.CreateFromString("b7ad6b7169203331"));
+
+ var sourceEmpty = new TraceActivitySourceBuilder()
+ .SetResources(new Dictionary
+ {
+ ["telemetry.sdk.name"] = "easytrace",
+ ["telemetry.sdk.language"] = "dotnet",
+ ["telemetry.sdk.version"] = "1.0.0",
+ ["service.name"] = "unknown:dotnet",
+ })
+ .Build("TestSource");
+
+ var serializer = new ProtobufSerializer(new ProtobufStream(1_024), sourceEmpty);
+ var activity = new TraceActivity
+ {
+ OperationName = "TestActivity",
+ Kind = ActivityKind.Internal,
+ StartTime = DateTime.MinValue,
+ EndTime = DateTime.MinValue.AddSeconds(1),
+ Recorded = true,
+ };
+ activity.TraceId.Generate(moqIdentGenerator);
+ activity.SpanId.Generate(moqIdentGenerator);
+
+ // Act
+ serializer.Write(new TraceActivityRef(activity));
+
+ // Assert - verify snapshot.
+ return Verify(Convert.ToBase64String(serializer.Flush()), Settings);
+ }
+
+ [Test]
+ public Task SourceWithoutResources()
+ {
+ // Arrange
+ var moqIdentGenerator = MoqIdentGenerator.Set(
+ ActivityTraceId.CreateFromString("0af7651916cd43dd8448eb211c80319c"),
+ ActivitySpanId.CreateFromString("b7ad6b7169203331"));
+
+ var sourceEmpty = new TraceActivitySourceBuilder()
+ .SetResources([])
+ .Build("TestSource");
+
+ var serializer = new ProtobufSerializer(new ProtobufStream(1_024), sourceEmpty);
+ var activity = new TraceActivity
+ {
+ OperationName = "TestActivity",
+ Kind = ActivityKind.Internal,
+ StartTime = DateTime.MinValue.AddTicks(200),
+ EndTime = DateTime.MinValue.AddSeconds(1),
+ Recorded = true,
+ };
+ activity.TraceId.Generate(moqIdentGenerator);
+ activity.SpanId.Generate(moqIdentGenerator);
+
+ // Act
+ serializer.Write(new TraceActivityRef(activity));
+
+ // Assert - verify snapshot.
+ return Verify(Convert.ToBase64String(serializer.Flush()), Settings);
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace.Tests/Export/snapshots/OtlpExportTests.HttpExport.verified.txt b/src/EasyTrace.Tests/Export/snapshots/OtlpExportTests.HttpExport.verified.txt
new file mode 100644
index 0000000..6aa4edb
--- /dev/null
+++ b/src/EasyTrace.Tests/Export/snapshots/OtlpExportTests.HttpExport.verified.txt
@@ -0,0 +1,6 @@
+[
+ {
+ "key": "/v1/traces",
+ "value": "10, 194, 130, 128, 0, 10, 144, 129, 128, 0, 10, 161, 128, 128, 0, 10, 18, 116, 101, 108, 101, 109, 101, 116, 114, 121, 46, 115, 100, 107, 46, 110, 97, 109, 101, 18, 11, 10, 9, 101, 97, 115, 121, 116, 114, 97, 99, 101, 10, 162, 128, 128, 0, 10, 22, 116, 101, 108, 101, 109, 101, 116, 114, 121, 46, 115, 100, 107, 46, 108, 97, 110, 103, 117, 97, 103, 101, 18, 8, 10, 6, 100, 111, 116, 110, 101, 116, 10, 160, 128, 128, 0, 10, 21, 116, 101, 108, 101, 109, 101, 116, 114, 121, 46, 115, 100, 107, 46, 118, 101, 114, 115, 105, 111, 110, 18, 7, 10, 5, 49, 46, 48, 46, 48, 10, 153, 128, 128, 0, 10, 12, 115, 101, 114, 118, 105, 99, 101, 46, 110, 97, 109, 101, 18, 9, 10, 7, 117, 110, 107, 110, 111, 119, 110, 18, 168, 129, 128, 0, 10, 149, 128, 128, 0, 10, 19, 65, 99, 116, 105, 118, 105, 116, 121, 69, 120, 112, 111, 114, 116, 84, 101, 115, 116, 115, 18, 194, 128, 128, 0, 10, 16, 10, 247, 101, 25, 22, 205, 67, 221, 132, 72, 235, 33, 28, 128, 49, 156, 18, 8, 185, 173, 107, 113, 105, 32, 51, 49, 133, 1, 1, 1, 0, 0, 42, 10, 72, 116, 116, 112, 69, 120, 112, 111, 114, 116, 48, 1, 57, 32, 78, 67, 82, 207, 76, 35, 13, 65, 48, 117, 67, 82, 207, 76, 35, 13, 18, 194, 128, 128, 0, 10, 16, 10, 247, 101, 25, 22, 205, 67, 221, 132, 72, 235, 33, 28, 128, 49, 156, 18, 8, 183, 173, 107, 113, 105, 32, 51, 49, 133, 1, 1, 1, 0, 0, 42, 10, 72, 116, 116, 112, 69, 120, 112, 111, 114, 116, 48, 1, 57, 16, 39, 67, 82, 207, 76, 35, 13, 65, 64, 156, 67, 82, 207, 76, 35, 13",
+ },
+]
diff --git a/src/EasyTrace.Tests/Export/snapshots/ProtobufSerializeTests.SourceWithResources.verified.txt b/src/EasyTrace.Tests/Export/snapshots/ProtobufSerializeTests.SourceWithResources.verified.txt
new file mode 100644
index 0000000..884dc7b
--- /dev/null
+++ b/src/EasyTrace.Tests/Export/snapshots/ProtobufSerializeTests.SourceWithResources.verified.txt
@@ -0,0 +1 @@
+CvuBgAAKl4GAAAqhgIAAChJ0ZWxlbWV0cnkuc2RrLm5hbWUSCwoJZWFzeXRyYWNlCqKAgAAKFnRlbGVtZXRyeS5zZGsubGFuZ3VhZ2USCAoGZG90bmV0CqCAgAAKFXRlbGVtZXRyeS5zZGsudmVyc2lvbhIHCgUxLjAuMAqggIAACgxzZXJ2aWNlLm5hbWUSEAoOdW5rbm93bjpkb3RuZXQS2oCAAAqMgIAACgpUZXN0U291cmNlEsSAgAAKEAr3ZRkWzUPdhEjrIRyAMZwSCLeta3FpIDMxhQEBAQAAKgxUZXN0QWN0aXZpdHkwATkAABo96wOyoUEAyrR46wOyoQ==
\ No newline at end of file
diff --git a/src/EasyTrace.Tests/Export/snapshots/ProtobufSerializeTests.SourceWithoutResources.verified.txt b/src/EasyTrace.Tests/Export/snapshots/ProtobufSerializeTests.SourceWithoutResources.verified.txt
new file mode 100644
index 0000000..deeaabc
--- /dev/null
+++ b/src/EasyTrace.Tests/Export/snapshots/ProtobufSerializeTests.SourceWithoutResources.verified.txt
@@ -0,0 +1 @@
+CuSAgAAKgICAABLagIAACoyAgAAKClRlc3RTb3VyY2USxICAAAoQCvdlGRbNQ92ESOshHIAxnBIIt61rcWkgMzGFAQEBAAAqDFRlc3RBY3Rpdml0eTABOSBOGj3rA7KhQQDKtHjrA7Kh
\ No newline at end of file
diff --git a/src/EasyTrace.Tests/TestData/MoqExportInMemory.cs b/src/EasyTrace.Tests/TestData/MoqExportInMemory.cs
index bb5290d..cdd2a97 100644
--- a/src/EasyTrace.Tests/TestData/MoqExportInMemory.cs
+++ b/src/EasyTrace.Tests/TestData/MoqExportInMemory.cs
@@ -15,7 +15,11 @@ public void Export(scoped in TraceActivityRef activityRef)
$"{nameof(ITraceActivity.Source)}: {activityRef.Source.Name} {activityRef.Source.Version}|" +
$"{nameof(ITraceActivity.OperationName)}: {activityRef.OperationName}|" +
$"{nameof(ITraceActivity.Kind)}: {activityRef.Kind}|" +
- $"{nameof(ITraceActivity.StartTime)}: {activityRef.StartTime}|" +
- $"{nameof(ITraceActivity.EndTime)}: {activityRef.EndTime}");
+ $"{nameof(ITraceActivity.StartTime)}: {activityRef.StartTime.TimeOfDay}|" +
+ $"{nameof(ITraceActivity.EndTime)}: {activityRef.EndTime.TimeOfDay}");
+ }
+
+ public void Flush()
+ {
}
}
\ No newline at end of file
diff --git a/src/EasyTrace.Tests/TestData/MoqTimeProvider.cs b/src/EasyTrace.Tests/TestData/MoqTimeProvider.cs
index 93a45f7..14c0750 100644
--- a/src/EasyTrace.Tests/TestData/MoqTimeProvider.cs
+++ b/src/EasyTrace.Tests/TestData/MoqTimeProvider.cs
@@ -2,8 +2,10 @@
namespace EasyTrace.Tests.TestData;
-public class MoqTimeProvider: ITraceTimeProvider
+public class MoqTimeProvider : ITraceTimeProvider
{
+ private readonly DateTime _startTime = new (2000, 1, 1);
+
private long _ticks;
private const long TickStep = 100;
@@ -12,4 +14,6 @@ public TimeSpan GetTimestamp()
_ticks += TickStep;
return new TimeSpan(_ticks);
}
+
+ public DateTime GetDateTime() => _startTime.Add(GetTimestamp());
}
\ No newline at end of file
diff --git a/src/EasyTrace.Tests/packages.lock.json b/src/EasyTrace.Tests/packages.lock.json
index e26bf6d..ca004e7 100644
--- a/src/EasyTrace.Tests/packages.lock.json
+++ b/src/EasyTrace.Tests/packages.lock.json
@@ -83,6 +83,11 @@
"Newtonsoft.Json": "13.0.1"
}
},
+ "NetCoreServer": {
+ "type": "Transitive",
+ "resolved": "8.0.7",
+ "contentHash": "BhNiJ6EVxKS1eKqlHINy7BruLDc+xCH59oZxu7d2Hve8CzFki69PumVlf6vyAQvIVfxbZiHw/FI3UHNnMajuVg=="
+ },
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.1",
@@ -109,7 +114,10 @@
}
},
"easytrace": {
- "type": "Project"
+ "type": "Project",
+ "dependencies": {
+ "NetCoreServer": "[8.0.7, )"
+ }
}
}
}
diff --git a/src/EasyTrace/Activity/ITraceActivity.cs b/src/EasyTrace/Activity/ITraceActivity.cs
index 0c106c5..6446ce8 100644
--- a/src/EasyTrace/Activity/ITraceActivity.cs
+++ b/src/EasyTrace/Activity/ITraceActivity.cs
@@ -10,7 +10,9 @@ public interface ITraceActivity
TraceActivitySource Source { get; }
string OperationName { get; }
ActivityKind Kind { get; }
- TimeSpan StartTime { get; }
- TimeSpan EndTime { get; }
+ DateTime StartTime { get; }
+ DateTime EndTime { get; }
TimeSpan Duration { get; }
+ bool Recorded { get; }
+ bool RemoteParent { get; }
}
\ No newline at end of file
diff --git a/src/EasyTrace/Activity/TraceActivity.cs b/src/EasyTrace/Activity/TraceActivity.cs
index ec3d5a4..a3ffa6b 100644
--- a/src/EasyTrace/Activity/TraceActivity.cs
+++ b/src/EasyTrace/Activity/TraceActivity.cs
@@ -13,16 +13,17 @@ public class TraceActivity : ITraceActivity, ICopiable
public TraceActivitySource Source { get; internal set; } = TraceActivitySource.Empty;
public string OperationName { get; set; } = string.Empty;
public ActivityKind Kind { get; set; }
- public TimeSpan StartTime { get; set; }
- public TimeSpan EndTime { get; set; }
+ public DateTime StartTime { get; set; }
+ public DateTime EndTime { get; set; }
public TimeSpan Duration => EndTime - StartTime;
public bool Recorded { get; set; }
+ public bool RemoteParent { get; set; }
public void Clear()
{
OperationName = string.Empty;
- StartTime = TimeSpan.Zero;
- EndTime = TimeSpan.Zero;
+ StartTime = DateTime.MinValue;
+ EndTime = DateTime.MinValue;
}
public void CopyFrom(TraceActivity source)
@@ -39,6 +40,7 @@ public void CopyTo(TraceActivity destination)
destination.StartTime = StartTime;
destination.EndTime = EndTime;
destination.Recorded = Recorded;
+ destination.RemoteParent = RemoteParent;
destination.Kind = Kind;
}
}
\ No newline at end of file
diff --git a/src/EasyTrace/Activity/TraceActivityRef.cs b/src/EasyTrace/Activity/TraceActivityRef.cs
index 6edef65..0e2d48d 100644
--- a/src/EasyTrace/Activity/TraceActivityRef.cs
+++ b/src/EasyTrace/Activity/TraceActivityRef.cs
@@ -15,12 +15,16 @@ public readonly ref struct TraceActivityRef(TraceActivity activity)
public ActivityKind Kind => activity.Kind;
- public TimeSpan StartTime => activity.StartTime;
+ public DateTime StartTime => activity.StartTime;
- public TimeSpan EndTime => activity.EndTime;
+ public DateTime EndTime => activity.EndTime;
public TimeSpan Duration => activity.Duration;
+ public bool Recorded => activity.Recorded;
+
+ public bool RemoteParent => activity.RemoteParent;
+
void ICopiable.CopyFrom(TraceActivity source)
{
activity.CopyFrom(source);
diff --git a/src/EasyTrace/EasyTrace.csproj b/src/EasyTrace/EasyTrace.csproj
index 125f4c9..676fa15 100644
--- a/src/EasyTrace/EasyTrace.csproj
+++ b/src/EasyTrace/EasyTrace.csproj
@@ -6,4 +6,8 @@
enable
+
+
+
+
diff --git a/src/EasyTrace/Export/Batch/BatchExportWorker.cs b/src/EasyTrace/Export/Batch/BatchExportWorker.cs
index 2408635..21dc21c 100644
--- a/src/EasyTrace/Export/Batch/BatchExportWorker.cs
+++ b/src/EasyTrace/Export/Batch/BatchExportWorker.cs
@@ -115,8 +115,11 @@ private void ExporterProc()
var activities = CircularBuffer.Next(MaxExportBatchSize);
foreach (var activity in activities)
{
- _exporter.Export(new TraceActivityRef(activity));
+ scoped var activityRef = new TraceActivityRef(activity);
+ _exporter.Export(in activityRef);
}
+
+ _exporter.Flush();
}
}
}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Batch/BatchExporter.cs b/src/EasyTrace/Export/Batch/BatchExporter.cs
index 4e15f8d..a33e8c1 100644
--- a/src/EasyTrace/Export/Batch/BatchExporter.cs
+++ b/src/EasyTrace/Export/Batch/BatchExporter.cs
@@ -2,7 +2,7 @@
namespace EasyTrace.Export.Batch;
-public sealed class BatchExporter : ITraceActivityExporter, IDisposable
+public sealed class BatchExporter : IDisposable
where T : ITraceActivityExporter
{
private readonly BatchExportWorker _backgroundExporter;
@@ -16,7 +16,7 @@ public BatchExporter(T exporter, BatchExportOptions options)
_backgroundExporter.Start();
}
- public void Export(scoped in TraceActivityRef activityRef)
+ public void Handle(scoped in TraceActivityRef activityRef)
{
if (!_backgroundExporter.CircularBuffer.Push(in activityRef, 50_000))
{
diff --git a/src/EasyTrace/Export/Batch/GroupExporter.cs b/src/EasyTrace/Export/GroupExporter.cs
similarity index 66%
rename from src/EasyTrace/Export/Batch/GroupExporter.cs
rename to src/EasyTrace/Export/GroupExporter.cs
index deb1e53..1cea81a 100644
--- a/src/EasyTrace/Export/Batch/GroupExporter.cs
+++ b/src/EasyTrace/Export/GroupExporter.cs
@@ -1,6 +1,6 @@
using EasyTrace.Activity;
-namespace EasyTrace.Export.Batch;
+namespace EasyTrace.Export;
public sealed class GroupExporter(ITraceActivityExporter[] exporters) : ITraceActivityExporter
{
@@ -11,4 +11,12 @@ public void Export(scoped in TraceActivityRef activityRef)
exporter.Export(activityRef);
}
}
+
+ public void Flush()
+ {
+ foreach (var exporter in exporters)
+ {
+ exporter.Flush();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/ITraceActivityExporter.cs b/src/EasyTrace/Export/ITraceActivityExporter.cs
index 319ef8f..31e4513 100644
--- a/src/EasyTrace/Export/ITraceActivityExporter.cs
+++ b/src/EasyTrace/Export/ITraceActivityExporter.cs
@@ -5,4 +5,6 @@ namespace EasyTrace.Export;
public interface ITraceActivityExporter
{
void Export(scoped in TraceActivityRef activityRef);
+
+ void Flush();
}
diff --git a/src/EasyTrace/Export/Otlp/Grpc/GrpcExportParameters.cs b/src/EasyTrace/Export/Otlp/Grpc/GrpcExportParameters.cs
new file mode 100644
index 0000000..4870a44
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Grpc/GrpcExportParameters.cs
@@ -0,0 +1,14 @@
+namespace EasyTrace.Export.Otlp.Grpc;
+
+public class GrpcExportParameters
+{
+ public Uri EndPoint { get; init; } = new("http://localhost:4317");
+
+ public int BufferSize { get; init; } = 1024;
+
+ public void Validate()
+ {
+ ArgumentNullException.ThrowIfNull(EndPoint);
+ ArgumentOutOfRangeException.ThrowIfLessThan(BufferSize, 1024);
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Otlp/Grpc/GrpcExporter.cs b/src/EasyTrace/Export/Otlp/Grpc/GrpcExporter.cs
new file mode 100644
index 0000000..f76048b
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Grpc/GrpcExporter.cs
@@ -0,0 +1,64 @@
+using EasyTrace.Activity;
+using NetCoreServer;
+using FastHttpClient = NetCoreServer.HttpClient;
+
+namespace EasyTrace.Export.Otlp.Grpc;
+
+///
+/// [WIP] Export via GRPC.
+///
+internal class GrpcExporter(GrpcExportParameters parameters)
+ : FastHttpClient(parameters.EndPoint.Host, parameters.EndPoint.Port), ITraceActivityExporter
+{
+ private const string Url = "opentelemetry.proto.collector.trace.v1.TraceService/Export";
+ private const string ContentType = "application/grpc";
+
+ private readonly HttpRequest _request = new("POST", Url);
+
+ private readonly Dictionary _serializerBySource = new();
+
+ void ITraceActivityExporter.Export(scoped in TraceActivityRef activityRef)
+ {
+ if (!_serializerBySource.TryGetValue(activityRef.Source, out var serializer))
+ {
+ serializer = GrpcSerializer.Create(parameters.BufferSize, activityRef.Source);
+ _serializerBySource[activityRef.Source] = serializer;
+ }
+
+ serializer.Write(activityRef);
+ }
+
+ void ITraceActivityExporter.Flush()
+ {
+ if (_serializerBySource.Count == 0)
+ {
+ return;
+ }
+
+ // TODO: Send via HTTP2 (Grpc use HTTP2)
+ foreach (var (_, serializer) in _serializerBySource)
+ {
+ var bytes = serializer.Flush();
+
+ _request.SetHeader("TE", "trailers");
+ _request.SetHeader("Content-Type", ContentType);
+ _request.SetBody(bytes);
+
+ if (!IsConnected)
+ {
+ if (!Connect())
+ {
+ // TODO: Write error in log.
+ continue;
+ }
+ }
+
+ var byteCount = SendRequest(_request);
+ if (byteCount == 0)
+ {
+ // TODO: Write error in log.
+ continue;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Otlp/Grpc/GrpcSerializer.cs b/src/EasyTrace/Export/Otlp/Grpc/GrpcSerializer.cs
new file mode 100644
index 0000000..eab9635
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Grpc/GrpcSerializer.cs
@@ -0,0 +1,33 @@
+using System.Runtime.CompilerServices;
+using EasyTrace.Export.Otlp.Protobuf;
+
+namespace EasyTrace.Export.Otlp.Grpc;
+
+public sealed class GrpcSerializer : ProtobufSerializer
+{
+ private const int MessageLengthPosition = 1;
+ private const int MessageWritePosition = 5;
+
+ private GrpcSerializer(ProtobufStream stream, TraceActivitySource activitySource) : base(stream, activitySource)
+ {
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static GrpcSerializer Create(int capacity, TraceActivitySource activitySource)
+ {
+ var stream = new ProtobufStream(capacity);
+ // Grpc payload consists of 3 parts:
+ // byte 0 - Specifying if the payload is compressed.
+ // 1-4 byte - Specifies the length of payload in big endian format.
+ // 5 and above - Protobuf serialized data.
+ stream.Reserve(MessageWritePosition);
+ return new GrpcSerializer(stream, activitySource);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override ReadOnlySpan Flush()
+ {
+ Stream.WriteFixed32BigEndianFormat(MessageLengthPosition, (uint)Stream.Position - MessageWritePosition);
+ return base.Flush();
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Otlp/Http/HttpExportParameters.cs b/src/EasyTrace/Export/Otlp/Http/HttpExportParameters.cs
new file mode 100644
index 0000000..38e992d
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Http/HttpExportParameters.cs
@@ -0,0 +1,14 @@
+namespace EasyTrace.Export.Otlp.Http;
+
+public class HttpExportParameters
+{
+ public Uri EndPoint { get; init; } = new("http://127.0.0.1:4318");
+
+ public int BufferSize { get; init; } = 1024;
+
+ public void Validate()
+ {
+ ArgumentNullException.ThrowIfNull(EndPoint);
+ ArgumentOutOfRangeException.ThrowIfLessThan(BufferSize, 1024);
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Otlp/Http/HttpExporter.cs b/src/EasyTrace/Export/Otlp/Http/HttpExporter.cs
new file mode 100644
index 0000000..4ac1fc7
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Http/HttpExporter.cs
@@ -0,0 +1,65 @@
+using EasyTrace.Activity;
+using EasyTrace.Export.Otlp.Protobuf;
+using NetCoreServer;
+using FastHttpClient = NetCoreServer.HttpClient;
+
+namespace EasyTrace.Export.Otlp.Http;
+
+///
+/// Export via HTTP/1.1 + Protobuf.
+///
+public class HttpExporter(HttpExportParameters parameters)
+ : FastHttpClient(parameters.EndPoint.Host, parameters.EndPoint.Port), ITraceActivityExporter
+{
+ private const string Url = "/v1/traces";
+ private const string ContentType = "application/x-protobuf";
+
+ private readonly HttpRequest _request = new();
+ private readonly string _hostRequest = $"{parameters.EndPoint.Host}:{parameters.EndPoint.Port}";
+ private readonly Dictionary _serializerBySource = new();
+
+ void ITraceActivityExporter.Export(scoped in TraceActivityRef activityRef)
+ {
+ if (!_serializerBySource.TryGetValue(activityRef.Source, out var serializer))
+ {
+ serializer = new ProtobufSerializer(parameters.BufferSize, activityRef.Source);
+ _serializerBySource[activityRef.Source] = serializer;
+ }
+
+ serializer.Write(activityRef);
+ }
+
+ void ITraceActivityExporter.Flush()
+ {
+ if (_serializerBySource.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var (_, serializer) in _serializerBySource)
+ {
+ var bytes = serializer.Flush();
+
+ _request.SetBegin("POST", Url);
+ _request.SetHeader("Host", _hostRequest);
+ _request.SetHeader("Content-Type", ContentType);
+ _request.SetBody(bytes);
+
+ if (!IsConnected)
+ {
+ if (!Connect())
+ {
+ // TODO: Write error in log.
+ continue;
+ }
+ }
+
+ var byteCount = SendRequest(_request);
+ if (byteCount == 0)
+ {
+ // TODO: Write error in log.
+ continue;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Otlp/Protobuf/ProtobufFieldNumber.cs b/src/EasyTrace/Export/Otlp/Protobuf/ProtobufFieldNumber.cs
new file mode 100644
index 0000000..6c787ea
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Protobuf/ProtobufFieldNumber.cs
@@ -0,0 +1,51 @@
+namespace EasyTrace.Export.Otlp.Protobuf;
+
+public class ProtobufFieldNumber
+{
+ private readonly byte _value;
+
+ // Resource spans.
+ public static readonly ProtobufFieldNumber TracesData = new(1);
+
+ // Resource spans.
+ public static readonly ProtobufFieldNumber Resource = new(1);
+ public static readonly ProtobufFieldNumber ResourceSpans = new(2);
+
+ // Resource fields.
+ public static readonly ProtobufFieldNumber ResourceAttributes = new(1);
+
+ // Source fields.
+ public static readonly ProtobufFieldNumber Scope = new(1);
+ public static readonly ProtobufFieldNumber ScopeSpan = new(2);
+
+ // Activity fields.
+ public static readonly ProtobufFieldNumber TraceId = new(1);
+ public static readonly ProtobufFieldNumber SpanId = new(2);
+ public static readonly ProtobufFieldNumber TraceState = new(3);
+ public static readonly ProtobufFieldNumber ParentId = new(4);
+ public static readonly ProtobufFieldNumber Name = new(5);
+ public static readonly ProtobufFieldNumber Kind = new(6);
+ public static readonly ProtobufFieldNumber StartTimeUnixNano = new(7);
+ public static readonly ProtobufFieldNumber EndTimeUnixNano = new(8);
+ public static readonly ProtobufFieldNumber Attributes = new(9);
+ public static readonly ProtobufFieldNumber DroppedAttributesCount = new(10);
+ public static readonly ProtobufFieldNumber Events = new(11);
+ public static readonly ProtobufFieldNumber DroppedEventsCount = new(12);
+ public static readonly ProtobufFieldNumber Links = new(13);
+ public static readonly ProtobufFieldNumber DroppedLinksCount = new(14);
+ public static readonly ProtobufFieldNumber Status = new(15);
+ public static readonly ProtobufFieldNumber Flags = new(16);
+
+ // Other fields:
+ // .. KeyValue
+ internal const int Key = 1;
+
+ internal const int Value = 2;
+
+ // .. AnyValue
+ internal const int AnyValueAsString = 1;
+
+ private ProtobufFieldNumber(byte value) => _value = value;
+
+ public static implicit operator int(ProtobufFieldNumber fieldNumber) => fieldNumber._value;
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Otlp/Protobuf/ProtobufSerializer.cs b/src/EasyTrace/Export/Otlp/Protobuf/ProtobufSerializer.cs
new file mode 100644
index 0000000..92a70ed
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Protobuf/ProtobufSerializer.cs
@@ -0,0 +1,126 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using EasyTrace.Activity;
+
+namespace EasyTrace.Export.Otlp.Protobuf;
+
+public class ProtobufSerializer
+{
+ protected readonly ProtobufStream Stream;
+ private readonly int _traceLengthPosition;
+ private readonly int _spansLengthPosition;
+ private readonly int _messageWritePosition;
+
+ public ProtobufSerializer(int capacity, TraceActivitySource activitySource)
+ : this(new ProtobufStream(capacity), activitySource)
+ {
+ }
+
+ public ProtobufSerializer(ProtobufStream stream, TraceActivitySource activitySource)
+ {
+ Stream = stream;
+ // Message: Trace + Resource + Spans + [Source + [Activity]]
+ _traceLengthPosition = WriteTrace(Stream);
+ WriteResource(Stream, activitySource.Resources);
+ _spansLengthPosition = WriteSpans(Stream);
+ WriteSource(Stream, activitySource);
+ _messageWritePosition = Stream.Position;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(scoped in TraceActivityRef activityRef) => WriteActivity(Stream, activityRef);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public virtual ReadOnlySpan Flush()
+ {
+ Stream.WriteLength(_traceLengthPosition);
+ Stream.WriteLength(_spansLengthPosition);
+ var bytes = Stream.AsSpan();
+ Stream.Reset(_messageWritePosition);
+ return bytes;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int WriteTrace(ProtobufStream stream)
+ {
+ stream.WriteTag(ProtobufFieldNumber.TracesData, ProtobufWireType.Len);
+ return stream.ReserveForLength();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int WriteSpans(ProtobufStream stream)
+ {
+ stream.WriteTag(ProtobufFieldNumber.ResourceSpans, ProtobufWireType.Len);
+ return stream.ReserveForLength();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteResource(ProtobufStream stream, KeyValuePair[]? resources)
+ {
+ stream.WriteTag(ProtobufFieldNumber.Resource, ProtobufWireType.Len);
+ using var resourceLengthScope = stream.WriteLengthScope();
+
+ if (resources == null)
+ {
+ return;
+ }
+
+ foreach (var (attributeKey, attributeValue) in resources)
+ {
+ stream.WriteTag(ProtobufFieldNumber.ResourceAttributes, ProtobufWireType.Len);
+ using var attributeLengthScope = stream.WriteLengthScope();
+ stream.WriteKeyValueTag(attributeKey, attributeValue);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteSource(ProtobufStream stream, TraceActivitySource activitySource)
+ {
+ const int sourceName = 1;
+ const int sourceVersion = 2;
+
+ stream.WriteTag(ProtobufFieldNumber.Scope, ProtobufWireType.Len);
+ using var sourceLengthScope = stream.WriteLengthScope();
+
+ stream.WriteStringWithTag(sourceName, activitySource.Name);
+
+ if (activitySource.Version != null)
+ {
+ stream.WriteStringWithTag(sourceVersion, activitySource.Version);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteActivity(ProtobufStream stream, scoped in TraceActivityRef activity)
+ {
+ stream.WriteTag(ProtobufFieldNumber.ScopeSpan, ProtobufWireType.Len);
+ using var activityLengthScope = stream.WriteLengthScope();
+
+ stream.WriteByteArrayWithTag(ProtobufFieldNumber.TraceId, activity.TraceId.AsReadOnlySpan());
+ stream.WriteByteArrayWithTag(ProtobufFieldNumber.SpanId, activity.SpanId.AsReadOnlySpan());
+
+ var activityTraceFlags = activity.Recorded ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None;
+ var spanFlags = (uint)activityTraceFlags & 0x000000FF;
+
+ spanFlags |= 0x00000100;
+ if (activity.RemoteParent)
+ {
+ spanFlags |= 0x00000200;
+ }
+
+ stream.WriteFixed32WithTag(ProtobufFieldNumber.Flags, spanFlags);
+ stream.WriteStringWithTag(ProtobufFieldNumber.Name, activity.OperationName);
+ stream.WriteEnumWithTag(ProtobufFieldNumber.Kind, (int)activity.Kind + 1);
+ stream.WriteFixed64WithTag(ProtobufFieldNumber.StartTimeUnixNano, ToUnixTimeNanoseconds(activity.StartTime));
+ stream.WriteFixed64WithTag(ProtobufFieldNumber.EndTimeUnixNano, ToUnixTimeNanoseconds(activity.EndTime));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong ToUnixTimeNanoseconds(DateTime dateTime)
+ {
+ const long nanosecondsPerTicks = 100;
+ const long unixEpochTicks = 621355968000000000;
+
+ return (ulong)(dateTime.Ticks - unixEpochTicks) * nanosecondsPerTicks;
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Otlp/Protobuf/ProtobufStream.cs b/src/EasyTrace/Export/Otlp/Protobuf/ProtobufStream.cs
new file mode 100644
index 0000000..29c0a18
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Protobuf/ProtobufStream.cs
@@ -0,0 +1,271 @@
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace EasyTrace.Export.Otlp.Protobuf;
+
+public class ProtobufStream(int capacity)
+{
+ private const int ReserveSizeForLength = 4;
+ private static readonly Encoding Utf8Encoding = Encoding.UTF8;
+
+ private const uint UInt128 = 0x80;
+ private const int Fixed32Size = 4;
+ private const int Fixed64Size = 8;
+ private const int MaskBitsLow = 0b_0111_1111;
+ private const int MaskBitHigh = 0b_1000_0000;
+
+ private readonly byte[] _buffer = new byte[capacity];
+
+ public int Position { get; private set; }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span AsSpan() => _buffer.AsSpan(0, Position);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int ReserveForLength() => Reserve(ReserveSizeForLength);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Reserve(int length)
+ {
+ var position = Position;
+ Position += length;
+ return position;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Reset(int offset = 0) => Position = offset;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public LengthScope WriteLengthScope() => new(this);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteLength(int position)
+ {
+ WriteLength(position, Position - position - ReserveSizeForLength);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteLength(int position, int length)
+ {
+ var slice = _buffer.AsSpan(position, 4);
+ slice[0] = (byte)((length & MaskBitsLow) | MaskBitHigh);
+ slice[1] = (byte)(((length >> 7) & MaskBitsLow) | MaskBitHigh);
+ slice[2] = (byte)(((length >> 14) & MaskBitsLow) | MaskBitHigh);
+ slice[3] = (byte)((length >> 21) & MaskBitsLow);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteTag(int fieldNumber, ProtobufWireType type) =>
+ WriteVarInt32(GetTagValue(fieldNumber, type));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteEnumWithTag(int fieldNumber, int value)
+ {
+ WriteTag(fieldNumber, ProtobufWireType.VarInt);
+ _buffer[Position++] = (byte)value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteFixed32WithTag(int fieldNumber, uint value)
+ {
+ WriteTag(fieldNumber, ProtobufWireType.I32);
+ WriteFixed32LittleEndianFormat(value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteFixed64WithTag(int fieldNumber, ulong value)
+ {
+ WriteTag(fieldNumber, ProtobufWireType.I64);
+ WriteFixed64LittleEndianFormat(value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteByteArrayWithTag(int fieldNumber, ReadOnlySpan value)
+ {
+ WriteTag(fieldNumber, ProtobufWireType.Len);
+ WriteInt(value.Length);
+ value.CopyTo(_buffer.AsSpan(Position));
+ Position += value.Length;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteStringWithTag(int fieldNumber, string value)
+ {
+ Debug.Assert(value != null, "value was null");
+ WriteStringWithTag(fieldNumber, value.AsSpan());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteKeyValueTag(string key, ReadOnlySpan value)
+ {
+ WriteStringWithTag(ProtobufFieldNumber.Key, key);
+ var numberOfUtf8CharsInString = GetNumberOfUtf8CharsInString(value);
+ var serializedLengthSize = ComputeVarInt64Size((ulong)numberOfUtf8CharsInString);
+
+ WriteTag(ProtobufFieldNumber.Value, ProtobufWireType.Len);
+ WriteInt(numberOfUtf8CharsInString + 1 + serializedLengthSize);
+
+ WriteStringWithTag(ProtobufFieldNumber.AnyValueAsString, numberOfUtf8CharsInString, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteStringWithTag(int fieldNumber, ReadOnlySpan value)
+ {
+ var numberOfUtf8CharsInString = GetNumberOfUtf8CharsInString(value);
+ WriteStringWithTag(fieldNumber, numberOfUtf8CharsInString, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteVarInt32(uint value)
+ {
+ while (value >= UInt128)
+ {
+ _buffer[Position++] = (byte)(MaskBitHigh | (value & MaskBitsLow));
+ value >>= 7;
+ }
+
+ _buffer[Position++] = (byte)value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteInt(int length) => WriteVarInt32((uint)length);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteFixed32BigEndianFormat(int position, uint value)
+ {
+ Span span = new(_buffer, position, Fixed32Size);
+ BinaryPrimitives.WriteUInt32BigEndian(span, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteFixed32LittleEndianFormat(uint value)
+ {
+ Span span = new(_buffer, Position, Fixed32Size);
+ BinaryPrimitives.WriteUInt32LittleEndian(span, value);
+ Position += Fixed32Size;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteFixed64LittleEndianFormat(ulong value)
+ {
+ Span span = new(_buffer, Position, Fixed64Size);
+ BinaryPrimitives.WriteUInt64LittleEndian(span, value);
+ Position += Fixed64Size;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteStringWithTag(
+ int fieldNumber,
+ int numberOfUtf8CharsInString,
+ ReadOnlySpan value)
+ {
+ WriteTag(fieldNumber, ProtobufWireType.Len);
+ WriteInt(numberOfUtf8CharsInString);
+
+ var bytesWritten = Utf8Encoding.GetBytes(value, _buffer.AsSpan(Position));
+ Debug.Assert(bytesWritten == numberOfUtf8CharsInString, "bytesWritten did not match numberOfUtf8CharsInString");
+ Position += bytesWritten;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint GetTagValue(int fieldNumber, ProtobufWireType wireType) =>
+ ((uint)(fieldNumber << 3)) | (uint)wireType;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetNumberOfUtf8CharsInString(ReadOnlySpan value)
+ {
+ return Utf8Encoding.GetByteCount(value);
+ }
+
+ ///
+ /// Computes the number of bytes required to encode a 64-bit unsigned integer in Protocol Buffers' varint format.
+ ///
+ ///
+ /// Protocol Buffers uses variable-length encoding (varint) to serialize integers efficiently:
+ /// - Each byte uses 7 bits to encode the number and 1 bit (MSB) to indicate if more bytes follow
+ /// - The algorithm checks how many significant bits the number contains by shifting and masking
+ /// - Numbers are encoded in groups of 7 bits, from least to most significant
+ /// - Each group requires one byte, so the method returns the number of 7-bit groups needed
+ ///
+ /// Examples:
+ /// - Values 0-127 (7 bits) require 1 byte
+ /// - Values 128-16383 (14 bits) require 2 bytes
+ /// - Values 16384-2097151 (21 bits) require 3 bytes
+ /// And so on...
+ ///
+ /// For more details, see:
+ /// - Protocol Buffers encoding reference: https://developers.google.com/protocol-buffers/docs/encoding#varints.
+ ///
+ /// The unsigned 64-bit integer to be encoded.
+ /// Number of bytes needed to encode the value.
+ private static int ComputeVarInt64Size(ulong value)
+ {
+ if ((value & (0xffffffffffffffffL << 7)) == 0)
+ {
+ return 1;
+ }
+
+ if ((value & (0xffffffffffffffffL << 14)) == 0)
+ {
+ return 2;
+ }
+
+ if ((value & (0xffffffffffffffffL << 21)) == 0)
+ {
+ return 3;
+ }
+
+ if ((value & (0xffffffffffffffffL << 28)) == 0)
+ {
+ return 4;
+ }
+
+ if ((value & (0xffffffffffffffffL << 35)) == 0)
+ {
+ return 5;
+ }
+
+ if ((value & (0xffffffffffffffffL << 42)) == 0)
+ {
+ return 6;
+ }
+
+ if ((value & (0xffffffffffffffffL << 49)) == 0)
+ {
+ return 7;
+ }
+
+ if ((value & (0xffffffffffffffffL << 56)) == 0)
+ {
+ return 8;
+ }
+
+ if ((value & (0xffffffffffffffffL << 63)) == 0)
+ {
+ return 9;
+ }
+
+ return 10;
+ }
+}
+
+public readonly ref struct LengthScope : IDisposable
+{
+ private const int ReserveSizeForLength = 4;
+ private readonly ProtobufStream _stream;
+ private readonly int _position;
+
+ public LengthScope(ProtobufStream stream)
+ {
+ _position = stream.Position;
+ _stream = stream;
+ _stream.Reserve(ReserveSizeForLength);
+ }
+
+ public void Dispose()
+ {
+ _stream.WriteLength(_position, _stream.Position - (_position + ReserveSizeForLength));
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Export/Otlp/Protobuf/ProtobufWireType.cs b/src/EasyTrace/Export/Otlp/Protobuf/ProtobufWireType.cs
new file mode 100644
index 0000000..10f938e
--- /dev/null
+++ b/src/EasyTrace/Export/Otlp/Protobuf/ProtobufWireType.cs
@@ -0,0 +1,32 @@
+namespace EasyTrace.Export.Otlp.Protobuf;
+
+///
+/// Wire types within protobuf encoding.
+/// https://protobuf.dev/programming-guides/encoding/#structure.
+///
+public enum ProtobufWireType : uint
+{
+ ///
+ /// Variable-length integer.
+ /// Used for int32, int64, uint32, uint64, sint32, sint64, bool, enum.
+ ///
+ VarInt = 0,
+
+ ///
+ /// A fixed-length 64-bit value.
+ /// Used for fixed64, sfixed64, double.
+ ///
+ I64 = 1,
+
+ ///
+ /// A length-delimited value.
+ /// Used for string, bytes, embedded messages, packed repeated fields.
+ ///
+ Len = 2,
+
+ ///
+ /// A fixed-length 32-bit value.
+ /// Used for fixed32, sfixed32, float.
+ ///
+ I32 = 5,
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Identifier/Generator/Xoshiro256PlusPlus.cs b/src/EasyTrace/Identifier/Generator/Xoshiro256PlusPlus.cs
index 548502f..fcd35b7 100644
--- a/src/EasyTrace/Identifier/Generator/Xoshiro256PlusPlus.cs
+++ b/src/EasyTrace/Identifier/Generator/Xoshiro256PlusPlus.cs
@@ -1,110 +1,89 @@
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Security.Cryptography;
namespace EasyTrace.Identifier.Generator;
public class Xoshiro256PlusPlus : ITraceIdentifierGenerator
{
- private ulong _s0;
- private ulong _s1;
- private ulong _s2;
- private ulong _s3;
-
- public Xoshiro256PlusPlus()
- : this(GenerateCryptoSeed())
- {
- }
-
- public Xoshiro256PlusPlus(ulong seed)
- {
- if (seed == 0)
- seed = 123456789;
- InitState(seed);
- }
-
- private void InitState(ulong seed)
- {
- var x = seed;
- _s0 = SplitMix64(ref x);
- _s1 = SplitMix64(ref x);
- _s2 = SplitMix64(ref x);
- _s3 = SplitMix64(ref x);
- }
-
- public ulong NextUInt64()
- {
- var result = Rotl(_s0 + _s3, 23) + _s0;
-
- var t = _s1 << 17;
-
- _s2 ^= _s0;
- _s3 ^= _s1;
- _s1 ^= _s2;
- _s0 ^= _s3;
-
- _s2 ^= t;
-
- _s3 = Rotl(_s3, 45);
-
- return result;
- }
-
- public void NextBytes(Span buffer)
- {
- unchecked
+ private ulong _s0;
+ private ulong _s1;
+ private ulong _s2;
+ private ulong _s3;
+
+ public Xoshiro256PlusPlus()
+ : this(GenerateCryptoSeed())
+ {
+ }
+
+ public Xoshiro256PlusPlus(ulong seed)
+ {
+ if (seed == 0)
+ seed = 123456789;
+ InitState(seed);
+ }
+
+ private void InitState(ulong seed)
{
- var ulongBuffer = MemoryMarshal.Cast(buffer);
+ var x = seed;
+ _s0 = SplitMix64(ref x);
+ _s1 = SplitMix64(ref x);
+ _s2 = SplitMix64(ref x);
+ _s3 = SplitMix64(ref x);
+ }
+
+ public ulong NextUInt64()
+ {
+ var result = Rotl(_s0 + _s3, 23) + _s0;
+
+ var t = _s1 << 17;
- // Заполняем основную часть буфера блоками по 64 бита
- for (var i = 0; i < ulongBuffer.Length; i++)
- {
- ulongBuffer[i] = NextUInt64();
- }
+ _s2 ^= _s0;
+ _s3 ^= _s1;
+ _s1 ^= _s2;
+ _s0 ^= _s3;
- var remainingBytes = buffer.Length % 8;
- if (remainingBytes > 0)
- {
- // Генерируем финальное число для остатка
- var remainder = NextUInt64();
+ _s2 ^= t;
- // Индекс, с которого начинаются хвостовые байты
- var offset = buffer.Length - remainingBytes;
+ _s3 = Rotl(_s3, 45);
- // Побайтово копируем остаток
- for (var i = 0; i < remainingBytes; i++)
+ return result;
+ }
+
+ public void Generate(Span bytes)
+ {
+ if (bytes.Length != 8 && bytes.Length != 16)
{
- buffer[offset + i] = (byte)(remainder >> (i * 8));
+ throw new ArgumentOutOfRangeException(nameof(bytes));
}
- }
+
+ Unsafe.WriteUnaligned(ref bytes[0], NextUInt64());
+
+ if (bytes.Length == 16)
+ {
+ Unsafe.WriteUnaligned(ref bytes[8], NextUInt64());
+ }
+ }
+
+ private static ulong SplitMix64(ref ulong x)
+ {
+ x += 0x9e3779b97f4a7c15;
+ var z = x;
+ z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
+ z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
+ return z ^ (z >> 31);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong Rotl(ulong x, int k)
+ {
+ return (x << k) | (x >> (64 - k));
+ }
+
+ private static ulong GenerateCryptoSeed()
+ {
+ using var rng = RandomNumberGenerator.Create();
+ Span bytes = stackalloc byte[8];
+ rng.GetBytes(bytes);
+ return BitConverter.ToUInt64(bytes);
}
- }
-
- public void Generate(Span bytes)
- {
- NextBytes(bytes);
- }
-
- private static ulong SplitMix64(ref ulong x)
- {
- x += 0x9e3779b97f4a7c15;
- var z = x;
- z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
- z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
- return z ^ (z >> 31);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static ulong Rotl(ulong x, int k)
- {
- return (x << k) | (x >> (64 - k));
- }
-
- private static ulong GenerateCryptoSeed()
- {
- using var rng = RandomNumberGenerator.Create();
- var bytes = new byte[8];
- rng.GetBytes(bytes);
- return BitConverter.ToUInt64(bytes, 0);
- }
}
\ No newline at end of file
diff --git a/src/EasyTrace/Interceptor/GroupInterceptor.cs b/src/EasyTrace/Interceptor/GroupInterceptor.cs
new file mode 100644
index 0000000..d34fa64
--- /dev/null
+++ b/src/EasyTrace/Interceptor/GroupInterceptor.cs
@@ -0,0 +1,22 @@
+using EasyTrace.Activity;
+
+namespace EasyTrace.Interceptor;
+
+public class GroupInterceptor(List processors) : ITraceActivityInterceptor
+{
+ public void Start(scoped in TraceActivityRef activityRef)
+ {
+ foreach (var processor in processors)
+ {
+ processor.Start(activityRef);
+ }
+ }
+
+ public void Stop(scoped in TraceActivityRef activityRef)
+ {
+ foreach (var processor in processors)
+ {
+ processor.Stop(activityRef);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Interceptor/ITraceActivityInterceptor.cs b/src/EasyTrace/Interceptor/ITraceActivityInterceptor.cs
new file mode 100644
index 0000000..2481c88
--- /dev/null
+++ b/src/EasyTrace/Interceptor/ITraceActivityInterceptor.cs
@@ -0,0 +1,9 @@
+using EasyTrace.Activity;
+
+namespace EasyTrace.Interceptor;
+
+public interface ITraceActivityInterceptor
+{
+ void Start(scoped in TraceActivityRef activityRef);
+ void Stop(scoped in TraceActivityRef activityRef);
+}
\ No newline at end of file
diff --git a/src/EasyTrace/Time/ITraceTimeProvider.cs b/src/EasyTrace/Time/ITraceTimeProvider.cs
index 18a697c..6fa25c6 100644
--- a/src/EasyTrace/Time/ITraceTimeProvider.cs
+++ b/src/EasyTrace/Time/ITraceTimeProvider.cs
@@ -3,4 +3,5 @@
public interface ITraceTimeProvider
{
TimeSpan GetTimestamp();
+ DateTime GetDateTime();
}
\ No newline at end of file
diff --git a/src/EasyTrace/Time/TraceTimeProvider.cs b/src/EasyTrace/Time/TraceTimeProvider.cs
index 1cf0848..6be2a71 100644
--- a/src/EasyTrace/Time/TraceTimeProvider.cs
+++ b/src/EasyTrace/Time/TraceTimeProvider.cs
@@ -9,5 +9,5 @@ public class TraceTimeProvider : ITraceTimeProvider
public TimeSpan GetTimestamp() => Stopwatch.GetElapsedTime(_startTimestamp, Stopwatch.GetTimestamp());
- private DateTime ToDateTime(TimeSpan duration) => _startTimeInUtc.Add(duration);
+ public DateTime GetDateTime() => _startTimeInUtc.Add(GetTimestamp());
}
\ No newline at end of file
diff --git a/src/EasyTrace/TraceActivitySource.cs b/src/EasyTrace/TraceActivitySource.cs
index c305c48..7543369 100644
--- a/src/EasyTrace/TraceActivitySource.cs
+++ b/src/EasyTrace/TraceActivitySource.cs
@@ -2,13 +2,15 @@
using System.Runtime.CompilerServices;
using EasyTrace.Activity;
using EasyTrace.Export;
+using EasyTrace.Export.Batch;
using EasyTrace.Identifier;
using EasyTrace.Identifier.Generator;
+using EasyTrace.Interceptor;
using EasyTrace.Time;
namespace EasyTrace;
-public class TraceActivitySource(string name, Version? version = null)
+public class TraceActivitySource(string name, Version? version = null) : IDisposable
{
private static readonly ThreadLocal ParentActivityByThreadLocal = new();
@@ -16,7 +18,11 @@ public class TraceActivitySource(string name, Version? version = null)
internal ITraceTimeProvider TimeProvider { get; init; } = new TraceTimeProvider();
internal ITraceIdentifierGenerator IdentifierGenerator { get; init; } = new Xoshiro256PlusPlus();
- internal ITraceActivityExporter? Exporter { get; init; }
+ internal KeyValuePair[] Resources { get; init; } = [];
+ internal BatchExporter? BatchExporter { get; set; }
+ internal GroupInterceptor? GroupInterceptor { get; set; }
+
+ private bool _disposed;
private static TraceActivity? Parent
{
@@ -32,7 +38,7 @@ private static TraceActivity? Parent
[CallerMemberName] string operationName = "",
ActivityKind kind = ActivityKind.Internal)
{
- if (Exporter == null)
+ if (BatchExporter == null)
{
return null;
}
@@ -48,34 +54,75 @@ private static TraceActivity? Parent
activity.TraceId.CopyFrom(Parent.TraceId);
}
+ activity.SpanId.Generate(IdentifierGenerator);
activity.Source = this;
activity.Kind = kind;
activity.OperationName = operationName;
- activity.SpanId.Generate(IdentifierGenerator);
- activity.StartTime = TimeProvider.GetTimestamp();
+ activity.StartTime = TimeProvider.GetDateTime();
activity.Recorded = true;
+ // TODO: Support mark if parent is remote.
+ activity.RemoteParent = false;
Parent ??= activity;
+ scoped var activityRef = new TraceActivityRef(activity);
+ GroupInterceptor?.Start(activityRef);
+
return new TraceActivityScope(activity);
}
public void Stop(TraceActivity activity)
{
- if (activity.EndTime == TimeSpan.Zero)
+ try
+ {
+ if (activity.EndTime == DateTime.MinValue)
+ {
+ activity.EndTime = TimeProvider.GetDateTime();
+ }
+
+ scoped var activityRef = new TraceActivityRef(activity);
+
+ GroupInterceptor?.Stop(in activityRef);
+ BatchExporter?.Handle(in activityRef);
+
+ if (Parent == activity)
+ {
+ Parent = null;
+ }
+
+ activity.Clear();
+ }
+ finally
{
- activity.EndTime = TimeProvider.GetTimestamp();
+ TraceActivityPool.Shared.Return(activity);
}
+ }
- Exporter?.Export(new TraceActivityRef(activity));
+ ~TraceActivitySource()
+ {
+ Dispose(false);
+ }
- if (Parent == activity)
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_disposed)
{
- Parent = null;
+ return;
}
- activity.Clear();
+ if (disposing)
+ {
+ BatchExporter?.Dispose();
+ BatchExporter = null;
+ GroupInterceptor = null;
+ }
- TraceActivityPool.Shared.Return(activity);
+ _disposed = true;
}
}
\ No newline at end of file
diff --git a/src/EasyTrace/TraceActivitySourceBuilder.cs b/src/EasyTrace/TraceActivitySourceBuilder.cs
index 69354d4..6e9cb2a 100644
--- a/src/EasyTrace/TraceActivitySourceBuilder.cs
+++ b/src/EasyTrace/TraceActivitySourceBuilder.cs
@@ -1,16 +1,19 @@
using EasyTrace.Export;
using EasyTrace.Export.Batch;
+using EasyTrace.Export.Otlp.Grpc;
+using EasyTrace.Export.Otlp.Http;
using EasyTrace.Identifier;
using EasyTrace.Identifier.Generator;
+using EasyTrace.Interceptor;
using EasyTrace.Time;
namespace EasyTrace;
-// TODO: Configure sampler.
-// TODO: Configure/Add resources (export in Jaeger).
public class TraceActivitySourceBuilder
{
private readonly List _exporters = [];
+ private readonly List _interceptors = [];
+ private Dictionary _resources = GetResourceDefault();
private BatchExportOptions? _batchExportOptions;
private ITraceTimeProvider _timeProvider = new TraceTimeProvider();
private ITraceIdentifierGenerator _identifierGenerator = new Xoshiro256PlusPlus();
@@ -33,31 +36,76 @@ public TraceActivitySourceBuilder SetBatchExportOptions(BatchExportOptions batch
return this;
}
+ public TraceActivitySourceBuilder AddOtlpExporter(HttpExportParameters parameters)
+ {
+ AddExporter(new HttpExporter(parameters));
+ return this;
+ }
+
public TraceActivitySourceBuilder AddExporter(ITraceActivityExporter exporter)
{
_exporters.Add(exporter);
return this;
}
- public TraceActivitySource Build(string name, Version? version = null)
+ public TraceActivitySourceBuilder AddInterceptor(ITraceActivityInterceptor interceptor)
+ {
+ _interceptors.Add(interceptor);
+ return this;
+ }
+
+ public TraceActivitySourceBuilder AddResources(IEnumerable> resources)
{
- ITraceActivityExporter? exporter = null;
+ foreach (var (key, value) in resources)
+ {
+ _resources.Add(key, value);
+ }
+
+ return this;
+ }
+ public TraceActivitySourceBuilder SetResources(IEnumerable> resources)
+ {
+ _resources = new Dictionary(resources);
+ return this;
+ }
+
+ public TraceActivitySource Build(string name, Version? version = null)
+ {
+ BatchExporter? batchExporter = null;
if (_exporters.Count > 0)
{
- exporter = _exporters.Count == 1 ? _exporters[0] : new GroupExporter(_exporters.ToArray());
+ batchExporter = new BatchExporter(
+ _exporters.Count == 1 ? _exporters[0] : new GroupExporter([.. _exporters]),
+ _batchExportOptions ?? new BatchExportOptions());
+ }
- if (_batchExportOptions != null)
- {
- exporter = new BatchExporter(exporter, _batchExportOptions);
- }
+ GroupInterceptor? groupInterceptor = null;
+ if (_interceptors.Count > 0)
+ {
+ groupInterceptor = new GroupInterceptor(_interceptors);
}
return new TraceActivitySource(name, version)
{
TimeProvider = _timeProvider,
IdentifierGenerator = _identifierGenerator,
- Exporter = exporter,
+ Resources = [.. _resources],
+ BatchExporter = batchExporter,
+ GroupInterceptor = groupInterceptor,
+ };
+ }
+
+ private static Dictionary GetResourceDefault()
+ {
+ var entryAssemblyName = System.Reflection.Assembly.GetEntryAssembly()?.GetName();
+ var executingAssemblyName = System.Reflection.Assembly.GetExecutingAssembly().GetName();
+ return new Dictionary
+ {
+ ["telemetry.sdk.name"] = executingAssemblyName.Name!.ToLower(),
+ ["telemetry.sdk.language"] = "dotnet",
+ ["telemetry.sdk.version"] = executingAssemblyName.Version!.ToString(),
+ ["service.name"] = entryAssemblyName?.Name?.ToLower() ?? "unknown",
};
}
}
\ No newline at end of file
diff --git a/src/EasyTrace/packages.lock.json b/src/EasyTrace/packages.lock.json
index a07e21a..fbfd4b0 100644
--- a/src/EasyTrace/packages.lock.json
+++ b/src/EasyTrace/packages.lock.json
@@ -1,6 +1,13 @@
{
"version": 1,
"dependencies": {
- "net9.0": {}
+ "net9.0": {
+ "NetCoreServer": {
+ "type": "Direct",
+ "requested": "[8.0.7, )",
+ "resolved": "8.0.7",
+ "contentHash": "BhNiJ6EVxKS1eKqlHINy7BruLDc+xCH59oZxu7d2Hve8CzFki69PumVlf6vyAQvIVfxbZiHw/FI3UHNnMajuVg=="
+ }
+ }
}
}
\ No newline at end of file