logaru 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3192f41c03547e6173ca0002334fc95e8ee10a5d377891a84b5b543f53dd9306
4
+ data.tar.gz: c05dfdb1f92db1cb6518c3850360cc0a4a8f049e4e32bdd9a18404f66459f9d2
5
+ SHA512:
6
+ metadata.gz: f31e77c1b4b91420bc45aa4fffbacaa61995cc054a8e2fcac74fbdb06fe1a7fd2ee996302cab4e956d03bdd5f2123f5bfc75703e1784045c6c35db4c9d80ff37
7
+ data.tar.gz: 691e1faa39da9edb5971c8723fe1477be2749ef16853fe45c3d1491d68b51b74f7dc792ba4cba447465fea84130209c30107ea147b70d585dc84915f373a71ea
data/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2026-09-24
11
+
12
+ First stable release. Requires Ruby >= 3.3 and has no runtime dependencies.
13
+
14
+ ### Added
15
+
16
+ - `Logaru::Logger` with `debug`, `info`, `warn`, `error`, `fatal` and `unknown`, plus the generic `log(severity, message, progname:)`.
17
+ - Level filtering: a severity is accepted as a `Logaru::Level` constant, a name or a symbol, and entries below the configured level are discarded before formatting.
18
+ - `Logaru::Logger` writes to `$stdout` by default, to a log file (`String` or `Pathname`: parent directories created, append mode, handle reused, flushed on every write) or to any object responding to `#write`.
19
+ - `Logaru::Logger#formatter`, `#level`, `#output` and `#device` readers, plus `#close`, which releases the file opened by the logger so it can be rotated externally — the next write reopens it.
20
+ - `Logaru::Formatter` with a configurable pattern (`pattern:` or a block), pattern arity validation and `Logaru::Formatter::DEFAULT_PATTERN`.
21
+ - `Logaru::Level` severities, `coerce` and `name_for`.
22
+ - Error hierarchy under `Logaru::Error`: `InvalidFormatterError`, `InvalidLevelError`, `InvalidPatternError` and `InvalidOutputError`, all raised from `Logger.new`/`Formatter.new`.
23
+ - Thread safety: writes, device resolution and `#close` are serialized by a mutex and the log file is opened only once, so one logger can be shared between threads.
24
+
25
+ [Unreleased]: https://github.com/rpzerosixcode/logaru/compare/v1.0.0...HEAD
26
+ [1.0.0]: https://github.com/rpzerosixcode/logaru/releases/tag/v1.0.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rpzerosixcode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # Logaru
2
+
3
+ [![CI](https://github.com/rpzerosixcode/logaru/actions/workflows/ci.yml/badge.svg)](https://github.com/rpzerosixcode/logaru/actions/workflows/ci.yml)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D_3.3-ruby.svg)](https://www.ruby-lang.org)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
+
7
+ A Ruby library focused on clear, organized, and configurable log output.
8
+
9
+ > 🇧🇷 **Made in Brazil.**
10
+ >
11
+ > Logaru is a Brazilian Ruby gem, developed in Brazil.
12
+
13
+ ## Installation
14
+
15
+ Add Logaru to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "logaru"
19
+ ```
20
+
21
+ Then run:
22
+
23
+ ```bash
24
+ bundle install
25
+ ```
26
+
27
+ Or install it directly:
28
+
29
+ ```bash
30
+ gem install logaru
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```ruby
36
+ require "logaru"
37
+
38
+ Logaru::VERSION # => "1.0.0"
39
+
40
+ Logaru.root # => absolute path to the gem root
41
+ ```
42
+
43
+ The logger supports `debug`, `info`, `warn`, `error`, `fatal`, and `unknown` messages. Levels can be configured with a `Logaru::Level` constant, a level name, or a symbol:
44
+
45
+ ```ruby
46
+ logger = Logaru::Logger.new(level: :info)
47
+
48
+ logger.debug("This message is ignored")
49
+
50
+ logger.info("Application started", progname: "web")
51
+
52
+ logger.error("An unexpected error occurred")
53
+ ```
54
+
55
+ Pass `file:` to append messages to a file. Log files are opened on the first write, reused afterwards, and their parent directories are created automatically:
56
+
57
+ ```ruby
58
+ logger = Logaru::Logger.new(file: "log/application.log")
59
+
60
+ logger.info("Application started")
61
+
62
+ logger.output # => "log/application.log"
63
+
64
+ logger.device # => the File object used for writing
65
+
66
+ logger.close # closes the file; it is reopened on the next write
67
+ ```
68
+
69
+ A `Pathname` or any object responding to `#write` (such as `StringIO` or an already open `File`) is accepted as well. Writes are flushed by default (`sync: true`) so the file is always up to date, and the logger only closes files it opened itself — streams provided through `file:` remain the caller's responsibility.
70
+
71
+ Rotating the log is left to your tooling: call `logger.close`, rename or replace the file, and the next entry reopens the path (see [Architecture](docs/ARCHITECTURE.md#log-file-lifecycle)).
72
+
73
+ ### Formatters
74
+
75
+ Every logger builds its own `Logaru::Formatter`, so customizing the output never requires injecting one:
76
+
77
+ ```ruby
78
+ logger = Logaru::Logger.new(level: :debug, file: "log/application.log") do |severity, datetime, progname, message|
79
+ "[#{datetime}] #{progname || "app"} #{Logaru::Level.name_for(severity).upcase}: #{message}\n"
80
+ end
81
+ ```
82
+
83
+ The same pattern can be passed as an option, and a formatter instance can still be injected to share one configuration between loggers (`formatter:` and `pattern:` are mutually exclusive):
84
+
85
+ ```ruby
86
+ pattern = proc { |severity, datetime, progname, message| "#{severity} #{message}\n" }
87
+
88
+ Logaru::Logger.new(pattern: pattern)
89
+
90
+ formatter = Logaru::Formatter.new(pattern: pattern)
91
+
92
+ Logaru::Logger.new(formatter: formatter)
93
+
94
+ Logaru::Logger.new(formatter: formatter, pattern: pattern)
95
+
96
+ # => Logaru::InvalidFormatterError: pass either formatter or pattern, not both
97
+ ```
98
+
99
+ Formatters keep no global state, so each logger can use its own pattern. A pattern must be able to receive the four arguments (`severity`, `datetime`, `progname`, `message`); variadic patterns such as `|*arguments|` are accepted too:
100
+
101
+ ```ruby
102
+ Logaru::Formatter.new { |message| "#{message}\n" }
103
+
104
+ # => Logaru::InvalidPatternError: pattern must accept 4 arguments
105
+ ```
106
+
107
+ ### Thread safety
108
+
109
+ A logger can be shared between threads: writes are serialized, so entries are never interleaved and the log file is opened only once, even when several threads start together. Patterns run outside that lock, so they should not depend on mutable shared state — see [Concurrency](docs/ARCHITECTURE.md#concurrency).
110
+
111
+ ### Errors
112
+
113
+ Every error raised by Logaru inherits from `Logaru::Error`:
114
+
115
+ * `Logaru::InvalidFormatterError` — the formatter does not respond to `#format`.
116
+ * `Logaru::InvalidLevelError` — the configured severity is not supported.
117
+ * `Logaru::InvalidPatternError` — the formatter pattern is not callable or cannot receive the log arguments.
118
+ * `Logaru::InvalidOutputError` — the logger output is neither a path nor an object responding to `#write`.
119
+
120
+ ## Development
121
+
122
+ ```bash
123
+ git clone https://github.com/rpzerosixcode/logaru.git
124
+
125
+ cd logaru
126
+
127
+ bundle install
128
+ ```
129
+
130
+ Run the test suite:
131
+
132
+ ```bash
133
+ bundle exec rspec
134
+ ```
135
+
136
+ Run the linter (must be clean — CI fails on offenses):
137
+
138
+ ```bash
139
+ bundle exec rubocop
140
+ ```
141
+
142
+ Run both (default Rake task):
143
+
144
+ ```bash
145
+ bundle exec rake
146
+ ```
147
+
148
+ Sanity-check the gem build (also run in CI):
149
+
150
+ ```bash
151
+ gem build logaru.gemspec --strict
152
+ ```
153
+
154
+ ## Releasing
155
+
156
+ Release by pushing a tag that matches `Logaru::VERSION` (see `lib/logaru/version.rb`):
157
+
158
+ ```bash
159
+ git tag v0.1.0
160
+
161
+ git push origin v0.1.0
162
+ ```
163
+
164
+ `.github/workflows/release.yml` then runs the same checks as CI (RSpec, RuboCop and a strict gem build on Ruby 3.3, 3.4 and 4.0), checks the tag against `Logaru::VERSION`, publishes the gem to RubyGems and opens a GitHub release with the gem attached. Publishing uses RubyGems trusted publishing (OIDC), so no API token is stored in the repository — configure a trusted publisher for `logaru` on RubyGems.org (workflow file `.github/workflows/release.yml`) before the first tag.
165
+
166
+ ## Documentation
167
+
168
+ * [Architecture](docs/ARCHITECTURE.md) — components, entry lifecycle, output resolution and concurrency model.
169
+ * [Features](docs/FEATURES.md) — what the library does today and what is still missing.
170
+ * [Security](docs/SECURITY.md) — threat surface, known limitations and how to report a vulnerability.
171
+
172
+ The repository layout, the CI matrix and the Dependabot setup are documented in [Architecture](docs/ARCHITECTURE.md#repository-layout).
173
+
174
+ ## Requirements
175
+
176
+ * Ruby >= 3.3.0 (see `logaru.gemspec`).
177
+
178
+ ## License
179
+
180
+ Logaru is available under the MIT License — see [LICENSE](LICENSE).
181
+
182
+ ## Links
183
+
184
+ * Homepage: https://github.com/rpzerosixcode/logaru
185
+ * Changelog: https://github.com/rpzerosixcode/logaru/blob/main/CHANGELOG.md
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RuboCop::RakeTask.new(:rubocop)
10
+
11
+ desc "Run spec and rubocop"
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,107 @@
1
+ # Architecture
2
+
3
+ How Logaru is built internally. Installation and usage live in the [README](../README.md); the capability checklist is in [FEATURES.md](FEATURES.md).
4
+
5
+ ## Design goals
6
+
7
+ * **Formatting without global state:** each logger owns its formatter, so changing the output of one logger never changes another.
8
+ * **Fail fast:** level, formatter, pattern and output are validated in `Logger.new` instead of failing on the first write.
9
+ * **Cheap writes:** entries below the configured level are discarded before formatting, and the log file is opened once and reused.
10
+ * **Safe sharing:** one logger can be used by several threads without interleaved or lost entries.
11
+ * **No runtime dependencies:** standard library only (`fileutils`, `pathname`).
12
+
13
+ ## Components
14
+
15
+ | Component | File | Responsibility |
16
+ | --- | --- | --- |
17
+ | `Logaru` | `lib/logaru.rb` | Loads the components and exposes `Logaru.root`. |
18
+ | `Logaru::VERSION` | `lib/logaru/version.rb` | Gem version. |
19
+ | `Logaru::Level` | `lib/logaru/level.rb` | Numeric severities (`DEBUG`…`UNKNOWN`), `coerce` (constant, name or symbol) and `name_for`. |
20
+ | `Logaru::Formatter` | `lib/logaru/formatter.rb` | Owns the pattern, validates its arity and turns an entry into text (`#format`). |
21
+ | `Logaru::Logger` | `lib/logaru/logger.rb` | Filters by level, resolves the output, manages the log file and serializes writes with a mutex. |
22
+ | `Logaru::Error` | `lib/logaru/errors.rb` | Error hierarchy: `InvalidFormatterError`, `InvalidLevelError`, `InvalidPatternError`, `InvalidOutputError`. |
23
+
24
+ ## Entry lifecycle
25
+
26
+ ```text
27
+ logger.info("message", progname: "web")
28
+ -> severity = Level.coerce(INFO)
29
+ -> return if severity < @level # no formatting, no write, no file created
30
+ -> @formatter.format(severity, Time.now, progname, message) # outside the lock
31
+ -> @mutex.synchronize { resolved_device.write(text) }
32
+ ```
33
+
34
+ ## Output resolution
35
+
36
+ | `file:` value | Device used | Owner |
37
+ | --- | --- | --- |
38
+ | omitted or `nil` | `$stdout`, resolved on every write | process |
39
+ | `String`, `Pathname`, or an object with `to_path` and without `write` | file opened in append mode | the logger (`#close` releases it) |
40
+ | object responding to `#write` (`File`, `StringIO`, `Tempfile`, custom device) | the object itself | the caller, never closed by Logaru |
41
+
42
+ `Pathname` is classified explicitly: it responds to `#write`, but that call replaces the whole file, so treating it as a stream would overwrite the log on every entry. Path-like values are therefore detected before the `#write` check, and `#output` always returns the value given to `file:`.
43
+
44
+ ## Log file lifecycle
45
+
46
+ 1. Opened on the first write that passes the level, so a logger that logs nothing creates neither the file nor its directories.
47
+ 2. Parent directories are created with `FileUtils.mkdir_p`.
48
+ 3. Opened in append mode with `sync` (default `true`), so every entry is flushed on write.
49
+ 4. The handle is reused between writes and exposed by `#device`.
50
+ 5. `#close` releases only the file the logger opened, and the next write reopens the path in append mode — the supported path for external rotation (close, rename, keep logging).
51
+
52
+ ## Concurrency
53
+
54
+ * A single `Mutex` per logger serializes device resolution, writing and `#close`, so entries never interleave, none is lost and the file is opened exactly once even when threads start together.
55
+ * Formatting runs outside the lock (it is the expensive step), so patterns must not depend on mutable shared state — keep them pure.
56
+ * Locking is per logger and per process: Logaru does not use `flock`, so independent processes writing to the same file are not synchronized.
57
+
58
+ ## Repository layout
59
+
60
+ ```text
61
+ lib/
62
+ logaru.rb # entry point — Logaru namespace
63
+ logaru/version.rb # Logaru::VERSION
64
+ logaru/errors.rb # Logaru::Error and the specific errors
65
+ logaru/level.rb # Logaru::Level severities
66
+ logaru/formatter.rb # Logaru::Formatter — patterns and arity validation
67
+ logaru/logger.rb # Logaru::Logger — levels, output and file management
68
+
69
+ spec/
70
+ spec_helper.rb # RSpec configuration (loads spec/support)
71
+ unit/ # Unit tests
72
+ integration/ # Integration tests
73
+ e2e/ # End-to-end tests (placeholder)
74
+ support/ # Helpers / shared examples
75
+
76
+ docs/ # User documentation (README + this file, FEATURES, SECURITY)
77
+
78
+ .github/workflows/ # ci.yml — RSpec + RuboCop + gem build
79
+ .github/dependabot.yml # weekly dependency updates
80
+
81
+ Rakefile # default task: spec + rubocop
82
+ logaru.gemspec # Gem metadata and dependencies
83
+ Gemfile # source + gemspec
84
+ .rubocop.yml # Style rules
85
+ .rspec # --require spec_helper
86
+ ```
87
+
88
+ ## Automated checks
89
+
90
+ CI (`.github/workflows/ci.yml`) runs on every push and pull request to `main` and `develop`:
91
+
92
+ * RSpec suite and `bundle exec rubocop --parallel` on `ubuntu-latest` with Ruby `3.3`, `3.4` and `4.0` (fail-fast disabled);
93
+ * `gem build logaru.gemspec --strict` as a packaging sanity check;
94
+ * Dependabot opens weekly pull requests for Bundler and GitHub Actions dependencies.
95
+
96
+ Locally, `bundle exec rake` runs the same checks available in the development environment.
97
+
98
+ ## Release pipeline
99
+
100
+ `.github/workflows/release.yml` runs when a `v*` tag is pushed (the tagging steps are in the [README](../README.md#releasing)):
101
+
102
+ 1. **Validate** — the same matrix and checks as CI: RSpec, `bundle exec rubocop --parallel` and `gem build logaru.gemspec --strict` on `ubuntu-latest` with Ruby `3.3`, `3.4` and `4.0` (fail-fast disabled).
103
+ 2. **Publish** — fails unless the tag equals `Logaru::VERSION`, rebuilds the gem, authenticates to RubyGems over OIDC (trusted publishing), pushes the gem and creates the GitHub release with the gem attached.
104
+
105
+ The validate job is a mirror of the CI test job, so a tag can never publish code that CI would reject. Publishing runs `gem build logaru.gemspec --strict`, which writes `logaru-<version>.gem` in the workspace; locally, `bundle exec rake build` (from `bundler/gem_tasks`, required by the `Rakefile`) writes `pkg/logaru-<version>.gem`. `pkg/` and `*.gem` are git-ignored. The release job declares `id-token: write` for OIDC and `contents: write` to create the release.
106
+
107
+ `Logaru::VERSION` (`lib/logaru/version.rb`) is the single source of truth: the gemspec reads it, the workflow compares it with the tag and `Gemfile.lock` records it. A release therefore means bumping that file, moving the `Unreleased` entries into a dated section in `CHANGELOG.md` and pushing the `v<version>` tag.
data/docs/FEATURES.md ADDED
@@ -0,0 +1,38 @@
1
+ # Features
2
+
3
+ What Logaru offers in `1.0.0`. Usage examples are in the [README](../README.md); internal design in [ARCHITECTURE.md](ARCHITECTURE.md).
4
+
5
+ ## Severities and filtering
6
+
7
+ * Six severities: `debug`, `info`, `warn`, `error`, `fatal` and `unknown`, plus the generic `log(severity, message, progname:)`.
8
+ * A level can be given as a `Logaru::Level` constant, a name or a symbol, in any case.
9
+ * Entries below the configured level are discarded before formatting: they cost nothing and never create a log file. The default level is `debug`.
10
+
11
+ ## Output
12
+
13
+ * Standard output by default (`$stdout`, resolved on every write).
14
+ * Log files through a `String` or a `Pathname`: created on first use, parent directories created automatically, append mode, handle reused, one flush per entry.
15
+ * Any object responding to `#write` (`File`, `StringIO`, `Tempfile`, custom devices) — streams Logaru did not open are never closed by it.
16
+ * `sync: false` for buffered writes, `#close` to release the file, `#device` and `#output` for introspection.
17
+
18
+ ## Formatting
19
+
20
+ * Default pattern (`[<timestamp>] [<progname> ]<LEVEL>: <message>`) or a custom `pattern:`/block for each logger.
21
+ * A `Logaru::Formatter` instance can be built once and injected into several loggers.
22
+ * Patterns are validated when created: they must be callable and able to receive the four entry arguments (severity, datetime, progname, message). Lambdas, variadic blocks, optional parameters and callables without `#parameters` are supported.
23
+ * Optional `progname` per entry.
24
+
25
+ ## Concurrency
26
+
27
+ * A logger can be shared between threads: entries are written atomically and the log file is opened exactly once.
28
+ * `#close` is safe while other threads are logging.
29
+
30
+ ## Operations
31
+
32
+ * External rotation (rename or replace the file) works through `#close` and the automatic reopen on the next write.
33
+ * Invalid configuration (formatter, level, pattern, output) raises a `Logaru::Error` subclass at `Logger.new`.
34
+ * No runtime dependencies; Ruby >= 3.3.
35
+
36
+ ## Not available yet
37
+
38
+ Log rotation by size or date, asynchronous logging, ANSI colors, message sanitization (see [SECURITY.md](SECURITY.md)) and cross-process locking are not implemented. They are candidates for future minor versions and do not affect the `1.0` API, which follows Semantic Versioning.
data/docs/SECURITY.md ADDED
@@ -0,0 +1,38 @@
1
+ # Security
2
+
3
+ How Logaru handles untrusted input, and what it does not protect against.
4
+
5
+ Supported versions: the latest `1.x` release. `1.0.0` is the first stable version; fixes are prepared on `develop`, merged into `main` and published as a new patch or minor release from a tag matching `Logaru::VERSION`.
6
+
7
+ ## Threat surface
8
+
9
+ * No runtime dependencies — only the Ruby standard library (`fileutils`, `pathname`).
10
+ * No `eval`, no dynamic dispatch of user input, no deserialization and no execution of external content.
11
+ * No network access and no telemetry: the library neither collects nor transmits data.
12
+ * Inputs are the logger options (`level`, `file`, `formatter`, `pattern`, `sync`), the messages and the progname; they affect only local formatting and writing.
13
+ * Paths are used as given: `file:` is opened in append mode and missing parent directories are created. Logaru does not restrict or sandbox paths, so never build a log path from untrusted input.
14
+
15
+ ## Considerations
16
+
17
+ ### Control characters in messages (not mitigated)
18
+
19
+ Messages are written exactly as received, so control characters (`\e`, `\r`, `\n`, `\b`) can forge log lines or manipulate the terminal of whoever reads the log — for example `"ok\rINFO: all good"`. Neither `Logaru::Formatter` nor `Logaru::Logger` sanitizes messages: sanitize at the source, or return an escaped message from a custom pattern.
20
+
21
+ ### Sensitive content
22
+
23
+ Messages may contain personal data, credentials in URLs, tokens or payloads. The library does not mask, redact or filter anything: retention policy, masking and access control are the application's responsibility.
24
+
25
+ ### File permissions and lifetime
26
+
27
+ The file is created with the process defaults (`umask`/ACLs apply) in append mode, and missing parent directories are created with default permissions. The handle stays open until `Logger#close`, so release it before archiving or removing the file — on Windows an open file cannot be renamed or deleted.
28
+
29
+ ### Writing from several processes
30
+
31
+ Synchronization is per logger and per process. Two processes appending to the same file are not coordinated, so entries may interleave when writes are buffered: keep the default `sync: true`, use one writer per file, or send logs to an external collector.
32
+
33
+ ## Reporting a vulnerability
34
+
35
+ * Report privately through GitHub: **Security → Advisories → Report a vulnerability** (<https://github.com/rpzerosixcode/logaru/security/advisories/new>).
36
+ * Do not open a public issue for something exploitable.
37
+ * Include the version or commit, Ruby version, operating system, reproduction steps, impact and, when possible, a suggested fix.
38
+ * There is no bug bounty program. Gem releases require MFA (`rubygems_mfa_required`) and fixes are noted in the changelog.
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logaru
4
+ # Base class for errors raised by Logaru.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when a logger is configured with an invalid formatter.
8
+ class InvalidFormatterError < Error; end
9
+
10
+ # Raised when a logger receives an unsupported severity.
11
+ class InvalidLevelError < Error; end
12
+
13
+ # Raised when a formatter is configured with an unusable pattern.
14
+ class InvalidPatternError < Error; end
15
+
16
+ # Raised when a logger receives an output that cannot be written to.
17
+ class InvalidOutputError < Error; end
18
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "level"
5
+
6
+ module Logaru
7
+ # Formats log entries using a configurable pattern.
8
+ class Formatter
9
+ # Number of arguments every pattern must be able to receive.
10
+ PATTERN_ARITY = 4
11
+
12
+ # Pattern used when the formatter is instantiated without one.
13
+ DEFAULT_PATTERN = proc do |severity, datetime, progname, message|
14
+ prefix = progname ? "#{progname} " : ""
15
+ "[#{datetime}] #{prefix}#{Level.name_for(severity).upcase}: #{message}\n"
16
+ end
17
+
18
+ attr_reader :pattern
19
+
20
+ # Initializes the formatter with an optional pattern. A block takes
21
+ # precedence over the +pattern+ option, so both
22
+ # Formatter.new(pattern: proc { ... }) and Formatter.new { ... } work.
23
+ def initialize(pattern: DEFAULT_PATTERN, &block)
24
+ @pattern = block || pattern
25
+ validate_pattern!
26
+ end
27
+
28
+ # Formats a log entry using the configured pattern.
29
+ def format(severity, datetime, progname, message)
30
+ @pattern.call(severity, datetime, progname, message)
31
+ end
32
+
33
+ private
34
+
35
+ def validate_pattern!
36
+ raise InvalidPatternError, "pattern must respond to #call" unless @pattern.respond_to?(:call)
37
+ return if compatible_arity?
38
+
39
+ raise InvalidPatternError, "pattern must accept #{PATTERN_ARITY} arguments"
40
+ end
41
+
42
+ # Returns true when the pattern can be called with the log arguments.
43
+ def compatible_arity?
44
+ return true unless @pattern.respond_to?(:parameters)
45
+
46
+ parameters = @pattern.parameters
47
+ required = parameters.count { |type, _name| type == :req }
48
+ optional = parameters.count { |type, _name| type == :opt }
49
+ variadic = parameters.any? { |type, _name| type == :rest }
50
+
51
+ required <= PATTERN_ARITY && (variadic || required + optional >= PATTERN_ARITY)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Logaru
6
+ # Numeric values for the supported log severities.
7
+ class Level
8
+ DEBUG = 0
9
+ INFO = 1
10
+ WARN = 2
11
+ ERROR = 3
12
+ FATAL = 4
13
+ UNKNOWN = 5
14
+
15
+ # Maps level names to their numeric values.
16
+ LEVELS = {
17
+ "debug" => DEBUG,
18
+ "info" => INFO,
19
+ "warn" => WARN,
20
+ "error" => ERROR,
21
+ "fatal" => FATAL,
22
+ "unknown" => UNKNOWN,
23
+ }.freeze
24
+
25
+ class << self
26
+ # Converts a numeric or textual level to its numeric value.
27
+ def coerce(value)
28
+ return value if value.is_a?(Integer) && LEVELS.value?(value)
29
+
30
+ name = value.to_s.downcase
31
+ return LEVELS.fetch(name) if LEVELS.key?(name)
32
+
33
+ raise InvalidLevelError, "unsupported log level: #{value.inspect}"
34
+ end
35
+
36
+ # Returns the canonical name for a numeric or textual level.
37
+ def name_for(value)
38
+ LEVELS.key(coerce(value))
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ require_relative "errors"
7
+ require_relative "level"
8
+ require_relative "formatter"
9
+
10
+ module Logaru
11
+ # Writes log entries through a formatter to the console or a file.
12
+ class Logger
13
+ attr_reader :formatter, :level, :output
14
+
15
+ # Initializes the logger with a level and optional output and formatting.
16
+ #
17
+ # +file+ accepts a path (String or Pathname) or any object responding to
18
+ # #write, such as an open File or a StringIO. When it is omitted, entries
19
+ # are written to $stdout. +sync+ controls whether every write is flushed,
20
+ # which defaults to true so that log files are always up to date.
21
+ #
22
+ # Formatting is handled by a Logaru::Formatter built here. Pass +pattern+
23
+ # (or a block) to configure it, or +formatter+ to inject an instance —
24
+ # never both.
25
+ #
26
+ # Entries are written while holding a mutex, so the same logger can be
27
+ # shared between threads. The formatter pattern is called outside the
28
+ # lock, so it must not depend on mutable shared state.
29
+ def initialize(formatter: nil, level: Level::DEBUG, file: nil, sync: true, pattern: nil, &block)
30
+ validate_formatter_options!(formatter, pattern, block)
31
+ @level = Level.coerce(level)
32
+ @output = file
33
+ @sync = sync
34
+ @formatter = formatter || build_formatter(pattern, &block)
35
+ @resolved_device = nil
36
+ @owned = false
37
+ @mutex = Mutex.new
38
+ validate_formatter!
39
+ validate_output!
40
+ end
41
+
42
+ # Logs a message using the configured formatter.
43
+ def log(severity, message, progname: nil)
44
+ severity = Level.coerce(severity)
45
+ return if severity < @level
46
+
47
+ formatted = @formatter.format(severity, Time.now, progname, message)
48
+ write(formatted)
49
+ end
50
+
51
+ # Logs a debug message.
52
+ def debug(message, progname: nil)
53
+ log(Level::DEBUG, message, progname:)
54
+ end
55
+
56
+ # Logs an informational message.
57
+ def info(message, progname: nil)
58
+ log(Level::INFO, message, progname:)
59
+ end
60
+
61
+ # Logs a warning message.
62
+ def warn(message, progname: nil)
63
+ log(Level::WARN, message, progname:)
64
+ end
65
+
66
+ # Logs an error message.
67
+ def error(message, progname: nil)
68
+ log(Level::ERROR, message, progname:)
69
+ end
70
+
71
+ # Logs a fatal error message.
72
+ def fatal(message, progname: nil)
73
+ log(Level::FATAL, message, progname:)
74
+ end
75
+
76
+ # Logs a message with an unknown severity.
77
+ def unknown(message, progname: nil)
78
+ log(Level::UNKNOWN, message, progname:)
79
+ end
80
+
81
+ # Returns the IO used for writing, opening the log file on first use.
82
+ def device
83
+ @mutex.synchronize { resolved_device }
84
+ end
85
+
86
+ # Closes the log file opened by the logger. The file is reopened on the
87
+ # next write, which keeps external log rotation working. Streams and
88
+ # objects provided through +file+ are left untouched.
89
+ #
90
+ # Safe to call from another thread: it waits for the write in progress
91
+ # before releasing the handle.
92
+ def close
93
+ @mutex.synchronize do
94
+ @resolved_device.close if @owned && @resolved_device.respond_to?(:close)
95
+ @resolved_device = nil
96
+ @owned = false
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ # Rejects a formatter injected together with a pattern or a block.
103
+ def validate_formatter_options!(formatter, pattern, block)
104
+ return unless formatter && (pattern || block)
105
+
106
+ raise InvalidFormatterError, "pass either formatter or pattern, not both"
107
+ end
108
+
109
+ # Builds the formatter used when none is injected, honoring a pattern.
110
+ def build_formatter(pattern, &)
111
+ Formatter.new(pattern: pattern || Formatter::DEFAULT_PATTERN, &)
112
+ end
113
+
114
+ def validate_formatter!
115
+ return if @formatter.respond_to?(:format)
116
+
117
+ raise InvalidFormatterError, "formatter must respond to #format (use Logaru::Formatter.new)"
118
+ end
119
+
120
+ def validate_output!
121
+ return if @output.nil? || path_like?(@output) || @output.respond_to?(:write)
122
+
123
+ raise InvalidOutputError, "output must be a path or an object responding to #write"
124
+ end
125
+
126
+ def write(message)
127
+ @mutex.synchronize { resolved_device.write(message) }
128
+ end
129
+
130
+ # Returns the target of the log entries, opening the log file on first
131
+ # use and reusing it on the following writes. Callers must hold the mutex
132
+ # so the file is opened (and the parent directories created) only once.
133
+ def resolved_device
134
+ return $stdout if @output.nil?
135
+
136
+ @resolved_device ||= open_device
137
+ end
138
+
139
+ # Returns true when the value stands for a file path instead of a stream.
140
+ #
141
+ # Pathname is handled explicitly because it responds to #write, and that
142
+ # method writes the whole file in one call: treating a Pathname as a
143
+ # stream would make every entry overwrite the log file.
144
+ def path_like?(value)
145
+ value.is_a?(String) || value.is_a?(Pathname) || (!value.respond_to?(:write) && value.respond_to?(:to_path))
146
+ end
147
+
148
+ # Opens the log file for path targets and returns caller-owned streams as
149
+ # they are.
150
+ def open_device
151
+ return @output unless path_like?(@output)
152
+
153
+ @owned = true
154
+ open_file(@output)
155
+ end
156
+
157
+ # Opens (creating, when needed, the parent directories) the log file in
158
+ # append mode. The handle is kept open and released by #close, so entries
159
+ # are appended to the same file between writes.
160
+ def open_file(path)
161
+ path = path.to_path if path.respond_to?(:to_path)
162
+ FileUtils.mkdir_p(File.dirname(path))
163
+ # The handle is intentionally kept open until #close, so the block
164
+ # form of File.open would defeat the reuse between writes.
165
+ file = File.open(path, "a") # rubocop:disable Style/FileOpen
166
+ file.sync = @sync
167
+ file
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logaru
4
+ VERSION = "1.0.0"
5
+ end
data/lib/logaru.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "logaru/version"
4
+ require_relative "logaru/errors"
5
+ require_relative "logaru/level"
6
+ require_relative "logaru/formatter"
7
+ require_relative "logaru/logger"
8
+
9
+ # Entry point of the Logaru library.
10
+ module Logaru
11
+ class << self
12
+ # Returns the absolute path to the gem root directory.
13
+ def root
14
+ File.expand_path("..", __dir__)
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logaru
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - rpzerosixcode
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.4'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.90'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.90'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.7'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.7'
69
+ description: Ruby library focused on clear, organized, and configurable log output
70
+ for applications.
71
+ email:
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG.md
77
+ - LICENSE
78
+ - README.md
79
+ - Rakefile
80
+ - docs/ARCHITECTURE.md
81
+ - docs/FEATURES.md
82
+ - docs/SECURITY.md
83
+ - lib/logaru.rb
84
+ - lib/logaru/errors.rb
85
+ - lib/logaru/formatter.rb
86
+ - lib/logaru/level.rb
87
+ - lib/logaru/logger.rb
88
+ - lib/logaru/version.rb
89
+ homepage: https://github.com/rpzerosixcode/logaru
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ homepage_uri: https://github.com/rpzerosixcode/logaru
94
+ source_code_uri: https://github.com/rpzerosixcode/logaru/tree/main
95
+ documentation_uri: https://github.com/rpzerosixcode/logaru#readme
96
+ changelog_uri: https://github.com/rpzerosixcode/logaru/blob/main/CHANGELOG.md
97
+ keywords: ruby, logger, logging, logs
98
+ rubygems_mfa_required: 'true'
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 3.3.0
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.5.22
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: A Ruby library for clear, organized, and configurable log output.
118
+ test_files: []