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 @@ [![build](https://github.com/bugRanger/EasyTrace/actions/workflows/ci.yml/badge.svg)](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