Azure/diagnostics-eventflow

New input: ActivitySource

WhitWaldo opened this issue · 12 comments

I've been reading a good deal about OpenTelemetry and the plans to use System.Diagnostics.ActivitySource as the basis for the .NET implementation. I've read through both the DiagnosticSource user guide and the ActivitySource user guide and I understand that activities can be created as part of a DiagnosticSource in the 4.7 release, but given the .NET 5 preview (and that first link up there), it looks more as though ActivitySource is intended to be used independently of DiagnosticSource with OpenTelemetry.

To that end, I looked into what's necessary to create an input to EventFlow for ActivitySource and it looks like it can be nearly identical to DiagnosticSource except for this file, lines 56 through 65 since there's no name property on the listener. I believe I've gathered that the point of this is so it's hooking up subscribers on listener matches.

There's a listener name on the ActivitySource, but I don't see any of way accessing it from the ActivityListener? Do you have any ideas on this so I might add this input (absent DiagnosticSource)?

Thank you!

Whit, thank you for bringing this to our attention.

The hook up of ActivitySource looks simple enough, very similar to DiagnosticSource, like you said. There is quite a bit of new data exposed by ActivitySource events, so we probably need to spend some time familiarizing ourselves with the new API and determining what is the best way to translate the data available into the flat data structure that EventFlow's EventData uses.

For future reference:
ActivitySource design document part 1
ActivitySource design document part 2
GitHub issue with further design discussion
Document comparing OpenTelemetry and .NET Tracing API

Looking at hookup issue some more, looks like the details are being worked out still and are somewhat different from DiagnosticSource / DiagnosticListener.

One of the OpenTelemetry core goals is to be able to listen to arbitrary set of activity sources; this is exactly what EventFlow needs too, so I am not too worried about details changing, but it probably makes sense to wait a bit till the implementation stabilizes, and only then look into adding ActivitySourceInput to EventFlow.

For visibility, this Medium post indicates that the OpenTelemetry .NET package has entered beta, based on the .NET Activity API.

It indicates that System.Diagnostics.DiagnosticSource will be in a preview state until November 2020, presumably meaning it will GA alongside .NET 5.

@WhitWaldo, thank you!

This will probably have to wait till .NET 5.0 GA and the associated VS tooling catches up. ActivitySource is .NET 5.0-only; as of right now VS 2019 setup does not include .NET 5.0 SDK, so we won't be able to produce signed builds with Azure DevOps pipelines.

@karolz-ms Just following up on this now that 5.0 is GA. Since I'm using Service Fabric and it won't have .NET 5.0 support until this spring, this isn't a high priority yet, but it'd be nice to see it supported around the time I'm able to use it myself.

@WhitWaldo yep. We'll get this taken care of by end of this calendar year.

I wrote the DiagnosticSource input for EventFlow, and I have started moving towards using OpenTelemetry and ActivitySource instead.

If you do want to use ActivitySource with EventFlow, here's a quick ActivitySource sample that highlights a few key parts:

using (var listener = new ActivityListener
{
    ActivityStopped = activity =>
    {
        Console.WriteLine(new
        {
            activity.Recorded,

            activity.IdFormat,
            activity.Id,
            activity.Context.TraceId,
            activity.Context.SpanId,
            activity.ParentId,
            activity.ParentSpanId,

            activity.StartTimeUtc,
            activity.Duration,

            activity.OperationName,
            activity.DisplayName,

            activity.Kind,
            Bags = string.Join(";", activity.Baggage.Select(t => t.Key + ":" + t.Value)),
            Tags = string.Join(";", activity.TagObjects.Select(t => t.Key + ":" + t.Value)), // TagObjects is a superset of Tags

            Events = string.Join(",", activity.Events.Select(e => new
            {
                e.Name,
                e.Timestamp,
                Tags = string.Join(";", e.Tags.Select(t => t.Key + ":" + t.Value))
            })),
            Links = string.Join(",", activity.Links.Select(l => new
            {
                l.Context.TraceId,
                l.Context.SpanId,
                Tags = string.Join(";", l.Tags.Select(t => t.Key + ":" + t.Value))
            })),
            CustomProperty = activity.GetCustomProperty("prop"),
            
            SourceName = activity.Source.Name,
            SourceVersion = activity.Source.Version
        });
    },
    ShouldListenTo = source => source.Name == "my source",
    Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllDataAndRecorded
})
{
    ActivitySource.AddActivityListener(listener);

    Activity.DefaultIdFormat = ActivityIdFormat.W3C;
    using (var source = new ActivitySource("my source", "1.2.3"))
    {
        using (var activity = source.StartActivity("my activity"))
        {
            try
            {
                if (activity != null)
                {
                    activity.AddBaggage("bag", "value");
                    activity.AddTag("tag.string", "value");
                    activity.AddTag("tag.object", new { key = "value" });
                    activity.SetCustomProperty("prop", new { foo = "bar" });
                    throw new Exception("test");
                }
            }
            catch (Exception ex) when (activity != null)
            {
                // Add the OpenTelemetry.Api package and import the OpenTelemetry.Trace namespace to record exceptions in this way.
                activity.RecordException(ex);
            }
        }
    }
}

Also note that all you need is the System.Diagnostics.DiagnosticSource package version 5.0 or higher, you don't need to actually use .NET 5.0. It will work on older versions of .NET Core or on the full .NET Framework. There is, however, a bug in 5.0.0 where timestamps are not high precision for .NET Framework, the fix for which is due to be released with 5.0.1 of the package.

This also shows how to filter which ActivitySources you listen to by name, which was part of the question above. You can also choose to only sample specific activities, while this example samples everything and then only listens to a single source by name. When an activity is not sampled, StartActivity will return null.

I think it's different enough that probably needs a completely separate input for EventFlow, but it's quite easy to make a custom EventFlow input. You just need to work out how to translate an Activity into an EventData. You can always make your own custom input, then when you're happy with it, submit it to here. That's what I did with DiagnosticSource.

Thank you, @ghelyar I am getting close to submitting a PR with the new input, working on tests now https://github.com/karolz-ms/diagnostics-eventflow/tree/dev/karolz/activity-source

FWIW I think the serialization should be the responsibility of the output, not the input, but you're the expert. It would make it harder to do things like extract exceptions from events in the output if it's already serialized, and the output might want to serialize it differently anyway. Part of the reason I originally moved from EventSource to DiagnosticSource was to defer serialization.

Also, you don't need to read both Tags and TagObjects, because TagObjects contains all Tags.

You can also generate the dictionary of activity kind names once rather than either hard coding it or using Enum.GetName/ToString every time:

private static readonly IReadOnlyDictionary<ActivityKind, string> ActivityKindNames =
    Enum.GetValues(typeof(ActivityKind)).Cast<ActivityKind>().ToDictionary(k => k, k => k.ToString());

@ghelyar great points, I agree, thank you. I will make the changes you suggest.

@WhitWaldo @ghelyar this has been published on Nuget. Enjoy! ☺️