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

[examples] Add manual activities and custom metrics to ASP.NET Core example #4133

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions examples/AspNetCore/Controllers/WeatherForecastController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

namespace Examples.AspNetCore.Controllers;

using Examples.AspNetCore;
reyang marked this conversation as resolved.
Show resolved Hide resolved
using Microsoft.AspNetCore.Mvc;

[ApiController]
Expand All @@ -24,16 +25,18 @@ public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching",
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching",
};

private static readonly HttpClient HttpClient = new();

private readonly ILogger<WeatherForecastController> logger;
private readonly ITelemetryService telemetryService;

public WeatherForecastController(ILogger<WeatherForecastController> logger)
public WeatherForecastController(ILogger<WeatherForecastController> logger, ITelemetryService telemetryService)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
}

[HttpGet]
Expand All @@ -45,6 +48,11 @@ public IEnumerable<WeatherForecast> Get()
// how dependency calls will be captured and treated
// automatically as child of incoming request.
var res = HttpClient.GetStringAsync("http://google.com").Result;

// Manually create an activity. This will become a child of
danelson marked this conversation as resolved.
Show resolved Hide resolved
// the incoming request.
using var activity = this.telemetryService.ActivitySource.StartActivity("calculate forecast");

var rng = new Random();
var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Expand All @@ -54,6 +62,9 @@ public IEnumerable<WeatherForecast> Get()
})
.ToArray();

// Count the freezing days
this.telemetryService.FreezingDaysCounter.Add(forecast.Count(f => f.TemperatureC < 0));

