Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Telemetry Publishing via Activity & Activity Event #455

Merged
merged 15 commits into from
Jun 19, 2024

Conversation

rossgrambo
Copy link
Contributor

Why this PR?

#412

We now have our own ActivitySource and add an ActivityEvent on it. The event is essentially the EvaluationEvent that was previously passed to the Publisher.

Why our own ActivitySource instead of using the parent Activity?

We could emit the ActivityEvent onto whatever parent Activity is running. Sadly this approach has a few drawbacks:

  1. Listeners who want this event now have to listen to all activities.
  2. The parent code may not have an activity
  3. If a customer implements slow async logic (like reading targeting context from a db), it’s no longer grouped within FeatureManagement.

Visible Changes

  1. Adding publishing uses a different method:
builder.Services.AddFeatureManagement()
    .WithTargeting<HttpContextTargetingContextAccessor>()
    .AddTelemetryPublisher<ApplicationInsightsTelemetryPublisher>();

Becomes

builder.Services.AddFeatureManagement()
    .WithTargeting<HttpContextTargetingContextAccessor>()
    .AddAppInsightsTelemetryPublisher();
  1. Public types ApplicationInsightsTelemetryPublisher & ITelemetryPublisher have been removed entirely
  2. An Activity and Activity Event are available to listen to. Here's the schemas at a glance:
Activity: {
  Name: "Microsoft.FeatureManagement",
  Version: "1.0.0"
}
ActivityEvent: {
  Tags: {
    "TargetingId", evaluationEvent.TargetingContext?.UserId,
    "FeatureName", evaluationEvent.FeatureDefinition.Name,
    "Variant", evaluationEvent.Variant?.Name,
    "Enabled", evaluationEvent.Enabled,
    "VariantAssignmentReason", evaluationEvent.VariantAssignmentReason,
    "Version", ActivitySource.Version,
    ... // (Any items added to TelemetryMetadata)
  }
}


activityListener = new ActivityListener
{
ShouldListenTo = (activitySource) => activitySource.Name == "Microsoft.FeatureManagement",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value "Microsoft.FeatureManagement" is mentioned twice in different places. Consider extracting a proper constant for it that can be shared for type safety and refactor-friendliness.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout. The problem to me is that they're currently in different packages. One string is in the primary Microsoft.FeatureManagement while the other is in Microsoft.FeatureManagement.Telemetry.ApplicationInsights. I could expose it as a public or InternalsVisibleToAttribute to share between the packages but it didn't feel valuable enough.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal + InternalsVisibleTo would be the best in this case indeed to avoid overexposing it. I see it is a bit convoluted though still, probably not worth it as you said.

src/Microsoft.FeatureManagement/FeatureManager.cs Outdated Show resolved Hide resolved
src/Microsoft.FeatureManagement/FeatureManager.cs Outdated Show resolved Hide resolved
src/Microsoft.FeatureManagement/FeatureManager.cs Outdated Show resolved Hide resolved
}
}

ActivityEvent activityEvent = new ActivityEvent("feature_flag", DateTimeOffset.Now, tags);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider evaluating the DateTimeOffset.Now closer to the actual flag check and passing it as an argument so it reflects the actual moment a bit more accurately. By evaluating here, any added delay from the extra processing would end up influencing the recorded time.

