diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..1ff0c423042b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..3c4efe206bd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,261 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/src/Datadog.APM.sln b/src/Datadog.APM.sln new file mode 100644 index 000000000000..dd24ae39a04f --- /dev/null +++ b/src/Datadog.APM.sln @@ -0,0 +1,64 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.3 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Tracer", "Datadog.Tracer\Datadog.Tracer.csproj", "{5DFDF781-F24C-45B1-82EF-9125875A80A4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Tracer.Tests", "Datadog.Tracer.Tests\Datadog.Tracer.Tests.csproj", "{73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Datadog.Tracer.IntegrationTests", "Datadog.Tracer.IntegrationTests\Datadog.Tracer.IntegrationTests.csproj", "{0434F813-5F94-4195-8A2C-E2E755513822}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Debug|x64.Build.0 = Debug|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Debug|x86.Build.0 = Debug|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Release|Any CPU.Build.0 = Release|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Release|x64.ActiveCfg = Release|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Release|x64.Build.0 = Release|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Release|x86.ActiveCfg = Release|Any CPU + {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Release|x86.Build.0 = Release|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Debug|x64.ActiveCfg = Debug|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Debug|x64.Build.0 = Debug|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Debug|x86.Build.0 = Debug|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Release|Any CPU.Build.0 = Release|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Release|x64.ActiveCfg = Release|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Release|x64.Build.0 = Release|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Release|x86.ActiveCfg = Release|Any CPU + {73A1BE1C-9C8A-43FA-86A8-BF2744B4C1BB}.Release|x86.Build.0 = Release|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Debug|x64.ActiveCfg = Debug|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Debug|x64.Build.0 = Debug|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Debug|x86.ActiveCfg = Debug|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Debug|x86.Build.0 = Debug|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Release|Any CPU.Build.0 = Release|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Release|x64.ActiveCfg = Release|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Release|x64.Build.0 = Release|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Release|x86.ActiveCfg = Release|Any CPU + {0434F813-5F94-4195-8A2C-E2E755513822}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {160A1D00-1F5B-40F8-A155-621B4459D78F} + EndGlobalSection +EndGlobal diff --git a/src/Datadog.Tracer.IntegrationTests/Datadog.Tracer.IntegrationTests.csproj b/src/Datadog.Tracer.IntegrationTests/Datadog.Tracer.IntegrationTests.csproj new file mode 100644 index 000000000000..23a13e48127e --- /dev/null +++ b/src/Datadog.Tracer.IntegrationTests/Datadog.Tracer.IntegrationTests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + diff --git a/src/Datadog.Tracer.IntegrationTests/SendTracesToAgent.cs b/src/Datadog.Tracer.IntegrationTests/SendTracesToAgent.cs new file mode 100644 index 000000000000..acc8e1583c65 --- /dev/null +++ b/src/Datadog.Tracer.IntegrationTests/SendTracesToAgent.cs @@ -0,0 +1,41 @@ +using System; +using Xunit; + +namespace Datadog.Tracer.IntegrationTests +{ + public class SendTracesToAgent + { + [Fact] + public void MinimalSpan() + { + var tracer = new Tracer(new Api(new Uri("http://localhost:8126"))); + tracer.BuildSpan("Operation") + .WithTag(Tags.ResourceName, "This is a resource") + .Start() + .Finish(); + } + + [Fact] + public void CustomServiceName() + { + var tracer = new Tracer(new Api(new Uri("http://localhost:8126"))); + tracer.BuildSpan("Operation") + .WithTag(Tags.ResourceName, "This is a resource") + .WithTag(Tags.ServiceName, "Service1") + .Start() + .Finish(); + } + + [Fact] + public void Utf8Everywhere() + { + var tracer = new Tracer(new Api(new Uri("http://localhost:8126"))); + tracer.BuildSpan("Aᛗᚪᚾᚾᚪ") + .WithTag(Tags.ResourceName, "η γλώσσα μου έδωσαν ελληνική") + .WithTag(Tags.ServiceName, "На берегу пустынных волн") + .WithTag("யாமறிந்த", "ნუთუ კვლა") + .Start() + .Finish(); + } + } +} diff --git a/src/Datadog.Tracer.Tests/Datadog.Tracer.Tests.csproj b/src/Datadog.Tracer.Tests/Datadog.Tracer.Tests.csproj new file mode 100644 index 000000000000..e864eed71f86 --- /dev/null +++ b/src/Datadog.Tracer.Tests/Datadog.Tracer.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + + diff --git a/src/Datadog.Tracer.Tests/SpanBuilderTests.cs b/src/Datadog.Tracer.Tests/SpanBuilderTests.cs new file mode 100644 index 000000000000..265904a48a27 --- /dev/null +++ b/src/Datadog.Tracer.Tests/SpanBuilderTests.cs @@ -0,0 +1,168 @@ +using Moq; +using OpenTracing; +using System; +using Xunit; + +namespace Datadog.Tracer.Tests +{ + public class SpanBuilderTests + { + private Mock _tracerMock; + private TraceContext _traceContext; + private Func CreateSpanBuilder; + private const string _defaultServiceName = "DefaultServiceName"; + + public SpanBuilderTests() + { + _tracerMock = new Mock(MockBehavior.Strict); + _traceContext = new TraceContext(_tracerMock.Object); + _tracerMock.Setup(x => x.GetTraceContext()).Returns(_traceContext); + _tracerMock.Setup(x => x.DefaultServiceName).Returns(_defaultServiceName); + CreateSpanBuilder = () => new SpanBuilder(_tracerMock.Object, null); + } + + [Fact] + public void Start_NoServiceName_DefaultServiceNameIsSet() + { + var span = (Span)CreateSpanBuilder().Start(); + + Assert.Equal(_defaultServiceName, span.ServiceName); + } + + [Fact] + public void Start_NoParentProvided_RootSpan() + { + var span = CreateSpanBuilder().Start(); + var spanContext = (SpanContext)span.Context; + + Assert.Null(spanContext.ParentId); + Assert.NotEqual(0, spanContext.SpanId); + Assert.NotEqual(0, spanContext.TraceId); + } + + [Fact] + public void Start_AsChildOfSpan_ChildReferencesParent() + { + var root = (Span)CreateSpanBuilder().Start(); + var child = (Span)CreateSpanBuilder() + .AsChildOf(root) + .Start(); + + Assert.Null(root.Context.ParentId); + Assert.NotEqual(0, root.Context.SpanId); + Assert.NotEqual(0, root.Context.TraceId); + Assert.Equal(root.Context.SpanId, child.Context.ParentId); + Assert.Equal(root.Context.TraceId, child.Context.TraceId); + Assert.NotEqual(0, child.Context.SpanId); + } + + [Fact] + public void Start_AsChildOfSpanContext_ChildReferencesParent() + { + var root = (Span)CreateSpanBuilder().Start(); + var child = (Span)CreateSpanBuilder() + .AsChildOf(root.Context) + .Start(); + + Assert.Null(root.Context.ParentId); + Assert.NotEqual(0, root.Context.SpanId); + Assert.NotEqual(0, root.Context.TraceId); + Assert.Equal(root.Context.SpanId, child.Context.ParentId); + Assert.Equal(root.Context.TraceId, child.Context.TraceId); + Assert.NotEqual(0, child.Context.SpanId); + } + + [Fact] + public void Start_ReferenceAsChildOf_ChildReferencesParent() + { + var root = (Span)CreateSpanBuilder().Start(); + var child = (Span)CreateSpanBuilder() + .AddReference(References.ChildOf, root.Context) + .Start(); + + Assert.Null(root.Context.ParentId); + Assert.NotEqual(0, root.Context.SpanId); + Assert.NotEqual(0, root.Context.TraceId); + Assert.Equal(root.Context.SpanId, child.Context.ParentId); + Assert.Equal(root.Context.TraceId, child.Context.TraceId); + Assert.NotEqual(0, child.Context.SpanId); + } + + [Fact] + public void Start_WithTags_TagsAreProperlySet() + { + var span = (Span)CreateSpanBuilder() + .WithTag("StringKey", "What's tracing") + .WithTag("IntKey", 42) + .WithTag("DoubleKey", 1.618) + .WithTag("BoolKey", true) + .Start(); + + Assert.Equal("What's tracing", span.GetTag("StringKey")); + Assert.Equal("42", span.GetTag("IntKey")); + Assert.Equal("1.618", span.GetTag("DoubleKey")); + Assert.Equal("True", span.GetTag("BoolKey")); + } + + [Fact] + public void Start_SettingService_ServiceIsSet() + { + var span = (Span)CreateSpanBuilder() + .WithTag("service.name", "MyService") + .Start(); + + Assert.Equal("MyService", span.ServiceName); + } + + [Fact] + public void Start_SettingResource_ResourceIsSet() + { + var span = (Span)CreateSpanBuilder() + .WithTag("resource.name", "MyResource") + .Start(); + + Assert.Equal("MyResource", span.ResourceName); + } + + [Fact] + public void Start_SettingType_TypeIsSet() + { + var span = (Span)CreateSpanBuilder() + .WithTag("span.type", "web") + .Start(); + + Assert.Equal("web", span.Type); + } + + [Fact] + public void Start_SettingError_ErrorIsSet() + { + var span = (Span)CreateSpanBuilder() + .WithTag(OpenTracing.Tags.Error, true) + .Start(); + + Assert.Equal(true, span.Error); + } + + [Fact] + public void Start_WithStartTimeStamp_TimeStampProperlySet() + { + var startTime = new DateTimeOffset(2017, 01, 01, 0, 0, 0, TimeSpan.Zero); + var span = (Span)CreateSpanBuilder() + .WithStartTimestamp(startTime) + .Start(); + + Assert.Equal(startTime, span.StartTime); + } + + [Fact] + public void Start_SetOperationName_OperationNameProperlySet() + { + var spanBuilder = new SpanBuilder(_tracerMock.Object, "Op1"); + + var span = (Span)spanBuilder.Start(); + + Assert.Equal("Op1", span.OperationName); + } + } +} diff --git a/src/Datadog.Tracer.Tests/SpanTests.cs b/src/Datadog.Tracer.Tests/SpanTests.cs new file mode 100644 index 000000000000..1584e3f05e39 --- /dev/null +++ b/src/Datadog.Tracer.Tests/SpanTests.cs @@ -0,0 +1,112 @@ +using Moq; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Datadog.Tracer.Tests +{ + public class SpanTests + { + private Mock _tracerMock; + private Mock _traceContextMock; + + public SpanTests() + { + _tracerMock = new Mock(MockBehavior.Strict); + _traceContextMock = new Mock(MockBehavior.Strict); + _traceContextMock.Setup(x => x.CloseSpan(It.IsAny())); + _traceContextMock.Setup(x => x.GetCurrentSpanContext()).Returns(null); + _tracerMock.Setup(x => x.GetTraceContext()).Returns(_traceContextMock.Object); + _tracerMock.Setup(x => x.DefaultServiceName).Returns("DefaultServiceName"); + } + + [Fact] + public void SetTag_Tags_TagsAreProperlySet() + { + var span = new Span(_tracerMock.Object, null, null, null, null); + + span.SetTag("StringKey", "What's tracing"); + span.SetTag("IntKey", 42); + span.SetTag("DoubleKey", 1.618); + span.SetTag("BoolKey", true); + + _traceContextMock.Verify(x => x.CloseSpan(It.IsAny()), Times.Never); + Assert.Equal("What's tracing", span.GetTag("StringKey")); + Assert.Equal("42", span.GetTag("IntKey")); + Assert.Equal("1.618", span.GetTag("DoubleKey")); + Assert.Equal("True", span.GetTag("BoolKey")); + } + + [Fact] + public void SetOperationName_ValidOperationName_OperationNameIsProperlySet() + { + var span = new Span(_tracerMock.Object, null, null, null, null); + + span.SetOperationName("Op1"); + + _traceContextMock.Verify(x => x.CloseSpan(It.IsAny()), Times.Never); + Assert.Equal("Op1", span.OperationName); + } + + [Fact] + public void Finish_StartTimeInThePastWithNoEndTime_DurationProperlyComputed() + { + var startTime = DateTimeOffset.UtcNow.AddMinutes(-1); + var span = new Span(_tracerMock.Object, null, null, null, startTime); + + span.Finish(); + + _traceContextMock.Verify(x => x.CloseSpan(It.IsAny()), Times.Once); + Assert.True(span.Duration > TimeSpan.FromMinutes(1) && span.Duration < TimeSpan.FromMinutes(2)); + } + + [Fact] + public async Task Finish_NoEndTimeProvided_SpanWriten() + { + var span = new Span(_tracerMock.Object, null, null, null, null); + await Task.Delay(TimeSpan.FromMilliseconds(1)); + span.Finish(); + + _traceContextMock.Verify(x => x.CloseSpan(It.IsAny()), Times.Once); + Assert.True(span.Duration > TimeSpan.Zero); + } + + [Fact] + public void Finish_EndTimeProvided_SpanWritenWithCorrectDuration() + { + var startTime = DateTimeOffset.UtcNow; + var endTime = DateTime.UtcNow.AddMilliseconds(10); + var span = new Span(_tracerMock.Object, null, null, null, startTime); + + span.Finish(endTime); + + _traceContextMock.Verify(x => x.CloseSpan(It.IsAny()), Times.Once); + Assert.Equal(endTime - startTime, span.Duration); + } + + [Fact] + public void Finish_EndTimeInThePast_DurationIs0() + { + var startTime = DateTimeOffset.UtcNow; + var endTime = DateTime.UtcNow.AddMilliseconds(-10); + var span = new Span(_tracerMock.Object, null, null, null, startTime); + + span.Finish(endTime); + + _traceContextMock.Verify(x => x.CloseSpan(It.IsAny()), Times.Once); + Assert.Equal(TimeSpan.Zero, span.Duration); + } + + [Fact] + public void Dispose_ExitUsing_SpanWriten() + { + Span span; + using (span = new Span(_tracerMock.Object, null, null, null, null)) + { + } + + _traceContextMock.Verify(x => x.CloseSpan(It.IsAny()), Times.Once); + Assert.True(span.Duration > TimeSpan.Zero); + } + } +} diff --git a/src/Datadog.Tracer.Tests/TimeUtilsTests.cs b/src/Datadog.Tracer.Tests/TimeUtilsTests.cs new file mode 100644 index 000000000000..659823a228ec --- /dev/null +++ b/src/Datadog.Tracer.Tests/TimeUtilsTests.cs @@ -0,0 +1,22 @@ +using System; +using Xunit; + +namespace Datadog.Tracer.Tests +{ + public class TimeUtilsTests + { + [Fact] + public void ToUnixTimeNanoseconds_UnixEpoch_Zero() + { + var date = DateTimeOffset.FromUnixTimeMilliseconds(0); + Assert.Equal(0, date.ToUnixTimeNanoseconds()); + } + + [Fact] + public void ToUnixTimeNanoseconds_Now_CorrectMillisecondRoundedValue() + { + var date = DateTimeOffset.UtcNow; + Assert.Equal(date.ToUnixTimeMilliseconds(), date.ToUnixTimeNanoseconds() / 1000000); + } + } +} diff --git a/src/Datadog.Tracer.Tests/TracerTests.cs b/src/Datadog.Tracer.Tests/TracerTests.cs new file mode 100644 index 000000000000..0ecddc8afc67 --- /dev/null +++ b/src/Datadog.Tracer.Tests/TracerTests.cs @@ -0,0 +1,111 @@ +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Datadog.Tracer.Tests +{ + public class TracerTests + { + private Mock _apiMock = new Mock(); + + [Fact] + public void BuildSpan_NoParameter_DefaultParameters() + { + var tracer = new Tracer(_apiMock.Object); + + var builder = tracer.BuildSpan("Op1"); + var span = (Span)builder.Start(); + + Assert.Equal(Constants.UnkownService, span.ServiceName); + Assert.Equal("Op1", span.OperationName); + } + + [Fact] + public void BuildSpan_OneChild_ChildParentProperlySet() + { + var tracer = new Tracer(_apiMock.Object); + + var root = (Span)tracer + .BuildSpan("Root") + .Start(); + var child = (Span)tracer + .BuildSpan("Child") + .Start(); + + Assert.Equal(root.TraceContext, child.TraceContext); + Assert.Equal(root.Context.SpanId, child.Context.ParentId); + } + + [Fact] + public void BuildSpan_2ChildrenOfRoot_ChildrenParentProperlySet() + { + var tracer = new Tracer(_apiMock.Object); + + var root = (Span)tracer + .BuildSpan("Root") + .Start(); + var child1 = (Span)tracer + .BuildSpan("Child1") + .Start(); + child1.Finish(); + var child2 = (Span)tracer + .BuildSpan("Child2") + .Start(); + + Assert.Equal(root.TraceContext, child1.TraceContext); + Assert.Equal(root.Context.SpanId, child1.Context.ParentId); + Assert.Equal(root.TraceContext, child2.TraceContext); + Assert.Equal(root.Context.SpanId, child2.Context.ParentId); + } + + [Fact] + public void BuildSpan_2LevelChildren_ChildrenParentProperlySet() + { + var tracer = new Tracer(_apiMock.Object); + + var root = (Span)tracer + .BuildSpan("Root") + .Start(); + var child1 = (Span)tracer + .BuildSpan("Child1") + .Start(); + var child2 = (Span)tracer + .BuildSpan("Child2") + .Start(); + + Assert.Equal(root.TraceContext, child1.TraceContext); + Assert.Equal(root.Context.SpanId, child1.Context.ParentId); + Assert.Equal(root.TraceContext, child2.TraceContext); + Assert.Equal(child1.Context.SpanId, child2.Context.ParentId); + } + + [Fact] + public async Task BuildSpan_AsyncChildrenCreation_ChildrenParentProperlySet() + { + var tracer = new Tracer(_apiMock.Object); + var tcs = new TaskCompletionSource(); + + var root = (Span)tracer + .BuildSpan("Root") + .Start(); + + Func> createSpanAsync = async (t) => { await tcs.Task; return (Span)tracer.BuildSpan("AsyncChild").Start(); }; + var tasks = Enumerable.Range(0, 10).Select(x => createSpanAsync(tracer)).ToArray(); + + var syncChild = (Span)tracer.BuildSpan("SyncChild").Start(); + tcs.SetResult(true); + + Assert.Equal(root.TraceContext, syncChild.TraceContext); + Assert.Equal(root.Context.SpanId, syncChild.Context.ParentId); + foreach(var task in tasks) + { + var span = await task; + Assert.Equal(root.TraceContext, span.TraceContext); + Assert.Equal(root.Context.SpanId, span.Context.ParentId); + } + } + } +} diff --git a/src/Datadog.Tracer/Api.cs b/src/Datadog.Tracer/Api.cs new file mode 100644 index 000000000000..9bce35e85adb --- /dev/null +++ b/src/Datadog.Tracer/Api.cs @@ -0,0 +1,79 @@ +using MsgPack.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Datadog.Tracer +{ + internal class Api : IApi + { + private const string TracesPath = "/v0.3/traces"; + private const string ServicesPath = "/v0.3/services"; + + private static MessagePackSerializer>> _traceSerializer; + private static MessagePackSerializer _serviceSerializer; + + static Api() + { + var serializationContext = new SerializationContext(); + var spanSerializer = new SpanMessagePackSerializer(serializationContext); + var serviceSerializer = new ServiceInfoMessagePackSerializer(serializationContext); + serializationContext.ResolveSerializer += (sender, eventArgs) => { + if (eventArgs.TargetType == typeof(Span)) + { + eventArgs.SetSerializer(spanSerializer); + } + if (eventArgs.TargetType == typeof(ServiceInfo)) + { + eventArgs.SetSerializer(serviceSerializer); + } + }; + _traceSerializer = serializationContext.GetSerializer>>(); + _serviceSerializer = serializationContext.GetSerializer(); + } + + private Uri _tracesEndpoint; + private Uri _servicesEndpoint; + private HttpClient _client = new HttpClient(); + + public Api(Uri baseEndpoint) + { + _tracesEndpoint = new Uri(baseEndpoint, TracesPath); + _servicesEndpoint = new Uri(baseEndpoint, ServicesPath); + // TODO:bertrand add header for os version + _client.DefaultRequestHeaders.Add("Datadog-Meta-Lang", ".NET"); + _client.DefaultRequestHeaders.Add("Datadog-Meta-Lang-Interpreter", RuntimeInformation.FrameworkDescription); + _client.DefaultRequestHeaders.Add("Datadog-Meta-Tracer-Version", Assembly.GetEntryAssembly().GetName().Version.ToString()); + } + + public async Task SendTracesAsync(List> traces) + { + // TODO:bertrand avoid using a memory stream and stream the serialized content directly to the network + using (var ms = new MemoryStream()) + { + await _traceSerializer.PackAsync(ms, traces); + var content = new ByteArrayContent(ms.GetBuffer()); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/msgpack"); + var response = await _client.PostAsync(_tracesEndpoint, content); + response.EnsureSuccessStatusCode(); + } + } + + public async Task SendServiceAsync(ServiceInfo service) + { + // TODO:bertrand avoid using a memory stream and stream the serialized content directly to the network + using (var ms = new MemoryStream()) + { + await _serviceSerializer.PackAsync(ms, service); + var content = new ByteArrayContent(ms.GetBuffer()); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/msgpack"); + var response = await _client.PostAsync(_servicesEndpoint, content); + response.EnsureSuccessStatusCode(); + } + } + } +} diff --git a/src/Datadog.Tracer/AssemblyInfo.cs b/src/Datadog.Tracer/AssemblyInfo.cs new file mode 100644 index 000000000000..c7d78ef12023 --- /dev/null +++ b/src/Datadog.Tracer/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Datadog.Tracer.Tests")] +[assembly: InternalsVisibleTo("Datadog.Tracer.IntegrationTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Datadog.Tracer/Constants.cs b/src/Datadog.Tracer/Constants.cs new file mode 100644 index 000000000000..b7ebf0a2bf8b --- /dev/null +++ b/src/Datadog.Tracer/Constants.cs @@ -0,0 +1,9 @@ +namespace Datadog.Tracer +{ + internal static class Constants + { + public const string UnkownService = "UnkownService"; + public const string UnkownApp = "UnkownApp"; + public const string WebAppType = "web"; + } +} diff --git a/src/Datadog.Tracer/Datadog.Tracer.csproj b/src/Datadog.Tracer/Datadog.Tracer.csproj new file mode 100644 index 000000000000..2915eaff975d --- /dev/null +++ b/src/Datadog.Tracer/Datadog.Tracer.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/Datadog.Tracer/IApi.cs b/src/Datadog.Tracer/IApi.cs new file mode 100644 index 000000000000..588b636aed72 --- /dev/null +++ b/src/Datadog.Tracer/IApi.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Datadog.Tracer +{ + interface IApi + { + Task SendTracesAsync(List> traces); + + Task SendServiceAsync(ServiceInfo service); + } +} \ No newline at end of file diff --git a/src/Datadog.Tracer/IDatadogTracer.cs b/src/Datadog.Tracer/IDatadogTracer.cs new file mode 100644 index 000000000000..61341b78e797 --- /dev/null +++ b/src/Datadog.Tracer/IDatadogTracer.cs @@ -0,0 +1,14 @@ +using OpenTracing; +using System.Collections.Generic; + +namespace Datadog.Tracer +{ + internal interface IDatadogTracer : ITracer + { + string DefaultServiceName { get; } + + void Write(List span); + + ITraceContext GetTraceContext(); + } +} \ No newline at end of file diff --git a/src/Datadog.Tracer/ITraceContext.cs b/src/Datadog.Tracer/ITraceContext.cs new file mode 100644 index 000000000000..62bb5bb2100f --- /dev/null +++ b/src/Datadog.Tracer/ITraceContext.cs @@ -0,0 +1,13 @@ +namespace Datadog.Tracer +{ + internal interface ITraceContext + { + bool Sampled { get; set; } + + void AddSpan(Span span); + + void CloseSpan(Span span); + + SpanContext GetCurrentSpanContext(); + } +} \ No newline at end of file diff --git a/src/Datadog.Tracer/RandomUtils.cs b/src/Datadog.Tracer/RandomUtils.cs new file mode 100644 index 000000000000..804f46d85811 --- /dev/null +++ b/src/Datadog.Tracer/RandomUtils.cs @@ -0,0 +1,15 @@ +using System; + +namespace Datadog.Tracer +{ + public static class RandomUtils + { + public static UInt64 NextUInt63(this Random rnd) + { + // From https://stackoverflow.com/a/677390 + var buffer = new byte[sizeof(UInt64)]; + rnd.NextBytes(buffer); + return BitConverter.ToUInt64(buffer, 0) & (~(1 << 63)); + } + } +} diff --git a/src/Datadog.Tracer/ServiceInfo.cs b/src/Datadog.Tracer/ServiceInfo.cs new file mode 100644 index 000000000000..12a17bd2bcdd --- /dev/null +++ b/src/Datadog.Tracer/ServiceInfo.cs @@ -0,0 +1,11 @@ +namespace Datadog.Tracer +{ + public class ServiceInfo + { + public string ServiceName { get; set; } + + public string App { get; set; } + + public string AppType { get; set; } + } +} diff --git a/src/Datadog.Tracer/ServiceInfoMessagePackSerializer.cs b/src/Datadog.Tracer/ServiceInfoMessagePackSerializer.cs new file mode 100644 index 000000000000..c253c703959c --- /dev/null +++ b/src/Datadog.Tracer/ServiceInfoMessagePackSerializer.cs @@ -0,0 +1,28 @@ +using MsgPack; +using MsgPack.Serialization; + +namespace Datadog.Tracer +{ + internal class ServiceInfoMessagePackSerializer : MessagePackSerializer + { + public ServiceInfoMessagePackSerializer(SerializationContext context) : base(context) + { + } + + protected override void PackToCore(Packer packer, ServiceInfo serviceInfo) + { + packer.PackMapHeader(1); + packer.PackString(serviceInfo.ServiceName); + packer.PackMapHeader(2); + packer.PackString("app"); + packer.PackString(serviceInfo.App); + packer.PackString("app_type"); + packer.PackString(serviceInfo.AppType); + } + + protected override ServiceInfo UnpackFromCore(Unpacker unpacker) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Datadog.Tracer/Span.cs b/src/Datadog.Tracer/Span.cs new file mode 100644 index 000000000000..4acf334a0219 --- /dev/null +++ b/src/Datadog.Tracer/Span.cs @@ -0,0 +1,230 @@ +using OpenTracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Datadog.Tracer +{ + public class Span : ISpan + { + private Object _lock = new Object(); + private IDatadogTracer _tracer; + private Dictionary _tags; + private bool _isFinished; + private SpanContext _context; + private Stopwatch _sw; + + ISpanContext ISpan.Context => _context; + + internal SpanContext Context => _context; + + internal ITraceContext TraceContext => _context.TraceContext; + + internal DateTimeOffset StartTime { get; } + + internal TimeSpan Duration { get; private set; } + + internal string OperationName { get; private set; } + + internal string ResourceName { get; private set; } + + internal string ServiceName => _context.ServiceName; + + internal string Type { get; private set; } + + internal bool Error { get; private set; } + + internal bool IsRootSpan { get { return _context.ParentId == null; } } + + // This is threadsafe only if used after the span has been closed. + // It is acceptable because this property is internal. But if we were to make it public we would need to add some checks. + internal IReadOnlyDictionary Tags { get { return _tags; } } + + internal Span(IDatadogTracer tracer, SpanContext parent, string operationName, string serviceName, DateTimeOffset? start) + { + _tracer = tracer; + _context = new SpanContext(parent?.TraceContext ?? _tracer.GetTraceContext(), serviceName ?? parent?.ServiceName ?? tracer.DefaultServiceName); + OperationName = operationName; + if (start.HasValue) + { + StartTime = start.Value; + } + else + { + StartTime = DateTimeOffset.UtcNow; + _sw = Stopwatch.StartNew(); + } + } + + public void Dispose() + { + Finish(); + } + + public void Finish() + { + // If the startTime was explicitely provided, we don't use a StopWatch to compute the duration + if (_sw == null) + { + Finish(DateTimeOffset.UtcNow); + return; + } + else + { + var shouldCloseSpan = false; + lock (_lock) + { + if (!_isFinished) + { + Duration = _sw.Elapsed; + _isFinished = true; + shouldCloseSpan = true; + } + } + if (shouldCloseSpan) + { + _context.TraceContext.CloseSpan(this); + } + } + } + + public void Finish(DateTimeOffset finishTimestamp) + { + lock (_lock) + { + var shouldCloseSpan = false; + if (!_isFinished) + { + Duration = finishTimestamp - StartTime; + if (Duration < TimeSpan.Zero) + { + Duration = TimeSpan.Zero; + } + _isFinished = true; + shouldCloseSpan = true; + } + if (shouldCloseSpan) + { + _context.TraceContext.CloseSpan(this); + } + } + } + + public string GetBaggageItem(string key) + { + throw new NotImplementedException(); + } + + public ISpan Log(IEnumerable> fields) + { + throw new NotImplementedException(); + } + + public ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) + { + throw new NotImplementedException(); + } + + public ISpan Log(string eventName) + { + throw new NotImplementedException(); + } + + public ISpan Log(DateTimeOffset timestamp, string eventName) + { + throw new NotImplementedException(); + } + + public ISpan SetBaggageItem(string key, string value) + { + throw new NotImplementedException(); + } + + public ISpan SetOperationName(string operationName) + { + lock (_lock) + { + OperationName = operationName; + return this; + } + } + + public ISpan SetTag(string key, bool value) + { + return SetTag(key, value.ToString()); + } + + public ISpan SetTag(string key, double value) + { + return SetTag(key, value.ToString()); + } + + public ISpan SetTag(string key, int value) + { + return SetTag(key, value.ToString()); + } + + public ISpan SetTag(string key, string value) + { + lock (_lock) + { + if (_isFinished) + { + // TODO:log an error instead + } + switch (key) { + case Datadog.Tracer.Tags.ResourceName: + ResourceName = value; + return this; + case OpenTracing.Tags.Error: + Error = value == "True"; + return this; + case Datadog.Tracer.Tags.SpanType: + Type = value; + return this; + } + if (_tags == null) + { + _tags = new Dictionary(); + } + _tags[key] = value; + return this; + } + } + + internal string GetTag(string key) + { + lock (_lock) + { + string s = null; + _tags?.TryGetValue(key, out s); + return s; + } + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine($"TraceId: {_context.TraceId}"); + sb.AppendLine($"ParentId: {_context.ParentId}"); + sb.AppendLine($"SpanId: {_context.SpanId}"); + sb.AppendLine($"ServiceName: {_context.ServiceName}"); + sb.AppendLine($"OperationName: {OperationName}"); + sb.AppendLine($"Resource: {ResourceName}"); + sb.AppendLine($"Type: {Type}"); + sb.AppendLine($"Start: {StartTime}"); + sb.AppendLine($"Duration: {Duration}"); + sb.AppendLine($"Error: {Error}"); + sb.AppendLine($"Meta:"); + if(Tags != null) + { + foreach(var kv in Tags) + { + sb.Append($"\t{kv.Key}:{kv.Value}"); + } + } + return sb.ToString(); + } + } +} diff --git a/src/Datadog.Tracer/SpanBuilder.cs b/src/Datadog.Tracer/SpanBuilder.cs new file mode 100644 index 000000000000..299aaa8c9de1 --- /dev/null +++ b/src/Datadog.Tracer/SpanBuilder.cs @@ -0,0 +1,123 @@ +using System; +using OpenTracing; +using System.Collections.Generic; + +namespace Datadog.Tracer +{ + public class SpanBuilder : ISpanBuilder + { + private Object _lock = new Object(); + private IDatadogTracer _tracer; + private string _operationName; + private SpanContext _parent; + private DateTimeOffset? _start; + private Dictionary _tags; + private string _serviceName; + + internal SpanBuilder(IDatadogTracer tracer, string operationName) + { + _tracer = tracer; + _operationName = operationName; + } + + public ISpanBuilder AddReference(string referenceType, ISpanContext referencedContext) + { + lock (_lock) + { + if (referenceType == References.ChildOf) + { + _parent = referencedContext as SpanContext; + return this; + } + } + throw new NotImplementedException(); + } + + public ISpanBuilder AsChildOf(ISpan parent) + { + lock (_lock) + { + _parent = parent.Context as SpanContext; + return this; + } + } + + public ISpanBuilder AsChildOf(ISpanContext parent) + { + lock (_lock) + { + _parent = parent as SpanContext; + return this; + } + } + + public ISpanBuilder FollowsFrom(ISpan parent) + { + throw new NotImplementedException(); + } + + public ISpanBuilder FollowsFrom(ISpanContext parent) + { + throw new NotImplementedException(); + } + + public ISpan Start() + { + lock (_lock) + { + var span = new Span(_tracer, _parent, _operationName, _serviceName, _start); + span.TraceContext.AddSpan(span); + if (_tags != null) + { + foreach (var pair in _tags) + { + span.SetTag(pair.Key, pair.Value); + } + } + return span; + } + } + + public ISpanBuilder WithStartTimestamp(DateTimeOffset startTimestamp) + { + lock (_lock) + { + _start = startTimestamp; + return this; + } + } + + public ISpanBuilder WithTag(string key, bool value) + { + return WithTag(key, value.ToString()); + } + + public ISpanBuilder WithTag(string key, double value) + { + return WithTag(key, value.ToString()); + } + + public ISpanBuilder WithTag(string key, int value) + { + return WithTag(key, value.ToString()); + } + + public ISpanBuilder WithTag(string key, string value) + { + lock (_lock) + { + if (key == Tags.ServiceName) + { + _serviceName = value; + return this; + } + if (_tags == null) + { + _tags = new Dictionary(); + } + _tags[key] = value; + return this; + } + } + } +} diff --git a/src/Datadog.Tracer/SpanContext.cs b/src/Datadog.Tracer/SpanContext.cs new file mode 100644 index 000000000000..0f3df1def320 --- /dev/null +++ b/src/Datadog.Tracer/SpanContext.cs @@ -0,0 +1,48 @@ +using OpenTracing; +using System; +using System.Collections.Generic; + +namespace Datadog.Tracer +{ + internal class SpanContext : ISpanContext + { + public SpanContext Parent { get; } + + public UInt64 TraceId { get; } + + public UInt64? ParentId { get { return Parent?.SpanId; } } + + public UInt64 SpanId { get; } + + public string ServiceName { get; } + + public ITraceContext TraceContext { get; } + + public SpanContext(ITraceContext traceContext, string serviceName) + { + ServiceName = serviceName; + // TODO:bertrand pool the random objects + Random r = new Random(); + var parent = traceContext.GetCurrentSpanContext(); + if (parent != null) + { + Parent = parent; + TraceId = parent.TraceId; + ServiceName = parent.ServiceName; + TraceContext = parent.TraceContext; + SpanId = r.NextUInt63(); + } + else + { + TraceId = r.NextUInt63(); + SpanId = r.NextUInt63(); + TraceContext = traceContext; + } + } + + public IEnumerable> GetBaggageItems() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Datadog.Tracer/SpanMsgpackSerializer.cs b/src/Datadog.Tracer/SpanMsgpackSerializer.cs new file mode 100644 index 000000000000..70312d66daed --- /dev/null +++ b/src/Datadog.Tracer/SpanMsgpackSerializer.cs @@ -0,0 +1,69 @@ +using MsgPack; +using MsgPack.Serialization; +using System; + +namespace Datadog.Tracer +{ + internal class SpanMessagePackSerializer : MessagePackSerializer + { + public SpanMessagePackSerializer(SerializationContext context) : base(context) + { + } + + protected override void PackToCore(Packer packer, Span value) + { + // First, pack array length (or map length). + // It should be the number of members of the object to be serialized. + var len = 8; + if (value.Context.ParentId != null) + { + len += 1; + } + if (value.Error) + { + len += 1; + } + if (value.Tags != null) + { + len += 1; + } + packer.PackMapHeader(len); + packer.PackString("trace_id"); + packer.Pack(value.Context.TraceId); + packer.PackString("span_id"); + packer.Pack(value.Context.SpanId); + packer.PackString("name"); + packer.PackString(value.OperationName); + packer.PackString("resource"); + packer.PackString(value.ResourceName); + packer.PackString("service"); + packer.PackString(value.ServiceName); + packer.PackString("type"); + packer.PackString(value.Type); + packer.PackString("start"); + packer.Pack(value.StartTime.ToUnixTimeNanoseconds()); + packer.PackString("duration"); + packer.Pack(value.Duration.ToNanoseconds()); + if (value.Context.ParentId != null) + { + packer.PackString("parent_id"); + packer.Pack(value.Context.ParentId); + } + if (value.Error) + { + packer.PackString("error"); + packer.Pack(1); + } + if (value.Tags != null) + { + packer.PackString("meta"); + packer.Pack(value.Tags); + } + } + + protected override Span UnpackFromCore(Unpacker unpacker) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Datadog.Tracer/Tags.cs b/src/Datadog.Tracer/Tags.cs new file mode 100644 index 000000000000..a441f48ce95b --- /dev/null +++ b/src/Datadog.Tracer/Tags.cs @@ -0,0 +1,9 @@ +namespace Datadog.Tracer +{ + public static class Tags + { + public const string ServiceName = "service.name"; + public const string ResourceName = "resource.name"; + public const string SpanType = "span.type"; + } +} diff --git a/src/Datadog.Tracer/TimeUtils.cs b/src/Datadog.Tracer/TimeUtils.cs new file mode 100644 index 000000000000..489bd4e73d77 --- /dev/null +++ b/src/Datadog.Tracer/TimeUtils.cs @@ -0,0 +1,20 @@ +using System; + +namespace Datadog.Tracer +{ + static class TimeUtils + { + private const long NanoSecondsPerTick = 1000000 / TimeSpan.TicksPerMillisecond; + private static readonly long UnixEpochInTicks = DateTimeOffset.FromUnixTimeMilliseconds(0).Ticks; + + public static long ToUnixTimeNanoseconds(this DateTimeOffset dateTimeOffset) + { + return (dateTimeOffset.Ticks - UnixEpochInTicks) * NanoSecondsPerTick; + } + + public static long ToNanoseconds(this TimeSpan ts) + { + return ts.Ticks * NanoSecondsPerTick; + } + } +} diff --git a/src/Datadog.Tracer/TraceContext.cs b/src/Datadog.Tracer/TraceContext.cs new file mode 100644 index 000000000000..5e014149a3cd --- /dev/null +++ b/src/Datadog.Tracer/TraceContext.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Threading; + +namespace Datadog.Tracer +{ + internal class TraceContext : ITraceContext + { + private object _lock = new object(); + private IDatadogTracer _tracer; + private List _spans = new List(); + private int _openSpans = 0; + private AsyncLocal _currentSpanContext = new AsyncLocal(); + + public bool Sampled { get; set; } + + public TraceContext(IDatadogTracer tracer) + { + _tracer = tracer; + } + + public SpanContext GetCurrentSpanContext() + { + return _currentSpanContext.Value; + } + + public void AddSpan(Span span) + { + lock (_lock) + { + _currentSpanContext.Value = span.Context; + _spans.Add(span); + _openSpans++; + } + } + + public void CloseSpan(Span span) + { + lock (_lock) + { + _currentSpanContext.Value = _currentSpanContext.Value?.Parent; + _openSpans--; + if (span.IsRootSpan) + { + if (_openSpans != 0) + { + // TODO:bertrand log error and do not send anything + // Instead detect if we are being garbage collected and warn at that point + } + else + { + _tracer.Write(_spans); + } + // TODO:bertrand make sure the tracecontext is reset + } + } + } + } +} diff --git a/src/Datadog.Tracer/Tracer.cs b/src/Datadog.Tracer/Tracer.cs new file mode 100644 index 000000000000..469fac20e466 --- /dev/null +++ b/src/Datadog.Tracer/Tracer.cs @@ -0,0 +1,78 @@ +using OpenTracing; +using OpenTracing.Propagation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Datadog.Tracer +{ + internal class Tracer : ITracer, IDatadogTracer + { + private AsyncLocal _currentContext = new AsyncLocal(); + private string _defaultServiceName; + private IApi _api; + private Dictionary _services = new Dictionary(); + + string IDatadogTracer.DefaultServiceName => _defaultServiceName; + + public Tracer(IApi api, List serviceInfo = null, string defaultServiceName = Constants.UnkownService) + { + _api = api; + // TODO:bertrand be smarter about the service name + _defaultServiceName = defaultServiceName; + if (defaultServiceName == Constants.UnkownService) + { + _services[Constants.UnkownService] = new ServiceInfo + { + ServiceName = Constants.UnkownService, + App = Constants.UnkownApp, + AppType = Constants.WebAppType, + }; + } + if (serviceInfo != null) + { + foreach(var service in serviceInfo) + { + _services[service.ServiceName] = service; + } + } + // TODO:bertrand handle errors properly + Task.WhenAll(_services.Values.Select(_api.SendServiceAsync)).Wait(); + } + + public ISpanBuilder BuildSpan(string operationName) + { + return new SpanBuilder(this, operationName); + } + + public ISpanContext Extract(Format format, TCarrier carrier) + { + throw new NotImplementedException(); + } + + public void Inject(ISpanContext spanContext, Format format, TCarrier carrier) + { + throw new NotImplementedException(); + } + + + // Trick to keep the method from being accessed from outside the assembly while having it exposed as an interface. + // https://stackoverflow.com/a/18944374 + void IDatadogTracer.Write(List trace) + { + // TODO:bertrand should be non blocking + retry mechanism + _api.SendTracesAsync(new List> { trace }).Wait(); + } + + ITraceContext IDatadogTracer.GetTraceContext() + { + if(_currentContext.Value == null) + { + _currentContext.Value = new TraceContext(this); + } + return _currentContext.Value; + } + } +}