v0.1.1: read server config from TOML file
7 files changed, 297 insertions(+), 86 deletions(-)

M .hgignore
M README.md
A => aiosyslogd/config.py
M aiosyslogd/server.py
A => config/default-aiosyslogd.toml
M poetry.lock
M pyproject.toml
M .hgignore +1 -0
@@ 1,3 1,4 @@ 
 .mypy_cache
+aiosyslogd.toml
 syslog.db
 syslog.db-*

          
M README.md +160 -68
@@ 1,106 1,198 @@ 
-# **aioSyslog Server**
+# aiosyslogd
 
-**aioSyslog Server** is a high-performance, asynchronous Syslog server built with Python's asyncio. It is designed for efficiently receiving, parsing, and storing a large volume of syslog messages.
+**aiosyslogd** is a high-performance, asynchronous Syslog server built with Python's asyncio. It is designed for efficiently receiving, parsing, and storing a large volume of syslog messages.
 
 It features an optional integration with uvloop for a significant performance boost and can write messages to a SQLite database, automatically creating monthly tables and maintaining a Full-Text Search (FTS) index for fast queries.
 
-## **Key Features**
+## Key Features
 
-* **Asynchronous:** Built on asyncio to handle thousands of concurrent messages with minimal overhead.  
-* **Fast:** Supports uvloop for a C-based event loop implementation, making it one of the fastest ways to run asyncio.  
-* **SQLite Backend:** Optionally writes all incoming messages to a SQLite database.  
-* **Automatic Table Management:** Creates new tables for each month (SystemEventsYYYYMM) to keep the database organized and fast.  
-* **Full-Text Search:** Automatically maintains an FTS5 virtual table for powerful and fast message searching.  
-* **RFC5424** to RFC3164 **Conversion:** Includes a utility to convert modern RFC5424 formatted messages to the older, more widely compatible RFC3164 format.  
-* **Easy to Deploy:** Can be run directly from the command line with simple environment variable configuration.  
-* **Extensible:** Can be used as a library in your own Python applications.
+* **Asynchronous:** Built on asyncio to handle thousands of concurrent messages with minimal overhead.
+* **Fast:** Supports uvloop for a C-based event loop implementation, making it one of the fastest ways to run asyncio.
+* **SQLite Backend:** Optionally writes all incoming messages to a SQLite database.
+* **Automatic Table Management:** Creates new tables for each month (`SystemEventsYYYYMM`) to keep the database organized and fast.
+* **Full-Text Search:** Automatically maintains an `FTS5` virtual table for powerful and fast message searching.
+* **RFC5424 Conversion:** Includes a utility to convert older *RFC3164* formatted messages to the modern *RFC5424* format.
+* **Flexible Configuration:** Configure the server via a simple `aiosyslogd.toml` file.
 
-## **Installation**
+## Installation
 
-You can install the package directly from PyPI.
+You can install the package directly from its source repository or via pip.
 
 **Standard Installation:**
 
-pip install aiosyslog-server
+```console
+$ pip install aiosyslogd
+```
+
+**For Maximum Performance (with uvloop/winloop):**
 
-**For Maximum Performance (with uvloop):**
+To include the performance enhancements, install the speed extra:
+
+```console
+$ pip install 'aiosyslogd[speed]'
+```
+
+## Quick Start: Running the Server
 
-To include the uvloop performance enhancements, install it as an extra:
+The package installs a command-line script called aiosyslogd. You can run it directly from your terminal.
+
+```console
+$ aiosyslogd
+```
+
+On the first run, if an `aiosyslogd.toml` file is not found in the current directory, the server will create one with default settings and then start.
 
-pip install 'aiosyslog-server\[uvloop\]'
+The server will begin listening on 0.0.0.0:5140 and, if enabled in the configuration, create a syslog.db file in the current directory.
+
+## Configuration
 
-## **Quick Start: Running the Server**
+The server is configured using a TOML file. By default, it looks for `aiosyslogd.toml` in the current working directory.
+
+#### Default aiosyslogd.toml
 
