Skip to content

Commit d6a2691

Browse files
committed
Structured tracing on the client
This adds some basic structured tracing on the client. This is useful both larger applications, in which case you can use a structured tracing format like opentelemetry to create structured spans with timing info, contextualized logs, etc. It also serves as more detailed debug logging on simpler clients, which can help with debugging.
1 parent 5b854e3 commit d6a2691

12 files changed

Lines changed: 856 additions & 443 deletions

File tree

TODO.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ This is a list of things that are known to be missing, or ideas that could be im
55
- Flesh out the server and client SDK with tooling for ease if use.
66
- Make it even easier to implement custom node managers.
77
- Implement Part 4 7.41.2.3, encrypted secrets. We currently only support legacy secrets. We should also support more encryption algorithms for secrets.
8-
- Write some form of support for IssuedToken based authentication on the client.
98
- Implement a better framework for security checks on the server.
109
- Write a sophisticated server example with a persistent store. This would be a great way to verify the flexibility of the server.
1110
- Write some "bad ideas" servers, it would be nice to showcase how flexible this is.
1211
- Write a framework for method calls. The foundation for this has been laid with `TryFromVariant`, if we really wanted to we could use clever trait magic to let users simply define a rust method that takes in values that each implement a trait `MethodArg`, with a blanket impl for `TryFromVariant`, and return a tuple of results. Could be really powerful, but methods are a little niche.
1312
- Implement `Query`. I never got around to this, because the service is just so complex. Currently there is no way to actually implement it, since it won't work unless _all_ node managers implement it, and the core node managers don't.
14-
- Tracing and detailed logging in the client.

async-opcua-client/src/session/connect.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::sync::Arc;
22

33
use tokio::{pin, select};
4-
use tracing::info;
4+
use tracing::{info, info_span, Instrument};
55

