Structured Logging in Python
Structured logging is a powerful technique for improving the observability of your applications. Instead of just writing plain text messages, structured logging formats log data as key-value pairs, making it easier to search, filter, and analyze logs using tools like Elasticsearch, Splunk, or even simple grep commands. This challenge asks you to implement a basic structured logger that can log messages with different severity levels and associated metadata.
Problem Description
You are tasked with creating a Python class called StructuredLogger that provides a simple interface for structured logging. The logger should support the following severity levels: DEBUG, INFO, WARNING, ERROR, and CRITICAL. For each log message, you should be able to associate arbitrary metadata (a dictionary of key-value pairs) to provide context. The logger should then format the log message as a JSON-like string, including the severity level, timestamp, and metadata.
Key Requirements:
- Severity Levels: The logger must support the five standard Python logging severity levels.
- Metadata: The logger must accept a dictionary of metadata to be included in each log message.
- Timestamp: Each log message must include a timestamp (in ISO 8601 format).
- JSON-like Output: The log message should be formatted as a string that resembles JSON, but without strict JSON validation (e.g., no need to escape special characters). The format should be:
{"severity": "LEVEL", "timestamp": "YYYY-MM-DDTHH:MM:SS", "message": "MESSAGE", "metadata": {METADATA}}. - Error Handling: The logger should gracefully handle cases where the metadata is not a dictionary.
Expected Behavior:
When you call the log() method of the StructuredLogger class, it should:
- Accept a message string and an optional metadata dictionary.
- Format the message according to the specified format, including the severity level, timestamp, message, and metadata.
- Return the formatted log message string.
Edge Cases to Consider:
- Invalid severity levels (levels not in the defined set). The logger should default to
INFOin this case. - Metadata that is not a dictionary. The logger should log a warning and omit the metadata.
- Empty metadata dictionary. The metadata section should still be present in the output, but empty.
Examples
Example 1:
Input: logger = StructuredLogger(); logger.log("Application started", metadata={"version": "1.0.0"})
Output: '{"severity": "INFO", "timestamp": "2023-10-27T10:00:00", "message": "Application started", "metadata": {"version": "1.0.0"}}'
Explanation: The message is logged with INFO severity, a timestamp, and the provided metadata. The timestamp is a placeholder.
Example 2:
Input: logger = StructuredLogger(); logger.log("Error processing data", severity="ERROR", metadata="not a dictionary")
Output: '{"severity": "ERROR", "timestamp": "2023-10-27T10:01:00", "message": "Error processing data", "metadata": {}}'
Explanation: The message is logged with ERROR severity. The metadata is invalid (not a dictionary), so a warning is logged internally, and the metadata section is empty in the output. The timestamp is a placeholder.
Example 3:
Input: logger = StructuredLogger(); logger.log("Debug message", severity="DEBUG", metadata={})
Output: '{"severity": "DEBUG", "timestamp": "2023-10-27T10:02:00", "message": "Debug message", "metadata": {}}'
Explanation: The message is logged with DEBUG severity, and an empty metadata dictionary is included. The timestamp is a placeholder.
Constraints
- The timestamp must be in ISO 8601 format (YYYY-MM-DDTHH:MM:SS).
- The metadata must be a dictionary or omitted.
- The logger class should be named
StructuredLogger. - The log method should accept a message string, an optional severity level (string), and an optional metadata dictionary.
- The severity level defaults to "INFO" if not provided or invalid.
- The code should be reasonably efficient (avoid unnecessary loops or complex operations).
Notes
- You don't need to implement actual logging to a file or external service. The focus is on formatting the log message correctly.
- Consider using the
datetimemodule to generate the timestamp. - Think about how to handle invalid severity levels gracefully.
- The JSON-like format doesn't need to be perfectly valid JSON. Simple string formatting is sufficient.
- The timestamp should be generated at the time of logging.