Unverified Commit 15b43cd4 authored by David Kudera's avatar David Kudera
Browse files

Init

parents
Pipeline #184877574 failed with stages
in 2 minutes and 31 seconds
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "4.6.5",
"commands": [
"reportgenerator"
]
}
}
}
\ No newline at end of file
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = tab
[*.yml]
indent_style = space
indent_size = 2
[*.md]
indent_style = space
indent_size = 4
.idea
bin/
obj/
/packages/
src/*/nupkg
test/*/TestResults
test/*/TestCoverage
Tracer.sln.DotSettings.user
image: mcr.microsoft.com/dotnet/core/sdk:3.1-alpine
stages:
- test
- publish
before_script:
- apk add sed
- dotnet restore
- dotnet tool restore
- dotnet build
test:
stage: test
tags: [docker]
artifacts:
paths:
- tests.log
- test/Tests.Tracer/TestResults
- test/Tests.Tracer/TestCoverage
script:
- dotnet test --verbosity n --collect:"XPlat Code Coverage" test/Tests.Tracer -d tests.log
- dotnet reportgenerator "-reports:test/Tests.Tracer/TestResults/*/coverage.cobertura.xml" "-targetdir:test/Tests.Tracer/TestCoverage" -reporttypes:Html
publish:
stage: publish
tags: [docker]
only: [tags]
artifacts:
when: always
paths:
- src/Tracer/bin/Debug
- src/GoogleCloud/bin/Debug
script:
- sed -i "s/0.0.0-version-placeholder/${CI_COMMIT_TAG}/" src/Tracer/Tracer.csproj
- sed -i "s/0.0.0-version-placeholder/${CI_COMMIT_TAG}/" src/GoogleCloud/GoogleCloud.csproj
- dotnet build
- dotnet nuget push ./src/Tracer/nupkg/DKX.Tracer.${CI_COMMIT_TAG}.nupkg -k ${NUGET_API_KEY} -s https://api.nuget.org/v3/index.json
- dotnet nuget push ./src/GoogleCloud/nupkg/DKX.TracerGoogleCloud.${CI_COMMIT_TAG}.nupkg -k ${NUGET_API_KEY} -s https://api.nuget.org/v3/index.json
MIT License
Copyright (c) 2020 David Kudera
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# DKX.Tracer
Tracing library
## Installation
```bash
$ dotnet add package DKX.Tracer
```
## Exporters
* `DKX.Tracer.Exporters.VoidExporter`: exports nowhere
* `DKX.Tracer.Exporters.NonWaitingExporter`: exporter wrapper which does not wait for the result of the underlying exporter
* `DKX.TracerGoogleCloud.GoogleCloudExporter`: exports to Google Cloud Trace, must install package `DKX.TracerGoogleCloud`
## Usage
```c#
using DKX.Tracer;
using DKX.Tracer.Exporters;
var exporter = new VoidExporter();
var tracer = new Tracer(exporter);
await using (var rootSpan = tracer.Start("Root"))
{
using (var child = rootSpan.fork("Child 1"))
{
// todo: do something
}
}
```
Of course you can nest spans as much as you want.
## ASP.NET
There is middleware which will automatically wrap everything in a span.
```c#
using DKX.Tracer.AspNet;
public void Configure(IApplicationBuilder app)
{
// tracer should be the first middleware so it can wrap really everything
app.useDkxTracer();
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
```
## Current span provider
You can use `ICurrentSpanProvider` to get the currently opened span.
*It's better to first try to pass the span manually.*
```c#
using DKX.Tracer;
ICurrentSpanProvider currentSpanProvider = tracer.CurrentSpanProvider;
ISpan? currentSpan = currentSpanProvider.CurrentSpan;
```
## Attach additional information
Custom data can be added with `AddAttribute` methods on any span.
```c#
span.AddAttribute("key", "value");
```
It is also possible to attach `Exception`, `HttpRequest` and `HttpResponse` directly to root span to enrich the data set
automatically.
**Provided `TracerMiddleware` is doing that automatically.**
```c#
rootSpan.AttachHttpRequest(httpRequest);
rootSpan.AttachHttpResponse(httpResponse);
try {
// todo: process http request
} catch (Exception e) {
rootSpan.AttachException(e);
throw;
}
```

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tracer", "src\Tracer\Tracer.csproj", "{7950B5A6-44CC-40F5-8D32-ADC492C55321}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Tracer", "test\Tests.Tracer\Tests.Tracer.csproj", "{A5EF486A-F16A-4034-91F0-9C3BDFF73A9C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleCloud", "src\GoogleCloud\GoogleCloud.csproj", "{28A987AB-9882-4938-8294-122A0876D01C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7950B5A6-44CC-40F5-8D32-ADC492C55321}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7950B5A6-44CC-40F5-8D32-ADC492C55321}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7950B5A6-44CC-40F5-8D32-ADC492C55321}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7950B5A6-44CC-40F5-8D32-ADC492C55321}.Release|Any CPU.Build.0 = Release|Any CPU
{A5EF486A-F16A-4034-91F0-9C3BDFF73A9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A5EF486A-F16A-4034-91F0-9C3BDFF73A9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5EF486A-F16A-4034-91F0-9C3BDFF73A9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5EF486A-F16A-4034-91F0-9C3BDFF73A9C}.Release|Any CPU.Build.0 = Release|Any CPU
{28A987AB-9882-4938-8294-122A0876D01C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28A987AB-9882-4938-8294-122A0876D01C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28A987AB-9882-4938-8294-122A0876D01C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28A987AB-9882-4938-8294-122A0876D01C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>DKX.TracerGoogleCloud</RootNamespace>
<Nullable>enable</Nullable>
<PackageId>DKX.TracerGoogleCloud</PackageId>
<Title>DKX.TracerGoogleCloud</Title>
<Description>Google Cloud Trace exporter for DKX.Tracer</Description>
<PackageTags>http;trace;tracing;span;google;cloud</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://gitlab.com/dkx/dotnet/tracer/-/blob/master/README.md</PackageProjectUrl>
<RepositoryUrl>https://gitlab.com/dkx/dotnet/tracer</RepositoryUrl>
<RepositoryBranch>master</RepositoryBranch>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<!--Version>0.0.0-version-placeholder</Version-->
<Version>0.0.28</Version>
<Authors>david_kudera</Authors>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageOutputPath>./nupkg</PackageOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Tracer\Tracer.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Cloud.Trace.V2" Version="2.0.0" />
</ItemGroup>
</Project>
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using DKX.Tracer;
using DKX.Tracer.Attributes;
using DKX.Tracer.Exporters;
using Google.Cloud.Trace.V2;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Http.Extensions;
namespace DKX.TracerGoogleCloud
{
public class GoogleCloudExporter : IExporter
{
private readonly TraceServiceClient _client;
private readonly string _projectId;
private readonly string? _projectName;
private readonly string? _projectVersion;
public GoogleCloudExporter(TraceServiceClient client, string projectId, string? projectName = null, string? projectVersion = null)
{
_client = client;
_projectId = projectId;
_projectName = projectName;
_projectVersion = projectVersion;
}
public Task Save(IRootSpan span, CancellationToken cancellationToken)
{
AddBaseAttributes(span);
var googleSpans = ToGoogleSpans(span, CreateId(32));
return _client.BatchWriteSpansAsync(
$"projects/{_projectId}",
googleSpans,
cancellationToken
);
}
private IEnumerable<Google.Cloud.Trace.V2.Span> ToGoogleSpans(ISpan span, string traceId, string? parentSpanId = null)
{
if (!span.Closed)
{
throw new Exception($"Can not export span {span.Name}, it is not closed");
}
var result = new List<Google.Cloud.Trace.V2.Span>();
var googleSpan = new Google.Cloud.Trace.V2.Span();
var id = CreateId(16);
googleSpan.Name = CreateSpanName(traceId, id);
googleSpan.SpanId = id;
googleSpan.DisplayName = new TruncatableString {Value = span.Name};
googleSpan.StartTime = Timestamp.FromDateTime(span.StartedAt);
googleSpan.EndTime = Timestamp.FromDateTime((DateTime) span.ClosedAt!);
googleSpan.ChildSpanCount = span.Children.Count;
if (parentSpanId != null)
{
googleSpan.ParentSpanId = parentSpanId;
googleSpan.SameProcessAsParentSpan = true;
}
if (span.Attributes.Count > 0)
{
googleSpan.Attributes = new Google.Cloud.Trace.V2.Span.Types.Attributes();
foreach (var attribute in span.Attributes)
{
var value = new AttributeValue();
switch (attribute)
{
case StringAttribute attr: value.StringValue = new TruncatableString {Value = attr.Value}; break;
case NumberAttribute attr: value.IntValue = attr.Value; break;
case BooleanAttribute attr: value.BoolValue = attr.Value; break;
}
googleSpan.Attributes.AttributeMap.Add(attribute.Name, value);
}
}
result.Add(googleSpan);
if (span.Children.Count > 0)
{
foreach (var child in span.Children)
{
result.AddRange(ToGoogleSpans(child, traceId, id));
}
}
return result;
}
private string CreateSpanName(string traceId, string spanId)
{
return $"projects/{_projectId}/traces/{traceId}/spans/{spanId}";
}
private static string CreateId(int length)
{
const string valid = "0123456789ABCDEF";
StringBuilder res = new StringBuilder();
var rnd = new Random();
byte[] uintBuffer = new byte[sizeof(uint)];
while (length-- > 0)
{
rnd.NextBytes(uintBuffer);
var num = BitConverter.ToUInt32(uintBuffer, 0);
res.Append(valid[(int) (num % (uint) valid.Length)]);
}
return res.ToString();
}
private void AddBaseAttributes(IRootSpan span)
{
if (_projectName != null)
{
span.AddAttribute("g.co/gae/app/module", _projectName);
}
if (_projectVersion != null)
{
span.AddAttribute("g.co/gae/app/version", _projectVersion);
}
if (span.HttpRequest != null)
{
span.AddAttribute("/http/method", span.HttpRequest.Method);
span.AddAttribute("/http/host", span.HttpRequest.Host.Value);
span.AddAttribute("/http/path", span.HttpRequest.Path.Value);
span.AddAttribute("/http/url", span.HttpRequest.GetDisplayUrl());
try
{
span.AddAttribute("/http/request/size", span.HttpRequest.Body.Length);
}
catch (Exception)
{
// ignored
}
if (span.HttpRequest.Headers.ContainsKey("user-agent"))
{
span.AddAttribute("/http/user_agent", span.HttpRequest.Headers["user-agent"].ToString());
}
}
if (span.HttpResponse != null)
{
span.AddAttribute("/http/status_code", span.HttpResponse.StatusCode);
try
{
span.AddAttribute("/http/response/size", span.HttpResponse.Body.Length);
}
catch (Exception)
{
// ignored
}
}
if (span.Exception != null)
{
var type = span.Exception.GetType();
var statusCode = span.HttpRequest?.Method.ToUpper() == "OPTIONS" ? 200 : 500;
span.AddAttribute("/http/status_code", statusCode);
span.AddAttribute("/error/name", (type.Namespace == null ? "" : $"{type.Namespace}.") + type.Name);
span.AddAttribute("/error/message", span.Exception.Message);
}
}
}
}
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace DKX.Tracer.AspNet
{
public static class ApplicationBuilderExt
{
public static IApplicationBuilder useDkxTracer(this IApplicationBuilder builder)
{
return builder.UseMiddleware<TracerMiddleware>();
}
}
public static class HttpContextExt
{
public static IRootSpan GetTracerRootSpan(this HttpContext context)
{
if (!context.Items.ContainsKey(TracerMiddleware.RootSpanKey))
{
throw new Exception("There is no root span in current HttpContext");
}
return (context.Items[TracerMiddleware.RootSpanKey] as IRootSpan)!;
}
}
}
\ No newline at end of file
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace DKX.Tracer.AspNet
{
public class TracerMiddleware
{
public const string RootSpanKey = "dkx.tracer.root_span";
private readonly RequestDelegate _next;
public TracerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, ITracer tracer)
{
var method = context.Request.Method.ToUpper();
if (method == "OPTIONS")
{
await _next(context);
return;
}
await using var span = tracer.Start(method + ":" + context.Request.Path);
span.AttachHttpRequest(context.Request);
span.AttachHttpResponse(context.Response);
context.Items[RootSpanKey] = span;
try
{
await _next(context);
}
catch (Exception e)
{
span.AttachException(e);
throw;
}
}
}
}
\ No newline at end of file
namespace DKX.Tracer.Attributes
{
public class BooleanAttribute : IAttribute
{
public BooleanAttribute(string name, bool value)
{
Name = name;
Value = value;
}
public string Name { get; }
public bool Value { get; }
}
}
namespace DKX.Tracer.Attributes
{
public interface IAttribute
{
string Name { get; }
}
}
namespace DKX.Tracer.Attributes
{
public class NumberAttribute : IAttribute
{
public NumberAttribute(string name, long value)
{
Name = name;
Value = value;
}
public string Name { get; }
public long Value { get; }
}
}
namespace DKX.Tracer.Attributes
{
public class StringAttribute : IAttribute
{
public StringAttribute(string name, string value)
{
Name = name;
Value = value;
}
public string Name { get; }
public string Value { get; }
}
}
using System;
namespace DKX.Tracer
{
public class CurrentDateTimeProvider : IDateTimeProvider
{
public DateTime GetUtcNow()
{
return DateTime.UtcNow;
}
}
}
using DKX.Tracer.Events;
namespace DKX.Tracer
{
internal class CurrentSpanProvider : ICurrentSpanProvider
{
public ISpan? CurrentSpan { get; private set; }
public void SetCurrentSpan(ISpan? span)
{
if (CurrentSpan != null)
{
CurrentSpan.OnForked -= SpanOnForked;
CurrentSpan.OnClosed -= SpanOnClosed;
}
CurrentSpan = span;
if (span == null)
{
return;
}
span.OnForked += SpanOnForked;
span.OnClosed += SpanOnClosed;
}