diff --git a/Esp32_example.yaml b/Esp32_example.yaml new file mode 100644 index 0000000..ca8cd32 --- /dev/null +++ b/Esp32_example.yaml @@ -0,0 +1,310 @@ +# Complete example configuration for ESPHome with Syslog integration +# This demonstrates all major features of the Syslog component + +esphome: + name: esp32_syslog_example + friendly_name: ESP32 Syslog Example + on_boot: + priority: -100 + then: + - syslog.log: + level: 7 + tag: "[Boot]" + payload: "Device booted" + - lambda: |- + // Restore the filter string from the text sensor + id(syslog_component).set_filter_string(id(syslog_filter_text).state); + +esp32: + board: esp32dev + +# Enable detailed logging +logger: + level: DEBUG + baud_rate: 115200 + +# WiFi configuration +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # Fallback access point + ap: + ssid: "ESP32 Syslog Demo" + password: "configuredevice" + +# Enable Home Assistant API +api: + encryption: + key: !secret api_encryption_key + +# Enable over-the-air updates +ota: + password: !secret ota_password + +# Web server for status page +web_server: + port: 80 + +# Add time component (useful for accurate timestamps) +time: + - platform: homeassistant + id: homeassistant_time + +# Example sensors +sensor: + - platform: uptime + name: "Uptime" + update_interval: 60s + + - platform: wifi_signal + name: "WiFi Signal" + update_interval: 60s + + # Example DHT sensor + - platform: dht + pin: GPIO23 + model: DHT22 + temperature: + name: "Room Temperature" + id: room_temp + humidity: + name: "Room Humidity" + id: room_humidity + update_interval: 60s + +# Text component for filter management +text: + - platform: template + name: "Server IP" + id: syslog_server_ip + entity_category: config + mode: text + optimistic: true + restore_value: true + initial_value: "192.168.1.100" + on_value: + - lambda: 'id(syslog_component).set_server_ip(x);' + + - platform: template + name: "Filter Input" + id: syslog_filter_text + entity_category: config + optimistic: true + restore_value: true + mode: text + initial_value: "" # Set an initial empty value + on_value: + then: + - syslog.set_filter_string: + filter_string: !lambda 'return x;' + +number: + - platform: template + name: "Port" + id: syslog_server_port + entity_category: config + min_value: 1 + max_value: 65535 + step: 1 + initial_value: 514 + restore_value: true + optimistic: true + mode: box + on_value: + then: + - lambda: |- + id(syslog_component).set_server_port(x); + +# Switch for enabling/disabling syslog +switch: + - platform: template + name: "External Component Enable" + id: syslog_enable_switch + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + initial_value: true + on_turn_on: + - lambda: |- + id(syslog_component).set_globally_enabled(true); + on_turn_off: + - lambda: |- + id(syslog_component).set_globally_enabled(false); + + - platform: template + id: syslog_enable_logger + name: "Default Logger" + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + on_turn_on: + - lambda: 'id(syslog_component).set_enable_logger_messages(true);' + on_turn_off: + - lambda: 'id(syslog_component).set_enable_logger_messages(false);' + + - platform: template + id: syslog_enable_direct_logs + name: "Direct Logs" + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + on_turn_on: + - lambda: 'id(syslog_component).set_enable_direct_logs(true);' + on_turn_off: + - lambda: 'id(syslog_component).set_enable_direct_logs(false);' + + - platform: template + id: syslog_strip_colors + name: "Strip Colors" + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + on_turn_on: + - lambda: 'id(syslog_component).set_strip_colors(true);' + on_turn_off: + - lambda: 'id(syslog_component).set_strip_colors(false);' + + - platform: template + id: syslog_filter_mode + name: "Filter Mode (ON=Include/OFF=Exclude)" + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - lambda: 'id(syslog_component).set_filter_mode(true);' # Include mode + on_turn_off: + - lambda: 'id(syslog_component).set_filter_mode(false);' # Exclude mode + +# Load syslog component from GitHub +external_components: + - source: + type: git + url: https://github.com/TheStaticTurtle/esphome_syslog #need to currently be ttps://github.com/KrX3D/esphome_syslog till the PR is added + ref: main # or a specific commit SHA + refresh: always + +# Syslog configuration with all available options +syslog: + id: syslog_component + ip_address: "192.168.1.100" # IP address of your syslog server + port: 514 + client_id: esp32_living_room + strip_colors: true + enable_logger: true + enable_direct_logs: true + globally_enabled: true + min_level: INFO + filter_mode: exclude + filters: + - wifi + - mqtt + - api + filter_text: syslog_filter_text + direct_log_prefix: "direct" # Line will be output as "direct: " + logger_log_prefix: "logger" # Line will be output as "logger: " + +# Test buttons for various syslog features +button: + - platform: template + name: "Test Syslog Message" + on_press: + - syslog.log: + level: 6 # INFO level + tag: "button" + payload: "Test button was pressed!" + + - platform: template + name: "Add WiFi Filter" + entity_category: config + on_press: + - syslog.add_filter: + tag: "wifi" + + - platform: template + name: "Remove WiFi Filter" + entity_category: config + on_press: + - syslog.remove_filter: + tag: "wifi" + + - platform: template + name: "Clear All Filters" + entity_category: config + on_press: + - syslog.clear_filters: + + - platform: template + name: "Set Standard Filters" + entity_category: config + on_press: + - syslog.set_filter_string: + filter_string: "wifi,mqtt,api" + +# Automatically log temperature readings +interval: + - interval: 5min + then: + - lambda: |- + // Get current temperature and log it + float temp = id(room_temp).state; + char message[64]; + sprintf(message, "Current temperature is %.1f°C", temp); + + - syslog.log: + level: 6 # INFO level + tag: "temp_monitor" + payload: !lambda 'return message;' + +# Log events when temperature changes significantly +binary_sensor: + - platform: template + name: "Temperature Rise Alert" + lambda: |- + static float last_temp = 0; + float current_temp = id(room_temp).state; + bool significant_change = abs(current_temp - last_temp) > 2.0; + if (significant_change) { + last_temp = current_temp; + } + return significant_change; + on_press: + - syslog.log: + level: 4 # WARNING level + tag: "temp_alert" + payload: !lambda 'return "Temperature changed significantly to " + to_string(id(room_temp).state) + "°C";' + +select: + - platform: template + id: syslog_log_level + name: "Log Level" + entity_category: config + optimistic: true + options: + - "NONE" + - "ERROR" + - "WARN" + - "INFO" + - "CONFIG" + - "DEBUG" + - "VERBOSE" + - "VERY_VERBOSE" + initial_option: "DEBUG" + on_value: + - lambda: |- + if (x == "NONE") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_NONE); + else if (x == "ERROR") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_ERROR); + else if (x == "WARN") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_WARN); + else if (x == "INFO") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_INFO); + else if (x == "CONFIG") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_CONFIG); + else if (x == "DEBUG") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_DEBUG); + else if (x == "VERBOSE") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_VERBOSE); + else if (x == "VERY_VERBOSE") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_VERY_VERBOSE); diff --git a/README.md b/README.md index 5ac68d0..4e33a16 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,419 @@ -# esphome_syslog +# ESPHome Syslog Component -A simple syslog component for esphome. The component is designed to auto attach itself to the logger core module (like the MQTT component does with the `log_topic`) +A powerful and configurable Syslog component for ESPHome that forwards logs to a Syslog server. The component can automatically attach itself to the ESPHome logger core module (similar to how the MQTT component works with `log_topic`) while providing advanced filtering and configuration options. -This component uses the https://github.com/arcao/Syslog library version 2.0 at its core +## Features -## How to +- Forward ESPHome logs to a Syslog server +- Direct logging through automations +- Advanced filtering (include/exclude specific components) +- Configurable log levels +- Color code stripping +- Runtime reconfigurable (server IP, port, filters) +- Support for both IPv4 and IPv6 addresses +- Component-specific log prefixing +- Integration with Text components for filter management + +## Installation + +### Using External Components (Recommended) + +```yaml +external_components: + - source: + type: git + url: https://github.com/TheStaticTurtle/esphome_syslog + ref: main # or a specific commit SHA + refresh: always +``` + +### Manual Installation + +To install manually, locate your `esphome` folder, create a folder named `custom_components` (if it doesn't exist), navigate into it, and execute: -### Manually -To install, locate your `esphome` folder, create a folder named `custom_components` got into it and execute  ```shell git clone https://github.com/TheStaticTurtle/esphome_syslog.git mv esphome_syslog/components/syslog . rm -rf esphome_syslog ``` -### YAML + +## Basic Configuration + +The simplest configuration only requires adding the component to your ESPHome configuration: + ```yaml -external_components: - - source: github://TheStaticTurtle/esphome_syslog - components: [syslog] +syslog: ``` -### Configuration -Simply add this to the configuration of your esphome node:  +With this minimal configuration, the component will **broadcast logs to everyone on the network** using the default UDP port 514. + +## Configuration Options + +| Option | Type | Default | Description | +|-----------------------|-----------|-------------------|-------------------------------------------------------------------| +| `ip_address` | string | "255.255.255.255" | IP address of the Syslog server (IPv4 or IPv6) | +| `port` | integer | 514 | UDP port of the Syslog server | +| `client_id` | string | Device name | Client identifier in Syslog messages | +| `strip_colors` | boolean | true | Remove ESPHome color codes from log messages | +| `enable_logger` | boolean | true | Enable forwarding of ESPHome logger messages | +| `enable_direct_logs` | boolean | true | Enable direct logging through automations | +| `globally_enabled` | boolean | true | Global switch to enable/disable the component | +| `min_level` | string | "DEBUG" | Minimum log level to forward (ERROR, WARN, INFO, DEBUG, etc.) | +| `filter_mode` | string/bool | "exclude" | Filter mode: "include" or "exclude" (or true/false) | +| `filters` | list | [] | List of component tags to include/exclude | +| `filter_string` | string | "" | Comma-separated list of component tags to include/exclude | +| `filter_text` | id | - | Text component ID to display/update filter string | +| `direct_log_prefix` | string | "" | Prefix added to direct log messages | +| `logger_log_prefix` | string | "" | Prefix added to logger messages | + +## Configuration Options + +### Logging Configuration + +The following options allow you to customize how log messages are displayed: + +- `client_id`: String that sets the device name used in Syslog messages. This also controls the name of the log file created by the Syslog server. + - Example: When set to `"ESP32 C3"`, logs will show this identifier and be saved to `syslog-ESP32_C3.log` + +- `direct_log_prefix`: String that is added as a prefix to direct log messages. + - Example: When set to `"direct"`, direct log messages will be prefixed with this text + +- `logger_log_prefix`: String that is added as a prefix to logger messages. + - Example: When set to `"logger"`, logger messages will be prefixed with this text + +### Example Log Output + +When configured with: +```yaml +client_id: "ESP32_C3" +direct_log_prefix: "direct" +logger_log_prefix: "logger" +``` + +The logs will appear as: + +``` +Apr 8 18:30:11 ESP32_C3 direct:[[Boot]] - => Device booted +Apr 8 18:30:11 ESP32_C3 logger:[sensor] - [D][sensor:093]: 'WiFi Signal': Sending state -64.00000 dBm with 0 decimals of accuracy +``` + +Note that the `client_id` appears after the timestamp, and the prefixes are added to the beginning of the actual log message content. + +## Advanced Configuration Examples + +### Server Configuration with Port and Client ID + ```yaml syslog: + ip_address: "192.168.1.53" + port: 514 + client_id: "living_room_esp32" ``` -When used like this, the component will simply **broadcast its log to everyone on the network** to change this behavior you can add the `ip_address` and `port` option like this: +### Filtering by Log Level and Components + ```yaml syslog: -    ip_address: "192.168.1.53" -    port: 514 + ip_address: "192.168.1.53" + min_level: INFO + filter_mode: exclude + filters: + - wifi + - mqtt + - api ``` -Default behavior strips the esphome color tags from the log (The `033[0;xxx` and the `#033[0m`) if you do not want this set the `strip_colors` option to `False`. +This configuration: +- Only forwards logs of level INFO or higher +- Excludes logs from the wifi, mqtt, and api components + +### Using a Text Component for Filter Management -Default behavior also sets `enable_logger` to `True` if you wish to disable sending logger messages and only use the `syslog.log` action you can do so by setting it to `False`: +This allows runtime management of filters through Home Assistant or other frontends: -The action `syslog.log` has 3 settings: ```yaml -then: - - syslog.log: - level: 7 - tag: "custom_action" - payload: "My log message" -``` - -Due to the differences in log levels of syslog and esphome I had to translate them, here is a table: -| Esphome level                  | Syslog level | -|--------------------------------|--------------| -| ESPHOME_LOG_LEVEL_NONE         | LOG_EMERG    | -| ESPHOME_LOG_LEVEL_ERROR        | LOG_ERR      | -| ESPHOME_LOG_LEVEL_WARN         | LOG_WARNING  | -| ESPHOME_LOG_LEVEL_INFO         | LOG_INFO     | -| ESPHOME_LOG_LEVEL_CONFIG       | LOG_NOTICE   | -| ESPHOME_LOG_LEVEL_DEBUG        | LOG_DEBUG    | -| ESPHOME_LOG_LEVEL_VERBOSE      | LOG_DEBUG    | -| ESPHOME_LOG_LEVEL_VERY_VERBOSE | LOG_DEBUG    | - -This table is however open to discussion as it's my interpretation, if you want to change it you can do so in the `syslog_component.cpp` file and change the array at line 22 - -## Warning -This component should not break anything and should work with everything however if it doesn't please open an issue. -I have successfully tested this component with an esp8266 and an esp32. BUT The esp32 seems to have issue when there is a lot of thing to send very fast which you can see turing boot when it prints the config, see [my comment in issue #7](https://github.com/TheStaticTurtle/esphome_syslog/issues/7#issuecomment-1236194816) for more details . +text: + - platform: template + id: syslog_filter + name: "Syslog Filter" + mode: text + restore_value: true + +syslog: + filter_mode: include + filter_text: syslog_filter +``` + +### Full Control with Switches and UI Integration + +```yaml +# Syslog configuration with filter text +syslog: + id: syslog_component + filter_text: syslog_filter_text + +# Text component for filter management +text: + - platform: template + name: "Filter Input" + id: syslog_filter_text + entity_category: config + optimistic: true + restore_value: true + mode: text + on_value: + then: + - syslog.set_filter_string: + filter_string: !lambda 'return x;' + +# Text component for server IP +text: + - platform: template + name: "Server IP" + id: syslog_server_ip + entity_category: config + mode: text + optimistic: true + restore_value: true + initial_value: "192.168.1.100" + on_value: + - lambda: 'id(syslog_component).set_server_ip(x);' + +# Number component for port +number: + - platform: template + name: "Port" + id: syslog_server_port + entity_category: config + min_value: 1 + max_value: 65535 + step: 1 + initial_value: 514 + restore_value: true + optimistic: true + mode: box + on_value: + then: + - lambda: 'id(syslog_component).set_server_port(x);' + +# Main enable/disable switch +switch: + - platform: template + name: "Syslog Enable" + id: syslog_enable_switch + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + initial_value: true + on_turn_on: + - lambda: 'id(syslog_component).set_globally_enabled(true);' + on_turn_off: + - lambda: 'id(syslog_component).set_globally_enabled(false);' + +# Filter mode switch +switch: + - platform: template + id: syslog_filter_mode + name: "Filter Mode (ON=Include/OFF=Exclude)" + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - lambda: 'id(syslog_component).set_filter_mode(true);' # Include mode + on_turn_off: + - lambda: 'id(syslog_component).set_filter_mode(false);' # Exclude mode +``` + +### Log Level Selection with Dropdown + +```yaml +select: + - platform: template + id: syslog_log_level + name: "Log Level" + entity_category: config + optimistic: true + options: + - "NONE" + - "ERROR" + - "WARN" + - "INFO" + - "CONFIG" + - "DEBUG" + - "VERBOSE" + - "VERY_VERBOSE" + initial_option: "DEBUG" + on_value: + - lambda: |- + if (x == "NONE") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_NONE); + else if (x == "ERROR") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_ERROR); + else if (x == "WARN") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_WARN); + else if (x == "INFO") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_INFO); + else if (x == "CONFIG") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_CONFIG); + else if (x == "DEBUG") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_DEBUG); + else if (x == "VERBOSE") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_VERBOSE); + else if (x == "VERY_VERBOSE") + id(syslog_component).set_min_log_level(ESPHOME_LOG_LEVEL_VERY_VERBOSE); +``` + +### Saving and Restoring Filter Settings + +This configuration automatically restores filter settings on boot: + +```yaml +esphome: + name: esp32_syslog_example + on_boot: + priority: -100 + then: + - lambda: |- + // Restore the filter string from the text sensor + id(syslog_component).set_filter_string(id(syslog_filter_text).state); +``` + +## Automation Actions + +The component provides several automation actions: + +### Sending Custom Logs + +```yaml +button: + - platform: template + name: "Test Syslog" + on_press: + - syslog.log: + level: 6 # INFO level + tag: "custom_action" + payload: "Button pressed!" +``` + +### Managing Filters + +```yaml +button: + - platform: template + name: "Add Filter" + on_press: + - syslog.add_filter: + tag: "wifi" + + - platform: template + name: "Remove Filter" + on_press: + - syslog.remove_filter: + tag: "wifi" + + - platform: template + name: "Clear Filters" + on_press: + - syslog.clear_filters: + + - platform: template + name: "Set Filter String" + on_press: + - syslog.set_filter_string: + filter_string: "wifi,mqtt,api" +``` + +### Logging Sensor Data + +```yaml +interval: + - interval: 5min + then: + - lambda: |- + // Get current temperature and format message + float temp = id(room_temperature).state; + char message[64]; + sprintf(message, "Current temperature is %.1f°C", temp); + + - syslog.log: + level: 6 # INFO level + tag: "temp_monitor" + payload: !lambda 'return message;' +``` + +### Logging Important Events + +```yaml +binary_sensor: + - platform: template + name: "Temperature Rise Alert" + lambda: |- + static float last_temp = 0; + float current_temp = id(room_temp).state; + bool significant_change = abs(current_temp - last_temp) > 2.0; + if (significant_change) { + last_temp = current_temp; + } + return significant_change; + on_press: + - syslog.log: + level: 4 # WARNING level + tag: "temp_alert" + payload: !lambda 'return "Temperature changed significantly to " + to_string(id(room_temp).state) + "°C";' +``` + +## Log Levels Mapping + +Due to differences between ESPHome and Syslog log levels, the component maps them as follows: + +| ESPHome Level | Syslog Priority | +|----------------------------|-----------------| +| NONE | EMERG (0) | +| ERROR | ERR (3) | +| WARN | WARNING (4) | +| INFO | INFO (6) | +| CONFIG | NOTICE (5) | +| DEBUG | DEBUG (7) | +| VERBOSE | DEBUG (7) | +| VERY_VERBOSE | DEBUG (7) | + +## Example Log Output + +When properly configured, you'll see log messages in your Syslog server like these: + +``` +Apr 7 12:34:56 esp32_living_room wifi: WiFi connected to 'MyNetwork' +Apr 7 12:34:57 esp32_living_room direct:button: Button pressed! +Apr 7 12:34:58 esp32_living_room logger:sensor: Temperature: 22.5°C +Apr 7 12:35:00 esp32_living_room syslog: Filter string updated: 'wifi,mqtt' +``` + +## Complete Example YAML + +For a complete example configuration demonstrating all features of the Syslog component, please refer to the [example.yaml](https://github.com/TheStaticTurtle/esphome_syslog/blob/main/example.yaml) file in the repository. + +## Compatibility + +This component has been tested with: +- ESP8266 +- ESP32 + +Note: The ESP32 may experience issues when sending many log messages in rapid succession, such as during boot when configuration is printed. See [issue #7 comment](https://github.com/TheStaticTurtle/esphome_syslog/issues/7#issuecomment-1236194816) for more details. + +## Troubleshooting + +If you're not seeing logs on your Syslog server: + +1. Check that your server IP and port are correct +2. Ensure your network allows UDP traffic on the configured port +3. Verify that log levels and filters aren't blocking messages +4. Check if the component is globally enabled +5. For ESP32, try reducing the logging verbosity to prevent buffer overflow + +## Contributing + +Contributions are welcome! If you find a bug or want to add a feature, please open an issue or submit a pull request. + +## License + +This project is licensed under the ISC License - see the LICENSE file for details. \ No newline at end of file diff --git a/components/syslog/__init__.py b/components/syslog/__init__.py index af8d58b..e8d4af9 100644 --- a/components/syslog/__init__.py +++ b/components/syslog/__init__.py @@ -1,33 +1,98 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome import automation -from esphome.const import CONF_ID, CONF_IP_ADDRESS, CONF_PORT, CONF_CLIENT_ID, CONF_LEVEL, CONF_PAYLOAD, CONF_TAG -from esphome.components.logger import LOG_LEVELS, is_log_level +from esphome.const import ( + CONF_ID, + CONF_IP_ADDRESS, + CONF_PORT, + CONF_CLIENT_ID, + CONF_LEVEL, + CONF_PAYLOAD, + CONF_TAG, + CONF_MODE +) +from esphome.components import logger, text +# Configuration constants CONF_STRIP_COLORS = "strip_colors" CONF_ENABLE_LOGGER_MESSAGES = "enable_logger" +CONF_ENABLE_DIRECT_LOGS = "enable_direct_logs" +CONF_GLOBALLY_ENABLED = "globally_enabled" CONF_MIN_LEVEL = "min_level" +CONF_FILTER_MODE = "filter_mode" +CONF_INCLUDE = "include" +CONF_EXCLUDE = "exclude" +CONF_FILTERS = "filters" +CONF_FILTER_STRING = "filter_string" +CONF_FILTER_TEXT = "filter_text" +CONF_DIRECT_LOG_PREFIX = "direct_log_prefix" +CONF_LOGGER_LOG_PREFIX = "logger_log_prefix" -DEPENDENCIES = ['logger','network'] +# Component dependencies +DEPENDENCIES = ['logger', 'network', 'socket'] -debug_ns = cg.esphome_ns.namespace('debug') +# Namespace setup syslog_ns = cg.esphome_ns.namespace('syslog') +# Component class definitions SyslogComponent = syslog_ns.class_('SyslogComponent', cg.Component) SyslogLogAction = syslog_ns.class_('SyslogLogAction', automation.Action) +SyslogAddFilterAction = syslog_ns.class_('SyslogAddFilterAction', automation.Action) +SyslogRemoveFilterAction = syslog_ns.class_('SyslogRemoveFilterAction', automation.Action) +SyslogClearFiltersAction = syslog_ns.class_('SyslogClearFiltersAction', automation.Action) +SyslogSetFilterStringAction = syslog_ns.class_('SyslogSetFilterStringAction', automation.Action) -CONFIG_SCHEMA = cv.All( - cv.Schema({ - cv.GenerateID(): cv.declare_id(SyslogComponent), - cv.Optional(CONF_IP_ADDRESS, default="255.255.255.255"): cv.string_strict, - cv.Optional(CONF_PORT, default=514): cv.port, - cv.Optional(CONF_ENABLE_LOGGER_MESSAGES, default=True): cv.boolean, - cv.Optional(CONF_STRIP_COLORS, default=True): cv.boolean, - cv.Optional(CONF_MIN_LEVEL, default="DEBUG"): is_log_level, - }), - cv.only_with_arduino, -) +# Define all log levels in uppercase for validation +LOG_LEVEL_OPTIONS = [level.upper() for level in logger.LOG_LEVELS] + +# Validate filter mode +def validate_filter_mode(value): + """ + Validates the filter mode setting, accepting string or boolean values. + For strings: 'include' -> True, 'exclude' -> False + For booleans: True = include mode, False = exclude mode + """ + if isinstance(value, str): + if value.lower() == "include": + return True + elif value.lower() == "exclude": + return False + if isinstance(value, bool): + return value + raise cv.Invalid(f"Filter mode must be either 'include', 'exclude', True (include), or False (exclude)") + +# Custom validator for log levels that handles case insensitivity +def validate_log_level(value): + """ + Validates log level values against ESPHome's defined log levels, + handling case-insensitivity by converting to uppercase before validation. + """ + if isinstance(value, str): + upper_value = value.upper() + if upper_value in LOG_LEVEL_OPTIONS: + return upper_value + raise cv.Invalid(f"Unknown log level '{value}', valid options are {', '.join(LOG_LEVEL_OPTIONS)}.") +# Main component configuration schema +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SyslogComponent), + cv.Optional(CONF_IP_ADDRESS, default="255.255.255.255"): cv.string_strict, + cv.Optional(CONF_PORT, default=514): cv.port, + cv.Optional(CONF_CLIENT_ID): cv.string_strict, # Optional client ID, defaults to device name + cv.Optional(CONF_ENABLE_LOGGER_MESSAGES, default=True): cv.boolean, + cv.Optional(CONF_ENABLE_DIRECT_LOGS, default=True): cv.boolean, + cv.Optional(CONF_GLOBALLY_ENABLED, default=True): cv.boolean, + cv.Optional(CONF_STRIP_COLORS, default=True): cv.boolean, + cv.Optional(CONF_MIN_LEVEL, default="DEBUG"): validate_log_level, + cv.Optional(CONF_FILTER_MODE, default="exclude"): validate_filter_mode, + cv.Optional(CONF_FILTERS, default=[]): cv.ensure_list(cv.string), + cv.Optional(CONF_FILTER_STRING, default=""): cv.string, + cv.Optional(CONF_FILTER_TEXT): cv.use_id(text.Text), + cv.Optional(CONF_DIRECT_LOG_PREFIX, default=""): cv.string, + cv.Optional(CONF_LOGGER_LOG_PREFIX, default=""): cv.string, +}) + +# Action schemas SYSLOG_LOG_ACTION_SCHEMA = cv.Schema({ cv.GenerateID(): cv.use_id(SyslogComponent), cv.Required(CONF_LEVEL): cv.templatable(cv.int_range(min=0, max=7)), @@ -35,28 +100,118 @@ cv.Required(CONF_PAYLOAD): cv.templatable(cv.string), }) -def to_code(config): - cg.add_library('Syslog', '2.0.0') +SYSLOG_ADD_FILTER_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(SyslogComponent), + cv.Required(CONF_TAG): cv.templatable(cv.string), +}) +SYSLOG_REMOVE_FILTER_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(SyslogComponent), + cv.Required(CONF_TAG): cv.templatable(cv.string), +}) + +SYSLOG_CLEAR_FILTERS_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(SyslogComponent), +}) + +SYSLOG_SET_FILTER_STRING_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(SyslogComponent), + cv.Required(CONF_FILTER_STRING): cv.templatable(cv.string), +}) + +def to_code(config): + """ + Translates the YAML configuration to C++ code for the ESPHome runtime. + Handles component setup and configuration of all parameters. + """ var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) + # Configure basic settings cg.add(var.set_enable_logger_messages(config[CONF_ENABLE_LOGGER_MESSAGES])) + cg.add(var.set_enable_direct_logs(config[CONF_ENABLE_DIRECT_LOGS])) + cg.add(var.set_globally_enabled(config[CONF_GLOBALLY_ENABLED])) cg.add(var.set_strip_colors(config[CONF_STRIP_COLORS])) cg.add(var.set_server_ip(config[CONF_IP_ADDRESS])) cg.add(var.set_server_port(config[CONF_PORT])) - cg.add(var.set_min_log_level(LOG_LEVELS[config[CONF_MIN_LEVEL]])) + + # Set client ID if provided, otherwise defaults to device name + if CONF_CLIENT_ID in config: + cg.add(var.set_client_id(config[CONF_CLIENT_ID])) + + # Configure log level + cg.add(var.set_min_log_level(logger.LOG_LEVELS[config[CONF_MIN_LEVEL]])) + + # Configure filter mode + cg.add(var.set_filter_mode(config[CONF_FILTER_MODE])) + + # Configure log prefixes if provided + if CONF_DIRECT_LOG_PREFIX in config: + cg.add(var.set_direct_log_prefix(config[CONF_DIRECT_LOG_PREFIX])) + + if CONF_LOGGER_LOG_PREFIX in config: + cg.add(var.set_logger_log_prefix(config[CONF_LOGGER_LOG_PREFIX])) + + # Register the text sensor for filter string updates if provided + if CONF_FILTER_TEXT in config: + text_obj = yield cg.get_variable(config[CONF_FILTER_TEXT]) + cg.add(var.register_filter_string_text(text_obj)) + + # Parse filter string and add filters + if config[CONF_FILTER_STRING]: + # Set the filter string directly + cg.add(var.set_filter_string(config[CONF_FILTER_STRING])) + else: + # Use the original filters list if filter_string is empty + for filter_tag in config[CONF_FILTERS]: + cg.add(var.add_filter(filter_tag)) +# Register automation actions @automation.register_action('syslog.log', SyslogLogAction, SYSLOG_LOG_ACTION_SCHEMA) def syslog_log_action_to_code(config, action_id, template_arg, args): + """Registers the syslog.log action for automations""" paren = yield cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - + template_ = yield cg.templatable(config[CONF_LEVEL], args, cg.uint8) cg.add(var.set_level(template_)) template_ = yield cg.templatable(config[CONF_TAG], args, cg.std_string) cg.add(var.set_tag(template_)) template_ = yield cg.templatable(config[CONF_PAYLOAD], args, cg.std_string) cg.add(var.set_payload(template_)) + + yield var + +@automation.register_action('syslog.add_filter', SyslogAddFilterAction, SYSLOG_ADD_FILTER_SCHEMA) +def syslog_add_filter_action_to_code(config, action_id, template_arg, args): + """Registers the syslog.add_filter action for automations""" + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_TAG], args, cg.std_string) + cg.add(var.set_tag(template_)) + yield var +@automation.register_action('syslog.remove_filter', SyslogRemoveFilterAction, SYSLOG_REMOVE_FILTER_SCHEMA) +def syslog_remove_filter_action_to_code(config, action_id, template_arg, args): + """Registers the syslog.remove_filter action for automations""" + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_TAG], args, cg.std_string) + cg.add(var.set_tag(template_)) + yield var + +@automation.register_action('syslog.clear_filters', SyslogClearFiltersAction, SYSLOG_CLEAR_FILTERS_SCHEMA) +def syslog_clear_filters_action_to_code(config, action_id, template_arg, args): + """Registers the syslog.clear_filters action for automations""" + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + yield var + +@automation.register_action('syslog.set_filter_string', SyslogSetFilterStringAction, SYSLOG_SET_FILTER_STRING_SCHEMA) +def syslog_set_filter_string_action_to_code(config, action_id, template_arg, args): + """Registers the syslog.set_filter_string action for automations""" + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_FILTER_STRING], args, cg.std_string) + cg.add(var.set_filter_string(template_)) yield var diff --git a/components/syslog/syslog_component.cpp b/components/syslog/syslog_component.cpp index ea80947..6eeb26c 100644 --- a/components/syslog/syslog_component.cpp +++ b/components/syslog/syslog_component.cpp @@ -1,46 +1,179 @@ +// components/syslog/syslog_component.cpp + #include "syslog_component.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/version.h" +#include // for std::transform #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif -/* -#include "esphome/core/helpers.h" -#include "esphome/core/defines.h" -#include "esphome/core/version.h" -*/ + +#ifdef USE_SOCKET_IMPL_LWIP_TCP +#include +#define IPPROTO_UDP IP_PROTO_UDP +#endif namespace esphome { namespace syslog { static const char *TAG = "syslog"; -//https://github.com/arcao/Syslog/blob/master/src/Syslog.h#L37-44 -//https://github.com/esphome/esphome/blob/5c86f332b269fd3e4bffcbdf3359a021419effdd/esphome/core/log.h#L19-26 +// Map ESPHome log levels to syslog priorities +// https://github.com/arcao/Syslog/blob/master/src/Syslog.h#L37-44 +// https://github.com/esphome/esphome/blob/5c86f332b269fd3e4bffcbdf3359a021419effdd/esphome/core/log.h#L19-26 static const uint8_t esphome_to_syslog_log_levels[] = {0, 3, 4, 6, 5, 7, 7, 7}; +// Helper function to trim whitespace +static std::string trim(const std::string &str) { + size_t first = str.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) + return ""; + size_t last = str.find_last_not_of(" \t\r\n"); + return str.substr(first, (last - first + 1)); +} + +// Helper function to replace spaces with underscores +static std::string replace_spaces_with_underscores(const std::string &str) { + std::string result = str; + std::replace(result.begin(), result.end(), ' ', '_'); + return result; +} + +// Helper function to ensure prefix ends with ": " +static std::string normalize_prefix(const std::string &prefix) { + if (prefix.empty()) { + return prefix; + } + + std::string result = trim(prefix); + + // Replace any spaces with underscores + result = replace_spaces_with_underscores(result); + + // Check if it ends with colon and space + if (result.size() >= 2 && result.substr(result.size() - 2) == ": ") { + return result; + } + + // Check if it ends with just a colon + if (!result.empty() && result.back() == ':') { + return result + " "; + } + + // Otherwise add the colon and space + return result + ": "; +} + SyslogComponent::SyslogComponent() { this->settings_.client_id = App.get_name(); - // Get the WifiUDP client here instead of getting it in setup() to make sure we always have a client when calling log() - // Calling log() without the device connected should not be an issue since there is a wifi connected check and WifiUDP fails "silently" and doesn't generate an exception anyways - this->udp_ = new WiFiUDP(); + this->filter_include_mode = false; // Default to exclude mode + this->strip_colors = true; + this->enable_logger = true; + this->enable_direct_logs = true; // Enable direct logging by default + this->globally_enabled = true; // Enable component by default + this->filter_string = ""; // Initialize empty filter string + this->direct_log_prefix = ""; // Initialize empty direct log prefix + this->logger_log_prefix = ""; // Initialize empty logger log prefix } void SyslogComponent::setup() { - this->log(ESPHOME_LOG_LEVEL_INFO , "syslog", "Syslog started"); - ESP_LOGI(TAG, "Started"); + // If component is globally disabled, don't set up the socket + if (!this->globally_enabled) { + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, "Syslog component is disabled, skipping setup", LogSource::INTERNAL); + return; + } + // Close existing socket if it exists + if (this->socket_) { + this->socket_.reset(); + } + + // Set up the server address structure + this->server_socklen = 0; + + // Use the version-appropriate method for socket address setup + if (ESPHOME_VERSION_CODE >= VERSION_CODE(2024, 8, 0)) { + // Use the new method for ESPHome 2024.8.0 and later + this->server_socklen = socket::set_sockaddr((struct sockaddr *)&this->server, sizeof(this->server), + this->settings_.address, this->settings_.port); + } +#if USE_NETWORK_IPV6 + else if (this->settings_.address.find(':') != std::string::npos) { + // IPv6 address handling for older ESPHome versions + auto *server6 = reinterpret_cast(&this->server); + memset(server6, 0, sizeof(*server6)); + server6->sin6_family = AF_INET6; + server6->sin6_port = htons(this->settings_.port); + + ip6_addr_t ip6; + inet6_aton(this->settings_.address.c_str(), &ip6); + memcpy(server6->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); + this->server_socklen = sizeof(*server6); + } +#endif /* USE_NETWORK_IPV6 */ + else { + // IPv4 address handling for older ESPHome versions + auto *server4 = reinterpret_cast(&this->server); + memset(server4, 0, sizeof(*server4)); + server4->sin_family = AF_INET; + server4->sin_addr.s_addr = inet_addr(this->settings_.address.c_str()); + server4->sin_port = htons(this->settings_.port); + this->server_socklen = sizeof(*server4); + } + + // Check if we successfully set up the server address + if (!this->server_socklen) { + this->log(ESPHOME_LOG_LEVEL_ERROR, TAG, + "Failed to parse server IP address '" + this->settings_.address + "'", + LogSource::INTERNAL); + this->mark_failed(); + return; + } + + // Create UDP socket + this->socket_ = socket::socket(this->server.ss_family, SOCK_DGRAM, IPPROTO_UDP); + if (!this->socket_) { + this->log(ESPHOME_LOG_LEVEL_ERROR, TAG, "Failed to create UDP socket", LogSource::INTERNAL); + this->mark_failed(); + return; + } + + // Log successful startup + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, "------------------------ Syslog started ------------------------", LogSource::INTERNAL); + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, + "Started with server: " + this->settings_.address + " -> " + std::to_string(this->settings_.port), + LogSource::INTERNAL); + + // Set up logger callback if logger is available #ifdef USE_LOGGER - if (logger::global_logger != nullptr) { + if (logger::global_logger != nullptr && this->enable_logger) { logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { - if(!this->enable_logger || (level > this->settings_.min_log_level)) return; - if(this->strip_colors) { //Strips the "033[0;xxx" at the beginning and the "#033[0m" at the end of log messages + // Skip if component is disabled or level is filtered + if (!this->globally_enabled || (level > this->settings_.min_log_level)) + return; + + // Check if tag is filtered + std::string tag_str(tag); + if (!this->should_send_log(tag_str)) { + return; + } + + // Forward the log message, stripping color codes if configured + if (this->strip_colors) { + // Strip the ESPHome color codes: + // 033[0;xxx at beginning and 033[0m at end std::string org_msg(message); - this->log(level, tag, org_msg.substr(7, org_msg.size() -7 -4)); + if (org_msg.size() > 11) { // Ensure message is long enough to have color codes + this->log(level, tag, org_msg.substr(7, org_msg.size() - 7 - 4), LogSource::LOGGER); + } else { + // Message too short to have color codes, send as is + this->log(level, tag, message, LogSource::LOGGER); + } } else { - this->log(level, tag, message); + this->log(level, tag, message, LogSource::LOGGER); } }); } @@ -48,27 +181,306 @@ void SyslogComponent::setup() { } void SyslogComponent::loop() { + // Currently nothing to do in loop +} + +void SyslogComponent::set_server_ip(const std::string &address) { + if (this->settings_.address != address) { + // Store the new address + std::string old_address = this->settings_.address; + this->settings_.address = address; + + // Only attempt to recreate the socket if we're already set up + if (this->globally_enabled && this->is_setup()) { + // Recreate the socket with new address + this->setup(); + + // Log the change + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, + "Syslog server IP updated: " + old_address + " -> " + address, + LogSource::INTERNAL); + } + } +} + +void SyslogComponent::set_server_port(uint16_t port) { + if (this->settings_.port != port) { + // Store the old port for logging + uint16_t old_port = this->settings_.port; + this->settings_.port = port; + + // Only attempt to recreate the socket if we're already set up + if (this->globally_enabled && this->is_setup()) { + // Recreate the socket with new port + this->setup(); + + // Log the change + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, + "Syslog server port updated: " + std::to_string(old_port) + " -> " + + std::to_string(port), + LogSource::INTERNAL); + } + } +} + +void SyslogComponent::set_enable_logger_messages(bool en) { + if (this->enable_logger != en) { + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, + "Logger messages: " + std::string(this->enable_logger ? "enabled" : "disabled") + + " -> " + std::string(en ? "enabled" : "disabled"), + LogSource::INTERNAL); + this->enable_logger = en; + } +} + +void SyslogComponent::set_strip_colors(bool strip_colors) { + if (this->strip_colors != strip_colors) { + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, + "Strip colors: " + std::string(this->strip_colors ? "enabled" : "disabled") + + " -> " + std::string(strip_colors ? "enabled" : "disabled"), + LogSource::INTERNAL); + this->strip_colors = strip_colors; + } +} + +void SyslogComponent::set_enable_direct_logs(bool en) { + if (this->enable_direct_logs != en) { + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, + "Direct logging: " + std::string(this->enable_direct_logs ? "enabled" : "disabled") + + " -> " + std::string(en ? "enabled" : "disabled"), + LogSource::INTERNAL); + this->enable_direct_logs = en; + } +} + +void SyslogComponent::set_globally_enabled(bool en) { + if (this->globally_enabled != en) { + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, + "Syslog component: " + std::string(this->globally_enabled ? "enabled" : "disabled") + + " -> " + std::string(en ? "enabled" : "disabled"), + LogSource::INTERNAL); + + this->globally_enabled = en; + + // If enabling, make sure to set up the socket + if (en) { + this->setup(); + } else { + // If disabling, close any open socket + if (this->socket_) { + this->socket_.reset(); + } + } + } +} + +void SyslogComponent::add_filter(const std::string &tag) { + this->tag_filters.insert(tag); + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, "Added filter for tag: '" + tag + "'", LogSource::INTERNAL); +} + +void SyslogComponent::remove_filter(const std::string &tag) { + this->tag_filters.erase(tag); + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, "Removed filter for tag: '" + tag + "'", LogSource::INTERNAL); +} + +void SyslogComponent::clear_filters() { + this->tag_filters.clear(); + this->filter_string = ""; + + // Update text sensor if available + if (this->filter_string_text_ != nullptr) { + this->filter_string_text_->publish_state(""); + } + + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, "All filters cleared", LogSource::INTERNAL); +} + +void SyslogComponent::set_client_id(const std::string &client_id) { + // Replace spaces with underscores for client_id + this->settings_.client_id = replace_spaces_with_underscores(client_id); +} + +void SyslogComponent::set_direct_log_prefix(const std::string &prefix) { + this->direct_log_prefix = normalize_prefix(prefix); +} + +void SyslogComponent::set_logger_log_prefix(const std::string &prefix) { + this->logger_log_prefix = normalize_prefix(prefix); +} + +void SyslogComponent::set_filter_string(const std::string &filter_string) { + if (this->filter_string != filter_string) { + this->filter_string = filter_string; + + // Clear existing filters + this->tag_filters.clear(); + + // Parse the new filter string (comma-separated list) + if (!filter_string.empty()) { + size_t start = 0; + size_t end = 0; + + while ((end = filter_string.find(',', start)) != std::string::npos) { + std::string item = trim(filter_string.substr(start, end - start)); + + if (!item.empty()) { + this->add_filter(item); + } + + start = end + 1; + } + + // Add the last item + std::string item = trim(filter_string.substr(start)); + + if (!item.empty()) { + this->add_filter(item); + } + } + + // Update text sensor if available + if (this->filter_string_text_ != nullptr) { + this->filter_string_text_->publish_state(this->filter_string); + } + + this->log(ESPHOME_LOG_LEVEL_INFO, TAG, + "Filter string updated: '" + filter_string + "'", + LogSource::INTERNAL); + } +} + +bool SyslogComponent::has_filter(const std::string &tag) const { + return this->tag_filters.find(tag) != this->tag_filters.end(); } -void SyslogComponent::log(uint8_t level, const std::string &tag, const std::string &payload) { - level = level > 7 ? 7 : level; +std::vector SyslogComponent::get_filters() const { + std::vector result; + result.reserve(this->tag_filters.size()); // Optimize by pre-allocating + for (const auto &tag : this->tag_filters) { + result.push_back(tag); + } + return result; +} + +std::string SyslogComponent::extract_component_name(const std::string &tag) { + size_t colon_pos = tag.find(':'); + if (colon_pos != std::string::npos) { + return tag.substr(0, colon_pos); + } + return tag; +} + +bool SyslogComponent::should_send_log(const std::string &tag) { + // Extract component name from tag (before the colon) + std::string component = extract_component_name(tag); + + // Create a lower-case copy of the filter string for case-insensitive "all" check + std::string filter_lower = this->filter_string; + std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(), ::tolower); + + if (this->filter_include_mode) { + // In Include Mode: + // - "all" means include everything. + // - Empty filter means include nothing. + if (filter_lower == "all") { + return true; + } + if (this->filter_string.empty()) { + return false; + } + // Otherwise, include only if the component is in the filter list. + return this->has_filter(component); + } else { + // In Exclude Mode: + // - "all" means exclude everything. + // - Empty filter means include everything. + if (filter_lower == "all") { + return false; + } + if (this->filter_string.empty()) { + return true; + } + // Otherwise, exclude the log if the component is in the filter list. + return !this->has_filter(component); + } +} + +LogSource SyslogComponent::get_message_source(const std::string &tag) const { + // Check if tag starts with direct log prefix (if set) + if (!this->direct_log_prefix.empty() && + tag.substr(0, this->direct_log_prefix.length()) == this->direct_log_prefix) { + return LogSource::DIRECT; + } + + // Check if tag starts with logger log prefix (if set) + if (!this->logger_log_prefix.empty() && + tag.substr(0, this->logger_log_prefix.length()) == this->logger_log_prefix) { + return LogSource::LOGGER; + } + + // If tag is "syslog", it's an internal message + if (tag == "syslog") { + return LogSource::INTERNAL; + } + + // Otherwise assume it's a direct log + return LogSource::DIRECT; +} - // Simple check to make sure that there is connectivity, if not, log the issue and return - if(WiFi.status() != WL_CONNECTED) { - ESP_LOGW(TAG, "Tried to send \"%s\"@\"%s\" with level %d but Wifi isn't connected yet", tag.c_str(), payload.c_str(), level); +void SyslogComponent::log(uint8_t level, const std::string &tag, const std::string &payload, LogSource source) { + // Check if component is enabled + if (!this->globally_enabled || this->is_failed()) { + return; + } + + // For direct log calls, check the enable_direct_logs flag + if (source == LogSource::DIRECT && !this->enable_direct_logs && tag != "syslog") { + return; + } + + // Add this new check for logger messages + if (source == LogSource::LOGGER && !this->enable_logger) { return; } - Syslog syslog( - *this->udp_, - this->settings_.address.c_str(), - this->settings_.port, - this->settings_.client_id.c_str(), - tag.c_str(), - LOG_KERN - ); - if(!syslog.log(esphome_to_syslog_log_levels[level], payload.c_str())) { - ESP_LOGW(TAG, "Tried to send \"%s\"@\"%s\" with level %d but but failed for an unknown reason", tag.c_str(), payload.c_str(), level); + // Ensure level is valid + level = std::min(level, static_cast(7)); + + // Check if socket is available + if (!this->socket_) { + ESP_LOGW(TAG, "Tried to send \"%s\"@\"%s\" with level %d but socket isn't connected", + tag.c_str(), payload.c_str(), level); + return; + } + + // Apply prefixes based on source if configured + std::string modified_tag = tag; + + // Add source prefix if configured + if (source == LogSource::DIRECT && !this->direct_log_prefix.empty()) { + // Only add the prefix if it's not already there (avoids duplication on log actions) + if (modified_tag.substr(0, this->direct_log_prefix.length()) != this->direct_log_prefix) { + modified_tag = this->direct_log_prefix + modified_tag; + } + } else if (source == LogSource::LOGGER && !this->logger_log_prefix.empty()) { + // Only add the prefix if it's not already there + if (modified_tag.substr(0, this->logger_log_prefix.length()) != this->logger_log_prefix) { + modified_tag = this->logger_log_prefix + modified_tag; + } + } + + // Format according to syslog protocol + int pri = esphome_to_syslog_log_levels[level]; + std::string buf = str_sprintf("<%d>1 - %s %s - - - \xEF\xBB\xBF%s", + pri, this->settings_.client_id.c_str(), + modified_tag.c_str(), payload.c_str()); + + // Send the message + if (this->socket_->sendto(buf.c_str(), buf.length(), 0, + (struct sockaddr *)&this->server, this->server_socklen) < 0) { + ESP_LOGW(TAG, "Failed to send syslog message: \"%s\"@\"%s\"", + modified_tag.c_str(), payload.c_str()); } } diff --git a/components/syslog/syslog_component.h b/components/syslog/syslog_component.h index b1cd173..d2b34e9 100644 --- a/components/syslog/syslog_component.h +++ b/components/syslog/syslog_component.h @@ -1,3 +1,5 @@ +// components/syslog/syslog_component.h + #pragma once #ifndef SYSLOG_COMPONENT_H_0504CB6C_15D8_4AB4_A04C_8AF9063B737F #define SYSLOG_COMPONENT_H_0504CB6C_15D8_4AB4_A04C_8AF9063B737F @@ -6,30 +8,37 @@ #include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "esphome/core/log.h" -#include -#include - -#if defined ESP8266 || defined ARDUINO_ESP8266_ESP01 - #include -#else - #include -#endif - -#include +#include "esphome/components/socket/socket.h" +#include "esphome/components/text/text.h" +#include +#include namespace esphome { namespace syslog { -struct SYSLOGSettings { - std::string address; - uint16_t port; - std::string client_id; - int min_log_level; +/** + * @brief Defines the source of a log message + */ +enum class LogSource { + LOGGER, // Messages from the ESPHome logger system + DIRECT, // Messages from direct API calls (syslog.log action or lambda) + INTERNAL // Messages from the syslog component itself }; -//class UDP; +/** + * @brief Settings structure for Syslog configuration + */ +struct SyslogSettings { + std::string address; // IP address of the syslog server + uint16_t port; // Port of the syslog server + std::string client_id; // Client identifier to include in syslog messages + int min_log_level; // Minimum log level to forward +}; -class SyslogComponent : public Component { +/** + * @brief Component for sending logs to a Syslog server + */ +class SyslogComponent : public Component { public: SyslogComponent(); @@ -37,39 +46,166 @@ class SyslogComponent : public Component { void setup() override; void loop() override; + + // Runtime changeable settings + void set_server_ip(const std::string &address); + const std::string &get_server_ip() const { return this->settings_.address; } + + void set_server_port(uint16_t port); + uint16_t get_server_port() const { return this->settings_.port; } + + void set_client_id(const std::string &client_id); + const std::string &get_client_id() const { return this->settings_.client_id; } + + void set_min_log_level(int log_level) { this->settings_.min_log_level = log_level; } + int get_min_log_level() const { return this->settings_.min_log_level; } + + void set_enable_logger_messages(bool en); + bool get_enable_logger_messages() const { return this->enable_logger; } + + void set_strip_colors(bool strip_colors); + bool get_strip_colors() const { return this->strip_colors; } + + // Component state controls + void set_enable_direct_logs(bool en); + bool get_enable_direct_logs() const { return this->enable_direct_logs; } + + void set_globally_enabled(bool en); + bool get_globally_enabled() const { return this->globally_enabled; } + bool is_setup() const { return this->socket_ != nullptr; } + + // Log source prefixing + void set_direct_log_prefix(const std::string &prefix); + const std::string &get_direct_log_prefix() const { return this->direct_log_prefix; } + + void set_logger_log_prefix(const std::string &prefix); + const std::string &get_logger_log_prefix() const { return this->logger_log_prefix; } + + // Filter management + void set_filter_mode(bool include_mode) { this->filter_include_mode = include_mode; } + bool get_filter_mode() const { return this->filter_include_mode; } + + void clear_filters(); + void add_filter(const std::string &tag); + void remove_filter(const std::string &tag); + bool has_filter(const std::string &tag) const; + std::vector get_filters() const; + + // Filter string methods (comma-separated list) + void set_filter_string(const std::string &filter_string); + const std::string &get_filter_string() const { return this->filter_string; } + + // Register a text sensor for the filter string + void register_filter_string_text(text::Text *text) { + this->filter_string_text_ = text; + // Set initial value + if (this->filter_string_text_ != nullptr) { + this->filter_string_text_->publish_state(this->filter_string); + } + } + + // Main logging function + void log(uint8_t level, const std::string &tag, const std::string &payload, LogSource source = LogSource::DIRECT); + LogSource get_message_source(const std::string &tag) const; + + // Helper method to extract component name from the tag + static std::string extract_component_name(const std::string &tag); + + // Method to check if a tag should be filtered + bool should_send_log(const std::string &tag); - void set_server_ip(const std::string &address) { this->settings_.address = address; } - void set_server_port(uint16_t port) { this->settings_.port = port; } - void set_client_id(const std::string &client_id) { this->settings_.client_id = client_id; } - void set_min_log_level(int log_level) { this->settings_.min_log_level = log_level; } - - void set_enable_logger_messages(bool en) { this->enable_logger = en; } - void set_strip_colors(bool strip_colors) { this->strip_colors = strip_colors; } - - void log(uint8_t level, const std::string &tag, const std::string &payload); protected: - bool strip_colors; - bool enable_logger; - SYSLOGSettings settings_; - UDP *udp_ = NULL; + bool strip_colors; // Whether to strip color codes from logger messages + bool enable_logger; // Enable capturing from ESPHome logger + bool enable_direct_logs; // Enable direct API logging calls + bool globally_enabled; // Global on/off switch for the component + bool filter_include_mode; // Filter mode: true=include, false=exclude + std::set tag_filters; // Set of tags to filter + std::string filter_string; // Original comma-separated filter string + text::Text *filter_string_text_ = nullptr; // Text sensor for filter string + SyslogSettings settings_; // Connection settings + std::unique_ptr socket_ = nullptr; // UDP socket + struct sockaddr_storage server; // Server address + socklen_t server_socklen; // Server address length + + // Prefix settings for different log sources + std::string direct_log_prefix; // Prefix for direct logs + std::string logger_log_prefix; // Prefix for logger messages }; +/** + * @brief Action to send a log message + */ template class SyslogLogAction : public Action { public: - SyslogLogAction(SyslogComponent *parent) : parent_(parent) {} + explicit SyslogLogAction(SyslogComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(uint8_t, level) TEMPLATABLE_VALUE(std::string, tag) TEMPLATABLE_VALUE(std::string, payload) void play(Ts... x) override { - this->parent_->log(this->level_.value(x...), this->tag_.value(x...), this->payload_.value(x...)); + this->parent_->log(this->level_.value(x...), this->tag_.value(x...), this->payload_.value(x...), LogSource::DIRECT); } protected: SyslogComponent *parent_; }; +/** + * @brief Action to add a tag filter + */ +template class SyslogAddFilterAction : public Action { +public: + explicit SyslogAddFilterAction(SyslogComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, tag) + void play(Ts... x) override { + this->parent_->add_filter(this->tag_.value(x...)); + } +protected: + SyslogComponent *parent_; +}; + +/** + * @brief Action to remove a tag filter + */ +template class SyslogRemoveFilterAction : public Action { +public: + explicit SyslogRemoveFilterAction(SyslogComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, tag) + void play(Ts... x) override { + this->parent_->remove_filter(this->tag_.value(x...)); + } +protected: + SyslogComponent *parent_; +}; + +/** + * @brief Action to clear all filters + */ +template class SyslogClearFiltersAction : public Action { +public: + explicit SyslogClearFiltersAction(SyslogComponent *parent) : parent_(parent) {} + void play(Ts... x) override { + this->parent_->clear_filters(); + } +protected: + SyslogComponent *parent_; +}; + +/** + * @brief Action to set filter string + */ +template class SyslogSetFilterStringAction : public Action { +public: + explicit SyslogSetFilterStringAction(SyslogComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, filter_string) + void play(Ts... x) override { + this->parent_->set_filter_string(this->filter_string_.value(x...)); + } +protected: + SyslogComponent *parent_; +}; + } // namespace syslog } // namespace esphome - #endif