66
use crate::transport::{Connector, SecureChannelEventLoop, TransportPollResult};
77
use opcua_types::{NodeId, StatusCode};
@@ -40,9 +40,18 @@ impl SessionConnector {
4040
&self,
4141
connector: &T,
4242
) -> Result<(SecureChannelEventLoop<T::Transport>, SessionConnectMode), StatusCode> {
43-
let mut event_loop = self.inner.channel.connect_no_retry(connector).await?;
44-
45-
let activate_fut = self.ensure_and_activate_session();
43+
let span = info_span!(
44+
"Attempting to create and activate session to server",
45+
endpoint_url = connector.default_endpoint().endpoint_url.as_ref()
46+
);
47+
let mut event_loop = self
48+
.inner
49+
.channel
50+
.connect_no_retry(connector)
51+
.instrument(span.clone())
52+
.await?;
53+
54+
let activate_fut = self.ensure_and_activate_session().instrument(span.clone());
4655
pin!(activate_fut);
4756

4857
let res = loop {
@@ -59,7 +68,7 @@ impl SessionConnector {
5968
let id = match res {
6069
Ok(id) => id,
6170
Err(e) => {
62-
self.inner.channel.close_channel().await;
71+
self.inner.channel.close_channel().instrument(span).await;
6372

6473
loop {
6574
if matches!(event_loop.poll().await, TransportPollResult::Closed(_)) {
@@ -78,18 +87,18 @@ impl SessionConnector {
7887
let should_create_session = self.inner.session_id.load().is_null();
7988

8089
if should_create_session {
81-
self.inner.create_session().await?;
90+
self.inner.create_session().in_current_span().await?;
8291
}
8392

84-
let reconnect = match self.inner.activate_session().await {
93+
let reconnect = match self.inner.activate_session().in_current_span().await {
8594
Err(status_code) if !should_create_session => {
8695
info!(
8796
"Session activation failed on reconnect, error = {}, creating a new session",
8897
status_code
8998
);
9099
self.inner.reset();
91-
let id = self.inner.create_session().await?;
92-
self.inner.activate_session().await?;
100+
let id = self.inner.create_session().in_current_span().await?;
101+
self.inner.activate_session().in_current_span().await?;
93102
SessionConnectMode::NewSession(id)
94103
}
95104
Err(e) => return Err(e),
@@ -104,7 +113,10 @@ impl SessionConnector {
104113
};
105114

106115
if self.inner.recreate_subscriptions {
107-
self.inner.transfer_subscriptions_from_old_session().await;
116+
self.inner
117+
.transfer_subscriptions_from_old_session()
118+
.in_current_span()
119+
.await;
108120
}
109121

110122
Ok(reconnect)

async-opcua-client/src/session/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ pub(crate) fn process_unexpected_response(response: ResponseMessage) -> StatusCo
146146
service_fault.response_header.service_result
147147
}
148148
_ => {
149-
error!("Received an unexpected response to the request");
149+
error!(
150+
"Received an unexpected response to the request: {}",
151+
response.type_name()
152+
);
150153
StatusCode::BadUnknownResponse
151154
}
152155
}

async-opcua-client/src/session/services/attributes.rs

Lines changed: 106 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use std::time::Duration;
33
use crate::{
44
session::{
55
process_service_result, process_unexpected_response,
6-
request_builder::{builder_base, builder_debug, builder_error, RequestHeaderBuilder},
6+
request_builder::{
7+
builder_base, builder_debug, builder_error, builder_trace, RequestHeaderBuilder,
8+
},
79
UARequest,
810
},
911
AsyncSecureChannel, Session,
@@ -17,6 +19,7 @@ use opcua_types::{
1719
ReadResponse, ReadValueId, StatusCode, TimestampsToReturn, UpdateDataDetails,
1820
UpdateEventDetails, UpdateStructureDataDetails, WriteRequest, WriteResponse, WriteValue,
1921
};
22+
use tracing::{debug_span, Instrument};
2023

2124
/// Enumeration used with Session::history_read()
2225
#[derive(Debug, Clone)]
@@ -175,17 +178,32 @@ impl UARequest for Read {
175178
where
176179
Self: 'b,
177180
{
178-
if self.nodes_to_read.is_empty() {
179-
builder_error!(self, "read(), was not supplied with any nodes to read");
180-
return Err(StatusCode::BadNothingToDo);
181-
}
182-
let request = ReadRequest {
183-
request_header: self.header.header,
184-
max_age: self.max_age,
185-
timestamps_to_return: self.timestamps_to_return,
186-
nodes_to_read: Some(self.nodes_to_read),
181+
let span = debug_span!(
182+
"Sending Read request",
183+
nodes_to_read = self.nodes_to_read.len(),
184+
timestamps_to_return = ?self.timestamps_to_return,
185+
max_age = self.max_age
186+
);
187+
let request = {
188+
let _h = span.enter();
189+
if self.nodes_to_read.is_empty() {
190+
builder_error!(self, "read(), was not supplied with any nodes to read");
191+
return Err(StatusCode::BadNothingToDo);
192+
}
193+
ReadRequest {
194+
request_header: self.header.header,
195+
max_age: self.max_age,
196+
timestamps_to_return: self.timestamps_to_return,
197+
nodes_to_read: Some(self.nodes_to_read),
198+
}
187199
};
188-
let response = channel.send(request, self.header.timeout).await?;
200+
201+
let response = channel
202+
.send(request, self.header.timeout)
203+
.instrument(span.clone())
204+
.await?;
205+
206+
let _h = span.enter();
189207
if let ResponseMessage::Read(response) = response {
190208
builder_debug!(self, "read(), success");
191209
process_service_result(&response.response_header)?;
@@ -282,25 +300,39 @@ impl UARequest for HistoryRead {
282300
where
283301
Self: 'b,
284302
{
285-
let history_read_details = ExtensionObject::from(self.details);
286-
builder_debug!(
287-
self,
288-
"history_read() requested to read nodes {:?}",
289-
self.nodes_to_read
303+
let span = debug_span!(
304+
"Sending HistoryRead request",
305+
details = ?self.details,
306+
timestamps_to_return = ?self.timestamps_to_return,
307+
release_continuation_points = self.release_continuation_points,
308+
num_nodes_to_read = self.nodes_to_read.len()
290309
);
291-
let request = HistoryReadRequest {
292-
request_header: self.header.header,
293-
history_read_details,
294-
timestamps_to_return: self.timestamps_to_return,
295-
release_continuation_points: self.release_continuation_points,
296-
nodes_to_read: if self.nodes_to_read.is_empty() {
297-
None
298-
} else {
299-
Some(self.nodes_to_read)
300-
},
310+
let request = {
311+
let _h = span.enter();
312+
let history_read_details = ExtensionObject::from(self.details);
313+
builder_trace!(
314+
self,
315+
"history_read() requested to read nodes {:?}",
316+
self.nodes_to_read
317+
);
318+
HistoryReadRequest {
319+
request_header: self.header.header,
320+
history_read_details,
321+
timestamps_to_return: self.timestamps_to_return,
322+
release_continuation_points: self.release_continuation_points,
323+
nodes_to_read: if self.nodes_to_read.is_empty() {
324+
None
325+
} else {
326+
Some(self.nodes_to_read)
327+
},
328+
}
301329
};
302330

303-
let response = channel.send(request, self.header.timeout).await?;
331+
let response = channel
332+
.send(request, self.header.timeout)
333+
.instrument(span.clone())
334+
.await?;
335+
let _h = span.enter();
304336
if let ResponseMessage::HistoryRead(response) = response {
305337
builder_debug!(self, "history_read(), success");
306338
process_service_result(&response.response_header)?;
@@ -367,16 +399,26 @@ impl UARequest for Write {
367399
where
368400
Self: 'a,
369401
{
370-
if self.nodes_to_write.is_empty() {
371-
builder_error!(self, "write() was not supplied with any nodes to write");
372-
return Err(StatusCode::BadNothingToDo);
373-
}
374-
375-
let request = WriteRequest {
376-
request_header: self.header.header,
377-
nodes_to_write: Some(self.nodes_to_write.to_vec()),
402+
let span = debug_span!(
403+
"Sending Write request",
404+
num_nodes_to_write = self.nodes_to_write.len()
405+
);
406+
let request = {
407+
let _h = span.enter();
408+
if self.nodes_to_write.is_empty() {
409+
builder_error!(self, "write(), was not supplied with any nodes to write");
410+
return Err(StatusCode::BadNothingToDo);
411+
}
412+
WriteRequest {
413+
request_header: self.header.header,
414+
nodes_to_write: Some(self.nodes_to_write),
415+
}
378416
};
379-
let response = channel.send(request, self.header.timeout).await?;
417+
let response = channel
418+
.send(request, self.header.timeout)
419+
.instrument(span.clone())
420+
.await?;
421+
let _h = span.enter();
380422
if let ResponseMessage::Write(response) = response {
381423
builder_debug!(self, "write(), success");
382424
process_service_result(&response.response_header)?;
@@ -451,23 +493,35 @@ impl UARequest for HistoryUpdate {
451493
where
452494
Self: 'a,
453495
{
454-
if self.details.is_empty() {
455-
builder_error!(
456-
self,
457-
"history_update(), was not supplied with any detail to update"
458-
);
459-
return Err(StatusCode::BadNothingToDo);
460-
}
461-
let details = self
462-
.details
463-
.into_iter()
464-
.map(ExtensionObject::from)
465-
.collect();
466-
let request = HistoryUpdateRequest {
467-
request_header: self.header.header,
468-
history_update_details: Some(details),
496+
let span = debug_span!(
497+
"Sending HistoryUpdate request",
498+
num_details = self.details.len()
499+
);
500+
let request = {
501+
let _h = span.enter();
502+
if self.details.is_empty() {
503+
builder_error!(
504+
self,
505+
"history_update(), was not supplied with any detail to update"
506+
);
507+
return Err(StatusCode::BadNothingToDo);
508+
}
509+
let details = self
510+
.details
511+
.into_iter()
512+
.map(ExtensionObject::from)
513+
.collect();
514+
HistoryUpdateRequest {
515+
request_header: self.header.header,
516+
history_update_details: Some(details),
517+
}
469518
};
470-
let response = channel.send(request, self.header.timeout).await?;
519+
520+
let response = channel
521+
.send(request, self.header.timeout)
522+
.instrument(span.clone())
523+
.await?;
524+
let _h = span.enter();
471525
if let ResponseMessage::HistoryUpdate(response) = response {
472526
builder_error!(self, "history_update(), success");
473527
process_service_result(&response.response_header)?;

async-opcua-client/src/session/services/method.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::time::Duration;
33
use crate::{
44
session::{
55
process_unexpected_response,
6-
request_builder::{builder_base, builder_debug, builder_error, RequestHeaderBuilder},
6+
request_builder::{builder_base, builder_error, RequestHeaderBuilder},
77
session_error,
88
},
99
AsyncSecureChannel, Session, UARequest,
@@ -13,6 +13,7 @@ use opcua_types::{
1313
CallMethodRequest, CallMethodResult, CallRequest, CallResponse, IntegerId, MethodId, NodeId,
1414
ObjectId, StatusCode, TryFromVariant, Variant,
1515
};
16+
use tracing::{debug_span, Instrument};
1617

1718
#[derive(Debug, Clone)]
1819
/// Calls a list of methods on the server by sending a [`CallRequest`] to the server.
@@ -68,18 +69,29 @@ impl UARequest for Call {
6869
where
6970
Self: 'a,
7071
{
71-
if self.methods.is_empty() {
72-
builder_error!(self, "call(), was not supplied with any methods to call");
73-
return Err(StatusCode::BadNothingToDo);
74-
}
75-
76-
builder_debug!(self, "call()");
72+
let span = debug_span!(
73+
"Sending Call request",
74+
num_method_calls = self.methods.len()
75+
);
7776
let cnt = self.methods.len();
78-
let request = CallRequest {
79-
request_header: self.header.header,
80-
methods_to_call: Some(self.methods),
77+
let request = {
78+
let _h = span.enter();
79+
if self.methods.is_empty() {
80+
builder_error!(self, "call(), was not supplied with any methods to call");
81+
return Err(StatusCode::BadNothingToDo);
82+
}
83+
84+
CallRequest {
85+
request_header: self.header.header,
86+
methods_to_call: Some(self.methods),
87+
}
8188
};
82-
let response = channel.send(request, self.header.timeout).await?;
89+
90+
let response = channel
91+
.send(request, self.header.timeout)
92+
.instrument(span.clone())
93+
.await?;
94+
let _h = span.enter();
8395
if let ResponseMessage::Call(response) = response {
8496
if let Some(results) = &response.results {
8597
if results.len() != cnt {

0 commit comments

Comments
 (0)