-The package installs a command-line script called aiosyslog-server. You can run it directly from your terminal.
+If a configuration file is not found, this default version will be created:
+
+```toml
+[server]
+bind\_ip = "0.0.0.0"
+bind\_port = 5140
+debug = false
+log_dump = false
 
-\# Enable SQLite writing and run the server  
-export SQL\_WRITE=True  
-aiosyslog-server
+[sqlite]
+enabled = true
+database = "syslog.db"
+batch_size = 1000
+batch_timeout = 5
+sql_dump = false
+```
+
+#### Custom Configuration Path
 
-The server will start listening on 0.0.0.0:5140 and create a syslog.db file in the current directory.
+You can specify a custom path for the configuration file by setting the `AIOSYSLOGD_CONFIG` environment variable.
 
-## **Configuration**
+```console
+export AIOSYSLOGD_CONFIG="/etc/aiosyslogd/config.toml"
+$ aiosyslogd
+```
+
+When a custom path is provided, the server will **not** create a default file if it's missing and will exit with an error instead.
+
+### Configuration Options
 
-The server is configured via environment variables:
+| Section | Key | Description | Default |
+| :---- | :---- | :---- | :---- |
+| server | bind\_ip | The IP address the server should bind to. | "0.0.0.0" |
+| server | bind\_port | The UDP port to listen on. | 5140 |
+| server | debug | Set to true to enable verbose logging for parsing and database errors. | false |
+| server | log\_dump | Set to true to print every received message to the console. | false |
+| sqlite | enabled | Set to true to enable writing to the SQLite database. | true |
+| sqlite | database | The path to the SQLite database file. | "syslog.db" |
+| sqlite | batch\_size | The number of messages to batch together before writing to the database. | 1000 |
+| sqlite | batch\_timeout | The maximum time in seconds to wait before writing an incomplete batch. | 5 |
+| sqlite | sql\_dump | Set to true to print the SQL command and parameters before execution. | false |
+
+## Integrating with rsyslog
+
+You can use **rsyslog** as a robust, battle-tested frontend for **aiosyslogd**. This is useful for receiving logs on the standard privileged port (514) and then forwarding them to **aiosyslogd** running as a non-privileged user on a different port.
+
+Here are two common configurations:
 
-| Variable | Description | Default |
-| :---- | :---- | :---- |
-| SQL\_WRITE | Set to True to enable writing to the syslog.db SQLite database. | False |
-| BINDING\_IP | The IP address to bind the server to. | 0.0.0.0 |
-| BINDING\_PORT | The UDP port to listen on. | 5140 |
-| DEBUG | Set to True to enable verbose logging for parsing and database errors. | False |
-| LOG\_DUMP | Set to True to print every received message to the console. | False |
-| SQL\_DUMP | Set to True to print the SQL command and parameters before execution. | False |
+### 1\. Forwarding from an Existing rsyslog Instance
+
+If you already have an **rsyslog** server running and simply want to forward all logs to **aiosyslogd**, add the following lines to a new file in `/etc/rsyslog.d/`, such as `99-forward-to-aiosyslogd.conf`. This configuration includes queueing to prevent log loss if **aiosyslogd** is temporarily unavailable.
+
+**File: /etc/rsyslog.d/rsyslog-forward.conf**
+
+```
+# This forwards all logs (*) to the server running on localhost:5140
+# with queueing enabled for reliability.
+$ActionQueueFileName fwdRule1
+$ActionQueueMaxDiskSpace 1g
+$ActionQueueSaveOnShutdown on
+$ActionQueueType LinkedList
+$ActionResumeRetryCount -1
+*.* @127.0.0.1:5140
+```
+
+### 2\. Using rsyslog as a Dedicated Forwarder
 
