Skip to content
Open
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
12 changes: 11 additions & 1 deletion docs/DeveloperGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ Each project owns exactly one event-id class, named `<AssemblyToken>EventIds`, i

The token prefix is required because the stack uses `InternalsVisibleTo`: two `internal` classes with the same name in the same namespace collide across an IVT boundary (`CS0436`). The class holds one `public const int` offset per log class. Offsets are assigned in class-alphabetical order starting at 0; each block reserves at least five spare slots for future messages and is then rounded up to the next multiple of ten, so ids stay documented and managed in one place. Every log method sets `EventId = <AssemblyToken>EventIds.<Class> + <zero-based message index within that class>`.

##### Narrow exception: retained EventSource-compatibility ids

The four legacy `System.Diagnostics.Tracing.EventSource` providers (`OPC-UA-Core`, `OPC-UA-Client`, `OPC-UA-Server`, `Opc.Ua.ChannelManager`) were removed and replaced with `[LoggerMessage]` equivalents. Their compatibility log methods are a deliberate, narrow exception to the convention above: each keeps the exact numeric id, event name, level, message template, and structured fields the corresponding ETW event had, so consumers can preserve event identity when they migrate from ETW to `ILogger`. Concretely:

- The compatibility log class uses the **old provider name as its `ILogger` category** (e.g. `"OPC-UA-Core"`, `"Opc.Ua.ChannelManager"`) instead of the typed, per-class category used elsewhere in the project.
- `EventId` resolves to the **literal legacy numeric id** (e.g. `10` for `OPC-UA-Core`'s former `ServiceCallStart`) rather than a normal per-class offset. Keep these values in the affected project's `EventIds.cs`; compatibility ids are scoped to their own logger category, so they may intentionally overlap ordinary per-assembly values.
- Every compatibility method sets `EventName` explicitly (`[LoggerMessage(EventId = 10, EventName = "ServiceCallStart", Level = LogLevel.Trace, Message = "...")]`) so `EventId.Name` matches the original ETW event name exactly.
- ETW-only metadata (provider GUID, `Task`, `Keywords`, manifest) is **not** retained because there is no `ILogger` equivalent.
- Do **not** use this pattern for new log messages. It exists only to preserve the event identities that previously shipped through the four EventSource providers; see [Diagnostics.md](Diagnostics.md#high-speed-logging-and-source-generators) and [migrate/2.0.x/telemetry.md](migrate/2.0.x/telemetry.md) for the full removal/compatibility mapping.

#### Log class convention

- **One log class per file**, named `<PrimaryClass>Log`, `internal static partial`, appended at the end of the file inside the same namespace.
Expand Down Expand Up @@ -174,7 +184,7 @@ logger.ReadArrayZeroDimension(index, dimensions);
- [ ] Placeholders are named and match parameter names; no interpolation.
- [ ] Parameter types match the arguments; nullable only where needed; no `object`.
- [ ] Expensive arguments are guarded with `IsEnabled`; cheap ones are not.
- [ ] `EventId` uses the project's `<AssemblyToken>EventIds` offset (or a literal range for a shared/linked file).
- [ ] `EventId` uses the project's `<AssemblyToken>EventIds` offset (or a literal range for a shared/linked file, or a literal legacy id with an explicit `EventName` for a retained EventSource-compatibility message — see [the narrow exception](#narrow-exception-retained-eventsource-compatibility-ids)).
- [ ] When testing with a mocked `ILogger`, stub `IsEnabled(...) => true` and match on `EventId.Name`, not the (empty) source-generated state `ToString()`.

### Other common tasks
Expand Down
6 changes: 1 addition & 5 deletions docs/Diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,7 @@ Client transport channel manager &mdash; defined in
| `opc.ua.channel.refcount` | ObservableGauge&lt;long&gt; | &mdash; | `endpoint` | Reference count per channel entry. |
| `opc.ua.channel.participants` | ObservableGauge&lt;long&gt; | &mdash; | `endpoint` | Participant count per channel entry. |

Note: the `participant` tag carries the **kind prefix only**
(`Session`, `Discovery`, etc.). The per-instance participant id is
deliberately omitted to keep metric cardinality bounded; the full id
is available on the related Activity tags and EventSource events for
correlation.
Note: the `participant` tag carries the **kind prefix only** (`Session`, `Discovery`, etc.). The per-instance participant id is deliberately omitted to keep metric cardinality bounded; the full id is available on the related Activity tags and on the structured logs emitted under the `Opc.Ua.ChannelManager` logger category for correlation (see [Sessions.md](Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry)).

Client request duration &mdash; defined in `ClientBase.cs`:

Expand Down
62 changes: 26 additions & 36 deletions docs/Sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,36 +655,37 @@ ManagedSession session = await new ManagedSessionBuilder(configuration, telemetr
For migration notes on the budget-aware APIs, see
[the migration guide](MigrationGuide.md#shared-reconnect-budget-for-managedsession-and-the-channel-manager).

### Diagnostics surface contract — what tags and EventSource fields carry
### Diagnostics surface contract — what tags and structured log fields carry

The channel manager emits diagnostics through three independent
channels: `System.Diagnostics.Activity` tags (distributed tracing),
the `Opc.Ua.ChannelManager` `EventSource` (ETW / `dotnet-trace`), and
`System.Diagnostics.Metrics` instruments. Tag and field values surfaced
through any of these are restricted to **OPC UA-protocol-level fields
only**:
The channel manager emits diagnostics through three independent channels: `System.Diagnostics.Activity` tags (distributed tracing), structured `ILogger` logs under the `Opc.Ua.ChannelManager` category, and `System.Diagnostics.Metrics` instruments. The channel manager previously emitted its own `Opc.Ua.ChannelManager` `EventSource` (ETW / `dotnet-trace`); that provider is removed. The structured logs are the replacement — same category name, same event identities — routed through `Microsoft.Extensions.Logging` / OpenTelemetry logging instead of ETW.

Each event kept its original `EventId` and `EventName` on the `[LoggerMessage]` replacement, so tooling matching on the numeric id or name keeps working:

| EventId | EventName | Level | Message template |
|---|---|---|---|
| 1 | `ChannelOpened` | Information | `Channel opened. Endpoint={Endpoint}, Reverse={Reverse}, Refcount={Refcount}, Participants={ParticipantCount}` |
| 2 | `ChannelClosed` | Information | `Channel closed. Endpoint={Endpoint}, Reason={Reason}, Refcount={Refcount}, Participants={ParticipantCount}` |
| 3 | `StateChanged` | Information | `State changed. Endpoint={Endpoint}, Previous={PreviousState}, New={NewState}, Attempt={ReconnectAttempt}, Status={StatusCode}, ErrorMessage={ErrorMessage}` |
| 4 | `ReconnectStarted` | Information | `Reconnect started. Endpoint={Endpoint}, AttemptCount={AttemptCount}` |
| 5 | `ReconnectCompleted` | Information | `Reconnect completed. Endpoint={Endpoint}, AttemptCount={AttemptCount}, Outcome={Outcome}` |
| 6 | `ReconnectFailed` | Warning | `Reconnect failed. Endpoint={Endpoint}, Attempt={Attempt}, Outcome={Outcome}, Status={StatusCode}, ErrorMessage={ErrorMessage}` |
| 7 | `ParticipantAttached` | Information | `Participant attached. Endpoint={Endpoint}, Participant={ParticipantId}, Refcount={Refcount}, Participants={ParticipantCount}` |
| 8 | `ParticipantDetached` | Information | `Participant detached. Endpoint={Endpoint}, Participant={ParticipantId}, Refcount={Refcount}, Participants={ParticipantCount}` |

Tag and structured-field values surfaced through Activity tags or these logs are restricted to **OPC UA-protocol-level fields only**:

- `StatusCode` (numeric / symbolic), e.g. `BadSecureChannelClosed`
- `ServiceResult.SymbolicId`
- `ServiceResult.LocalizedText.Text` — the operator-facing message

Notably, the following are **never** sent to Activity tags or to
`EventSource` events:
Notably, the following are **never** sent to Activity tags or to the `Opc.Ua.ChannelManager` structured logs above:

- The full `ServiceResult.ToString()` serialization
- `ServiceResult.AdditionalInfo` — typically carries inner-exception
messages, file paths, internal IDs
- `Exception.StackTrace`, `Exception.Message`, or any other inner .NET
exception detail
- `ServiceResult.AdditionalInfo` — typically carries inner-exception messages, file paths, internal IDs
- `Exception.StackTrace`, `Exception.Message`, or any other inner .NET exception detail
- `Inner` recursion into `ServiceResult.InnerResult`

This keeps internal client/server diagnostics out of distributed
tracing backends where operators with telemetry-read access (but no
production-debug access) should not be able to read internal failure
detail. Full failure context — including stack traces and
`AdditionalInfo` — flows only through the local `ILogger.LogDebug`
path on the manager, where it stays under the host's log-access
controls.
This keeps internal client/server diagnostics out of distributed tracing and logging backends where operators with telemetry-read access (but no production-debug access) should not be able to read internal failure detail. Full failure context — including stack traces and `AdditionalInfo` — flows only through a separate, local `ILogger.LogDebug` call on the manager, where it stays under the host's log-access controls; it is never folded into the `Opc.Ua.ChannelManager`-category events listed above.

The metric tag set is also bounded for routine operation:

Expand All @@ -696,22 +697,11 @@ The metric tag set is also bounded for routine operation:
| `opc.ua.channel.gate.wait` | `endpoint` |
| `opc.ua.channel.participant.timeout.count` / `opc.ua.channel.participant.recreate.count` | `endpoint`, `participant` (+ `success` on recreate) |

`outcome` is one of `success`, `transient-failure`, `policy-exhausted`,
`fatal-channel`. `reason` is one of `lease-released`,
`manager-disposed`, `faulted`. `endpoint` cardinality is bounded by the
number of distinct OPC UA endpoint URLs the application connects to.

> The `participant` tag carries the **kind prefix** of the
> participant identifier (e.g. `"Session"`, `"Client"`), not the
> per-instance suffix. This keeps cardinality bounded by the small set
> of participant kinds rather than growing with every session /
> reconnect-storm participant ever created. The full per-instance
> `IReconnectParticipant.Id` is preserved on Activity tags and
> EventSource events so individual sessions remain correlatable in
> distributed traces. Custom participants that don't use the
> "kind-`-`-instance" naming convention contribute their full id to
> the tag, so prefer the prefix-then-suffix shape for new participant
> types.
`outcome` is one of `success`, `transient-failure`, `policy-exhausted`, `fatal-channel`. `reason` is one of `lease-released`, `manager-disposed`, `faulted`. `endpoint` cardinality is bounded by the number of distinct OPC UA endpoint URLs the application connects to.

> The `participant` tag carries the **kind prefix** of the participant identifier (e.g. `"Session"`, `"Client"`), not the per-instance suffix. This keeps cardinality bounded by the small set of participant kinds rather than growing with every session / reconnect-storm participant ever created. The full per-instance `IReconnectParticipant.Id` is preserved on Activity tags and on the `Opc.Ua.ChannelManager` structured logs above so individual sessions remain correlatable in distributed traces. Custom participants that don't use the "kind-`-`-instance" naming convention contribute their full id to the tag, so prefer the prefix-then-suffix shape for new participant types.

For the removal of the old `EventSource` providers (including this one) and migration guidance for `EventListener` / `dotnet-trace` consumers, see [migrate/2.0.x/telemetry.md](migrate/2.0.x/telemetry.md#etw-eventsource-provider-removal).

### DI registration

Expand Down
77 changes: 76 additions & 1 deletion docs/migrate/2.0.x/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,80 @@ obtained from `ITelemetryContext.CreateLogger<T>()`.

---

## ETW `EventSource` provider removal

> **When to read this:** Read this if your application or tooling attaches an `EventListener`, `dotnet-trace`, PerfView, or any other ETW consumer to one of the stack's `System.Diagnostics.Tracing.EventSource` providers, or if it sets `ClientTraceFlags.EventLog` on a client.

The stack shipped four internal `EventSource` providers for high-performance tracing. All four are **removed** in 2.0; there is no compile-time or runtime fallback. Their events are replaced one-for-one by `[LoggerMessage]`-generated `ILogger` calls that preserve the original numeric `EventId`, `EventId.Name`, mapped level, message template, and structured fields, so the *event identity* survives even though the transport changes from ETW to `Microsoft.Extensions.Logging` / OpenTelemetry logging.

| Removed provider (ETW name) | Assembly | Replacement `ILogger` category |
|---|---|---|
| `OPC-UA-Core` | `Opc.Ua.Core` | `OPC-UA-Core` |
| `OPC-UA-Client` | `Opc.Ua.Client` | `OPC-UA-Client` |
| `OPC-UA-Server` | `Opc.Ua.Server` | `OPC-UA-Server` |
| `Opc.Ua.ChannelManager` | `Opc.Ua.Core` (client channel manager) | `Opc.Ua.ChannelManager` |

The replacement logger category is always the **exact old ETW provider name**, not the assembly's usual typed category. Migrated filters can therefore keep the same identifying string (for example, `AddFilter("OPC-UA-Client", LogLevel.Trace)`) after they move from an ETW provider subscription to `ILogger` configuration. `EventLevel` mapped to `LogLevel` on a like-for-like basis (`Verbose` &rarr; `Trace`, `Informational` &rarr; `Information`, `Warning` &rarr; `Warning`, `Error`/`Critical` &rarr; `Error`/`Critical`). See [DeveloperGuide.md — narrow exception: retained EventSource-compatibility ids](../../DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) for the authoring-side rules and [Sessions.md — diagnostics surface contract](../../Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) for the full `Opc.Ua.ChannelManager` event table.

**Not retained:** the ETW provider GUID, `EventTask`/`EventKeywords` definitions, and the ETW manifest. There is no `ILogger` equivalent for these, and providers/consumers that depended on them (raw ETW session subscribers keyed by provider GUID, manifest-based decoders) must move to `Microsoft.Extensions.Logging` category/event-name filtering instead.

### `ClientTraceFlags.EventLog` removed (source breaking)

`ClientTraceFlags.EventLog` — the flag that routed `ClientBase` request/response tracing to the (now-removed) `OPC-UA-Core` `EventSource` — is **removed**. Code that references it fails to compile:

```csharp
// OLD - no longer compiles (ClientTraceFlags.EventLog removed)
client.ActivityTraceFlags = ClientTraceFlags.Log | ClientTraceFlags.EventLog;

// NEW - ClientTraceFlags.Log alone now emits the canonical OPC-UA-Core
// compatibility events (same EventId / EventName / message as the old
// EventLog flag); the separate per-call ClientBase structured logs that
// ClientTraceFlags.Log used to emit are replaced by these events
client.ActivityTraceFlags = ClientTraceFlags.Log;
```

`ClientTraceFlags.Log` now emits the canonical `OPC-UA-Core` request events (`ServiceCallStart` / `ServiceCallStop` / `ServiceCallBadStop`) through the `OPC-UA-Core` logger category, so existing filters or dashboards keyed on those event names keep matching without further changes. The separate `SendResponse` compatibility event is emitted by `TcpServerChannel` under the same category and is not controlled by `ClientTraceFlags`. There is no separate flag for the compatibility events; remove any reference to `ClientTraceFlags.EventLog` from your code.

This consolidation also adopts the legacy event levels and fields: request start/stop move from `Information` to `Trace`, failure moves from `Error` to `Warning`, and the former per-call elapsed-time field is no longer part of these log records (request duration remains available through `opc.ua.client.request.duration`). Configure the `OPC-UA-Core` category at `Trace` if you need successful request start/stop records.

### Migrating `EventListener` / `dotnet-trace` consumers

If your code or tooling previously attached to one of the four providers, replace it with the `Microsoft.Extensions.Logging` / OpenTelemetry logging equivalent:

```csharp
// OLD - ETW EventListener attached to a stack provider (removed in 2.0)
public sealed class ClientEventListener : EventListener
{
protected override void OnEventSourceCreated(EventSource source)
{
if (source.Name == "OPC-UA-Client")
{
EnableEvents(source, EventLevel.Verbose);
}
}

protected override void OnEventWritten(EventWrittenEventArgs e)
{
Console.WriteLine($"{e.EventName}: {string.Join(", ", e.Payload ?? [])}");
}
}

// NEW - ILoggerProvider filtered to the same category name
builder.Services.AddLogging(b => b
.AddFilter("OPC-UA-Client", LogLevel.Trace)
.AddConsole());

// NEW - OpenTelemetry Logs SDK, same category
builder.Services.AddOpenTelemetry().WithLogging(l => l
.AddProcessor(/* your exporter of choice */));
```

`dotnet-trace` / PerfView users who captured `OPC-UA-Core`, `OPC-UA-Client`, `OPC-UA-Server`, or `Opc.Ua.ChannelManager` as ETW providers should instead configure an `ILoggerProvider` (Console, OpenTelemetry OTLP, Application Insights, etc.) filtered to the matching category name, or use the [`Microsoft-Extensions-Logging` ETW provider](https://learn.microsoft.com/dotnet/core/diagnostics/logging-tracing) that `dotnet-trace` already understands for standard `ILogger` output, if ETW-shaped capture is still required.

> **Out of scope.** The OPC UA information-model `EventSourceRegistry` / `HasEventSource` reference type (`Opc.Ua.Server.Fluent`, used for Alarms & Conditions modelling) and the BCL's own `System.Buffers.ArrayPoolEventSource` are unrelated to this removal — neither is a stack-owned ETW provider, and neither is affected by this migration.

---

## Migration utilities

To aid migration the stack provides:
Expand Down Expand Up @@ -253,6 +327,7 @@ test time.
- [`docs/Diagnostics.md`](../../Diagnostics.md) &mdash; full
end-state usage and extensibility guidance for `ITelemetryContext`
(custom contexts, OpenTelemetry wiring, metrics inventory).
- [`docs/Sessions.md` — diagnostics surface contract](../../Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) &mdash; full `Opc.Ua.ChannelManager` compatibility event table and safe-field policy.
- [`docs/DeveloperGuide.md` — narrow exception: retained EventSource-compatibility ids](../../DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) &mdash; authoring rules for the retained compatibility ids.
- [2.0 migration index](README.md) &mdash; analyzer quick-start + symptom → sub-doc table.
- [Migration Guide](../../MigrationGuide.md) &mdash; landing page across versions.

Loading
Loading