Normalize log output format for human-readable stdout#98
Conversation
Replace JSON output with human-readable format for stdout logs: [YYYY-MM-DD HH:MM:SS] [LEVEL] message - Add HumanReadableFormatter class to formatter.py - Use HumanReadableFormatter for StreamHandler (stdout) - Keep JsonFormatter for BufferHandler (API responses) - Handle both Python-native logs and C runtime JSON logs - Convert Unix timestamps to human-readable datetime This is Phase 1 of the logging normalization effort. API endpoint /api/runtime-logs still returns JSON format. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 of logging normalization - convert direct printf/fprintf calls in C code to use the centralized logging system. Files modified: - plugin_driver.c: Convert 73 printf/fprintf calls to log_info/log_error/log_warn/log_debug - plugin_config.c: Convert 2 printf calls to log_debug - plugin_logger.c: Convert init warnings to log_warn, format fallback output - journal_buffer.c: Convert 2 fprintf calls to log_error - plc_main.c: Format early init error with timestamp - watchdog.c: Format critical error with timestamp Fallback logging (when central logging unavailable) now uses human-readable format: [YYYY-MM-DD HH:MM:SS] [LEVEL] message Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
plugin_logger.c is compiled into native plugins, not the main runtime. Using log_warn/log_error directly causes undefined symbol errors when loading plugins. Replace with PLUGIN_LOGGER_STDERR macro that uses fprintf with the human-readable timestamp format. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert print() statements in webserver Python files to use the centralized logging system. Files modified: - plugin_config_model.py: 6 print -> logger.info/error/warning - credentials.py: 10 print -> logger.info/error/warning - config.py: 2 print -> logger.info/error - app.py: 2 print -> logger.info Note: Python plugin files (modbus_master_memory.py) need a dedicated logging solution similar to opcua_logging.py and are deferred to Phase 4. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update Python plugin logger fallback print statements to use the standard timestamp format for consistency: - shared/plugin_logger.py: Add timestamp to _fallback_print() - opcua/opcua_logging.py: Add timestamps to info/warn/error/debug fallbacks Format: [YYYY-MM-DD HH:MM:SS] [LEVEL] [PREFIX] message Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move diagnostic and verbose logging from INFO to DEBUG level: - opcua_security.py: Certificate details, security setup debug blocks - user_manager.py: Per-connection authentication details - address_space.py: Node creation details - server.py: Endpoint verification, setup progress - plugin.py: Internal initialization steps - callbacks.py: Permission callback registration details - synchronization.py: Sync manager internal state - config.py: Configuration parsing details - opcua_memory.py: Metadata caching details Keep as INFO: Important status messages (server started/stopped, plugin initialized, security initialized, sync loop status) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR standardizes log output to a human-readable format [YYYY-MM-DD HH:MM:SS] [LEVEL] message for stdout while preserving JSON format for API responses. The changes include adding a HumanReadableFormatter class, converting 84 printf/fprintf calls in C code and ~40 print() calls in Python to use centralized logging, and reducing verbose OPC-UA plugin logging by changing ~50 INFO-level logs to DEBUG.
Changes:
- Implemented dual formatters:
HumanReadableFormatterfor stdout andJsonFormatterfor API - Converted direct output calls (printf/print) across C and Python to logging system
- Reduced OPC-UA plugin verbosity by demoting diagnostic logs from INFO to DEBUG
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| webserver/logger/formatter.py | Added HumanReadableFormatter class to parse JSON logs and format as [timestamp] [level] message |
| webserver/logger/logger.py | Changed StreamHandler to use HumanReadableFormatter instead of JsonFormatter |
| webserver/logger/init.py | Updated exports and StreamHandler formatter |
| webserver/plugin_config_model.py | Converted 6 print() calls to logger.info/error/warning |
| webserver/credentials.py | Converted 10 print() calls to logger.info/error/warning |
| webserver/config.py | Converted 2 print() calls to logger.info/error |
| webserver/app.py | Converted 2 print() calls to logger.info |
| core/src/plc_app/utils/watchdog.c | Converted fprintf to formatted timestamp output |
| core/src/plc_app/plc_main.c | Converted fprintf to formatted timestamp output |
| core/src/plc_app/journal_buffer.c | Converted 2 fprintf calls to log_error |
| core/src/drivers/plugins/python/shared/plugin_logger.py | Updated fallback print() to include timestamp |
| core/src/drivers/plugins/python/opcua/*.py | Changed ~50 verbose log_info calls to log_debug across 9 files |
| core/src/drivers/plugins/native/plugin_logger.c | Added PLUGIN_LOGGER_STDERR macro and updated fallback to match format |
| core/src/drivers/plugin_driver.c | Converted 73 printf/fprintf calls to log_info/error/warn/debug |
| core/src/drivers/plugin_config.c | Converted 2 printf calls to log_debug |
| docs/LOGGING_NORMALIZATION_PLAN.md | Added comprehensive documentation of logging changes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| from webserver.logger import get_logger | ||
|
|
||
| logger, _ = get_logger(__name__) |
There was a problem hiding this comment.
The underscore variable name for the second return value is unclear. Consider using a more descriptive name like buffer_handler or add a comment explaining it represents an unused buffer handler.
| logger, _ = get_logger(__name__) | |
| logger, buffer_handler = get_logger(__name__) # buffer_handler is provided by get_logger but not used in this module |
| /* Helper macro for formatted stderr output matching human-readable log format */ | ||
| #define PLUGIN_LOGGER_STDERR(level, fmt, ...) \ | ||
| do \ | ||
| { \ | ||
| time_t now = time(NULL); \ | ||
| struct tm t; \ | ||
| gmtime_r(&now, &t); \ | ||
| char time_buf[20]; \ | ||
| strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &t); \ |
There was a problem hiding this comment.
The PLUGIN_LOGGER_STDERR macro calls time() and gmtime_r() on every invocation. For frequently called logging, consider caching the timestamp or using a less expensive time source.
| /* Helper macro for formatted stderr output matching human-readable log format */ | |
| #define PLUGIN_LOGGER_STDERR(level, fmt, ...) \ | |
| do \ | |
| { \ | |
| time_t now = time(NULL); \ | |
| struct tm t; \ | |
| gmtime_r(&now, &t); \ | |
| char time_buf[20]; \ | |
| strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &t); \ | |
| /* Cached time helper for formatted stderr output */ | |
| static void plugin_logger_get_time_str(char *buffer, size_t buffer_size) | |
| { | |
| static time_t last_time = (time_t)0; | |
| static char cached_time_buf[20] = {0}; | |
| time_t now = time(NULL); | |
| if (now != last_time) | |
| { | |
| struct tm t; | |
| if (gmtime_r(&now, &t) != NULL) | |
| { | |
| /* Ensure consistent format: "YYYY-MM-DD HH:MM:SS" */ | |
| if (strftime(cached_time_buf, sizeof(cached_time_buf), "%Y-%m-%d %H:%M:%S", &t) == 0) | |
| { | |
| /* Fallback in unlikely case of formatting failure */ | |
| cached_time_buf[0] = '\0'; | |
| } | |
| } | |
| else | |
| { | |
| cached_time_buf[0] = '\0'; | |
| } | |
| last_time = now; | |
| } | |
| if (buffer && buffer_size > 0) | |
| { | |
| /* Copy cached timestamp into caller's buffer */ | |
| strncpy(buffer, cached_time_buf, buffer_size - 1); | |
| buffer[buffer_size - 1] = '\0'; | |
| } | |
| } | |
| /* Helper macro for formatted stderr output matching human-readable log format */ | |
| #define PLUGIN_LOGGER_STDERR(level, fmt, ...) \ | |
| do \ | |
| { \ | |
| char time_buf[20]; \ | |
| plugin_logger_get_time_str(time_buf, sizeof(time_buf)); \ |
| /* Fallback: format to match human-readable log format */ | ||
| time_t now = time(NULL); | ||
| struct tm t; | ||
| gmtime_r(&now, &t); | ||
| char time_buf[20]; | ||
| strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &t); | ||
| printf("[%s] [%s] [%s] %s\n", time_buf, level, logger->plugin_name, message); |
There was a problem hiding this comment.
This timestamp formatting code is duplicated in the PLUGIN_LOGGER_STDERR macro above. Consider extracting it into a helper function to avoid duplication.
| time_t now_time = time(NULL); | ||
| struct tm t; | ||
| gmtime_r(&now_time, &t); | ||
| char time_buf[20]; | ||
| strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &t); | ||
| fprintf(stderr, "[%s] [ERROR] Watchdog: No heartbeat! PLC unresponsive.\n", time_buf); |
There was a problem hiding this comment.
This timestamp formatting code is duplicated across multiple C files (watchdog.c, plc_main.c, plugin_logger.c). Consider creating a shared helper function in log.c to reduce duplication.
| log_error("Error: 'init' function not found or not callable in module '%s' - this function " | ||
| "is required", |
There was a problem hiding this comment.
The error message spans two lines but reads as a single sentence. This could be confusing. Consider reformatting to 'Error: Required function "init" not found or not callable in module "%s"' on a single line.
| log_error("Error: 'init' function not found or not callable in module '%s' - this function " | |
| "is required", | |
| log_error("Error: Required function \"init\" not found or not callable in module \"%s\"", |
Summary
Changes
Phase 1: Python Logging Formatter
HumanReadableFormatterclass that converts JSON logs to[YYYY-MM-DD HH:MM:SS] [LEVEL] messageformatPhase 2: C printf/fprintf Conversions
plugin_driver.cto log_info/log_error/log_warn/log_debugplugin_config.c,journal_buffer.c,plc_main.c,watchdog.cPhase 3: Python print() Conversions
Phase 4: Plugin Fallback Updates
shared/plugin_logger.pyandopcua/opcua_logging.pywith timestamp formatPhase 5: OPC-UA Verbose Logging Fix
Before/After
Before:
After:
(Verbose security/certificate details now only appear with
--print-debug)Test plan
/api/runtime-logsstill returns JSON--print-debugflag shows DEBUG level messages🤖 Generated with Claude Code