-### **Example rsyslog Configuration**
+If you want rsyslog to listen on the standard syslog port 514/udp and do nothing but forward to aiosyslogd, you can use a minimal configuration like this. This is a common pattern for privilege separation, allowing aiosyslogd to run as a non-root user.
+
+**File: /etc/rsyslog.conf (Minimal Example)**
+
+```
+# Minimal rsyslog.conf to listen on port 514 and forward to aiosyslogd
 
-To forward logs from rsyslog to this server, you can add the following to your rsyslog.conf or as a new file in /etc/rsyslog.d/.
+# --- Global Settings ---
+$WorkDirectory /var/lib/rsyslog
+$FileOwner root
+$FileGroup adm
+$FileCreateMode 0640
+$DirCreateMode 0755
+$Umask 0022
 
-**File: /etc/rsyslog.d/99-forward-to-aiosyslog.conf**
+# --- Modules ---
+# Unload modules we don't need
+module(load="immark" mode="off")
+module(load="imuxsock" mode="off")
+# Load the UDP input module
+module(load="imudp")
+input(
+    type="imudp"
+    port="514"
+)
 
-\# This forwards all logs (\*) to the server running on localhost:5140  
-\*.\* @127.0.0.1:5140
+# --- Forwarding Rule ---
+# Forward all received messages to aiosyslogd
+$ActionQueueFileName fwdToAiosyslogd
+$ActionQueueMaxDiskSpace 1g
+$ActionQueueSaveOnShutdown on
+$ActionQueueType LinkedList
+$ActionResumeRetryCount -1
+*.* @127.0.0.1:5140
+```
 
-## **Using as a Library**
+## Using as a Library
 
 You can also import and use the SyslogUDPServer in your own asyncio application.
 
-import asyncio  
-from aiosyslog import SyslogUDPServer
+```python
+import asyncio
+from aiosyslogd.server import SyslogUDPServer
 
-async def main():  
-    \# The server is configured programmatically or via environment variables  
-    \# For library use, it's better to pass config directly.  
-    \# This example still relies on env vars for simplicity.  
-    server \= SyslogUDPServer(host="0.0.0.0", port=5141)
+async def main():
+    # The server is configured via aiosyslogd.toml by default.
+    # To configure programmatically, you would need to modify the
+    # server class or bypass the config-loading mechanism.
+    server = await SyslogUDPServer.create(host="0.0.0.0", port=5141)
 
-    loop \= asyncio.get\_running\_loop()  
-      
-    \# Start the UDP server endpoint  
-    transport, protocol \= await loop.create\_datagram\_endpoint(  
-        lambda: server,  
-        local\_addr=(server.host, server.port)  
+    loop = asyncio.get_running_loop()
+
+    # Start the UDP server endpoint
+    transport, protocol = await loop.create_datagram_endpoint(
+        lambda: server,
+        local_addr=(server.host, server.port)
     )
 
-    print("Custom server running. Press Ctrl+C to stop.")  
-    try:  
-        await asyncio.Event().wait()  
-    except (KeyboardInterrupt, asyncio.CancelledError):  
-        pass  
-    finally:  
-        print("Shutting down custom server.")  
-        transport.close()  
+    print("Custom server running. Press Ctrl+C to stop.")
+    try:
+        await asyncio.Event().wait()
+    except (KeyboardInterrupt, asyncio.CancelledError):
+        pass
+    finally:
+        print("Shutting down custom server.")
+        transport.close()
         await server.shutdown()
 
-if \_\_name\_\_ \== "\_\_main\_\_":  
-    \# Remember to set SQL\_WRITE=True if you want to save to DB  
-    \# os.environ\['SQL\_WRITE'\] \= 'True'  
+if __name__ == "__main__":
     asyncio.run(main())
+```
+
+## Contributing
 
-## **Contributing**
-
-Contributions are welcome\! If you find a bug or have a feature request, please open an issue on the GitHub repository.
+Contributions are welcome\! If you find a bug or have a feature request, please open an issue on the project's repository.
 
-## **License**
+## License
 
-This project is licensed under the **MIT License**. See the LICENSE file for details.
  No newline at end of file
