Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ explicit telemetry context**.
`ITelemetryContext Telemetry { get; }`.
- `ITransportBindingFactory<T>.Create(...)` now takes
`ITelemetryContext`.
- `ITelemetryContext` gains `CreateMeter(Assembly)` and
`GetActivitySource(Assembly)` for component-specific telemetry.

### System context

Expand Down
42 changes: 31 additions & 11 deletions docs/Diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ public interface ITelemetryContext
// Creates a new Meter for recording metrics (caller disposes).
Meter CreateMeter();

// Creates a Meter for a specific assembly (caller disposes).
Meter CreateMeter(Assembly assembly);

// Factory used to create typed ILogger instances.
ILoggerFactory LoggerFactory { get; }

// Shared ActivitySource representing the current assembly/component.
ActivitySource ActivitySource { get; }

// Gets the shared ActivitySource for a specific assembly.
ActivitySource GetActivitySource(Assembly assembly);
}
```

Expand All @@ -55,16 +61,22 @@ public static class TelemetryExtensions
ILogger CreateLogger(this ITelemetryContext context, string categoryName);
ILogger<T> CreateLogger<T>(this ITelemetryContext context);

// Captures the calling assembly and creates its Meter.
Meter CreateMeter(this ITelemetryContext context);

// Captures the calling assembly and gets its shared ActivitySource.
ActivitySource GetActivitySource(this ITelemetryContext context);

// Starts a new Activity with the shared ActivitySource.
Activity StartActivity(this ITelemetryContext context, string activityName, ActivityKind kind = ActivityKind.Internal);
}
```

**Always use the extension methods**. They guarantee a non-null logger
and activity even when the supplied `ITelemetryContext` is `null` or
returns `null` from a property: in release builds the fallback is a
backwards-compatible trace logger; in debug builds it is a debug-check
logger that throws if used so missing telemetry is caught early.
The metric and tracing members on `ITelemetryContext` identify the
component assembly at the call site. The extension methods provide the
same behavior when the supplied `ITelemetryContext` may be `null`, using
the default context when necessary. In debug builds, the fallback also
reports a debug check so missing telemetry is caught early.

### Obtaining a telemetry context

Expand Down Expand Up @@ -112,8 +124,11 @@ right place to materialize the context.

### Using the telemetry context

Always use the extension methods to obtain loggers, meters, and
activities. The returned instance is guaranteed to be non-null.
Use the context directly when it is non-null. Use the extension methods
when a context may be null; the returned instance is guaranteed to be
non-null. Because `ITelemetryContext.CreateMeter()` shadows its extension,
invoke the nullable meter fallback explicitly as
`TelemetryExtensions.CreateMeter(telemetry)`.

```csharp
// Obtain telemetry context
Expand Down Expand Up @@ -142,6 +157,11 @@ cached by the `ILoggerProvider`, so obtaining one is cheap. Still,
be mindful of the per-object reference cost when creating loggers for
very large object populations (`NodeState`, `NodeId`).

Resolve source names before registering an `ActivityListener`, and keep its
`ShouldListenTo` filter free of source lookups. Creating an `ActivitySource`
synchronously invokes existing listeners; resolving a missing source inside
that filter can recursively re-enter source creation.

`ConsoleReferenceClient` and `ConsoleReferenceServer` show the full
pattern end-to-end.

Expand Down Expand Up @@ -277,12 +297,12 @@ looks like:
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("opc-ua-client"))
.WithTracing(t => t
.AddSource("Opc.Ua.Core") // names used by stack ActivitySources
.AddSource("Opc.Ua.Client.Session")
.AddSource("Opc.Ua.Core*") // assembly-qualified source names
.AddSource("Opc.Ua.Client*")
.AddOtlpExporter())
.WithMetrics(m => m
.AddMeter("Opc.Ua.Client.*") // wildcard match for stack meters
.AddMeter("Opc.Ua.Server.*")
.AddMeter("Opc.Ua.Client*") // assembly-qualified meter names
.AddMeter("Opc.Ua.Server*")
.AddOtlpExporter());
```

Expand Down
2 changes: 2 additions & 0 deletions docs/migrate/2.0.x/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ explicit telemetry context**.
`ITelemetryContext Telemetry { get; }`.
- `ITransportBindingFactory<T>.Create(...)` now takes
`ITelemetryContext`.
- `ITelemetryContext` gains `CreateMeter(Assembly)` and
`GetActivitySource(Assembly)` for component-specific telemetry.

