Add session and subscription ids to client and server request logging - #4189
Add session and subscription ids to client and server request logging#4189TimJoehnk wants to merge 15 commits into
Conversation
…de of the master port) Ports the release/1.5.378 logging change to the master LoggerMessage idiom for the server request path: SessionPublishQueue (2), StandardServer (3) and Subscription (2). Each message gains the id as a source-generated log parameter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Continues the master port: the eight MonitoredItem messages gain SubscriptionId (and MonitoredItemId on the publish message) as source-generated log parameters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the server side of the master port: ConditionRefresh, ConditionRefresh2, DeleteSubscriptions, SetPublishingMode, Publish ReceivedFromClient and Publish False Alarm gain the ids as source-generated log parameters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Append SessionId to the 19 request-path log messages in the client Session that did not carry it, so lines from concurrent sessions can be told apart in a host that holds more than one connection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Append SessionId to the 13 publish and republish path log messages in the classic subscription engine. The publish response messages read the id from the session the request was actually sent on, not from the current context, so a response that arrives after a reconnect is still attributed to the session that produced it. The NOTIFICATION RECEIVED message keeps its legacy event id and event name; only the message template gains the field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Append SessionId to the 26 log messages in the classic client subscription, and SubscriptionId to the four that named neither their subscription nor its session (the state change callback exception, the incoming message error, and the PublishStateChanged event error). Subscription.Session is nullable and is cleared when a subscription is removed from its session, so the added arguments are read as Session?.SessionId. RestoreTriggeringAsync captures the reference once before its loop instead: the null test in ?. resets the nullable flow state, and the foreach back-edge would carry that to the existing SetTriggeringAsync call at the top of the loop body. The captured local also reports the session the call was actually made on rather than a blank. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three data change validation warnings named only the client handle, which is unique per subscription and not per host, so two subscriptions could produce the same MonitoredItemId in one log file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three server messages were missed in the earlier monitored item commit. The two queue restore errors gain SubscriptionId. DEQUEUE VALUE identified nothing at all, and its message is shared with DataChangeQueueHandler, which holds a monitored item id but has no subscription or session in scope, so it gains MonitoredItemId only rather than a subscription id that would have to be faked at one of the two call sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Deleted and Config trace states already name their session; Items and Publish/Monitor did not. All four now do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ClientEventsRetainLegacyContracts calls the two legacy-shaped client events directly, so it needs the new argument. Their event ids, names and levels are unchanged and every property the test already pinned is still present; assert the new one too, so the field is covered by the same contract test rather than only by the call sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SubscriptionSubscriptionIdDelayedMessageSequenceNumber gained a sessionId parameter earlier on this branch, but its only call site sits inside an #if DEBUG block and was not updated, so Opc.Ua.Client failed to compile in Debug with CS7036 while Release built clean. The call sites for the other messages were found by changing a declaration and letting the compiler list them, which cannot see code the active configuration excludes. This is the only logger call under a preprocessor guard whose symbol is not always defined; the two under #if OPCUA_V1_CLIENT were checked, because that symbol is defined unconditionally in the project file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four messages were left uncorrelated while their immediate neighbours were
enriched, which made the omissions look arbitrary:
- PUBLISH #{RequestHandle} SENT and RECEIVED, the two legacy publish events.
SENT reads the engine context, RECEIVED reads the sessionId parameter of
OnPublishComplete, so a response is attributed to the session that sent
the request even after a reconnect.
- Server Call={RequestType}, Id={RequestId}, from the operation context.
- Keep alive read failed, which carries the same EndpointUrl and
RequestCount fields as KEEP ALIVE LATE but had no session id, from the
same instance property the neighbouring keep-alive message uses.
The first and third are two of the three lines used to motivate this change,
so they should not have been the ones left out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves diagnosability of interleaved client/server request-path logs by adding SessionId (and SubscriptionId where available) to existing source-generated logging templates and method signatures across the classic client session/subscription pipeline and key server subscription/publish paths. It also extends the compatibility logging test to pin the new legacy-event field.
Changes:
- Append
SessionId/SubscriptionIdfields to existing[LoggerMessage]templates and generated method parameter lists (client + server). - Thread session/subscription IDs through various publish/notification/monitored-item logging call sites.
- Update
OpcUaClientCompatibilityLoggingTeststo assertSessionIdis present on legacy-contract events.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Opc.Ua.Client.Tests/OpcUaClientCompatibilityLoggingTests.cs | Extends legacy-contract event assertions to include SessionId. |
| src/Opc.Ua.Client/Session/Session.cs | Adds SessionId context to session lifecycle and subscription transfer/reactivation logs. |
| src/Opc.Ua.Client/Session/Subscription/ClassicSubscriptionEngine.cs | Adds SessionId to publish/notification legacy events and related request-path logs. |
| src/Opc.Ua.Client/Subscription/Classic/Subscription.cs | Adds SessionId/SubscriptionId context to classic subscription logs and restores triggering with stable session reference. |
| src/Opc.Ua.Client/Subscription/Classic/MonitoredItem.cs | Adds subscription/session context to monitored item timestamp/overflow warnings. |
| src/Opc.Ua.Server/Server/StandardServer.cs | Adds SessionId to session activation/audit-adjacent logging and server-call compatibility event. |
| src/Opc.Ua.Server/Subscription/SubscriptionManager.cs | Adds SessionId/SubscriptionId context to condition refresh, publish, and error logs. |
| src/Opc.Ua.Server/Subscription/Subscription.cs | Adds session/subscription identifiers to transfer/invariant/state tracing logs. |
| src/Opc.Ua.Server/Subscription/SessionPublishQueue.cs | Adds SessionId/SubscriptionId context to publish queue logs. |
| src/Opc.Ua.Server/Subscription/MonitoredItem/MonitoredItem.cs | Adds SubscriptionId context to monitored item logs and related error paths. |
| src/Opc.Ua.Server/Subscription/MonitoredItem/QueueHandler/DataChangeQueueHandler.cs | Adds monitored-item id to the shared dequeue-value log call. |
| src/Opc.Ua.Server/MonitoredItemQueueLog.cs | Extends shared dequeue-value template/signature to include MonitoredItemId. |
marcschier
left a comment
There was a problem hiding this comment.
@TimJoehnk thanks for the contribution. Could you fix the build failures and address the copilot review comments?
CompatibilityServerCall, ClientEventPublishStart and ClientEventPublishStop gained a sessionId parameter in the previous commit, but their call sites in the legacy-contract tests were not updated, so the solution build failed and every downstream test job reported no results. CI reported only the server one, because the build stopped there. The two client call sites were queued behind it and would have failed the next run. The server test now pins SessionId on the ServerCall event, matching what the client test already does for SubscriptionState and NotificationReceived. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- "Durable subscription was create" -> "was created". The template is already being edited here, and the correction changes no event id or parameter. - PUBLISH Assigned To Subscription kept its trailing "." and now carries the session id as a separate sentence, instead of turning that "." into a ",". The point of appending is that a scraper matching the existing text keeps working, and rewriting the existing suffix broke that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@TimJoehnk - sorry :-) can you see the test failures in the checks? If I need to restart them, let me know, else might need more fixes. |
The three session id assertions compared the recorded property against the
NodeId that was passed, but the logging infrastructure renders a NodeId to
a string before it reaches the sink, so they failed with
Expected: i=4711
But was: "i=4711"
The surrounding assertions all use primitives, which do survive as typed
values, so nothing indicated that a reference type behaves differently.
Comparing the rendered form on both sides holds either way, and is what
the assertion is really about: that the id reaches the log correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@marcschier Hi, seems like some of the tests failed with some BadCertificateUntrustedError, not sure how my changes could produce that. https://opcfoundation.visualstudio.com/opcua-netstandard/_build/results?buildId=16660&view=logs&j=774e4f28-8ec7-53ad-6205-e6078fceecb6&t=57298d97-dbde-5109-04aa-00ce31c3a4f4&s=1e35748d-e13f-5adb-76ba-94c7aba178d3 |
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Some of the integration tests have some flakiness kinks, I kicked it off again and will monitor. |
Proposed changes
Request-path log messages in the client and server carry no session context, which makes
them unusable for diagnosing an application that holds several sessions at once — a
gateway, an aggregating client, or any host with more than one connection. Lines like
interleave across sessions in one log file with nothing to separate them. The same applies
on the server side to messages that name a monitored item but not its subscription.
This adds
SessionId— andSubscriptionIdwhere the call site has one — to the existingsource-generated log messages in the client and server request paths.
Scope is deliberately narrow. The change touches message templates and parameter lists
only:
internal static partial class …Logtype11 source files and 1 test, +531/−259. 81 messages gain an id.
Session/Session.cs,Session/Subscription/ClassicSubscriptionEngine.cs,Subscription/Classic/Subscription.cs,Subscription/Classic/MonitoredItem.csServer/StandardServer.cs,Subscription/SubscriptionManager.cs,Subscription/Subscription.cs,Subscription/SessionPublishQueue.cs,Subscription/MonitoredItem/MonitoredItem.cs,Subscription/MonitoredItem/QueueHandler/DataChangeQueueHandler.cs,MonitoredItemQueueLog.csPoints worth a reviewer's attention
Two legacy-contract events gain a field.
ClientEventSubscriptionState(
LegacySubscriptionStateId/"SubscriptionState") andClientEventNotificationReceived(
LegacyNotificationReceivedId/"NotificationReceived") now carrySessionId. Theirevent ids, event names and levels are unchanged, and every property
OpcUaClientCompatibilityLoggingTestspins is still present — the test is extended to pinthe new one as well. If you would rather the legacy-shaped events stay byte-identical, say
so and I will drop the id from those two; the sibling non-legacy messages at the same call
sites already carry it.
DEQUEUE VALUEgains onlyMonitoredItemId, not a subscription id. Its message isshared between
MonitoredItemandDataChangeQueueHandler, and the latter holds amonitored item id but has no subscription or session in scope. The message previously
identified nothing at all; adding the id it can actually supply seemed better than
inventing a subscription id at one of the two call sites.
QUEUE OVERFLOWinSentMessageQueueis left alone. That class is constructed with aFunc<uint> subscriptionIdProviderand no session, so there is no session id to add. Themessage already carries
SubId={SubscriptionId}.Server trace states made consistent.
Subscription.TraceStatehas four cases;Deletedand
Configalready named their session,ItemsandPublish/Monitordid not. All fourdo now.
One local added in
Subscription.RestoreTriggeringAsync:ISession session = Session;before the
foreach.Session?.SessionIdinside that loop does not compile —?.is anull test, so it resets the nullable flow state for
Session, and theforeachback-edgecarries that state to the top of the loop body, which makes the pre-existing
Session.SetTriggeringAsync(...)a CS8602 error. Capturing the reference once avoidsdisturbing existing code and is more useful than
?.anyway: it reports the session thecall was actually made on rather than a blank.
Publish responses are attributed to the session that sent the request. In
ClassicSubscriptionEngine.OnPublishCompletethe added arguments read thesessionIdparameter rather than
m_context.SessionId, so a response arriving after a reconnect isstill attributed to the session that produced it.
Null-safety of the added arguments
Reviewed at every call site:
Subscription.SessionisISession?and is cleared when a subscription is removedfrom its session, so those arguments are
Session?.SessionIdor read from the capturedlocal.
[MemberNotNull]onVerifySession()/VerifySessionAndSubscriptionState()satisfies the compiler but only describes the moment the guard returns, not the state
after the following
await— and several of these arguments sit incatchblocks, wherean NRE would have replaced a
ServiceResultExceptionand escaped.SessionderivesSessionIdfromSessionClient, which is non-nullable.OperationContext.SessionIdandSubscription.SessionIdalready returndefaultwhen there is no session, so those arguments cannot throw.
Subscription.DeleteAsyncsynthesises anOperationContextwith no session, soSessionIdlogs empty on that path. That is expected, not a defect.Related Issues
None — no existing issue covers this. Happy to open one for tracking if you prefer.
Types of changes
Checklist
Opc.Ua.ClientandOpc.Ua.Serverbuild clean for every target framework(net472, net48, netstandard2.1, net8.0, net9.0, net10.0) with
--no-incremental:0 errors, 0 warnings under the repository's
TreatWarningsAsErrors.Local results, Release/net8.0:
Opc.Ua.Server.TestsOpc.Ua.Subscriptions.Classic.TestsOpc.Ua.Client.TestsandOpc.Ua.Sessions.Testsare still running here; I will post theirresults as a comment rather than hold the PR for them.
The test output also confirms the placeholders bind at runtime, which compilation alone does
not show — for example, from the transfer tests:
No tests are added beyond the compatibility-test extension: the change alters message
templates and arguments, which the suites otherwise do not assert on. Happy to add coverage
if you would like a specific message pinned.
The CI and CodeQL boxes are left blank deliberately — per CONTRIBUTING the pipelines do not
start for outside contributors until a maintainer comments
/azp run, so there is nothingfor me to act on yet.
Further comments
Why the generated log methods change signature. With
[LoggerMessage]the messagetemplate and its parameters are a single declaration, so adding an id to a message
necessarily adds a parameter to the generated method. Every one of them lives in an
internal static partial class …Logtype, so none of it is public API.Scope is the classic session and subscription path.
ManagedSession,DefaultSubscriptionEngine,MessageProcessor,Subscription/SubscriptionManager.csandSubscription/MonitoredItemManager.csare untouched.Placeholders are appended rather than woven into existing message text so that log
scrapers matching on the leading part of a message keep working.