Skip to content
33 changes: 24 additions & 9 deletions docs/Diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public interface ITelemetryContext
// Shared ActivitySource representing the current assembly/component.
ActivitySource ActivitySource { get; }
}

public interface IAssemblyTelemetryContext
Comment thread
marcschier marked this conversation as resolved.
Resolved
{
Meter CreateMeter(Assembly assembly);
ActivitySource GetActivitySource(Assembly assembly);
}
```

One abstraction covers all three telemetry pillars (logs, traces,
Expand All @@ -55,16 +61,24 @@ 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.
**Always use the extension methods**. The metric and tracing extensions
capture the component assembly at the call site, and all extensions
provide a default context when the supplied `ITelemetryContext` is
`null`. They use `IAssemblyTelemetryContext` when available and preserve
the fixed sources returned by existing `ITelemetryContext`
implementations otherwise. In debug builds, the fallback also reports a
debug check so missing telemetry is caught early.

### Obtaining a telemetry context

Expand Down Expand Up @@ -334,10 +348,11 @@ needing a full OpenTelemetry pipeline.

The stack creates one `Meter` per assembly that records measurements.
The meter's **name is the assembly name** of the component that
created it (via `ITelemetryContext.CreateMeter()` &rarr;
`Assembly.GetCallingAssembly().FullName`). Most tooling matches with
wildcards, so subscribe with `AddMeter("Opc.Ua.Core*", "Opc.Ua.Client*")`
to pick up everything the stack emits today.
created it (via `TelemetryExtensions.CreateMeter()` capturing
`Assembly.GetCallingAssembly()` and passing it to the context). Most
tooling matches with wildcards, so subscribe with
`AddMeter("Opc.Ua.Core*", "Opc.Ua.Client*")` to pick up everything the
stack emits today.

The instruments below are the complete current inventory. Tag values
are documented next to the tag key.
Expand Down
3 changes: 3 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,9 @@ explicit telemetry context**.
`ITelemetryContext Telemetry { get; }`.
- `ITransportBindingFactory<T>.Create(...)` now takes
`ITelemetryContext`.
- `IAssemblyTelemetryContext` is an optional extension for contexts that
create distinct meters and activity sources per component assembly.
Existing `ITelemetryContext` implementations remain compatible.

### System context

Expand Down
2 changes: 1 addition & 1 deletion src/Opc.Ua.Client/NodeCache/NodeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public NodeCache(
m_values = valuesBuilder.Build();
m_refs = refsBuilder.Build();

m_meter = telemetry.CreateMeter();
m_meter = TelemetryExtensions.CreateMeter(telemetry);
RegisterMetrics(m_meter, "nodes", m_nodes);
RegisterMetrics(m_meter, "values", m_values);
RegisterMetrics(m_meter, "references", m_refs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public CertificateCache(
int privateKeyCacheCapacity = kDefaultPrivateKeyCacheCapacity,
TimeSpan? privateKeyTtl = null)
{
m_meter = telemetry?.CreateMeter();
m_meter = telemetry == null
? null
: TelemetryExtensions.CreateMeter(telemetry);

privateKeyTtl ??= s_defaultPrivateKeyTtl;

Expand Down
2 changes: 1 addition & 1 deletion src/Opc.Ua.Core/Security/Crypto/CryptoProviderAuditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public CryptoProviderAuditor(
m_logger = telemetry.CreateLogger<CryptoProviderAuditor>();
m_policy = policy;
m_policies = policies ?? SecurityPolicies.Default;
m_meter = telemetry.CreateMeter();
m_meter = TelemetryExtensions.CreateMeter(telemetry);
Comment thread
marcschier marked this conversation as resolved.
Resolved

m_meter.CreateObservableGauge(
"opc.ua.crypto.providers",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,9 @@ private ClientChannelManager(
if (enableGeneralTelemetry)
{
Logger = TelemetryExtensions.CreateLogger<ClientChannelManager>(telemetry);
m_meter = telemetry?.CreateMeter();
m_meter = telemetry == null
? null
: TelemetryExtensions.CreateMeter(telemetry);
m_metrics = m_meter != null
? new ClientChannelManagerMetrics(this, m_meter)
: null;
Expand Down
2 changes: 1 addition & 1 deletion src/Opc.Ua.Core/Stack/Client/ClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public ClientBase(ITransportChannel channel, ITelemetryContext telemetry)
{
m_logger = telemetry.CreateLogger<ClientBase>();
m_eventLogger = telemetry.CreateLogger(CoreEventIds.CoreCompatibilityCategory);
m_meter = telemetry.CreateMeter();
m_meter = TelemetryExtensions.CreateMeter(telemetry);

if (channel == null)
{
Expand Down
57 changes: 57 additions & 0 deletions src/Opc.Ua.Types/Diagnostics/IAssemblyTelemetryContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/

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

namespace Opc.Ua
{
/// <summary>
/// Provides assembly-aware tracing and metrics.
/// </summary>
public interface IAssemblyTelemetryContext : ITelemetryContext
{
/// <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>
/// 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);
}
}
22 changes: 18 additions & 4 deletions src/Opc.Ua.Types/Diagnostics/TelemetryContextBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ namespace Opc.Ua
/// <summary>
/// Base telemetry context implementation
/// </summary>
public class TelemetryContextBase : ITelemetryContext
public class TelemetryContextBase : IAssemblyTelemetryContext
{
/// <inheritdoc/>
public ILoggerFactory LoggerFactory { get; }
Expand Down 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
48 changes: 45 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,47 @@ 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;
}
ITelemetryContext context = telemetry ?? Default;
return context is IAssemblyTelemetryContext assemblyContext
? assemblyContext.CreateMeter(assembly)
: context.CreateMeter();
Comment thread
marcschier marked this conversation as resolved.
Resolved
}

/// <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;
}
ITelemetryContext context = telemetry ?? Default;
return context is IAssemblyTelemetryContext assemblyContext
? assemblyContext.GetActivitySource(assembly)
: context.ActivitySource;
}

/// <summary>
Expand All @@ -116,10 +143,25 @@ 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;
}
ITelemetryContext context = telemetry ?? Default;
ActivitySource activitySource = context is IAssemblyTelemetryContext assemblyContext
? assemblyContext.GetActivitySource(assembly)
: context.ActivitySource;
return activitySource.StartActivity(name, kind);
}

/// <summary>
Expand Down
Loading
Loading