### System context

Expand Down
14 changes: 14 additions & 0 deletions fuzzing/Common/Fuzz.Tools/Logging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,23 @@ public Meter CreateMeter()
return new Meter("Fuzzing", "1.0.0");
}

/// <inheritdoc/>
public Meter CreateMeter(System.Reflection.Assembly assembly)
{
assembly.ThrowIfNull(nameof(assembly));
return CreateMeter();
}

/// <inheritdoc/>
public ActivitySource ActivitySource { get; }

/// <inheritdoc/>
public ActivitySource GetActivitySource(System.Reflection.Assembly assembly)
{
assembly.ThrowIfNull(nameof(assembly));
return ActivitySource;
}

/// <inheritdoc/>
public void Dispose()
{
Expand Down
14 changes: 14 additions & 0 deletions samples/Reference/ConsoleReferenceServer/ConsoleUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,23 @@ public Meter CreateMeter()
return new Meter("Quickstarts", "1.0.0");
}

/// <inheritdoc/>
public Meter CreateMeter(System.Reflection.Assembly assembly)
{
assembly.ThrowIfNull(nameof(assembly));
return CreateMeter();
}

/// <inheritdoc/>
public ActivitySource ActivitySource { get; }

/// <inheritdoc/>
public ActivitySource GetActivitySource(System.Reflection.Assembly assembly)
{
assembly.ThrowIfNull(nameof(assembly));
return ActivitySource;
}

/// <inheritdoc/>
public void Dispose()
{
Expand Down
12 changes: 12 additions & 0 deletions src/Opc.Ua.Core.Diagnostics/Dissection/OfflineSecureChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,18 @@ public System.Diagnostics.Metrics.Meter CreateMeter()
return new System.Diagnostics.Metrics.Meter(
"Opc.Ua.Core.Diagnostics.OfflineSecureChannel");
}

public System.Diagnostics.Metrics.Meter CreateMeter(
System.Reflection.Assembly assembly)
{
return CreateMeter();
}

public System.Diagnostics.ActivitySource GetActivitySource(
System.Reflection.Assembly assembly)
{
return ActivitySource;
}
}
}
}
10 changes: 10 additions & 0 deletions src/Opc.Ua.Core.Diagnostics/Dissection/ServiceCallReassembler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,16 @@ public Meter CreateMeter()
return new Meter("Opc.Ua.Core.Diagnostics.ServiceCallReassembler");
}

public Meter CreateMeter(System.Reflection.Assembly assembly)
{
return CreateMeter();
}

public ActivitySource GetActivitySource(System.Reflection.Assembly assembly)
{
return ActivitySource;
}

private NoopTelemetryContext()
{
}
Expand Down
10 changes: 10 additions & 0 deletions src/Opc.Ua.Core.Diagnostics/Replay/MockClientReplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,16 @@ public Meter CreateMeter()
return new Meter("Opc.Ua.Core.Diagnostics.MockClientReplay");
}

public Meter CreateMeter(System.Reflection.Assembly assembly)
{
return CreateMeter();
}

public ActivitySource GetActivitySource(System.Reflection.Assembly assembly)
{
return ActivitySource;
}

private NoopTelemetryContext()
{
}
Expand Down
17 changes: 17 additions & 0 deletions src/Opc.Ua.Types/Diagnostics/ITelemetryContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Reflection;
using Microsoft.Extensions.Logging;

namespace Opc.Ua
Expand All @@ -45,6 +46,14 @@ public interface ITelemetryContext
/// </summary>
Meter CreateMeter();

/// <summary>
/// Create the meter instance for the specified assembly.
/// The caller is responsible to dispose the meter instance
/// returned.
/// </summary>
/// <param name="assembly">The component assembly producing the metrics.</param>
Meter CreateMeter(Assembly assembly);

/// <summary>
/// Access the logger factory to create logger objects.
/// </summary>
Expand All @@ -56,5 +65,13 @@ public interface ITelemetryContext
/// held as part of the telemetry context.
/// </summary>
ActivitySource ActivitySource { get; }