+This project is licensed under the **MIT License**.

          
A => aiosyslogd/config.py +76 -0
@@ 0,0 1,76 @@ 
+# aiosyslogd/config.py
+# -*- coding: utf-8 -*-
+import os
+import toml
+from typing import Any, Dict
+
+# --- Default Configuration ---
+DEFAULT_CONFIG: Dict[str, Any] = {
+    "server": {
+        "bind_ip": "0.0.0.0",
+        "bind_port": 5140,
+        "debug": False,
+        "log_dump": False,
+    },
+    "sqlite": {
+        "enabled": True,
+        "database": "syslog.db",
+        "batch_size": 1000,
+        "batch_timeout": 5,
+        "sql_dump": False,
+    },
+}
+
+DEFAULT_CONFIG_FILENAME = "aiosyslogd.toml"
+
+
+def _create_default_config(path: str) -> Dict[str, Any]:
+    """Creates the default aiosyslogd.toml file at the given path."""
+    print(f"Configuration file not found. Creating a default '{path}'...")
+    with open(path, "w") as f:
+        toml.dump(DEFAULT_CONFIG, f)
+    print(
+        f"Default configuration file created. Please review '{path}' and restart the server if needed."
+    )
+    return DEFAULT_CONFIG
+
+
+def load_config() -> Dict[str, Any]:
+    """
+    Loads configuration from a TOML file.
+
+    It first checks for the 'AIOSYSLOGD_CONFIG' environment variable for a custom path.
+    If the variable is not set, it falls back to 'aiosyslogd.toml' in the current directory.
+
+    - If a custom path is specified and the file doesn't exist, the server will exit with an error.
+    - If the default file ('aiosyslogd.toml') doesn't exist, it will be created automatically.
+    """
+    config_path_from_env: str | None = os.environ.get("AIOSYSLOGD_CONFIG")
+
+    if config_path_from_env:
+        config_path: str = config_path_from_env
+        is_custom_path: bool = True
+    else:
+        config_path = DEFAULT_CONFIG_FILENAME
+        is_custom_path = False
+
+    print(f"Attempting to load configuration from: {config_path}")
+
+    try:
+        with open(config_path, "r") as f:
+            return toml.load(f)
+    except FileNotFoundError:
+        if is_custom_path:
+            # If a custom path was provided and it doesn't exist, it's an error.
+            print(
+                f"Error: Configuration file not found at the specified path: {config_path}"
+            )
+            raise SystemExit(
+                "Aborting: Could not find the specified configuration file."
+            )
+        else:
+            # If the default file is not found, create it.
+            return _create_default_config(config_path)
+    except toml.TomlDecodeError as e:
+        print(f"Error decoding TOML file {config_path}: {e}")
+        raise SystemExit("Aborting due to invalid configuration file.")

          
M aiosyslogd/server.py +19 -15
@@ 2,6 2,7 @@ 
 # -*- coding: utf-8 -*-
 ## Syslog Server in Python with asyncio and SQLite.
 
+from . import config
 from .priority import SyslogMatrix
 from .rfc5424 import RFC5424_PATTERN
 from .rfc5424 import normalize_to_rfc5424

          
@@ 10,7 11,6 @@ from types import ModuleType
 from typing import Dict, Any, Tuple, List, Type, Self
 import aiosqlite
 import asyncio
-import os
 import re
 import signal
 

          
@@ 21,17 21,22 @@ try:
 except ImportError:
     pass
 
+# --- Configuration ---
+# Load configuration from aiosyslogd.toml
+CFG = config.load_config()
 
-# --- Configuration ---
-# Environment variables are now read inside the functions that use them.
-DEBUG: bool = os.environ.get("DEBUG") == "True"
-LOG_DUMP: bool = os.environ.get("LOG_DUMP") == "True"
-SQL_DUMP: bool = os.environ.get("SQL_DUMP") == "True"
-SQL_WRITE: bool = os.environ.get("SQL_WRITE") == "True"
-BINDING_IP: str = os.environ.get("BINDING_IP", "0.0.0.0")
-BINDING_PORT: int = int(os.environ.get("BINDING_PORT", "5140"))
-BATCH_SIZE: int = int(os.environ.get("BATCH_SIZE", "1000"))
-BATCH_TIMEOUT: int = int(os.environ.get("BATCH_TIMEOUT", "5"))
+# Server settings
+DEBUG: bool = CFG.get("server", {}).get("debug", False)
+LOG_DUMP: bool = CFG.get("server", {}).get("log_dump", False)
+BINDING_IP: str = CFG.get("server", {}).get("bind_ip", "0.0.0.0")
+BINDING_PORT: int = int(CFG.get("server", {}).get("bind_port", 5140))
+
+# SQLite settings
+SQL_WRITE: bool = CFG.get("sqlite", {}).get("enabled", False)
+SQL_DUMP: bool = CFG.get("sqlite", {}).get("sql_dump", False)
+SQLITE_DB_PATH: str = CFG.get("sqlite", {}).get("database", "syslog.db")
+BATCH_SIZE: int = int(CFG.get("sqlite", {}).get("batch_size", 1000))
+BATCH_TIMEOUT: int = int(CFG.get("sqlite", {}).get("batch_timeout", 5))
 
 
 class SyslogUDPServer(asyncio.DatagramProtocol):

          
