Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Features Added

* Add Logs to the Documents which are exported to LiveMetrics.
These are displayed in the right-side of the Live Metrics UX.
([]())

### Breaking Changes

### Bugs Fixed
Expand Down
13 changes: 10 additions & 3 deletions sdk/monitor/Azure.Monitor.OpenTelemetry.LiveMetrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@ These are provided without support and are not intended for production workloads
The following examples demonstrate how to add the Live Metrics client to your OpenTelemetry configuration.

```csharp
Sdk.CreateTracerProviderBuilder()
.AddLiveMetrics(o => o.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000")
.Build();
// This method gets called by the runtime. Use this method to add services to the container.
var builder = WebApplication.CreateBuilder(args);

// The following line enables Azure Monitor Distro.
builder.Services.AddOpenTelemetry().AddLiveMetrics(x => x.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000")

// This code adds other services for your application.
builder.Services.AddMvc();

var app = builder.Build();
```

For a complete example see [`Program.cs`](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/monitor/Azure.Monitor.OpenTelemetry.LiveMetrics/tests/Azure.Monitor.OpenTelemetry.LiveMetrics.Demo/Program.cs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public LiveMetricsExporterOptions() { }
}
public static partial class LiveMetricsExtensions
{
public static OpenTelemetry.Trace.TracerProviderBuilder AddLiveMetrics(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action<Azure.Monitor.OpenTelemetry.LiveMetrics.LiveMetricsExporterOptions> configure = null, string name = null) { throw null; }
public static OpenTelemetry.OpenTelemetryBuilder AddLiveMetrics(this OpenTelemetry.OpenTelemetryBuilder builder, System.Action<Azure.Monitor.OpenTelemetry.LiveMetrics.LiveMetricsExporterOptions> configure = null) { throw null; }
}
}
namespace Azure.Monitor.OpenTelemetry.LiveMetrics.Models
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public LiveMetricsExporterOptions() { }
}
public static partial class LiveMetricsExtensions
{
public static OpenTelemetry.Trace.TracerProviderBuilder AddLiveMetrics(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action<Azure.Monitor.OpenTelemetry.LiveMetrics.LiveMetricsExporterOptions> configure = null, string name = null) { throw null; }
public static OpenTelemetry.OpenTelemetryBuilder AddLiveMetrics(this OpenTelemetry.OpenTelemetryBuilder builder, System.Action<Azure.Monitor.OpenTelemetry.LiveMetrics.LiveMetricsExporterOptions> configure = null) { throw null; }
}
}
namespace Azure.Monitor.OpenTelemetry.LiveMetrics.Models
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Azure.Core" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.Console" VersionOverride="1.6.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can't we achieve this feature without this reference?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It could, but I don't think we want to.

This PR delivers a single extension method that configures LiveMetrics for both signals.
This extension method uses OpenTelemetryBuilder which comes from OpenTelemetry.Extensions.Hosting.

public static OpenTelemetryBuilder AddLiveMetrics(this OpenTelemetryBuilder builder, Action<LiveMetricsExporterOptions> configure = null)

Without this package, a user would need to configure our Manager singleton to the ServiceCollection and set LiveMetrics on both signals. I don't think we should take this approach.

Second, as we're preparing to shift our implementation to the Distro, it makes sense to take common dependencies now. This will make that shift easier.

</ItemGroup>

<!-- Shared sorce from Azure.Monitor.OpenTelemetry.Exporter -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ internal sealed partial class Manager : IDisposable
private readonly string _streamId = Guid.NewGuid().ToString(); // StreamId should be unique per application instance.
private bool _disposedValue;

public Manager(LiveMetricsExporterOptions options, IPlatform platform)
public Manager(LiveMetricsExporterOptions options) : this (options, new DefaultPlatform())
{
}

