{"id":37066881,"url":"https://github.com/fractalwillie/chronologix","last_synced_at":"2026-01-14T07:51:01.300Z","repository":{"id":291685316,"uuid":"978444355","full_name":"fractalwillie/chronologix","owner":"fractalwillie","description":"Asynchronous, modular Python logger with time-based rollover, level filtering, multi-sink output, and more.","archived":false,"fork":false,"pushed_at":"2025-05-22T22:19:29.000Z","size":86,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-05T06:34:25.995Z","etag":null,"topics":["async","asyncio","logging","logging-library","python"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/chronologix/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fractalwillie.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-06T02:07:15.000Z","updated_at":"2025-05-22T22:19:27.000Z","dependencies_parsed_at":"2025-05-06T02:49:30.619Z","dependency_job_id":null,"html_url":"https://github.com/fractalwillie/chronologix","commit_stats":null,"previous_names":["fractalwillie/chronologix"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/fractalwillie/chronologix","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractalwillie%2Fchronologix","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractalwillie%2Fchronologix/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractalwillie%2Fchronologix/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractalwillie%2Fchronologix/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fractalwillie","download_url":"https://codeload.github.com/fractalwillie/chronologix/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractalwillie%2Fchronologix/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28413510,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T05:26:33.345Z","status":"ssl_error","status_checked_at":"2026-01-14T05:21:57.251Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","asyncio","logging","logging-library","python"],"created_at":"2026-01-14T07:51:00.412Z","updated_at":"2026-01-14T07:51:01.287Z","avatar_url":"https://github.com/fractalwillie.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Chronologix\n\nChronologix is a fully asynchronous, modular logging system for Python.\n\nIt writes structured log files across multiple named sinks, supports time-based chunking, and avoids the standard logging module completely.\n\n---\n\n## Features\n\n-  Fully async logging \n-  Time-based rollover (e.g. every `24h`, `1h`, `15m`)\n-  Multiple independent log sinks with custom filters\n-  Optional mirror sink that records everything above set threshold\n-  Log level filtering per sink (`DEBUG`, `ERROR`, etc.)\n-  Buffered file I/O with async batching and graceful failure handling\n-  Config validation with clear error feedback\n-  Custom log paths via `str` or `pathlib.Path`\n-  Predictable file and folder structure for automated processing\n-  Optional terminal output (stdout/stderr) with level filtering\n-  Optional time-based log deletion (retain policy)\n-  Optional output format control (`text` or `json`) per sink/mirror\n-  Optional automatic log compression (`zip` or `tar.gz`) on every rollover\n-  Optional async log hooks for custom alerts, event tracking, or anything else you need\n-  Optional timezone alignment for chunk folders and timestamps\n-  Custom core, no dependance on Python's logging module, no global state\n\n---\n\n## Installation\n\nChronologix requires **Python 3.7+**.\n```bash\npip install chronologix\n```\n\n---\n\n## Usage example\n\n```python\nimport asyncio\nfrom chronologix import LogConfig, LogManager\n\nconfig = LogConfig(\n    base_log_dir=\"my_logs\",\n    interval=\"1h\",  # rollover every hour\n    sinks={\n        \"app\": {\"file\": \"app.json\", \"min_level\": \"INFO\", \"format\": \"json\"}, # logs INFO and above into app.json file\n        \"errors\": {\"file\": \"errors.json\", \"min_level\": \"ERROR\", \"format\": \"json\"}, # logs ERROR and above into errors.json file\n    },\n    mirror={\n        \"file\": \"audit.json\",  # captures all messages regardless of sink\n        \"min_level\": \"NOTSET\", # optional: \"min_level\": \"NOTSET\" defaults to \"NOTSET\" if not specified\n        \"format\": \"json\" # optional: \"format\": \"json\" defaults to \"text\" if not specified\n    },\n    cli_echo={\n        \"enabled\": True,  # print all logs to terminal (stdout)\n        # optional: \"min_level\": \"INFO\" defaults to NOTSET if not specified\n    },\n    timestamp_format=\"%H:%M:%S.%f\",\n    retain=\"1h\", # deletes log folders older than 1 hour\n    compression={\n        \"enabled\": True,\n        \"compress_format\": \"tar.gz\"  # optional, defaults to \"zip\" if omitted\n    },\n    hooks={\n        \"handlers\": [\n            {\"func\": some_hook_function, \"min_level\": \"CRITICAL\"}\n        ]\n    },\n    timezone=\"Europe/Prague\"\n)\n\nlogger = LogManager(config)\n\n# async log hook function (can be anything, database insertion, chat message etc.)\nasync def some_hook_function(log):\n    print(f\"[Hook] Critical Error detected:\" log[\"timestamp\"] - log[\"level\"] - log[\"message\"])\n\n\nasync def divide(a, b):\n    try:\n        result = a / b\n        await logger.log(f\"Division result: {result}\", level=\"INFO\")  # level passed as argument, goes to app + mirror\n    except Exception as e:\n        await logger.error(f\"Exception occurred: {e}\")  # wrapper method - .error normalized to ERROR min_level, goes to errors + app + mirror\n\n# showcase of several different methods of logging\nasync def main():\n    await logger.start() # needs to be called before any logging happens\n    await logger.log(\"Some NOTSET level msg\")  # defaults to NOTSET, goes to mirror only\n    await logger.debug(\"Some DEBUG level msg\")  # goes to mirror only (app min_level = INFO)\n    await logger.info(\"Some INFO level msg\")  # app + mirror\n    await logger.warning(\"Some WARNING level msg\")  # app + mirror\n    await logger.error(\"Some ERROR level msg\")  # errors + app + mirror\n    await logger.CRITICAL(\"Some CRITICAL level msg\")  # errors + app + mirror (upper/lowercase doesn't matter, they're normalized before processing)\n    await divide(10, 0)  # triggers zero division error → errors + mirror\n    await logger.stop()\n\n\n```\nThis example will produce following:\n- Two new folder per hour like \"2025-05-04__14-00/\" and \"2025-05-04__15-00/\" inside my_logs/\n- Three log files inside each: app.json (INFO and above), errors.json (ERROR and above), audit.json (NOTSET)\n- All logs formatted as json objects - {\"timestamp\": \"14:02:19:287248\", \"level\": \"INFO\", \"message\": \"Some msg\"}\n- The exception will be logged to both sinks and mirror.\n- Messages without level (like \"Some NOTSET level msg\") will be treated as NOTSET and only land in sinks that accept that level (here: audit.json mirror file).\n- Level filtering and routing is automatic. You don’t specify a target sink, only a level (or nothing).\n- All logs reflected in terminal through stdout.\n- All subfolders inside my_logs/ are parsed on every rollover. Those older than 1 hour are deleted.\n- The subfolder from the previous interval will be compressed into .tar.gz before cleanup on every rollover\n- Compressed archives are saved next to their original folder (e.g., 2025-05-04__14-00.zip)\n- `some_hook_function(log)` prints the critical error based on the `min_level` set in hooks config\n- Folder rollover times, names, and timestamps in logs reflect Europe/Prague timezone \n\n---\n\n## Path structure\n\nYou can set the log output folder using either a string path or a `pathlib.Path` object.\n\nExamples:\n```python\nLogConfig(base_log_dir=\"logs\")  # relative to current working dir\nLogConfig(base_log_dir=\"/var/log/chronologix\")  # absolute path (Linux)\nLogConfig(base_log_dir=Path(\"~/.chronologix\").expanduser())  # user home dir\n```\nChronologix will create any missing folders automatically.\n\n---\n\n## Intervals\n\nThe `interval` controls how frequently Chronologix creates a new folder and rotates the log files.\n\nSupported values:\n- `\"24h\"`\n- `\"12h\"`\n- `\"6h\"`\n- `\"3h\"`\n- `\"1h\"`\n- `\"30m\"`\n- `\"15m\"`\n- `\"5m\"`\n\nEach interval corresponds to a different granularity of time-based chunking:\n- `interval=\"24h\"` → folders like `2025-05-04/` → `2025-05-05/`\n- `interval=\"1h\"` → folders like `2025-05-04__14-00/` → `2025-05-04__15-00/`\n\n---\n\n## Sinks\n\nEach sink is defined by:\n- a `file` name (relative to the chunk folder) and file extension (`.log`, `.txt`, `.json`, `.jsonl`)\n- a `min_level` that controls what gets written (`NOTSET`, `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`)\n- a `format` (`text` or `json`) that controls the output structure. It's optional and defaults to `text` when not included\n\nExample:\n```python\nsinks={\n    \"debug\":  {\"file\": \"debug.log\", \"min_level\": \"NOTSET\"},\n    \"alerts\": {\"file\": \"alerts.log\", \"min_level\": \"CRITICAL\"},\n}\n```\nA single message may be written to multiple sinks if its level qualifies.\nYou can define as many sinks as needed or just a single one.\n\n---\n\n## Mirroring\n\nYou can configure an optional mirror file to capture all logs that match or exceed a threshold:\n```python\nmirror = {\n    \"file\": \"all.log\",\n    \"min_level\": \"DEBUG\"  # optional, defaults to \"NOTSET\"\n    \"format\": \"text\" # optional, defaults to \"text\" if not included, can be set to \"json\" for JSON output format\n}\n```\nThis is useful for debugging, auditing, or fallback catch-all logging.\nThe `mirror` is limited to a single file.\n\n---\n\n## Log Levels\n\nChronologix supports configurable log level thresholds for each sink and a single mirror.\nThis allows you to filter out lower-priority messages from specific log files.\n\n### Hierarchy\n\nLevels are evaluated by their severity:\n```python\nLOG_LEVELS = {\n    \"NOTSET\": 0,\n    \"DEBUG\": 10,\n    \"INFO\": 20,\n    \"WARNING\": 30,\n    \"ERROR\": 40,\n    \"CRITICAL\": 50 \n}\n```\n- You can use `.log(\"msg\", level=\"WARNING\")` or `.warning(\"msg\")`.\n- Levels are automatically routed to all eligible sinks.\n- If no level is given, NOTSET is assumed.\n\nExample:\n```python\nlogger = LogManager(config)\nawait logger.start()\nawait logger.log(\"msg\") # NOTSET\nawait logger.log(\"msg\", level=\"INFO\") # INFO\nawait logger.error(\"msg\") # ERROR\nawait logger.DEBUG(\"msg\") # DEBUG\n```\n\n### Using Chronologix without log levels\n\nIf you don’t want log level filtering simply set your sink's `min_level` to `NOTSET`.\n\nExample:\n```python\nsinks={\n    \"logging\":  {\"file\": \"logging.log\", \"min_level\": \"NOTSET\"},\n}\n\nawait logger.log(\"Something happened\") # if no level is provided .log defaults to NOTSET\n```\n\n---\n\n## Async Log Hooks\n\nChronologix supports optional async hooks that run custom code whenever a new log message is processed.\n\nYou can use this to:\n- Trigger alerts (e.g., notify on CRITICAL logs)\n- Forward logs to external services (e.g., HTTP, chatbots)\n- Store logs in a database\n- And much more\n\nExample:\n```python\nasync def telegram_hook(log: dict):\n    time = log[\"timestamp\"]\n    level = log[\"level\"]\n    msg = log[\"message\"]\n    \n    message = f\"{time} - {level} - {msg}\"\n    payload = {\n        \"chat_id\": TG_GROUP_ID,\n        \"text\": message\n    }\n\n    try:\n        async with aiohttp.ClientSession() as session:\n            async with session.post(\n                f\"https://api.telegram.org/bot{TG_BOT}/sendMessage\", json=payload\n            ) as resp:\n                if resp.status != 200:\n                    print(f\"[HOOK] Failed to send message to Telegram: {resp.status}\")\n    except Exception as e:\n        print(f\"[HOOK] Telegram hook exception: {e}\")\n\nconfig = LogConfig(\n    ...\n    hooks={\n        \"handlers\": [telegram_hook]  # you can add multiple functions\n    }\n)\n```\n\nEach handler must be an async function and receives a dict like:\n```python\n{\n    \"timestamp\": \"14:01:32.120013\",\n    \"level\": \"CRITICAL\",\n    \"message\": \"Something bad happened\"\n}\n```\n\nYou can also provide level filtering per hook:\n```python\nhooks={\n    \"handlers\": [\n        {\"func\": telegram_hook, \"min_level\": \"ERROR\"}\n    ]\n}\n```\n\nHooks run in isolation and will never crash your logger. Exceptions are caught and printed to stderr.\n\n---\n\n## Log format \u0026 file extensions\n\nYou can control the output format of each log file individually.\n\nSupported `format`:\n- `\"text\"` (default)\n- `\"json\"`\n\nSupported `file` extensions:\n- `.txt`\n- `.log`\n- `.json`\n- `.jsonl`\n\nTo enable JSON output:\n```python\nsinks={\n    \"debug\": {\"file\": \"debug.json\", \"min_level\": \"DEBUG\", \"format\": \"json\"},\n},\nmirror={\n    \"file\": \"all.json\",\n    \"format\": \"json\"\n}\n```\nEach log message will then be written as a JSON object:\n```json\n{\"timestamp\": \"14:02:19.123456\", \"level\": \"INFO\", \"message\": \"Some INFO level msg\"}\n```\n\n- `format` is optional and defaults to `\"text\"` if not specified\n\n- `file` extension doesn't need to match `format` (e.g., you can have `debug.txt` in JSON `format`)\n\n- `cli_echo` always uses `text` format regardless of `format` settings\n\n---\n\n## Terminal output\n\nChronologix can optionally echo log messages to your terminal.\n\nThis can be useful during development or debugging when you want to see logs in real-time, while still keeping structured log files.\n\nYou can configure this with the `cli_echo` option:\n\n### Simple format\nPrint to stdout only:\n```python\ncli_echo = {\n    \"enabled\": True,\n    \"min_level\": \"INFO\"  # optional, defaults to NOTSET\n}\n```\n\n### Advanced format\nSplit logs between stdout and stderr:\n```python\ncli_echo = {\n    \"stdout\": {\"min_level\": \"INFO\"},     # INFO and WARNING go to stdout\n    \"stderr\": {\"min_level\": \"ERROR\"}     # ERROR and CRITICAL go to stderr\n}\n```\n\n- You can use stdout/stderr individually, or both.\n- `stderr` takes precedence if a message qualifies for both.\n- If `enabled: False` or no config is provided, terminal output is disabled.\n\n---\n\n## Time-based log deletion\n\nAutomate log cleanup by configuring `retain` parameter in LogConfig.\n\nExample:\n```python\nLogConfig(\n    retain=\"1h\"\n)\n```\nThe subfolders in which the logs are nested are parsed on every rollover, and those older than 1 hour are deleted.\n\nSupported time units:\n- `m` - minutes\n- `h` - hours\n- `d` - days\n- `w` - weeks\n\n`retain` is disabled in default config.\n\nIf both compression and retention are configured, compression **always runs before** cleanup to avoid deleting subfolders mid-archive.\n\n**Important**: `retain` must be equal to or longer than the rollover `interval`.\n\n---\n\n## Log compression\n\nAutomate compression of previous log subfolders after each rollover.\n\n### Enabling compression\n\nTo enable log compression, configure the `compression` parameter in LogConfig:\n```python\ncompression={\n    \"enabled\": True  # enables compression using default format: zip\n}\n```\nOr specify a format:\n```python\ncompression={\n    \"enabled\": True,\n    \"compress_format\": \"tar.gz\"  # or \"zip\"\n}\n```\n\nIf you want to delete the original log subfolder after compression, configure `retain` with the same amount of time as the rollover `interval` (e.g. `interval=\"24h\"` and `retain=\"1d\"`).\n\n### Behavior\n\n- On every rollover, the subfolder from the previous time interval is compressed\n- Compression runs before log deletion (if `retain` is enabled)\n- Compressed archives are saved next to log folders (e.g. `2025-05-04__14-00.zip`)\n- Already-compressed folders are skipped on future rollovers\n- The current and next interval folders are never compressed\n- Compression supports both `.zip` and `.tar.gz` using Python’s built-in libraries\n\n---\n\n## Timestamp formatting\n\nCustomize timestamp formatting using any valid strftime directive.\n\nExamples:\n\n    - %H:%M:%S → 14:02:19\n\n    - %H:%M:%S.%f → 14:02:19.123456\n\n    - %Y-%m-%d %H:%M:%S → 2025-05-04 14:02:19\n\nInvalid formats are rejected with a descriptive LogConfigError.\n\n---\n\n## Timezone support\n\nBy default, all timestamps and folder names are based on **UTC**.\n\nYou can optionally set a custom timezone using the `timezone` parameter in `LogConfig`.\n\nChronologix uses Python's built-in [zoneinfo](https://docs.python.org/3/library/zoneinfo.html) module for timezone resolution. This requires **Python 3.9+**.\n\nExample:\n```python\nLogConfig(\n    ...\n    timezone=\"Europe/Prague\"\n)\n```\n\nSupported values are standard IANA zone names like:\n- \"Europe/Prague\"\n- \"America/New_York\"\n- \"Asia/Tokyo\"\n- \"Etc/GMT+2\"\n\nIf the timezone is invalid, a `LogConfigError` will be raised on startup with a list of valid options.\n\n### Behavior\n\n- Folder rollover times and names reflect the selected timezone\n- Timestamps in log entries are aligned to the specified timezone\n- All time-based operations (retain, compression, etc.) are now fully timezone-aware\n- If no timezone is provided, Chronologix defaults to \"UTC\"\n\n---\n\n## Log structure\n\n```lua\nmy_logs/\n└── 2025-05-04__14-00/\n    ├── app.log\n    ├── errors.log\n    └── audit.log\n└── 2025-05-04__15-00/\n    ├── app.log\n    ├── errors.log\n    └── audit.log\n```\nFolders are aligned to the start of the interval (__14-00) and created ahead of time to mitigate latency for smooth rollover.\n\n---\n\n## Default config\n\nIf you use the default constructor, Chronologix behaves like this:\n```python\nfrom chronologix import LogConfig\n\nconfig = LogConfig()\nlogger = LogManager(config)\nawait logger.start()\n```\n`LogConfig()` is equivalent to:\n```python\nLogConfig(\n    base_log_dir=\"logs\",\n    interval=\"24h\",\n    sinks={\n        \"debug\": {\"file\": \"debug.log\", \"min_level\": \"NOTSET\"},\n        \"errors\": {\"file\": \"errors.log\", \"min_level\": \"ERROR\"}\n    },\n    mirror=None,\n    timestamp_format=\"%H:%M:%S\",\n    cli_echo=None,\n    retain=None,\n    compression=None,\n    hooks=None,\n    timezone=\"UTC\"\n)\n```\n\n---\n\n## But why?\n\nThe idea to build this package came from direct need while working on my private trading software. \nI hadn't found anything that would check all the boxes and satisfy my OCD, so I decided to build it myself. \nAt first, it was just a module tailored for my program, but then I realized it could be useful for others. \nSo it felt like the perfect opportunity to finally open source something.\nThe core of Chronologix is built on my original logging module, but I tried to make it as flexible as possible to cater to different needs.\n\n---\n\n## Contributing\n\nFeel free to reach out if you have any suggestions or ideas. \nI'm open to collaboration and improvements.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffractalwillie%2Fchronologix","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffractalwillie%2Fchronologix","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffractalwillie%2Fchronologix/lists"}