Maybe grab it even before the various ISessionManager calls (I don't know exactly what that abstraction is responsible for so take with a grain of salt).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be more accurate for when the actual check started, but I think we'd rather be as close as possible to the return of the final result. Reason being so metrics emitted at or after this timestamp can be expected to use the new result. If we did it at the beginning and their implementation of ISessionManager was slow, the timestamps would be less reliable to correlate.

Copy link
Contributor Author

@rossgrambo rossgrambo May 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although- I could use the end of the activity to get the timestamp I'm referring to, not sure if that would be as reliable though. But in that same line of thinking, the start of the activity could be the considered the start.

@@ -34,6 +34,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We refers Microsoft.Extensions.Logging instead of the abstraction package in Microsoft.FeatureManagement project.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhiyuanliang-ms Microsoft.Extensions.Hosting.Abstractions is required here because of the hosted service usage. If the project can reference just the Abstractions package, that will generate less dependencies for consumers which is a good thing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, in Microsoft.FeatureManagement.csproj we referenceMicrosoft.Extensions.Logging instead of Microsoft.Extensions.Logging.Abstraction`. we can change it to abstraction as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha. If possible, that would make sense indeed, for the same reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll hold off on that change in this PR but agreed we should investigate.

/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public Task StartAsync(CancellationToken cancellationToken)
{
appInsightsPublisher = new ApplicationInsightsEventPublisher(_telemetryClient);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason why we do not follow the opentelemtry's implementation

This hosted service is aimed to ensure that the ApplicationInsightsEventPublisher will be instantiated. Besides, we can take advantage of the DI system to manage the lifecycle of it.

Instead of injecting TelemetryClient to the HostedService, IServiceProvider is injected and serviceProvider.GetService() is called.

AddAppInsightsTelemetryPublisher method should also register ApplicationInsightsEventPublisher as singleton.

Copy link
Contributor Author

@rossgrambo rossgrambo May 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a few things to talk about here- one, is that linked OpenTelemetry implementation is actually setting up their ActivitySources, not ActivityListeners. Their call to setup the listeners is a part of .WithTracing()

WithTracing creates a few things, but it eventually constructs TracerProviderSdk which creates the listeners during construction and explicitly has a shutdown function and a dispose function to handle disposing the listener: https://github.com/open-telemetry/opentelemetry-dotnet/blob/c38cc273bcd03949897d1585e9bc083962cb5f22/src/OpenTelemetry/Trace/TracerProviderSdk.cs#L352

However, we are instead using the IHostedService to handle the "shutdown" (OnStop) and dispose options for our ActivityListeners. It appears I forgot this- because at some point after my design I ripped out IDisposable on the IHostedService- which means in its current state, this would not dispose correctly in a bad exit where OnStop isn't called. 😅

The main question is, who should dispose the publisher? The options to me are:

  1. IHostedService. If so- it should dispose it appropriately on both shutdown and when sp disposes the IHostedService.
  2. ServiceProvider. If so- the disposal will happen automatically which is clean. However- this means we also have an IHostedService that stays around in the background for no reason. Stopping the hosted service would do nothing and disposing it does nothing but remove it. There's also no clear way to undo the IHostedService start without doing a full disposal of the entire ServiceProvider.

So in short- it came down to preference. I ended up landing on 1, but there's been a bit of pushback about it, so I'll likely switch over to 2.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just need some hook to create the instance, could an option 3 using a IStartupFilter implementation also work? That interface provides IServiceProvider, is executed in the beginning of the pipeline, and doesn't "linger" like the hosted service would.

Contrary to HostedService, it also executes before the application actually starts listening to anything, which might actually be a positive here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IStartupFilter would be a great fit here- the issue is that it's only available in ASP.NET Core. Currently this telemetry publishing is possible without any ASP.NET dependency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah you are absolutely right @rossgrambo , I honestly completely forgot about that important detail when suggesting it here 😅.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rossgrambo I assume you meant that it is limited to Microsoft.Extensions.Hosting.Abstractions v8+. This doesn't seem to be tied to the framework but to the library itself. Supporting it would be a matter of bumping the dependency on Microsoft.FeatureManagement.Telemetry.ApplicationInsights.

Regardless, I'm curious where you found that. The page for the interface says it is supported since v6:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedlifecycleservice?view=net-8.0#applies-to

Can you elaborate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I may have been mislead by the article then. The article linked earlier mentions it a couple times as a "new thing in .NET 8". Even the title says it's new in .NET 8. When searching for it- I saw the same mention of .NET 8 in other articles too.

But I just implemented it and ran it successfully with .net 6 and net48. So I suppose it's available?

Copy link
Contributor Author

@rossgrambo rossgrambo Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay I found the issue. The StartingAsync and StoppingAsync methods aren't called unless the project is running >= .NET8.

I could branch the logic on which .NET we're in- but I think I'll just stick with the HostedService StartAsync for now- unless there's a good way around this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*Updated to use service provider here as @zhiyuanliang-ms suggested

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay I found the issue. The StartingAsync and StoppingAsync methods aren't called unless the project is running >= .NET8.

Ohhh that is very weird! Documentation on this interface should be updated to reflect this limitation I think!

Sorry for leading you down this path @rossgrambo , I had no idea.

rossgrambo and others added 3 commits May 31, 2024 12:59
Co-authored-by: Juliano Leal Goncalves <julealgon@gmail.com>
Co-authored-by: Juliano Leal Goncalves <julealgon@gmail.com>
rossgrambo and others added 2 commits June 11, 2024 13:03
Co-authored-by: Juliano Leal Goncalves <julealgon@gmail.com>
/// <returns>The feature management builder.</returns>
public static IFeatureManagementBuilder AddAppInsightsTelemetryPublisher(this IFeatureManagementBuilder builder)
{
if (builder == null)
Copy link
Contributor

@zhiyuanliang-ms zhiyuanliang-ms Jun 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add these null check for the WithTargeting extension method? It is fine if we want to put it in a new PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do another PR- because I'm not even sure this null check makes sense. We're using an extension parameter, which I believe means this method couldn't even be called unless the instance existed.

Co-authored-by: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
@jimmyca15
Copy link
Member

Thanks for the thorough review on this @julealgon !

@rossgrambo rossgrambo merged commit 1143722 into preview Jun 19, 2024
2 checks passed
}
}

var activityEvent = new ActivityEvent("feature_flag", DateTimeOffset.UtcNow, tags);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this use snake casing instead of pascal like all the other fields?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is following OpenTelemetry's guide for the naming and I didn't feel strongly to deviate. It made more sense when we were emitting both OpenTelemetry and FeatureManagement fields, but I still don't feel we need to deviate.

Copy link
Member

@jimmyca15 jimmyca15 Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the only place where any external naming convention is used, and the naming deviates from the casing of all of our tags. Seems that it should be FeatureFlag

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. PR: #464

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants