Logging
The library uses the tracing crate for contextual logging. Rust users should refer to the tracing documentation for details.
In comparison, the bindings use a rigid logging interface with a single callback method to record a message. Configurable options include:
LogLevelthat controls which messages are generated- How and if to print the time as part of the message
- Line or JSON based output
The LogLevel is set to Info by default. This will record Info, Warn, and Error messages. The Debug and Trace levels are generally only useful if debugging an issue with the underlying runtime.
Protocol decoding is always logged at the Info level and is configured separately on a per session basis.
Configuration
- Rust
- C
- C++
- Java
- C#
// Initialize logging
let log_level: tracing::Level = args.log_level.into();
tracing_subscriber::fmt()
.with_max_level(log_level)
.with_target(false)
.init();
// callback which will receive log messages
void on_log_message(dnp3_log_level_t level, const char *msg, void *context) { printf("%s", msg); }
dnp3_logger_t get_logger(void)
{
return (dnp3_logger_t){
// function pointer where log messages will be sent
.on_message = &on_log_message,
// no context to free
.on_destroy = NULL,
// optional context argument applied to all log callbacks
.ctx = NULL,
};
}
// initialize logging with the default configuration
dnp3_configure_logging(dnp3_logging_config_init(), get_logger());
class Logger : public dnp3::Logger {
void on_message(dnp3::LogLevel level, const char *message) override { std::cout << message; }
};
std::unique_ptr<dnp3::Logger> create_logger()
{
return std::make_unique<Logger>();
}
dnp3::Logging::configure(dnp3::LoggingConfig(), create_logger());
// callback interface used to receive log messages
class ConsoleLogger implements Logger {
@Override
public void onMessage(LogLevel level, String message) {
System.out.print(message);
}
}
Logging.configure(new LoggingConfig(), new ConsoleLogger());
// callback interface used to receive log messages
class ConsoleLogger : ILogger
{
public void OnMessage(LogLevel level, string message)
{
Console.Write(message);
}
}
// Initialize logging with the default configuration
// This may only be called once during program initialization
Logging.Configure(
new LoggingConfig(),
new ConsoleLogger()
);
The bindings use the tracing_subscriber crate internally. If you use Rust, you can pick which tracing backend to use.
Example output
Logs include contextual metadata so you can:
- Determine which communication session produced the message
- Understand what state the software was in when the event occurred
INFO dnp3-master-tcp-client{endpoint="127.0.0.1:20000"}: connected to 127.0.0.1:20000
INFO dnp3-master-tcp-client{endpoint="127.0.0.1:20000"}:task{type=Function(DisableUnsolicited) dest=1024}: APP TX - ctrl: [fir: true fin: true con: false uns: false seq: 0] func: DisableUnsolicited ... (len = 9)
INFO dnp3-master-tcp-client{endpoint="127.0.0.1:20000"}:task{type=Function(DisableUnsolicited) dest=1024}: APP RX - ctrl: [fir: true fin: true con: true uns: true seq: 0] func: UnsolicitedResponse iin1: [DEVICE_RESTART] iin2: [] ... (len = 0)
WARN dnp3-master-tcp-client{endpoint="127.0.0.1:20000"}:task{type=Function(DisableUnsolicited) dest=1024}: device restart detected (address == 1024)
INFO dnp3-master-tcp-client{endpoint="127.0.0.1:20000"}:task{type=Function(DisableUnsolicited) dest=1024}: APP TX - ctrl: [fir: true fin: true con: false uns: true seq: 0] func: Confirm ... (len = 0)
INFO dnp3-master-tcp-client{endpoint="127.0.0.1:20000"}:task{type=Function(DisableUnsolicited) dest=1024}: APP RX - ctrl: [fir: true fin: true con: false uns: false seq: 0] func: Response iin1: [DEVICE_RESTART] iin2: [] ... (len = 0)
Protocol decoding
Configure protocol decoding per communication channel (TCP socket, serial port, etc.) by setting DecodeLevel when creating a master or outstation, or adjust it at runtime. This controls decoding for each layer of the protocol stack:
- Application-layer headers, object headers, and object values
- Transport-layer header and payload
- Link-layer header and payload
- Physical-layer length and data bytes
See the language-specific API documentation for enumeration values. Decoding can show individual field values within objects:
APP TX - ctrl: [fir: true fin: true con: false uns: false seq: 1] func: Write ... (len = 6)
g80v1 : Internal Indications - Packed format - 1-byte start/stop - [7, 7]
index: 7 false
Protocol decoding is always output at the Info log level. If left enabled, it can be too verbose in a production system. When you're debugging a communication issue, try adjusting the application-layer decoding first to gain visibility into the messages exchanged.
Application-layer object value decoding redacts the passwords and authentication keys used in file transfer. The transport, link, and physical layers can log raw bytes, which include these secrets.