Architecture
Overall architecture
log_ctrl operates independently of the process that writes the logs.
┌───────────────────────────┐
│ application / DeviceServer│
│ logger │
└─────────────┬─────────────┘
│ write
▼
log directory
▲
│ read only
│
┌─────────────┴─────────────┐
│ log_ctrl │
│ │
│ LogCtrl │
│ ├─ log_names() │
│ ├─ dates() │
│ ├─ raw_files() │
│ ├─ exists() │
│ └─ tail() │
│ │
│ _LogStream │
│ ├─ read() │
│ └─ read_raw() │
└─────────────┬─────────────┘
│
│ grpc_frame
▼
┌───────────────────────────┐
│ DeviceProxy("log") │
│ │
│ SyncLogGrpcClient │
│ AsyncLogGrpcClient │
└───────────────────────────┘
No log_ctrl-specific RPC needs to be added to the log-writing process.
LogCtrl
LogCtrl is a controller class that accesses the log directory locally.
Its principal state consists of:
log_direncoding
These are its configuration attributes.
On initialization, log_dir is normalized using Path.resolve(),
and verified to be an existing directory.
Methods exposed as ordinary RPCs
log_names()
dates(log_name)
raw_files()
exists(log_name, date=None)
tail(log_name, lines=100, date=None)
These methods are bound to the client through the grpc_frame
Describe/Call mechanism.
LogCtrl contains no gRPC-specific
logic; as a local class it handles file discovery, validation, and reading log tails.
log_names()
For files directly under log_dir, it parses:
<log_name>.log
<log_name>.log.YYYY-MM-DD
and builds the set of log_name values.
Names are discovered from both current and archived logs, so a name may be returned even if its current .log
file does not exist.
dates()
For the specified log_name, the method searches for existing files matching:
<log_name>.log.YYYY-MM-DD
Only files matching this pattern are considered.
Date suffixes are validated with datetime.strptime(..., "%Y-%m-%d")
and returned in descending date order.
raw_files()
Path.rglob("*") recursively scans the directory tree under log_dir.
No filename convention is applied; all files are returned as paths relative to
log_dir.
This provides a separate troubleshooting path for inspecting the actual file layout.
_LogStream
File contents are not returned as one ordinary RPC response. Instead, the grpc_frame
StreamCall mechanism is used.
The streaming interface provides:
read(log_name, date=None)
read_raw(relative_path)
These are the streaming methods.
read() uses the normal log-path resolution in LogCtrl, while read_raw()
uses raw relative-path resolution.
The file is read in binary chunks of chunk_size bytes,
bytes
bytes
bytes
...
which are yielded in sequence.
Client side
When log_ctrl.server_grpc is imported,
register_device_proxy(
"log",
async_client_cls=AsyncLogGrpcClient,
sync_client_cls=SyncLogGrpcClient,
...
)
registers the log client with DeviceProxy.
The caller only needs to create:
client = DeviceProxy(
"log",
server_ip="127.0.0.1",
server_port=50060,
async_mode=False,
)
to obtain the log-specific client.
Ordinary RPCs
log_names(), dates(), raw_files(), exists(), and tail()
are bound transparently by grpc_frame from the LogCtrl Describe metadata.
File contents
read() and read_raw() are implemented by the device-specific clients.
The synchronous client iterates over stream_call() and appends chunks to a bytearray,
ultimately returning bytes.
Applications therefore do not need to handle StreamCall or chunk boundaries.
Path resolution
An ordinary log path is resolved from:
log_name
↓
<log_name>.log
or:
log_name + date
↓
<log_name>.log.YYYY-MM-DD
These mappings determine the file path.
A log_name containing directory components or .log is rejected.
A relative path supplied to read_raw() is joined with log_dir and passed through resolve();
the resolved path must remain inside log_dir. This prevents ../
and similar paths from accessing files outside the log directory.
Lifecycle
The server entry point log_grpc_server() performs the following steps:
Create
LogCtrl.Create the stream handler.
Create the
grpc_framegRPC server.Bind to the requested host and port.
Start the server.
Verify startup with a self-connection.
Wait until shutdown.
This sequence is managed by the server entry point.
The client is created with DeviceProxy. After use, call:
client.close()
to close the channel.