/// <summary>
/// Get an activity source for the specified assembly.
/// Do not dispose the activity source returned as it is
/// held as part of the telemetry context.
/// </summary>
/// <param name="assembly">The component assembly producing the activities.</param>
ActivitySource GetActivitySource(Assembly assembly);
}
}
20 changes: 17 additions & 3 deletions src/Opc.Ua.Types/Diagnostics/TelemetryContextBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@ public Meter CreateMeter()
{
assembly = typeof(TelemetryContextBase).Assembly;
}
(string name, string version) = GetAssemblyInfo(assembly);
return CreateMeter(assembly);
}

/// <inheritdoc/>
public Meter CreateMeter(Assembly assembly)
{
(string name, string version) = GetAssemblyInfo(
assembly ?? throw new ArgumentNullException(nameof(assembly)));
return new Meter(name, version);
}

Expand All @@ -92,11 +99,18 @@ public ActivitySource ActivitySource
{
assembly = typeof(TelemetryContextBase).Assembly;
}
return s_sources.GetOrAdd(GetAssemblyInfo(assembly),
key => new ActivitySource(key.Item1, key.Item2));
return GetActivitySource(assembly);
}
}

/// <inheritdoc/>
public ActivitySource GetActivitySource(Assembly assembly)
{
return s_sources.GetOrAdd(
GetAssemblyInfo(assembly ?? throw new ArgumentNullException(nameof(assembly))),
key => new ActivitySource(key.Item1, key.Item2));
}

private static (string, string) GetAssemblyInfo(Assembly assembly)
{
return s_cache.GetOrAdd(assembly, GetAssemblyInfoCore);
Expand Down
40 changes: 37 additions & 3 deletions src/Opc.Ua.Types/Diagnostics/TelemetryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -92,21 +93,41 @@ public static ILogger CreateLogger(this ITelemetryContext? telemetry, string cat
/// </summary>
/// <param name="telemetry">The telemetry context to use</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public static Meter CreateMeter(this ITelemetryContext? telemetry)
{
DebugCheck(telemetry);
return telemetry?.CreateMeter() ?? Default.CreateMeter();
Assembly assembly;
try
{
assembly = Assembly.GetCallingAssembly();
}
catch (PlatformNotSupportedException)
{
assembly = typeof(TelemetryExtensions).Assembly;
}
return telemetry?.CreateMeter(assembly) ?? Default.CreateMeter(assembly);
}

/// <summary>
/// Get activity source
/// </summary>
/// <param name="telemetry">The telemetry context to use</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public static ActivitySource GetActivitySource(this ITelemetryContext? telemetry)
{
DebugCheck(telemetry);
return telemetry?.ActivitySource ?? Default.ActivitySource;
Assembly assembly;
try
{
assembly = Assembly.GetCallingAssembly();
}
catch (PlatformNotSupportedException)
{
assembly = typeof(TelemetryExtensions).Assembly;
}
return telemetry?.GetActivitySource(assembly) ?? Default.GetActivitySource(assembly);
}

/// <summary>
Expand All @@ -116,10 +137,23 @@ public static ActivitySource GetActivitySource(this ITelemetryContext? telemetry
/// <param name="name">The name of the caller</param>
/// <param name="kind">The activity kind</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public static Activity? StartActivity(this ITelemetryContext? telemetry,
[CallerMemberName] string name = "", ActivityKind kind = ActivityKind.Internal)
{
return telemetry.GetActivitySource().StartActivity(name, kind);
DebugCheck(telemetry);
Assembly assembly;
try
{
assembly = Assembly.GetCallingAssembly();
}
catch (PlatformNotSupportedException)
{
assembly = typeof(TelemetryExtensions).Assembly;
}
ActivitySource activitySource =
telemetry?.GetActivitySource(assembly) ?? Default.GetActivitySource(assembly);
return activitySource.StartActivity(name, kind);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,15 @@ public Meter CreateMeter()
{
return new Meter("Opc.Ua.Core.Diagnostics.Tests");
}

public Meter CreateMeter(System.Reflection.Assembly assembly)
{
return CreateMeter();
}

public ActivitySource GetActivitySource(System.Reflection.Assembly assembly)
{
return ActivitySource;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ public Meter CreateMeter()
{
return new("Stub");
}

public Meter CreateMeter(System.Reflection.Assembly assembly)
{
return CreateMeter();
}

public ActivitySource GetActivitySource(System.Reflection.Assembly assembly)
{
return ActivitySource;
}
}

private sealed class BufferManagerFactoryStub : IBufferManagerFactory
Expand Down
Loading
Loading