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;
+ }
+ }
+}