Skip to content

Commit

Permalink
Add ActivitySourceInput
Browse files Browse the repository at this point in the history
Fixes issue Azure#364

Includes re-organization of the README file to make navigation easier
  • Loading branch information
karolz-ms committed Jan 7, 2021
1 parent 617aa3f commit 651f72b
Show file tree
Hide file tree
Showing 23 changed files with 1,607 additions and 89 deletions.
264 changes: 216 additions & 48 deletions README.md

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions Warsaw.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2027
# Visual Studio Version 16
VisualStudioVersion = 16.0.30804.86
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{FC86C736-6428-431B-B83F-940BF7182757}"
EndProject
Expand Down Expand Up @@ -90,6 +90,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Event
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Diagnostics.EventFlow.Signing", "src\Microsoft.Diagnostics.EventFlow.Signing\Microsoft.Diagnostics.EventFlow.Signing.csproj", "{69AA6421-82A5-43BF-AD27-8E2C172FF8CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource", "src\Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource\Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource.csproj", "{4B9EF551-2476-4494-9A5E-C52A97505CD4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -330,6 +332,14 @@ Global
{69AA6421-82A5-43BF-AD27-8E2C172FF8CE}.Release|Any CPU.Build.0 = Release|Any CPU
{69AA6421-82A5-43BF-AD27-8E2C172FF8CE}.Release|x64.ActiveCfg = Release|Any CPU
{69AA6421-82A5-43BF-AD27-8E2C172FF8CE}.Release|x64.Build.0 = Release|Any CPU
{4B9EF551-2476-4494-9A5E-C52A97505CD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B9EF551-2476-4494-9A5E-C52A97505CD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B9EF551-2476-4494-9A5E-C52A97505CD4}.Debug|x64.ActiveCfg = Debug|Any CPU
{4B9EF551-2476-4494-9A5E-C52A97505CD4}.Debug|x64.Build.0 = Debug|Any CPU
{4B9EF551-2476-4494-9A5E-C52A97505CD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B9EF551-2476-4494-9A5E-C52A97505CD4}.Release|Any CPU.Build.0 = Release|Any CPU
{4B9EF551-2476-4494-9A5E-C52A97505CD4}.Release|x64.ActiveCfg = Release|Any CPU
{4B9EF551-2476-4494-9A5E-C52A97505CD4}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -364,6 +374,7 @@ Global
{C1CB9BDC-2F26-46AA-9F2C-F9C12E9A3418} = {FC86C736-6428-431B-B83F-940BF7182757}
{A53665AF-C4B4-4DD8-B002-F8065C1A3D10} = {47200F40-43E1-4B09-B803-A921FED2BF05}
{69AA6421-82A5-43BF-AD27-8E2C172FF8CE} = {FC86C736-6428-431B-B83F-940BF7182757}
{4B9EF551-2476-4494-9A5E-C52A97505CD4} = {47200F40-43E1-4B09-B803-A921FED2BF05}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8F1BC23F-956D-4D83-B675-710B743F08EF}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ private static void CreateItemFactories(
inputFactories["Log4net"] = "Microsoft.Diagnostics.EventFlow.Inputs.Log4netFactory, Microsoft.Diagnostics.EventFlow.Inputs.Log4net";
inputFactories["NLog"] = "Microsoft.Diagnostics.EventFlow.Inputs.NLogInputFactory, Microsoft.Diagnostics.EventFlow.Inputs.NLog";
inputFactories["DiagnosticSource"] = "Microsoft.Diagnostics.EventFlow.Inputs.DiagnosticSource.DiagnosticSourceInputFactory, Microsoft.Diagnostics.EventFlow.Inputs.DiagnosticSource";
inputFactories["ActivitySource"] = "Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource.ActivitySourceInputFactory, Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource";

outputFactories = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
outputFactories["ApplicationInsights"] = "Microsoft.Diagnostics.EventFlow.Outputs.ApplicationInsightsOutputFactory, Microsoft.Diagnostics.EventFlow.Outputs.ApplicationInsights";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
using System;
using System.Text;

namespace Microsoft.Diagnostics.EventFlow.Utilities.Etw
namespace Microsoft.Diagnostics.EventFlow
{
/// <summary>
/// Provides a cached reusable instance of a StringBuilder per thread. It is an optimization that reduces the number of instances constructed and collected.
/// </summary>
internal static class StringBuilderCache
public static class StringBuilderCache
{
// The value 360 was chosen in discussion with performance experts as a compromise between using
// as litle memory (per thread) as possible and still covering a large part of short-lived
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<Description>Defines core interfaces and types that comprise Microsoft.Diagnostics.EventFlow library.</Description>
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
<VersionPrefix>1.10.0</VersionPrefix>
<VersionPrefix>1.10.1</VersionPrefix>
<Authors>Microsoft</Authors>
<TargetFrameworks>netstandard1.6;netstandard2.0;net452;net471</TargetFrameworks>
<AssemblyName>Microsoft.Diagnostics.EventFlow.Core</AssemblyName>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using System.Diagnostics;
using System.Text;

using Microsoft.Diagnostics.EventFlow;

namespace Microsoft.Diagnostics.EventFlow.Utilities.Etw
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<TargetFrameworks>netstandard1.6;net452;netstandard2.0;net471</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AssemblyName>Microsoft.Diagnostics.EventFlow.EtwUtilities</AssemblyName>
<VersionPrefix>1.8.0</VersionPrefix>
<VersionPrefix>1.8.1</VersionPrefix>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageId>Microsoft.Diagnostics.EventFlow.EtwUtilities</PackageId>
<PackageTags>Microsoft;Diagnostics;EventFlow;Utilities;Event Tracing for Windows</PackageTags>
Expand Down Expand Up @@ -45,6 +45,7 @@

<ItemGroup>
<PackageReference Include="Validation" Version="2.4.18" />
<ProjectReference Include="..\Microsoft.Diagnostics.EventFlow.Core\Microsoft.Diagnostics.EventFlow.Core.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Validation;

using Microsoft.Diagnostics.EventFlow.Configuration;

namespace Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource
{
public class ActivitySourceInput : IObservable<EventData>, IDisposable
{
private const int SamplingDecisionCacheFlushThreshold = 1024;

// Using a static dictionary like this is faster than doing Enum.GetName()
private static readonly IDictionary<ActivityKind, string> ActivityKindNames =
Enum.GetValues(typeof(ActivityKind)).Cast<ActivityKind>().ToDictionary(k => k, k => k.ToString());

private EventFlowSubject<EventData> subject_;
private ActivityListener activityListener_;
private ActivitySourceInputConfiguration configuration_;
private ConcurrentDictionary<string, (ActivitySamplingResult CapturedData, CapturedActivityEvents CapturedEvents)> activitySampling_;
private bool hasUnrestrictedSources_;
private IHealthReporter healthReporter_;

public ActivitySourceInput(IConfiguration configuration, IHealthReporter healthReporter)
{
Requires.NotNull(configuration, nameof(configuration));
Requires.NotNull(healthReporter, nameof(healthReporter));

var inputConfiguration = new ActivitySourceInputConfiguration();
try
{
configuration.Bind(inputConfiguration);
}
catch
{
healthReporter.ReportProblem(
$"Invalid {nameof(ActivitySourceInput)} configuration encountered: '{configuration.ToString()}'",
EventFlowContextIdentifiers.Configuration);
throw;
}

Initialize(inputConfiguration, healthReporter);
}

public ActivitySourceInput(ActivitySourceInputConfiguration configuration, IHealthReporter healthReporter)
{
Requires.NotNull(configuration, nameof(configuration));
Requires.NotNull(healthReporter, nameof(healthReporter));

Initialize(configuration, healthReporter);
}

public JsonSerializerSettings SerializerSettings { get; set; }
public ActivitySourceInputConfiguration Configuration => configuration_;

public void Dispose()
{
activityListener_.Dispose();
subject_.Dispose();
}

public IDisposable Subscribe(IObserver<EventData> observer)
{
return subject_.Subscribe(observer);
}

private void Initialize(ActivitySourceInputConfiguration configuration, IHealthReporter healthReporter)
{
healthReporter_ = healthReporter;
configuration_ = configuration.DeepClone();
subject_ = new EventFlowSubject<EventData>();
activitySampling_ = new ConcurrentDictionary<string, (ActivitySamplingResult CapturedData, CapturedActivityEvents CapturedEvents)>();
activityListener_ = new ActivityListener();
SerializerSettings = EventFlowJsonUtilities.GetDefaultSerializerSettings();

if (configuration_.Sources.Count == 0)
{
healthReporter.ReportWarning(
$"{nameof(ActivitySourceInput)}: configuration has no data sources. No activity data will be captured.",
EventFlowContextIdentifiers.Configuration);
}

var removed = configuration_.Sources.RemoveAll(s => s.CapturedData == ActivitySamplingResult.None);
if (removed > 0)
{
healthReporter.ReportWarning(
$"{nameof(ActivitySourceInput)}: configuration has sources with CapturedData = None. These sources will be ignored.",
EventFlowContextIdentifiers.Configuration);
}

hasUnrestrictedSources_ = configuration_.Sources.Any(s => string.IsNullOrWhiteSpace(s.ActivitySourceName));

activityListener_.Sample = (ref ActivityCreationOptions<ActivityContext> activityOptions)
=> DetermineActivitySampling(activityOptions.Source.Name, activityOptions.Name).CapturedData;
activityListener_.SampleUsingParentId = (ref ActivityCreationOptions<string> activityOptions)
=> DetermineActivitySampling(activityOptions.Source.Name, activityOptions.Name).CapturedData;
activityListener_.ShouldListenTo = ShouldListenTo;
activityListener_.ActivityStarted = OnActivityStarted;
activityListener_.ActivityStopped = OnActivityStopped;

System.Diagnostics.ActivitySource.AddActivityListener(activityListener_);
}

private (ActivitySamplingResult CapturedData, CapturedActivityEvents CapturedEvents) DetermineActivitySampling(string activitySourceName, string activityName)
{
string activityKey = activitySourceName + ":" + activityName;

if (activitySampling_.TryGetValue(activityKey, out var samplingSpec))
{
return samplingSpec;
}

foreach(var sc in configuration_.Sources)
{
bool sourceMatches = string.IsNullOrWhiteSpace(sc.ActivitySourceName) || StringComparer.OrdinalIgnoreCase.Equals(activitySourceName, sc.ActivitySourceName);
bool nameMatches = string.IsNullOrWhiteSpace(sc.ActivityName) || StringComparer.OrdinalIgnoreCase.Equals(activityName, sc.ActivityName);

if (sourceMatches && nameMatches)
{
FlushSamplingInfoCacheIfNeeded();

activitySampling_.AddOrUpdate(activityKey, (sc.CapturedData, sc.CapturedEvents), (_, _) => (sc.CapturedData, sc.CapturedEvents));

return (sc.CapturedData, sc.CapturedEvents);
}
}

return (ActivitySamplingResult.None, CapturedActivityEvents.None);
}

private bool ShouldListenTo(System.Diagnostics.ActivitySource activitySource)
{
if (hasUnrestrictedSources_)
{
return true;
}

bool found = configuration_.Sources.Any(s =>
StringComparer.OrdinalIgnoreCase.Equals(activitySource.Name, s.ActivitySourceName) &&
s.CapturedEvents != CapturedActivityEvents.None);
return found;
}

private void OnActivityStarted(Activity activity)
{
(var capturedData, var capturedEvents) = DetermineActivitySampling(activity.Source.Name, activity.DisplayName);
if (
capturedData == ActivitySamplingResult.None ||
capturedEvents == CapturedActivityEvents.None ||
(capturedEvents & CapturedActivityEvents.Start) == 0)
{
return;
}

EventData e = ToEventData(activity, capturedData);
subject_.OnNext(e);
}

private void OnActivityStopped(Activity activity)
{
(var capturedData, var capturedEvents) = DetermineActivitySampling(activity.Source.Name, activity.DisplayName);
if (
capturedData == ActivitySamplingResult.None ||
capturedEvents == CapturedActivityEvents.None ||
(capturedEvents & CapturedActivityEvents.Stop) == 0)
{
return;
}

EventData e = ToEventData(activity, capturedData);
subject_.OnNext(e);
}

private EventData ToEventData(Activity activity, ActivitySamplingResult capturedData)
{
EventData e = new EventData
{
ProviderName = nameof(ActivitySourceInput),
Timestamp = activity.StartTimeUtc,
Level = LogLevel.Informational,
Keywords = (long) activity.ActivityTraceFlags
};

// Property names following OpenTelemetry conventions https://github.com/open-telemetry/opentelemetry-specification
e.Payload["Name"] = activity.DisplayName;
e.Payload["SpanId"] = activity.Id;
e.Payload["ParentSpanId"] = activity.ParentSpanId.ToHexString();
e.Payload["StartTime"] = e.Timestamp;
if (activity.Duration != TimeSpan.Zero)
{
e.Payload["EndTime"] = activity.StartTimeUtc + activity.Duration;
}
e.Payload["TraceId"] = activity.TraceId.ToHexString();
if (ActivityKindNames.TryGetValue(activity.Kind, out string activityKindName))
{
e.Payload["SpanKind"] = activityKindName;
}
e.Payload["IsRecording"] = activity.Recorded;

// The following property additions may cause name conflicts, so using AddPayloadProperty() to handle them.
foreach(var el in activity.Baggage)
{
AddPayloadProperty(e, el.Key, el.Value);
}

AddPayloadProperty(e, "ActivitySourceName", activity.Source.Name);

if (capturedData == ActivitySamplingResult.AllData || capturedData == ActivitySamplingResult.AllDataAndRecorded)
{
// Activity.Tags is a subset of Activity.TagObjects that have string value,
// so it is sufficient to just iterate over Activity.TagObjects to capture all activity tags.
foreach(var tagObject in activity.TagObjects)
{
AddPayloadProperty(e, tagObject.Key, tagObject.Value);
}

if (activity.Events.Any())
{
AddPayloadProperty(e, "Events", activity.Events);
}

if (activity.Links.Any())
{
AddPayloadProperty(e, "Links", activity.Links);
}
}

return e;
}

private void AddPayloadProperty(EventData e, string propertyName, object propertyValue)
{
e.AddPayloadProperty(propertyName, propertyValue, healthReporter_, nameof(ActivitySourceInput));
}

private void FlushSamplingInfoCacheIfNeeded()
{
if (activitySampling_.Count > SamplingDecisionCacheFlushThreshold)
{
lock(activitySampling_)
{
if (activitySampling_.Count > SamplingDecisionCacheFlushThreshold)
{
activitySampling_.Clear();
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

using Microsoft.Extensions.Configuration;

namespace Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource
{
public class ActivitySourceInputFactory : IPipelineItemFactory<ActivitySourceInput>
{
public ActivitySourceInput CreateItem(IConfiguration configuration, IHealthReporter healthReporter)
{
return new ActivitySourceInput(configuration, healthReporter);
}
}
}
Loading

0 comments on commit 651f72b

Please sign in to comment.