pw_system: Add log timestamp and console support
- Use time since boot as timestamp
- Remove obsolete TODOs
- Support log drop count message
- Print timestamp in a human-readable way
- Provide option for user-defined timestamp decoding
Change-Id: Ia5b30f0025877784edbce782e88e8165ed97d471
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/78281
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Carlos Chinchilla <cachinchilla@google.com>
diff --git a/pw_system/BUILD.bazel b/pw_system/BUILD.bazel
index 2479bc5..443c132 100644
--- a/pw_system/BUILD.bazel
+++ b/pw_system/BUILD.bazel
@@ -42,6 +42,7 @@
":logger",
":rpc",
"//pw_bytes",
+ "//pw_chrono:system_clock",
"//pw_log",
"//pw_log:proto_utils",
"//pw_log_rpc:log_service",
@@ -120,7 +121,6 @@
],
)
-# TODO(cachinchilla): Make the target provide this.
pw_cc_library(
name = "target_io",
srcs = [
@@ -134,7 +134,6 @@
],
)
-# TODO(cachinchilla): Make the target provide this.
pw_cc_library(
name = "target_hooks",
hdrs = [
diff --git a/pw_system/BUILD.gn b/pw_system/BUILD.gn
index 1f97b71..0670e31 100644
--- a/pw_system/BUILD.gn
+++ b/pw_system/BUILD.gn
@@ -68,6 +68,7 @@
deps = [
":rpc",
"$dir_pw_bytes",
+ "$dir_pw_chrono:system_clock",
"$dir_pw_log",
"$dir_pw_log:proto_utils",
"$dir_pw_log_rpc:rpc_log_drain",
diff --git a/pw_system/CMakeLists.txt b/pw_system/CMakeLists.txt
index 2cc1baf..c06c4e9 100644
--- a/pw_system/CMakeLists.txt
+++ b/pw_system/CMakeLists.txt
@@ -31,6 +31,7 @@
PRIVATE_DEPS
"pw_system.rpc"
"pw_bytes"
+ "pw_chrono.system_clock"
"pw_log"
"pw_log.proto_utils"
"pw_log_rpc.rpc_log_drain"
diff --git a/pw_system/log.cc b/pw_system/log.cc
index ebd4a88..e0f47b4 100644
--- a/pw_system/log.cc
+++ b/pw_system/log.cc
@@ -18,6 +18,7 @@
#include <cstddef>
#include "pw_bytes/span.h"
+#include "pw_chrono/system_clock.h"
#include "pw_log/proto_utils.h"
#include "pw_log_rpc/rpc_log_drain.h"
#include "pw_log_rpc/rpc_log_drain_map.h"
@@ -60,6 +61,8 @@
log_rpc::RpcLogDrainMap drain_map(drains);
+const int64_t boot_time_count =
+ pw::chrono::SystemClock::now().time_since_epoch().count();
} // namespace
// Deferred log buffer, for storing log entries while logging_thread_ streams
@@ -79,12 +82,14 @@
return log_service;
}
+// Provides time since boot in units defined by the target's pw_chrono backend.
int64_t GetTimestamp() {
- // TODO(cachinchilla): update method to get timestamp according to target.
- return 0;
+ return pw::chrono::SystemClock::now().time_since_epoch().count() -
+ boot_time_count;
}
-// TODO (cachinchilla): Finish this!
+// Implementation for tokenized log handling. This will be optimized out for
+// devices that only use string logging.
extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
pw_tokenizer_Payload payload, const uint8_t message[], size_t size_bytes) {
log_tokenized::Metadata metadata = payload;
diff --git a/pw_system/py/pw_system/console.py b/pw_system/py/pw_system/console.py
index 8badbee..2a676e0 100644
--- a/pw_system/py/pw_system/console.py
+++ b/pw_system/py/pw_system/console.py
@@ -32,6 +32,7 @@
"""
import argparse
+import datetime
import glob
from inspect import cleandoc
import logging
@@ -260,10 +261,18 @@
if serial_debug:
serial_impl = SerialWithLogging
+ timestamp_decoder = None
if socket_addr is None:
serial_device = serial_impl(device, baudrate, timeout=1)
read = lambda: serial_device.read(8192)
write = serial_device.write
+
+ # Overwrite decoder for serial device.
+ def milliseconds_to_string(timestamp):
+ """Parses milliseconds since boot to a human-readable string."""
+ return str(datetime.timedelta(seconds=timestamp / 1e3))[:-3]
+
+ timestamp_decoder = milliseconds_to_string
else:
try:
socket_device = SocketClientImpl(socket_addr)
@@ -278,6 +287,7 @@
write,
protos,
detokenizer,
+ timestamp_decoder=timestamp_decoder,
rpc_timeout_s=5)
_start_ipython_terminal(device_client, serial_debug, config_file)
diff --git a/pw_system/py/pw_system/device.py b/pw_system/py/pw_system/device.py
index 4b57004..bc8796a 100644
--- a/pw_system/py/pw_system/device.py
+++ b/pw_system/py/pw_system/device.py
@@ -13,10 +13,11 @@
# the License.
"""Device classes to interact with targets via RPC."""
+import datetime
import logging
from pathlib import Path
from types import ModuleType
-from typing import Any, List, Union, Optional
+from typing import Any, Callable, List, Union, Optional
from pw_hdlc.rpc import HdlcRpcClient, default_channels
import pw_log_tokenized
@@ -44,6 +45,7 @@
write,
proto_library: List[Union[ModuleType, Path]],
detokenizer: Optional[Detokenizer],
+ timestamp_decoder: Optional[Callable[[int], str]],
rpc_timeout_s=5):
self.channel_id = channel_id
self.protos = proto_library
@@ -51,6 +53,7 @@
self.logger = DEFAULT_DEVICE_LOGGER
self.logger.setLevel(logging.DEBUG) # Allow all device logs through.
+ self.timestamp_decoder = timestamp_decoder
callback_client_impl = callback_client.Impl(
default_unary_timeout_s=rpc_timeout_s,
@@ -95,8 +98,9 @@
def _log_entries_proto_parser(self, log_entries_proto: log_pb2.LogEntries):
for log_proto in log_entries_proto.entries:
- timestamp = log_proto.timestamp
- level = (log_proto.line_level & 0x7)
+ decoded_timestamp = self.decode_timestamp(log_proto.timestamp)
+ # Parse level and convert to logging module level number.
+ level = (log_proto.line_level & 0x7) * 10
if self.detokenizer:
message = str(
decode_optionally_tokenized(self.detokenizer,
@@ -104,18 +108,43 @@
else:
message = log_proto.message.decode("utf-8")
log = pw_log_tokenized.FormatStringWithMetadata(message)
- self._emit_device_log(level, '', timestamp, log.module,
- log.message, **dict(log.fields))
+ decoded_message = log.message
- def _emit_device_log(self, level: int, source_name: str, timestamp: float,
+ # Handle dropped count.
+ if log_proto.dropped:
+ level = logging.WARNING
+ decoded_timestamp = ''
+ decoded_message = f'Dropped {log_proto.dropped} log'
+ if log_proto.dropped > 1:
+ decoded_message += 's'
+
+ self._emit_device_log(level, '', decoded_timestamp, log.module,
+ decoded_message, **dict(log.fields))
+
+ def _emit_device_log(self, level: int, source_name: str, timestamp: str,
module_name: str, message: str, **metadata_fields):
+ # Fields used for console table view
fields = metadata_fields
fields['source_name'] = source_name
fields['timestamp'] = timestamp
- self.logger.log(level * 10,
- '[%s] %16.3f %s%s',
+ fields['msg'] = message
+ fields['module'] = module_name
+
+ # Format used for file or stdout logging.
+ self.logger.log(level,
+ '[%s] %s %s%s',
source_name,
timestamp,
f'{module_name} '.lstrip(),
message,
extra=dict(extra_metadata_fields=fields))
+
+ def decode_timestamp(self, timestamp: int) -> str:
+ """Decodes timestamp to a human-readable value.
+
+ Defaults to interpreting the input timestamp as nanoseconds since boot.
+ Devices can override this to match their timestamp units.
+ """
+ if self.timestamp_decoder:
+ return self.timestamp_decoder(timestamp)
+ return str(datetime.timedelta(seconds=timestamp / 1e9))[:-3]