{"id":31377509,"url":"https://github.com/DeRuina/timberjack","last_synced_at":"2025-09-28T05:02:09.366Z","repository":{"id":290343115,"uuid":"974081543","full_name":"DeRuina/timberjack","owner":"DeRuina","description":"Timberjack is a Go log rolling library with support for size-based, time-based, and manual rotation.","archived":false,"fork":false,"pushed_at":"2025-09-16T12:50:35.000Z","size":139,"stargazers_count":49,"open_issues_count":2,"forks_count":8,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-09-16T14:44:06.661Z","etag":null,"topics":["log-management","log-rolling","log-rotation","logging","rotation","time-based-rotation"],"latest_commit_sha":null,"homepage":"","language":"Go","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/DeRuina.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-28T08:23:34.000Z","updated_at":"2025-09-16T12:43:53.000Z","dependencies_parsed_at":"2025-04-28T10:41:18.893Z","dependency_job_id":"e004e76e-f08d-4a15-9fb8-d12a8d31cc4c","html_url":"https://github.com/DeRuina/timberjack","commit_stats":null,"previous_names":["deruina/timberjack"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/DeRuina/timberjack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeRuina%2Ftimberjack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeRuina%2Ftimberjack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeRuina%2Ftimberjack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeRuina%2Ftimberjack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DeRuina","download_url":"https://codeload.github.com/DeRuina/timberjack/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeRuina%2Ftimberjack/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":277326294,"owners_count":25799445,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-28T02:00:08.834Z","response_time":79,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["log-management","log-rolling","log-rotation","logging","rotation","time-based-rotation"],"created_at":"2025-09-28T05:00:47.719Z","updated_at":"2025-09-28T05:02:09.359Z","avatar_url":"https://github.com/DeRuina.png","language":"Go","funding_links":[],"categories":["Logging","日志记录"],"sub_categories":["Search and Analytic Databases","检索及分析资料库"],"readme":"# timberjack [![Go Reference](https://pkg.go.dev/badge/github.com/DeRuina/timberjack.svg)](https://pkg.go.dev/github.com/DeRuina/timberjack) [![Go Report Card](https://goreportcard.com/badge/github.com/DeRuina/timberjack)](https://goreportcard.com/report/github.com/DeRuina/timberjack) ![Audit](https://github.com/DeRuina/timberjack/actions/workflows/audit.yaml/badge.svg) ![Version](https://img.shields.io/github/v/tag/DeRuina/timberjack?sort=semver) [![Coverage Status](https://coveralls.io/repos/github/DeRuina/timberjack/badge.svg)](https://coveralls.io/github/DeRuina/timberjack)\n\n\n### Timberjack is a Go package for writing logs to rolling files.\n\nTimberjack is a forked and enhanced version of [`lumberjack`](https://github.com/natefinch/lumberjack), adding time-based rotation, clock-scheduled rotation, and opt-in compression (gzip or zstd).\nPackage `timberjack` provides a rolling logger with support for size-based and time-based log rotation.\n\n\n## Installation\n\n```bash\ngo get github.com/DeRuina/timberjack\n```\n\n\n## Import\n\n```go\nimport \"github.com/DeRuina/timberjack\"\n```\n\nTimberjack is a pluggable component that manages log file writing and rotation. It works with any logger that writes to an `io.Writer`, including the standard library’s `log` package.\n\n\u003e ⚠️ Timberjack assumes **one process** writes to a given file. Reusing the same config from multiple\n\u003e processes on the same machine may lead to unexpected behavior.\n\n\n## Example\n\nTo use timberjack with the standard library's `log` package, including interval-based and scheduled minute/daily rotation:\n\n```go\nimport (\n\t\"log\"\n\t\"time\"\n\t\"github.com/DeRuina/timberjack\"\n)\n\nfunc main() {\n\tlogger := \u0026timberjack.Logger{\n\t\tFilename:         \"/var/log/myapp/foo.log\",   // Choose an appropriate path\n\t\tMaxSize:          500,                        // megabytes\n\t\tMaxBackups:       3,                          // backups\n\t\tMaxAge:           28,                         // days\n    Compression:      \"gzip\",                     // \"none\" | \"gzip\" | \"zstd\" (preferred over legacy Compress)\n\t\tLocalTime:        true,                       // default: false (use UTC)\n\t\tRotationInterval: 24 * time.Hour,             // Rotate daily if no other rotation met\n\t\tRotateAtMinutes:  []int{0, 15, 30, 45},       // Also rotate at HH:00, HH:15, HH:30, HH:45\n\t\tRotateAt:         []string{\"00:00\", \"12:00\"}, // Also rotate at 00:00 and 12:00 each day\n   \tBackupTimeFormat: \"2006-01-02-15-04-05\",      // Rotated files will have format \u003clogfilename\u003e-2006-01-02-15-04-05-\u003creason\u003e.log\n    AppendTimeAfterExt:   true,                    // put timestamp after \".log\" (foo.log-\u003ctimestamp\u003e-\u003creason\u003e)\n    \n\t}\n\tlog.SetOutput(logger)\n\tdefer logger.Close() // Ensure logger is closed on application exit to stop goroutines\n\n\tlog.Println(\"Application started\")\n\t// ... your application logic ...\n\tlog.Println(\"Application shutting down\")\n}\n```\n\nManual rotation (e.g. on `SIGHUP`):\n\n```go\nimport (\n    \"log\"\n    \"os\"\n    \"os/signal\"\n    \"syscall\"\n\n    \"github.com/DeRuina/timberjack\"\n)\n\n\nfunc main() {\n    l := \u0026timberjack.Logger{ Filename: \"/var/log/myapp/foo.log\" }\n    log.SetOutput(l)\n    defer l.Close()\n\n    // Manual rotation on SIGHUP\n    c := make(chan os.Signal, 1)\n    signal.Notify(c, syscall.SIGHUP)\n\n    go func() {\n        for range c {\n            // 1) Classic behavior: auto-pick \"time\" (if due) or \"size\"\n            // _ = l.Rotate()\n\n            // 2) New: tag the backup with your own reason\n            _ = l.RotateWithReason(\"reload\")\n        }\n    }()\n\n    // ...\n}\n```\n\n\n## Logger Configuration\n\n```go\ntype Logger struct {\n    Filename          string        // File to write logs to\n    MaxSize           int           // Max size (MB) before rotation (default: 100)\n    MaxAge            int           // Max age (days) to retain old logs\n    MaxBackups        int           // Max number of backups to keep\n    LocalTime         bool          // Use local time in rotated filenames\n\n    // Compression controls post-rotation compression:\n    //   \"none\" | \"gzip\" | \"zstd\"\n    // Unknown/empty default to \"none\", unless the legacy Compress is true (see below).\n    Compression       string\n\n    // Deprecated: prefer Compression.\n    // If Compression is empty and Compress is true = gzip compression.\n    // Back-compat shim for old configs; will be removed in v2.\n    Compress          bool\n\n\n    RotationInterval  time.Duration // Rotate after this duration (if \u003e 0)\n    RotateAtMinutes   []int         // Specific minutes within an hour (0–59) to trigger rotation\n    RotateAt          []string      // Specific daily times (HH:MM, 24-hour) to trigger rotation\n    BackupTimeFormat  string        // Optional. If unset or invalid, defaults to 2006-01-02T15-04-05.000 (with fallback warning)\n    AppendTimeAfterExt    bool      // if true, name backups like foo.log-\u003ctimestamp\u003e-\u003creason\u003e defaults to foo-\u003ctimestamp\u003e-\u003creason\u003e.log\n}\n```\n\n\n## How Rotation Works\n\n1. **Size-Based**: If a write operation causes the current log file to exceed `MaxSize`, the file is rotated before the write. The backup filename will include `-size` as the reason.\n2. **Time-Based (Interval)**: If `RotationInterval` is set (e.g., `24 * time.Hour` for daily rotation) and this duration has passed since the last rotation (of any type that updates the interval timer), the file is rotated upon the next write. The backup filename will include `-time` as the reason.\n3. **Scheduled (Clock-Aligned)**: If `RotateAtMinutes` and/or `RotateAt` are configured (e.g., `[]int{0,30}` → rotate at `HH:00` and `HH:30`; or `[]string{\"00:00\"}` → rotate at midnight), a background goroutine triggers rotation at those times. These rotations use `-time` as the reason.\n4. **Manual**: \n    - `Logger.Rotate()` forces rotation now. The backup reason will be `\"time\"` if an interval rotation is due, otherwise `\"size\"`.\n    - `Logger.RotateWithReason(\"your-reason\")` forces rotation and tags the backup with your **sanitized** reason (see below). If the provided reason is empty after sanitization, it falls back to the same behavior as Rotate().\n\nRotated files are renamed using the pattern:\n\nBy default, rotated files are named:\n```\n\u003cname\u003e-\u003ctimestamp\u003e-\u003creason\u003e.log\n```\n\nFor example:\n\n```\n/var/log/myapp/foo-2025-04-30T15-00-00.000-size.log\n/var/log/myapp/foo-2025-04-30T22-15-42.123-time.log\n/var/log/myapp/foo-2025-05-01T10-30-00.000-time.log.gz  (if compressed)\n```\n\nIf you prefer the extension to stay attached to the live name (better shell TAB completion),\n\nset `AppendTimeAfterExt: true`:\n\n```\n\u003cname\u003e.log-\u003ctimestamp\u003e-\u003creason\u003e\n```\n\nFor example:\n\n```\n/var/log/myapp/foo.log-2025-04-30T15-00-00.000-size\n/var/log/myapp/foo.log-2025-04-30T22-15-42.123-time\n/var/log/myapp/foo.log-2025-05-01T10-30-00.000-time.gz (if compressed)\n```\n\nManual rotation with a custom reason `_ = logger.RotateWithReason(\"reload-now v2\")`\n\nFor example:\n\n```\nfoo-2025-05-01T10-30-00.000-reload-now-v2.log\n```\n\n### Compression\n\n- Pick the algorithm with `Compression: \"none\" | \"gzip\" | \"zstd\"`.\n- **Precedence**: If Compression is set, it **wins**. If it’s empty, legacy `Compress: true` means gzip; else no compression.\n- Outputs use `.gz` or `.zst` suffix accordingly.\n- Compression happens after rotation in a background goroutine.\n- **Deprecation**: `Compress` is kept only for backward compatibility with old configs. It’s ignored when `Compression` is set. **It will be removed in v2**.\n\n### Cleanup\n\nOn each new log file creation, timberjack:\n- Deletes backups exceeding `MaxBackups` (keeps the newest rotations).\n- Deletes backups older than `MaxAge` days.\n- Compresses uncompressed backups if compression is enabled.\n\n### Rotation modes at a glance\n\n| Mode                           | Configure with                                | Trigger                                                             | Anchor                       | Background goroutine? | Rotates with zero writes? | Updates `lastRotationTime` | Backup suffix                                             | Notes                                                                                                             |\n| ------------------------------ | --------------------------------------------- | ------------------------------------------------------------------- | ---------------------------- | :-------------------: | :-----------------------: | :------------------------: | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |\n| **Size-based**                 | `MaxSize`                                     | A write would exceed `MaxSize`                                      | N/A                          |           No          |             No            |           **No**           | `-size`                                                   | Always active. A single write larger than `MaxSize` returns an error.                                             |\n| **Interval-based**             | `RotationInterval \u003e 0`                        | On **next write** after `now - lastRotationTime ≥ RotationInterval` | Duration since last rotation |           No          |             No            |     **Yes** (to `now`)     | `-time`                                                   | “Every N” rotations; not aligned to the wall clock.                                                               |\n| **Scheduled minute-based**     | `RotateAtMinutes` (e.g. `[]int{0,30}`)        | At each `HH:MM` where minute matches                                | Clock minute marks           |        **Yes**        |          **Yes**          |           **Yes**          | `-time`                                                   | Expands minutes across all 24 hours. Invalid minutes are ignored **with a warning**. De-duplicated vs `RotateAt`. |\n| **Scheduled daily fixed time** | `RotateAt` (e.g. `[]string{\"00:00\",\"12:00\"}`) | At each listed `HH:MM` daily                                        | Clock minute marks           |        **Yes**        |          **Yes**          |           **Yes**          | `-time`                                                   | Ideal for “rotate at midnight”. De-duplicated vs `RotateAtMinutes`.                                               |\n| **Manual**                     | `Logger.Rotate()`                             | When called                                                         | Immediate                    |           No          |            N/A            |           **No**           | `-time` if an interval rotation is due; otherwise `-size` | Handy for SIGHUP.\n| **Manual (custom reason)**   | `Logger.RotateWithReason(s)`     | When called | Immediate | No | N/A | **No** | `-\u003csanitized reason\u003e` | Falls back to `Rotate()` behavior if `s` sanitizes to empty. |\n\n\u003e **Time zone:** scheduling and filename timestamps use UTC by default, or local time if `LocalTime: true`.\n\u003e **Sanitized reason:** lowercase; `[a-z0-9_-]` only,  trims edge, max 32. \n\n## ⚠️ Rotation Notes \u0026 Warnings\n\n* **`BackupTimeFormat` Values must be valid and should not change after initialization**  \n  The `BackupTimeFormat` value **must be valid** and must follow the timestamp layout rules\n  specified here: https://pkg.go.dev/time#pkg-constants. `BackupTimeFormat` supports more formats but it's recommended to use standard formats. If an **invalid** `BackupTimeFormat` is configured, Timberjack logs a warning to `os.Stderr` and falls back to the default format: `2006-01-02T15-04-05.000`. Rotation will still work, but the resulting filenames may not match your expectations.\n\n* **Invalid `RotateAtMinutes`/`RotateAt` Values**  \n  Values outside the valid range (`0–59`) for `RotateAtMinutes` or invalid time (`HH:MM`) for `RotateAt` or duplicates in `RotateAtMinutes`/`RotateAt` are ignored with a warning to stderr. Rotation continues with the valid schedule.\n\n* **Logger Must Be Closed**\n  Always call `logger.Close()` when done logging. This shuts down internal goroutines used for scheduled rotation and cleanup. Failing to close the logger can result in orphaned background processes, open file handles, and memory leaks.\n\n* **Size-Based Rotation Is Always Active**\n  Regardless of `RotationInterval` or `RotateAtMinutes`/`RotateAt`, size-based rotation is always enforced. If a write causes the log to exceed `MaxSize` (default: 100MB), it triggers an immediate rotation.\n\n* **If Only `RotationInterval` Is Set**\n  The logger rotates after the configured time has passed since the **last rotation**, regardless of file size.\n\n* **If Only `RotateAtMinutes`/`RotateAt` Is Set**\n  The logger rotates **at the clock times** specified, regardless of file size or duration passed. This is handled by a background goroutine. Rotated logs can be empty if no write has occurred.\n\n* **If Both Are Set**  \n  Both time-based strategies (`RotationInterval` and `RotateAtMinutes`) are evaluated. Whichever condition occurs first triggers rotation. However:\n\n  * Both update the internal `lastRotationTime` field.\n  * This means if a rotation happens due to `RotateAtMinutes`/`RotateAt`, it resets the interval timer, potentially **delaying or preventing** a `RotationInterval`-based rotation.\n\n  This behavior ensures you won’t get redundant rotations, but it may make `RotationInterval` feel unpredictable if `RotateAtMinutes`/`RotateAt` is also configured.\n\n## Contributing\n\nWe welcome contributions!\nPlease see our [contributing guidelines](CONTRIBUTING.md) before submitting a pull request.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDeRuina%2Ftimberjack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FDeRuina%2Ftimberjack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDeRuina%2Ftimberjack/lists"}