private Manager(LiveMetricsExporterOptions options, IPlatform platform)
{
options.Retry.MaxRetries = 0; // prevent Azure.Core from automatically retrying.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
#nullable disable

using System;
using Azure.Monitor.OpenTelemetry.Exporter.Internals.Platform;
using Azure.Monitor.OpenTelemetry.LiveMetrics.Internals;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry.Metrics;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Trace;

namespace Azure.Monitor.OpenTelemetry.LiveMetrics
Expand All @@ -19,57 +21,66 @@ namespace Azure.Monitor.OpenTelemetry.LiveMetrics
public static class LiveMetricsExtensions
{
/// <summary>
/// Adds Live Metrics to the TracerProvider.
/// Configures Azure Monitor Live Metrics for distributed tracing and logging.
/// </summary>
/// <param name="builder"><see cref="TracerProviderBuilder"/> builder to use.</param>
/// <param name="builder"><see cref="OpenTelemetryBuilder"/>.</param>
/// <param name="configure">Callback action for configuring <see cref="LiveMetricsExporterOptions"/>.</param>
/// <param name="name">Name which is used when retrieving options.</param>
/// <returns>The instance of <see cref="TracerProviderBuilder"/> to chain the calls.</returns>
public static TracerProviderBuilder AddLiveMetrics(
this TracerProviderBuilder builder,
Action<LiveMetricsExporterOptions> configure = null,
string name = null)
/// <returns>The supplied <see cref="OpenTelemetryBuilder"/> for chaining calls.</returns>
/// <exception cref="ArgumentNullException">Throws an exception if OpenTelemetryBuilder is null.</exception>
public static OpenTelemetryBuilder AddLiveMetrics(this OpenTelemetryBuilder builder, Action<LiveMetricsExporterOptions> configure = null)
{
if (builder == null)
if (builder.Services == null)
{
throw new ArgumentNullException(nameof(builder));
throw new ArgumentNullException(nameof(builder.Services));
}

var finalOptionsName = name ?? Options.DefaultName;
// Register a user provided configuration for Options.
if (configure != null)
{
builder.Services.Configure(configure);
}

builder.ConfigureServices(services =>
// Register a singleton for the internal Manager.
builder.Services.TryAddSingleton<Manager>(implementationFactory: serviceProvider =>
{
if (name != null && configure != null)
{
// If we are using named options we register the
// configuration delegate into options pipeline.
services.Configure(finalOptionsName, configure);
}
var options = serviceProvider.GetRequiredService<IOptions<LiveMetricsExporterOptions>>().Value;
return new Manager(options);
});

return builder.AddProcessor(sp =>
// Register the LiveMetricsActivityProcessor.
builder.WithTracing(configure: builder =>
{
// SETUP OPTIONS
LiveMetricsExporterOptions exporterOptions;
builder.AddLiveMetrics();
});

if (name == null)
// Register the LiveMetricsLogProcessor.
builder.Services.AddLogging(configure: logging =>
{
logging.AddOpenTelemetry(configure: options =>
{
exporterOptions = sp.GetRequiredService<IOptionsFactory<LiveMetricsExporterOptions>>().Create(finalOptionsName);
options.AddLiveMetrics();
});
});

// Configuration delegate is executed inline on the fresh instance.
configure?.Invoke(exporterOptions);
}
else
{
// When using named options we can properly utilize Options
// API to create or reuse an instance.
exporterOptions = sp.GetRequiredService<IOptionsMonitor<LiveMetricsExporterOptions>>().Get(finalOptionsName);
}
return builder;
}

// INITIALIZE INTERNALS
var manager = new Manager(exporterOptions, new DefaultPlatform());
private static TracerProviderBuilder AddLiveMetrics(this TracerProviderBuilder builder)
{
return builder.AddProcessor(serviceProvider =>
{
var manager = serviceProvider.GetRequiredService<Manager>();
return new LiveMetricsActivityProcessor(manager);
});
}

private static OpenTelemetryLoggerOptions AddLiveMetrics(this OpenTelemetryLoggerOptions loggerOptions)
{
return loggerOptions.AddProcessor(serviceProvider =>
{
var manager = serviceProvider.GetRequiredService<Manager>();
return new LiveMetricsLogProcessor(manager);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Monitor.OpenTelemetry.LiveMetrics.Internals;
using Azure.Monitor.OpenTelemetry.LiveMetrics.Internals.DataCollection;
using OpenTelemetry;
using OpenTelemetry.Logs;

namespace Azure.Monitor.OpenTelemetry.LiveMetrics
{
internal class LiveMetricsLogProcessor : BaseProcessor<LogRecord>
{
private bool _disposed;
private LiveMetricsResource? _resource;
private readonly Manager _manager;

internal LiveMetricsResource? LiveMetricsResource => _resource ??= ParentProvider?.GetResource().CreateAzureMonitorResource();

public LiveMetricsLogProcessor(Manager manager)
{
_manager = manager;
}

public override void OnEnd(LogRecord data)
{
// Check if live metrics is enabled.
if (!_manager.ShouldCollect())
{
return;
}

// Resource is not available at initialization and must be set later.
if (_manager.LiveMetricsResource == null && LiveMetricsResource != null)
{
_manager.LiveMetricsResource = LiveMetricsResource;
}

if (data.Exception is null)
{
_manager._documentBuffer.AddLogDocument(data);
}
else
{
_manager._documentBuffer.AddExceptionDocument(data.Exception);
}
}

protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
try
{
_manager.Dispose();
}
catch (System.Exception)
{
}
}

_disposed = true;
}

base.Dispose(disposing);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using OpenTelemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Trace;

namespace Azure.Monitor.OpenTelemetry.LiveMetrics.Demo
Expand All @@ -13,6 +14,7 @@ internal class Program
{
private const string ActivitySourceName = "MyCompany.MyProduct.MyLibrary";
private static readonly ActivitySource s_activitySource = new(ActivitySourceName);
private static ILogger _logger;

private const string ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000";

Expand All @@ -23,10 +25,21 @@ internal class Program

public static async Task Main(string[] args)
{
using TracerProvider tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(ActivitySourceName)
.AddLiveMetrics(configure => configure.ConnectionString = ConnectionString)
.Build();
// Until this console app can be replaced with an ASP.NET Core app,
// we need to manually configure a ServiceCollection.
IServiceCollection services = new ServiceCollection();
services.AddOpenTelemetry()
.AddLiveMetrics(x => x.ConnectionString = ConnectionString)
.WithTracing(configure: builder => builder.AddSource(ActivitySourceName));
var serviceProvider = services.BuildServiceProvider();

// Retrieve the TracerProvider - this is only necessary because this is a Console app.
// In an ASP.NET Core app, this is handled by the OpenTelemetry.Extensions.Hosting package.
var tracerProvider = serviceProvider.GetRequiredService<TracerProvider>();

// Retrieve an instance of the logger
_logger = serviceProvider.GetService<ILogger<Program>>();
Debug.Assert(_logger != null);

Console.WriteLine("Press any key to stop the loop.");

Expand Down Expand Up @@ -71,7 +84,7 @@ private static async Task GenerateTelemetry()
Console.WriteLine("Request Exception");
try
{
throw new Exception("Test exception");
throw new Exception("Test Request Exception");
}
catch (Exception ex)
{
Expand All @@ -94,7 +107,7 @@ private static async Task GenerateTelemetry()
Console.WriteLine("Dependency Exception");
try
{
throw new Exception("Test exception");
throw new Exception("Test Dependency Exception");
}
catch (Exception ex)
{
Expand All @@ -105,6 +118,38 @@ private static async Task GenerateTelemetry()
}
}

// Logs
if (GetRandomBool(percent: 70))
{
Console.WriteLine("Log");

_logger.Log(
logLevel: LogLevel.Information,
eventId: 0,
exception: null,
message: "Hello {name}.",
args: new object[] { "World" });

// Exception
if (GetRandomBool(percent: 40))
{
Console.WriteLine("Log Exception");
try
{
throw new Exception("Test Log Exception");
}
catch (Exception ex)
{
_logger.Log(
logLevel: LogLevel.Error,
eventId: 0,
exception: ex,
message: "Hello {name}.",
args: new object[] { "World" });
}
}
}

// Release the allocated memory
memoryChunk = null;
}
Expand Down