this.logger.LogInformation(
"WeatherForecasts generated {count}: {forecasts}",
forecast.Length,
Expand Down
29 changes: 29 additions & 0 deletions examples/AspNetCore/ITelemetryService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// <copyright file="ITelemetryService.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

namespace Examples.AspNetCore;

using System.Diagnostics;
using System.Diagnostics.Metrics;

public interface ITelemetryService
Copy link

Choose a reason for hiding this comment

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

@davidfowl may disagree, but creating an interface feels like unnecessary ceremony to me. Presumably this is an internal API boundary between two different components in the same library so I would not expect any alternate implementations to exist. I think the other usual argument is that someone might want to mock the interface for testing, but I doubt they could produce a useful mock unless the interface abstracted them from all the concrete types. If we need to delve into it I have some thoughts on how this can be tested without mocking.

If we do wind up keeping the interface I'd suggest not making it public.

Copy link
Contributor

@davidfowl davidfowl Feb 3, 2023

Choose a reason for hiding this comment

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

We don’t need an interface, just an implementation would suffice. Though, I think naturally people would expect to see an interface (even though there's nothing really mockable here).

{
ActivitySource ActivitySource { get; }

Meter Meter { get; }

Counter<int> FreezingDaysCounter { get; }
}
14 changes: 13 additions & 1 deletion examples/AspNetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// </copyright>

using System.Reflection;
using Examples.AspNetCore;
Copy link
Member

Choose a reason for hiding this comment

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

"using" or "namespace"?

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 think using is correct. This allows me to reference Instrumentation here. As far as I know you cannot define a namespace with these top level statements

using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Instrumentation.AspNetCore;
Expand All @@ -34,12 +35,19 @@
// Note: Switch between Console/OTLP by setting UseLogExporter in appsettings.json.
var logExporter = appBuilder.Configuration.GetValue<string>("UseLogExporter").ToLowerInvariant();

var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
danelson marked this conversation as resolved.
Show resolved Hide resolved

// Build a resource configuration action to set service information.
Action<ResourceBuilder> configureResource = r => r.AddService(
serviceName: appBuilder.Configuration.GetValue<string>("ServiceName"),
serviceVersion: Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown",
serviceVersion: version,
serviceInstanceId: Environment.MachineName);

// Create a container to hold ActivitySource, Meter, and all
// instruments so that DI can be used to inject these dependencies
var telemetryService = new TelemetryService(version);
appBuilder.Services.AddSingleton<ITelemetryService>(telemetryService);

// Configure OpenTelemetry tracing & metrics with auto-start using the
// StartWithHost extension from OpenTelemetry.Extensions.Hosting.
appBuilder.Services.AddOpenTelemetry()
Expand All @@ -48,7 +56,9 @@
{
// Tracing

// Ensure the TracerProvider subscribes to any custom ActivitySources.
builder
.AddSource(telemetryService.ActivitySource.Name)
.SetSampler(new AlwaysOnSampler())
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation();
Expand Down Expand Up @@ -98,7 +108,9 @@
{
// Metrics

// Ensure the MeterProvider subscribes to any custom Meters.
builder
.AddMeter(telemetryService.Meter.Name)
.AddRuntimeInstrumentation()
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation();
Expand Down
38 changes: 38 additions & 0 deletions examples/AspNetCore/TelemetryService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// <copyright file="TelemetryService.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

namespace Examples.AspNetCore;

using System.Diagnostics;
using System.Diagnostics.Metrics;

public class TelemetryService : ITelemetryService
Copy link

Choose a reason for hiding this comment

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

I'd recommend not appending 'Service' on this type name. All types that get added to a DI container can be refered to as services, but it is not usually added to the type name.

Naming nit: how about 'Instrumentation' instead of 'Telemetry'?

{
private const string InstrumentationScopeName = "Examples.AspNetCore";

public TelemetryService(string version)
Copy link

Choose a reason for hiding this comment

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

I'd recommend not having a version parameter here. Its not clear to me we are gaining any value by parameterizing it. We can inline typeof(TelemetryService).Assembly.GetName().Version?.ToString() below.

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 parameterized it so that it was guaranteed to match the version defined as part of the provider's resource. Not that I would necessarily expect either of these to change.

Slightly related but I dislike how the original code was setting "unknown" in the case of null since it is an optional parameter and already handled nicely.

{
this.ActivitySource = new ActivitySource(InstrumentationScopeName, version);
Copy link
Member

Choose a reason for hiding this comment

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

Couple things for discussion:

  1. Where do we dispose some of these properties?
  2. Do we want to make sure that at maximum 1 instance can exist? (rather than assuming that folks who use DI pattern would ensure it is singleton)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. A few comments regarding your first question
    1. TelemetryService or ITelemetryService (if the is registered like appBuilder.Services.AddSingleton<ITelemetryService> does the interface have to implement IDisposable?) could implement IDisposable and I believe it will be handled on application shutdown. Is there a concern regarding order of disposal between ActivitySource and TracerProvider or Meter and MeterProvider?
    2. The examples that use statics don't dispose of ActivitySource and Meter. Is this a concern?
      1. https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs#L29
      2. private static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0");
        private static readonly Counter<long> MyFruitCounter = MyMeter.CreateCounter<long>("MyFruitCounter");
      3. private static readonly Meter MyMeter = new("MyMeter");
        private static readonly Meter MyMeter2 = new("MyMeter2");

Copy link

Choose a reason for hiding this comment

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

  • I'd recommend TelemetryService implement IDisposable (it is not necessary for ITelemetryService to extend IDisposable if we still have the interface at all).

  • I don't think there is any concern on the relative disposal order, any order should work.

  • I don't think we should try to enforce that only one instance of this object exists. For a developer unit testing their own telemetry generation it may be quite useful to have multiple instances existing even if OTel wouldn't support transmitting the telemetry sent from those multiple instances. Longer term we probably want OTel to handle situations where an app may have multiple identically named Meters existing in the process within different DI containers. These Meters might be serviced by different instances of the OTel telemetry pipeline per container, or they might be tagged in some way to allow them to be consilidated in a single pipeline.

  • The reason the statics don't worry about disposal is they assume the lifetime of the Meter is the same as the lifetime of the process and the OS will release all resource on process exit. However in a DI container there is a presumption that the container might be shorter lived than the process (for example in a unit test).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Disposal of ActivitySource and Meter has been added.

this.Meter = new Meter(InstrumentationScopeName, version);
this.FreezingDaysCounter = this.Meter.CreateCounter<int>("weather.days.freezing", "The number of days where the temperature is below freezing");
}

public ActivitySource ActivitySource { get; }

public Meter Meter { get; }

public Counter<int> FreezingDaysCounter { get; }
}