@@ 59,7 64,7 @@ class SyslogUDPServer(asyncio.DatagramPr
         print(f"aiosyslogd starting on UDP {host}:{port}...")
         if SQL_WRITE:
             print(
-                f"SQLite writing ENABLED. Batch size: {BATCH_SIZE}, Timeout: {BATCH_TIMEOUT}s"
+                f"SQLite writing ENABLED to '{SQLITE_DB_PATH}'. Batch size: {BATCH_SIZE}, Timeout: {BATCH_TIMEOUT}s"
             )
             await server.connect_to_sqlite()
         if DEBUG:

          
@@ 217,12 222,11 @@ class SyslogUDPServer(asyncio.DatagramPr
 
     async def connect_to_sqlite(self) -> None:
         """Initializes the database connection."""
-        # No `async with` here as we want to keep the connection open.
-        self.db = await aiosqlite.connect("syslog.db")
+        self.db = await aiosqlite.connect(SQLITE_DB_PATH)
         await self.db.execute("PRAGMA journal_mode=WAL")
         await self.db.execute("PRAGMA auto_vacuum = FULL")
         await self.db.commit()
-        print("SQLite database connected.")
+        print(f"SQLite database '{SQLITE_DB_PATH}' connected.")
 
     async def create_monthly_table(self, year_month: str) -> str:
         """Creates tables for the given month if they don't exist."""

          
A => config/default-aiosyslogd.toml +12 -0
@@ 0,0 1,12 @@ 
+[server]
+bind_ip = "0.0.0.0"
+bind_port = 5140
+debug = false
+log_dump = false
+
+[sqlite]
+enabled = true
+database = "syslog.db"
+batch_size = 1000
+batch_timeout = 5
+sql_dump = false

          
M poetry.lock +25 -1
@@ 383,6 383,30 @@ pytest = ">=4.6"
 testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
 
 [[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+groups = ["main"]
+files = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+
+[[package]]
+name = "types-toml"
+version = "0.10.8.20240310"
+description = "Typing stubs for toml"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+    {file = "types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331"},
+    {file = "types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d"},
+]
+
+[[package]]
 name = "typing-extensions"
 version = "4.14.0"
 description = "Backported and Experimental Type Hints for Python 3.9+"

          
@@ 476,4 500,4 @@ speed = ["uvloop", "winloop"]
 [metadata]
 lock-version = "2.1"
 python-versions = ">=3.11"
-content-hash = "20167cd8fc16aa8ba19c714e451053ef6db7518f589746233f2648c4c09ac13c"
+content-hash = "1bb00996bd91d81d231b0dcf778c6eb7053c9f9b833ae173e5e34cdbd4df7672"

          
M pyproject.toml +4 -2
@@ 1,6 1,6 @@ 
 [project]
 name = "aiosyslogd"
-version = "0.1.0"
+version = "0.1.1"
 description = "Asynchronous Syslog server using asyncio, with an optional uvloop integration and SQLite backend."
 authors = [
     {name = "Chaiwat Suttipongsakul",email = "cwt@bashell.com"}

          
@@ 9,7 9,8 @@ license = {text = "MIT"}
 readme = "README.md"
 requires-python = ">=3.11"
 dependencies = [
-    "aiosqlite (>=0.21.0)"
+    "aiosqlite (>=0.21.0)",
+    "toml (>=0.10.2)"
 ]
 
 [project.optional-dependencies]

          
@@ 31,6 32,7 @@ black = "^25.1.0"
 pytest = "^8.3.0"
 pytest-asyncio = "^0.26.0"
 pytest-cov = "^6.1.0"
+types-toml = "^0.10.8"
 
 [tool.pytest.ini_options]
 asyncio_default_fixture_loop_scope = "function"