pax_global_header00006660000000000000000000000064151543732120014515gustar00rootroot0000000000000052 comment=be88592651b1995787dbe2d1315912e65236d97f bdurand-lumberjack-ac97435/000077500000000000000000000000001515437321200156125ustar00rootroot00000000000000bdurand-lumberjack-ac97435/.github/000077500000000000000000000000001515437321200171525ustar00rootroot00000000000000bdurand-lumberjack-ac97435/.github/dependabot.yml000066400000000000000000000004701515437321200220030ustar00rootroot00000000000000# Dependabot update strategy version: 2 updates: - package-ecosystem: bundler directory: "/" schedule: interval: daily allow: # Automatically keep all runtime dependencies updated - dependency-name: "*" dependency-type: "production" versioning-strategy: lockfile-only bdurand-lumberjack-ac97435/.github/workflows/000077500000000000000000000000001515437321200212075ustar00rootroot00000000000000bdurand-lumberjack-ac97435/.github/workflows/continuous_integration.yml000066400000000000000000000031361515437321200265460ustar00rootroot00000000000000name: Continuous Integration on: push: branches: - main - actions-* tags: - v* pull_request: branches-ignore: - actions-* workflow_dispatch: env: BUNDLE_CLEAN: "true" BUNDLE_PATH: vendor/bundle BUNDLE_JOBS: 3 BUNDLE_RETRY: 3 jobs: build: name: build ruby-${{ matrix.ruby }} ${{ matrix.appraisal }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - ruby: "ruby" standardrb: true yard: true - ruby: "jruby" - ruby: "3.4" - ruby: "3.3" - ruby: "3.2" - ruby: "3.1" - ruby: "3.0" - ruby: "2.7" steps: - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby }} uses: ruby/setup-ruby@v1 with: ruby-version: "${{ matrix.ruby }}" - name: Setup bundler if: matrix.bundler != '' run: | gem uninstall bundler --all gem install bundler --no-document --version ${{ matrix.bundler }} - name: Set Appraisal bundle if: matrix.appraisal != '' run: | echo "using gemfile gemfiles/${{ matrix.appraisal }}.gemfile" bundle config set gemfile "gemfiles/${{ matrix.appraisal }}.gemfile" - name: Install gems run: | bundle install - name: Run Tests run: bundle exec rake - name: standardrb if: matrix.standardrb == true run: bundle exec standardrb - name: yard if: matrix.yard == true run: bundle exec yard --fail-on-warning bdurand-lumberjack-ac97435/.gitignore000066400000000000000000000002131515437321200175760ustar00rootroot00000000000000.tm_properties .bundle /gemfiles/*.lock .yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ .DS_Store Gemfile.lock .ruby-version bdurand-lumberjack-ac97435/.standard.yml000066400000000000000000000002631515437321200202140ustar00rootroot00000000000000ruby_version: 2.7 format: progress ignore: - '**/*': - Standard/SemanticBlocks - Performance/RegexpMatch - 'spec/**/*': - Lint/UselessAssignment - Lint/Void bdurand-lumberjack-ac97435/ARCHITECTURE.md000066400000000000000000000277171515437321200200340ustar00rootroot00000000000000# Lumberjack Logging Framework Architecture This document provides a comprehensive overview of the Lumberjack logging framework architecture, illustrating how the various components work together to provide a flexible, high-performance logging solution for Ruby applications. ## Overview Lumberjack is a structured logging framework that extends Ruby's standard Logger with advanced features including: - **Structured logging** with attributes (key-value pairs) - **Context isolation** for scoping logging behavior - **Flexible output devices** supporting files, streams, and custom destinations - **Customizable formatters** for messages and attributes - **Thread and fiber safety** for concurrent applications - **Hierarchical logger forking** for component isolation ## Core Architecture The framework follows a layered architecture with clear separation of concerns: ```mermaid classDiagram %% Core Logger Classes class Logger { +Device device +Context context +EntryFormatter formatter +initialize(device, options) +info(message, attributes) +debug(message, attributes) +error(message, attributes) +add_entry(severity, message, progname, attributes) +tag(attributes, &block) +context(&block) +fork(options) ForkedLogger } class ContextLogger { <> +level() Integer +level=(value) void +progname() String +progname=(value) void +add_entry(severity, message, progname, attributes) +tag(attributes, &block) +context(&block) +attributes() Hash } class ForkedLogger { +Logger parent_logger +Context context +initialize(parent_logger) +add_entry(severity, message, progname, attributes) } %% Context and Attribute Management class Context { +Hash attributes +Integer level +String progname +Integer default_severity +initialize(parent_context) +assign_attributes(attributes) +clear_attributes() } class AttributesHelper { +Hash attributes +initialize(attributes) +update(attributes) +delete(*names) +[](key) Object +[]=(key, value) } %% Entry and Formatting class LogEntry { +Time time +Integer severity +String message +String progname +Integer pid +Hash attributes +initialize(time, severity, message, progname, pid, attributes) +severity_label() String +to_s() String } class EntryFormatter { +Formatter message_formatter +AttributeFormatter attribute_formatter +format(message, attributes) Array +format_class(classes, formatter, *args) self +format_message(classes, formatter, *args) self +format_attributes(classes, formatter, *args) self +format_attribute_name(names, formatter, *args) self +call(severity, timestamp, progname, msg) String } class Formatter { +Hash class_formatters +add(klass, formatter, *args, &block) +remove(klass) +format(message) Object } class AttributeFormatter { +Hash attribute_formatters +Formatter class_formatter +Formatter default_formatter +add(names_or_classes, formatter, *args, &block) self +add_class(classes, formatter, *args, &block) self +add_attribute(names, formatter, *args, &block) self +default(formatter, *args, &block) self +remove_class(classes) self +remove_attribute(names) self +format(attributes) Hash +include_class?(class_or_name) Boolean } %% Device Architecture class Device { <> +write(entry) void +flush() void +close() void +reopen(logdev) void +datetime_format() String +datetime_format=(format) void } class DeviceWriter["Device::Writer"] { +IO stream +Template template +Buffer buffer +initialize(stream, options) +write(entry) void +flush() void +close() void +reopen(logdev) void +datetime_format() String +datetime_format=(format) void } class DeviceLogFile["Device::LogFile"] { +String path +initialize(path, options) +reopen(logdev) void } class DeviceDateRollingLogFile["Device::DateRollingLogFile"] { +String path +String frequency +initialize(path, options) +roll_file?() Boolean } class DeviceSizeRollingLogFile["Device::SizeRollingLogFile"] { +String path +Integer max_size +Integer keep +initialize(path, options) +roll_file?() Boolean } class DeviceMulti["Device::Multi"] { +Array devices +initialize(*devices) +write(entry) void +flush() void +close() void +reopen(logdev) void +datetime_format() String +datetime_format=(format) void } class DeviceTest["Device::Test"] { +Array entries +Integer max_entries +initialize(options) +write(entry) void +include?(options) Boolean +match(**options) LogEntry +clear() void } class DeviceNull["Device::Null"] { +initialize() +write(entry) void } class DeviceLoggerWrapper["Device::LoggerWrapper"] { +Logger logger +initialize(logger) +write(entry) void } class DeviceBuffer["Device::Buffer"] { +Array values +Integer size +initialize() +<<(string) void +empty?() Boolean +pop!() Array +clear() void } %% Template System class Template { +String template +String time_format +String attribute_format +initialize(template, options) +call(entry) String } %% Utility Classes class Utils { +deprecated(method, message, &block) Object +hostname() String +current_line() String +flatten_attributes(hash) Hash +expand_attributes(hash) Hash +global_pid() String +thread_name() String +global_thread_id() String } class Severity { +level_to_label(level) String +label_to_level(label) Integer +coerce(value) Integer } %% Relationships Logger --|> ContextLogger : implements ForkedLogger --|> Logger : inherits Logger --* Device : uses Logger --* Context : has Logger --* EntryFormatter : uses ForkedLogger --* Logger : forwards to Context --* AttributesHelper : uses EntryFormatter --* Formatter : uses EntryFormatter --* AttributeFormatter : uses AttributeFormatter --* Formatter : uses Device <|-- DeviceWriter : implements Device <|-- DeviceMulti : implements Device <|-- DeviceTest : implements Device <|-- DeviceNull : implements Device <|-- DeviceLoggerWrapper : implements DeviceWriter <|-- DeviceLogFile : inherits DeviceLogFile <|-- DeviceDateRollingLogFile : inherits DeviceLogFile <|-- DeviceSizeRollingLogFile : inherits DeviceWriter --* Template : uses DeviceWriter --* DeviceBuffer : uses Logger --> LogEntry : creates EntryFormatter --> LogEntry : processes Device --> LogEntry : receives DeviceMulti --* Device : aggregates DeviceLoggerWrapper --* Logger : forwards to DeviceTest --* LogEntry : stores ``` ## Component Responsibilities ### Core Logger Components **Logger** - Main entry point for logging operations - Manages device, context, and formatting coordination - Provides standard logging methods (info, debug, error, etc.) - Handles context creation and attribute management **ContextLogger** - Mixin providing context-aware logging capabilities - Manages level, progname, and attribute scoping - Supports hierarchical contexts and attribute inheritance - Thread and fiber-safe context isolation **ForkedLogger** - Creates isolated logger instances that forward to parent loggers - Enables component-specific logging configuration - Maintains independent context while sharing output infrastructure ### Context and Attribute Management **Context** - Stores scoped logging configuration (level, progname, attributes) - Supports hierarchical inheritance from parent contexts - Provides isolation for block-scoped logging behavior **AttributesHelper** - Manages attribute hash manipulation and access - Supports dot notation for nested attribute access - Handles attribute merging and deletion operations ### Entry Processing Pipeline **LogEntry** - Immutable data structure representing a single log event - Contains all metadata: timestamp, severity, message, attributes - Provides formatted string representation for output **EntryFormatter** - Coordinates message and attribute formatting - Delegates to specialized formatters for different data types - Handles complex formatting scenarios with embedded attributes **Formatter & AttributeFormatter** - Class-based and name-based formatting rules - Recursive formatting for nested data structures - Extensible formatting system with built-in formatters ### Device Architecture **Device (Abstract Base)** - Defines interface for log output destinations - Supports lifecycle methods (flush, close, reopen) - Enables pluggable output architecture **WriterDevice** - Writes formatted entries to IO streams - Supports templated output formatting - Handles encoding and error recovery **MultiDevice** - Broadcasts entries to multiple target devices - Enables redundant logging and output splitting - Maintains consistent state across all targets **Specialized Devices** - **LogFileDevice**: File-based logging with rotation - **TestDevice**: In-memory capture for testing - **NullDevice**: Silent operation for performance testing - **LoggerDevice**: Forwards to other Logger instances ## Logging Flow Sequence The following sequence diagram illustrates the complete flow of a log entry through the Lumberjack framework: ```mermaid sequenceDiagram participant App as Application participant Logger as Logger participant Context as Context participant EntryFormatter as EntryFormatter participant MsgFormatter as Message Formatter participant AttrFormatter as Attribute Formatter participant Device as Device participant Template as Template participant Output as Output Stream %% Context Setup App->>Logger: context do |ctx| Logger->>Context: new(parent_context) Context-->>Logger: context instance App->>Logger: tag(user_id: 123) Logger->>Context: assign_attributes({user_id: 123}) %% Logging Call App->>Logger: info("User login", ip: "192.168.1.1") Logger->>Logger: check level >= INFO %% Entry Creation Logger->>Logger: merge_all_attributes() Note over Logger: Combines global, context, and local attributes Logger->>LogEntry: new(time, severity, message, progname, pid, attributes) LogEntry-->>Logger: entry instance %% Entry Formatting Logger->>EntryFormatter: format(message, attributes) EntryFormatter->>MsgFormatter: format("User login") MsgFormatter-->>EntryFormatter: formatted message EntryFormatter->>AttrFormatter: format({user_id: 123, ip: "192.168.1.1"}) AttrFormatter-->>EntryFormatter: formatted attributes EntryFormatter-->>Logger: [formatted_message, formatted_attributes] %% Device Writing Logger->>Device: write(entry) alt WriterDevice Device->>Template: call(entry) Template-->>Device: formatted string Device->>Output: write(formatted_string) Output-->>Device: success else MultiDevice Device->>Device: devices.each loop Each Target Device Device->>Device: target.write(entry) end else TestDevice Device->>Device: entries << entry end Device-->>Logger: success Logger-->>App: true %% Context Cleanup Note over Logger,Context: Context automatically cleaned up when block exits ``` bdurand-lumberjack-ac97435/CHANGELOG.md000066400000000000000000000506711515437321200174340ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## 2.0.5 ### Added - Added `isatty` method on loggers for IO compatibility. ## 2.0.4 ### Fixed - Hardened multi-threaded context locals handling to ensure compatibility with all Ruby implementations. ## 2.0.3 ### Added - Added `isolation_level` to loggers. By default this is set to `:fiber` which isolates logger contexts to the current fiber. That is each fiber will get its own context stack and starting a new fiber will start with a clean context. Setting `isolation_level` to `:thread` will isolate contexts to the current thread instead. This is useful if your application does not share fibers between threads and you want to maintain context across fibers in the same thread. ## 2.0.2 ### Changed - Attempts to use the logger inside a logging call (e.g. from a in a formatter) will now print the log message to STDERR. Previously such log messages would be silently dropped in order to prevent infinite recursion. ## 2.0.1 ### Fixed - Merge attributes in forked loggers in the correct order so that attributes from the forked logger override those from the parent logger. ## 2.0.0 This is a major update with several breaking changes. See the [upgrade guide](UPGRADE_GUIDE.md) for details on breaking changes. ### Added - Added `Lumberjack::EntryFormatter` class to provide a unified interface for formatting log entry details. Going forward this is the preferred way to define log entry formatters. `Lumberjack::Logger#formatter` now returns an entry formatter. - Added `Lumberjack::Logger#tag!` as the preferred method for adding global tags to a logger. - Added `Lumberjack::Logger#untag!` to remove global tags from a logger. - Added `Lumberjack::Logger#in_context?` as a replacement for `Lumberjack::Logger#in_tag_context?` and `Lumberjack.in_context?` as a replacement for `Lumberjack.context?`. - Added `Lumberjack::Logger#tag_all_contexts` as a means to add attributes to parent context blocks. This allows setting attributes for the scope of the outermost context block. - Added `ensure_context` methods to `Lumberjack` and `Lumberjack::Logger` to ensure that a logging context exists and only create one if necessary. - Added IO compatibility methods for logging. Calling `logger.write`, `logger.puts`, `logger.print`, or `logger.printf` will write log entries. The severity of the log entries can be set with `default_severity`. - Added `Lumberjack::Device::LoggerWrapper` as a device that forwards entries to another Lumberjack logger. - Added `Lumberjack::Device::Test` class for use in testing logging functionality. This device will buffer log entries and has `match?` and `include?` methods that can be used for assertions in tests. - Added support for standard library `Logger::Formatter`. This is for compatibility with the standard library `Logger`. If a standard library logger is passed to `Lumberjack::Logger` as the formatter, it will override the template when writing to a stream. Tags are not available in the output when using a standard library formatter. - Classes can now define their own formatting in logs by implementing the `to_log_format` method. If an object responds to this method, it will be called in lieu of looking up the formatter by class. This allows a pattern of defining log formatting along with the code rather than in a an initializer. - Tag formatters can now add class formatters by class name using the `add_class` method. This allows setting a class formatter before the class has been loaded. - A tag format can now be passed to the `Lumberjack::Template` class to specify how to format tag name/value pairs. The default is "[%s:%s]". - Added `TRACE` logging level for logging at an even lower level than `DEBUG`. `Lumberjack::Logger#trace` can be used to log messages at this level. - Added `Lumberjack::ForkedLogger` which is a wrapper around a logger with a separate context. A forked logger has a parent logger which it will write its log entries through. It will inherit the level, progname, and tags from a parent logger, but has its own local context isolated from the parent logger. You can change the level, progname, and add tags on the forked logger without impacting the parent logger. Forked loggers can be obtained from the current logger by calling `Lumberjack::Logger#fork`. - Added `Lumberjack::Utils.current_line` as a helper method for getting the current line of code. - Added `Lumberjack.build_formatter` as a helper method for building entry formatters. - Added merge method on formatters to allow merging in formats from other formatters. - Templates can now use variations on the severity label with a format option added to the placeholder: `{{severity(padded)}}`, `{{severity(char)}}`, `{{severity(emoji)}}`, and `{{severity(level)}}`. - Log entries in templates can now be colorized by severity with the `colorize: true`. - Added `Lumberjack::Formatter::Tags` for formatting attributes as "tags" in the logs. Arrays of values will be formatted as "[val1] [val2]" and hashes will be formatted as "[key1=value1] [key2=value2]". - Added `Lumberjack::FormatterRegistry` as a means of associating formatters with a symbol. Symbols can be used when adding class and attribute formatters. This extends the behavior previously limited to the built in formatters so that users can define their own formatters and register them for use. - Added `Lumberjack::DeviceRegistry` as a means for associating devices with a symbol. Symbols can then be passed to the constructor when creating a logger and the logger will take care of instantiating the device. - Added `Lumberjack::TemplateRegistry` as a means for associating templates with a symbol. Symbols can then be passed to the logger constructor in lieu of the template definition. - Added `Lumberjack::Logger#clear_attributes` to remove all attributes from the logger. - Added `Lumberjack::MessageAttributes` to replace `Lumberjack::Formatter::TaggedMessage`. - Added `Lumberjack::RemapAttribute` to facilitate attribute remapping in attribute formatters. ### Changed - `Lumberjack::Logger` now inherits from `::Logger` instead of just having API compatibility with the standard library `Logger` class. - **Breaking Change** The default log level is now DEBUG instead of INFO. - The severity label for log entries with an unknown level is now ANY instead of UNKNOWN. - **Breaking Change** Changing logger level or progname inside a context block will now only be in effect inside the block. - **Breaking Change** `LumberJack::Logger#context` now yields a `Lumberjack::Context` rather than a `Lumberjack::TagContext`. It must be called with a block and can no longer be used to return the current context. `Lumberjack.context` must also now be called with a block. - `Lumberjack::TagContext` has been renamed to `Lumberjack::AttributesHelper`. - `Lumberjack::TagFormatter` has been renamed to `Lumberjack::AttributeFormatter`. - **Breaking Change** `Lumberjack::Formatter` no longer includes any default formats. You can still get the default formatter with `Lumberjack::Formatter.default`. You can use the `include` method to merge in the default formats from this formatter. You can also use the default formatter by passing in `formatter: :default` in the logger constructor. The `empty` method has been deprecated since it is no longer needed. - `Lumberjack::Logger#add_entry` does not check the logger level and will add the entry regardless of the severity. This method is an internal API method and is now documented as such. - Logging to files will now use the standard library `Logger::LogDevice` class for file output and rolling. - The `Lumberjack::Device::Writer` class now takes an `autoflush` option. Setting it to false will disable synchronous I/O. - `Lumberjack.tag` can now be called with a block to set up a new context. ### Removed - **Breaking Change** Removed deprecated unit of work id code. These have been replaced with tags. - **Breaking Change** Removed deprecated support for setting global tags with `Lumberjack::Logger#tag`. Now calling `tag` outside of a block or context will be ignored. Use `tag!` to set global tags on a logger. - Removed internal buffer from the `Lumberjack::Device::Writer` class. This functionality was more useful in the days of slower I/O operations when logs were written to spinning hard disks. The functionality is no longer as useful and is not worth the overhead. The `Lumberjack::Logger.last_flushed_at` method has also been removed. If you need buffered logging, use the new `Lumberjack::Device::Buffer` class to wrap another device. - **Breaking Change** When adding a formatter with `Lumberjack::Formatter#add` you can no longer pass the formatter as a class name (i.e. this won't work: `formatter.add(MyClass, "Lumberjack::Formatter::IdFormatter"); the formatter can be a class, symbol, callable object, or a block). - Removed support for Ruby versions < 2.7. ### Deprecated - `Lumberjack::Logger` now takes keyword arguments instead of an options hash. If you were passing in options as a hash, you now need to doublesplat it: `Lumberjack::Logger.new(stream, **options)`. - "Tags" are now called "attributes" to better align with best practices. In logging parlance "tags" are generally an array of strings. The main interface to adding log attributes with `Lumberjack::Logger#tag` has not changed. In this case we are using "tag" as a verb as in "to tag a log entry with attributes". The public interfaces that used "tag" in the method names have all been deprecated and will be removed in a future release. - `Lumberjack.context_tags` - `Lumberjack::Logger#tags` - `Lumberjack::Logger#tag_value` - `Lumberjack::Logger#tagged` - `Lumberjack::Logger#silence` - `Lumberjack::Logger#log_at` - `Lumberjack::Logger#untagged` - `Lumberjack::Logger#tag_formatter` - `Lumberjack::Logger#in_tag_context?` - `Lumberjack::Logger#tag_globally` - `Lumberjack::Logger#remove_tag` - `Lumberjack::Logger#set_progname` - `Lumberjack::LogEntry#tag` - `Lumberjack::LogEntry#tags` - `Lumberjack::LogEntry#nested_tags` - `Lumberjack::Utils.flatten_tags` - `Lumberjack::Utils.expand_tags` - `Lumberjack::TagContext` - `Lumberjack::TagFormatter` - `Lumberjack::TagFormatter#add` - `Lumberjack::TagFormatter#remove` - `Lumberjack::Tags` - `Lumberjack::Formatter::TaggedMessage` - `Lumberjack::Device::RollingLogFile` - `Lumberjack::Device::SizeRollingLogFile` - The Rails compatibility methods on `Lumberjack::Logger` (`tagged`, `silence`, `log_at`) have been moved to the [lumberjack_rails](https://github.com/bdurand/lumberjack_rails) gem. Installing that gem will restore these methods in a non-deprecated form. - Templates now use mustache syntax for placeholders instead of the colon prefix (i.e. `{{message}}` instead of `:message`). The `:tags` placeholder is also now called `{{attributes}}`. ## 1.4.2 ### Fixed - Fixed issue where calling `Lumberjack::LogEntry#tag` would raise an error if there were no tags set on the log entry. ## 1.4.1 ### Changed - Catch errors when formatting values so that it doesn't prevent logging. Otherwise there can be no way to log that the error occurred. Values that produced errors in the formatter will now be shown in the logs as "". ## 1.4.0 ### Changed - Tags are consistently flattened internally to dot notation keys. This makes tag handling more consistent when using nested hashes as tag values. This changes how nested tags are merged, though. Now when new nested tags are set they will be merged into the existing tags rather than replacing them entirely. So `logger.tag(foo: {bar: "baz"})` will now merge the `foo.bar` tag into the existing tags rather than replacing the entire `foo` tag. - The `Lumberjack::Logger#context` method can now be called without a block. When called with a block it sets up a new tag context for the block. When called without a block, it returns the current tag context in a `Lumberjack::TagContext` object which can be used to add tags to the current context. - Tags in `Lumberjack::LogEntry` are now always stored as a hash of flattened keys. This means that when tags are set on a log entry, they will be automatically flattened to dot notation keys. The `tag` method will return a hash of sub-tags if the tag name is a tag prefix. ### Added - Added `Lumberjack::LogEntry#nested_tags` method to return the tags as a nested hash structure. ## 1.3.4 ### Added - Added `Lumberjack::Logger#with_progname` alias for `set_progname` to match the naming convention used for setting temporary levels. ### Fixed - Ensure that the safety check for circular calls to `Lumberjack::Logger#add_entry` cannot lose state. ## 1.3.3 ### Added - Added `Lumberjack::Utils#expand_tags` method to expand a hash of tags that may contain nested hashes or dot notation keys. ### Changed - Updated `Lumberjack::Utils#flatten_tags` to convert all keys to strings. ## 1.3.2 ### Fixed - Fixed `NoMethodError` when setting the device via the `Lumberjack::Logger#device=` method. ## 1.3.1 ### Added - Added `Lumberjack::Logger#context` method to set up a context block for the logger. This is the same as calling `Lumberjack::Logger#tag` with an empty hash. - Log entries now remove empty tag values so they don't have to be removed downstream. ### Fixed - ActiveSupport::TaggedLogger now calls `Lumberjack::Logger#tag_globally` to prevent deprecation warnings. ## 1.3.0 ### Added - Added `Lumberjack::Formatter::TaggedMessage` to allow extracting tags from log messages via a formatter in order to better support structured logging of objects. - Added built in `:round` formatter to round numbers to a specified number of decimal places. - Added built in `:redact` formatter to redact sensitive information from log tags. - Added support in `Lumberjack::TagFormatter` for class formatters. Class formatters will be applied to any tag values that match the class. - Apply formatters to enumerable values in tags. Name formatters are applied using dot syntax when a tag value contains a hash. - Added support for a dedicated message formatter that can override the default formatter on the log message. - Added support for setting tags from the request environment in `Lumberjack::Rack::Context` middleware. - Added helper methods to generate global PID's and thread ids. - Added `Lumberjack::Logger#tag_globally` to explicitly set a global tag for all loggers. - Added `Lumberjack::Logger#tag_value` to get the value of a tag by name from the current tag context. - Added `Lumberjack::Utils.hostname` to get the hostname in UTF-8 encoding. - Added `Lumberjack::Utils.global_pid` to get a global process id in a consistent format. - Added `Lumberjack::Utils.global_thread_id` to get a thread id in a consistent format. - Added `Lumberjack::Utils.thread_name` to get a thread name in a consistent format. - Added support for `ActiveSupport::Logging.logger_outputs_to?` to check if a logger is outputting to a specific IO stream. - Added `Lumberjack::Logger#log_at` method to temporarily set the log level for a block of code for compatibility with ActiveSupport loggers. ### Changed - Default date/time format for log entries is now ISO-8601 with microsecond precision. - Tags that are set to hash values will now be flattened into dot-separated keys in templates. ### Removed - Removed support for Ruby versions < 2.5. ### Deprecated - All unit of work related functionality from version 1.0 has been officially deprecated and will be removed in version 2.0. Use tags instead to set a global context for log entries. - Calling `Lumberjack::Logger#tag` without a block is deprecated. Use `Lumberjack::Logger#tag_globally` instead. ## 1.2.10 ### Added - Added `with_level` method for compatibility with the latest standard library logger gem. ### Fixed - Fixed typo in magic frozen string literal comments. (thanks @andyw8 and @steveclarke) ## 1.2.9 ### Added - Allow passing in formatters as class names when adding them. - Allow passing in formatters initialization arguments when adding them. - Add truncate formatter for capping the length of log messages. ## 1.2.8 ### Added - Add `Logger#untagged` to remove previously set logging tags from a block. - Return result of the block when a block is passed to `Logger#tag`. ## 1.2.7 ### Fixed - Allow passing frozen hashes to `Logger#tag`. Tags passed to this method are now duplicated so the logger maintains it's own copy of the hash. ## 1.2.6 ### Added - Add Logger#remove_tag ### Fixed - Fix `Logger#tag` so it only ads to the current block's logger tags instead of the global tags if called inside a `Logger#tag` block. ## 1.2.5 ### Added - Add support for bang methods (error!) for setting the log level. ### Fixed - Fixed logic with recursive reference guard in StructuredFormatter so it only suppresses Enumerable references. ## 1.2.4 ### Added - Enhance `ActiveSupport::TaggedLogging` support so code that Lumberjack loggers can be wrapped with a tagged logger. ## 1.2.3 ### Fixed - Fix structured formatter so no-recursive, duplicate references are allowed. ## 1.2.2 ### Fixed - Prevent infinite loops in the structured formatter where objects have backreferences to each other. ## 1.2.1 ### Fixed - Prevent infinite loops where logging a statement triggers the logger. ## 1.2.0 ### Added - Enable compatibility with `ActiveSupport::TaggedLogger` by calling `tagged_logger!` on a logger. - Add `tag_formatter` to logger to specify formatting of tags for output. - Allow adding and removing classes by name to formatters. - Allow adding and removing multiple classes in a single call to a formatter. - Allow using symbols and strings as log level for silencing a logger. - Ensure flusher thread gets stopped when logger is closed. - Add writer for logger device attribute. - Handle passing an array of devices to a multi device. - Helper method to get a tag with a specified name. - Add strip formatter to strip whitespace from strings. - Support non-alpha numeric characters in template variables. - Add backtrace cleaner to ExceptionFormatter. ## 1.1.1 ### Added - Replace Procs in tag values with the value of calling the Proc in log entries. ## 1.1.0 ### Added - Change `Lumberjack::Logger` to inherit from ::Logger - Add support for tags on log messages - Add global tag context for all loggers - Add per logger tags and tag contexts - Reimplement unit of work id as a tag on log entries - Add support for setting datetime format on log devices - Performance optimizations - Add Multi device to output to multiple devices - Add `DateTimeFormatter`, `IdFormatter`, `ObjectFormatter`, and `StructuredFormatter` - Add rack `Context` middleware for setting thread global context - Add support for modules in formatters ### Removed - End support for ruby versions < 2.3 ## 1.0.13 ### Added - Added `:min_roll_check` option to `Lumberjack::Device::RollingLogFile` to reduce file system checks. Default is now to only check if a file needs to be rolled at most once per second. - Force immutable strings for Ruby versions that support them. ### Changed - Reduce amount of code executed inside a mutex lock when writing to the logger stream. ## 1.0.12 ### Added - Add support for `ActionDispatch` request id for better Rails compatibility. ## 1.0.11 ### Fixed - Fix Ruby 2.4 deprecation warning on Fixnum (thanks @koic). - Fix gemspec files to be flat array (thanks @e2). ## 1.0.10 ### Added - Expose option to manually roll log files. ### Changed - Minor code cleanup. ## 1.0.9 ### Added - Add method so Formatter is compatible with `ActiveSupport` logging extensions. ## 1.0.8 ### Fixed - Fix another internal variable name conflict with `ActiveSupport` logging extensions. ## 1.0.7 ### Fixed - Fix broken formatter attribute method. ## 1.0.6 ### Fixed - Fix internal variable name conflict with `ActiveSupport` logging extensions. ## 1.0.5 ### Changed - Update docs. - Remove autoload calls to make thread safe. - Make compatible with Ruby 2.1.1 Pathname. - Make compatible with standard library Logger's use of progname as default message. ## 1.0.4 ### Added - Add ability to supply a unit of work id for a block instead of having one generated every time. ## 1.0.3 ### Fixed - Change log file output format to binary to avoid encoding warnings. - Fixed bug in log file rolling that left the file locked. ## 1.0.2 ### Fixed - Remove deprecation warnings under ruby 1.9.3. - Add more error checking around file rolling. ## 1.0.1 ### Fixed - Writes are no longer buffered by default. ## 1.0.0 ### Added - Initial release bdurand-lumberjack-ac97435/Gemfile000066400000000000000000000006001515437321200171010ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem "rake" gem "irb" gem "rspec", "~> 3.12" gem "timecop" gem "standard", require: false, platforms: [:mri] gem "simplecov", require: false, platforms: [:mri] gem "yard", require: false, platforms: [:mri] group :development do gem "ruby-prof", require: false, platforms: [:mri] gem "memory_profiler", require: false, platforms: [:mri] end bdurand-lumberjack-ac97435/MIT_LICENSE.txt000066400000000000000000000020401515437321200201420ustar00rootroot00000000000000Copyright (c) 2011 Brian Durand Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bdurand-lumberjack-ac97435/README.md000066400000000000000000000774731515437321200171130ustar00rootroot00000000000000# Lumberjack [![Continuous Integration](https://github.com/bdurand/lumberjack/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/lumberjack/actions/workflows/continuous_integration.yml) [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard) [![Gem Version](https://badge.fury.io/rb/lumberjack.svg)](https://badge.fury.io/rb/lumberjack) Lumberjack is an extension to the Ruby standard library `Logger` class, designed to provide advanced, flexible, and structured logging for Ruby applications. It builds on the familiar `Logger` API, adding powerful features for modern logging needs: - **Attributes for Structured Logging:** Use attributes to attach structured, machine-readable metadata to log entries, enabling better filtering, searching, and analytics. - **Context Isolation:** Isolate logging behavior to specific blocks of code. The attributes, level, and progname for the logger can all be changed in a context block and only impact the log messages created within that block. - **Formatters:** Control how objects are logged with customizable formatters for messages and attributes. - **Devices and Templates:** Choose from a variety of output devices and templates to define the format and destination of your logs. - **Forked Loggers:** Create independent logger instances that inherit context from a parent logger, allowing for isolated logging configurations in different parts of your application. - **Testing Tools:** Built-in testing devices and helpers make it easy to assert logging behavior in your test suite. The philosophy behind the library is to promote use of structured logging with the standard Ruby Logger API as a foundation. The developer of a piece of functionality should only need to worry about the data that needs to be logged for that functionality and not how it is logged or formatted. Loggers can be setup with global attributes and formatters that handle these concerns. ## Table of Contents - [Usage](#usage) - [Context Isolation](#context-isolation) - [Context Blocks](#context-blocks) - [Nested Context Blocks](#nested-context-blocks) - [Forking Loggers](#forking-loggers) - [Isolation Level](#isolation-level) - [Structured Logging With Attributes](#structured-logging-with-attributes) - [Basic Attribute Logging](#basic-attribute-logging) - [Adding attributes to the logger](#adding-attributes-to-the-logger) - [Global Logger Attributes](#global-logger-attributes) - [Nested Attributes and Complex Data](#nested-attributes-and-complex-data) - [Attribute Inheritance and Merging](#attribute-inheritance-and-merging) - [Working with Array Attributes](#working-with-array-attributes) - [Formatters](#formatters) - [Message Formatters](#message-formatters) - [Attribute Formatters](#attribute-formatters) - [Built-in Formatters](#built-in-formatters) - [Custom Formatters](#custom-formatters) - [Building An Entry Formatter](#building-an-entry-formatter) - [Merging Formatters](#merging-formatters) - [Devices and Templates](#devices-and-templates) - [Built-in Devices](#built-in-devices) - [Custom Devices](#custom-devices) - [Templates](#templates) - [Testing Utilities](#testing-utilities) - [Using As A Stream](#using-as-a-stream) - [Integrations](#integrations) - [Installation](#installation) - [Contributing](#contributing) - [License](#license) ## Usage ### Context Isolation Lumberjack provides context isolation that allow you to temporarily modify logging behavior for specific blocks of code or create independent logger instances. This is particularly useful for isolating logging configuration in different parts of your application without affecting the global logger state. #### Context Blocks Context blocks allow you to temporarily change the logger's configuration (level, progname, and attributes) for a specific block of code. When the block exits, the logger returns to its previous state. Context blocks and forked loggers are thread and fiber-safe, maintaining isolation across concurrent operations: ```ruby logger.level = :info # Temporarily change log level for debugging a specific section logger.context do logger.level = :debug logger.debug("This debug message will be logged") end # Back to info level - debug messages are filtered out again logger.debug("This won't be logged") ``` You can use `with_level`, `with_progname`, and `tag` to setup a context block with a specific level, progname, or attributes. As a best practice, every main unit of work in your application (i.e. HTTP request, background job, etc.) should have a context block. This ensures that any attributes or changes to the logger are scoped to that unit of work and do not leak into other parts of the application. ##### Nested Context Blocks Context blocks can be nested, with inner contexts inheriting and potentially overriding outer context settings: ```ruby logger.context do logger.tag(user_id: 123, service: "api") logger.info("API request started") # Includes user_id: 123, service: "api" logger.context(endpoint: "/users", service: "user_service") do logger.tag(service: "user_service", endpoint: "/users") logger.info("Processing user data") # Includes: user_id: 123, service: "user_service", endpoint: "/users" end logger.info("API request completed") # Back to: user_id: 123, service: "api" end ``` #### Forking Loggers Logger forking creates independent logger instances that inherit the parent logger's current context. Changes made to the forked logger will not affect the parent logger. Forked loggers are useful when there is a section of your application that requires different logging behavior. Forked loggers are cheap to create, so you can use them liberally. ```ruby main_logger = Lumberjack::Logger.new main_logger.tag!(version: "1.0.0") # Create a forked logger for a specific component user_service_logger = main_logger.fork(progname: "UserService", level: :debug) user_service_logger.tag!(component: "user_management") user_service_logger.debug("Debug info") # Includes version and component attributes main_logger.info("Main logger info") # Includes only version attribute main_logger.debug("Main logger debug info") # Not logged since level is :info ``` #### Isolation Level By default each fiber will have its own logging context. This is useful in asynchronous applications where multiple fibers may be running concurrently. You can change the isolation level to `:thread` if you want each thread to have its own logging context instead. ```ruby Lumberjack.isolation_level = :thread # Set isolation level globally logger = Lumberjack::Logger.new(STDOUT, isolation_level: :thread) # Set isolation level per logger ``` Fiber isolation is the safest behavior since it completely isolates local contexts. However, in applications where threads are the main unit of work and fibers are never shared across threads, thread isolation may be more appropriate. Otherwise you can end up with the logger losing the context block when switching fibers within the same thread. ### Structured Logging With Attributes Lumberjack extends standard logging with **attributes** (structured key-value pairs) that add context and metadata to your log entries. This enables powerful filtering, searching, and analytics capabilities. #### Basic Attribute Logging Add attributes with any logging method: ```ruby # Add attributes to individual log calls logger.info("User logged in", user_id: 123, ip_address: "192.168.1.100") logger.error("Payment failed", user_id: 123, amount: 29.99, error: "card_declined") # Attributes can be any type logger.debug("Processing data", records_count: 1500, processing_time: 2.34, metadata: { batch_id: "abc-123", source: "api" }, timestamp: Time.now ) ``` > [!Note] > Attributes are passed in log statements in the little used `progname` argument that is defined in the standard Ruby Logger API. This attribute is normally used to set a specific program name for the log entry that overrides the default program name on the logger. > > The only difference in the API is that Lumberjack loggers can take a Hash to set attributes instead of just a string. You can still pass a string to override the program name. #### Adding attributes to the logger Use the `tag` method to tag the the current context with attributes. The attributes will be included in all log entries within that context. ```ruby logger.context do logger.tag(user_id: current_user.id, session: "abc-def") logger.info("Session started") # Includes user_id and session logger.debug("Loading user preferences") # Includes user_id and session logger.info("Dashboard rendered") # Includes user_id and session end logger.info("Outside of context") # Does not include user_id or session ``` You can also use the `tag` method with a block to open a new context and assign attributes. ```ruby # Apply attributes to all log entries within the block logger.tag(user_id: 123, session: "abc-def") do logger.info("Session started") logger.debug("Loading user preferences") logger.info("Dashboard rendered") end ``` Calling `tag` outside of a context without a block is a no-op and has no effect on the logger. You can also use the `tag_all_contexts` method to add attributes to all parent context blocks. This can be useful in cases where you need to set an attribute that should be included in all subsequent log entries for the duration of the process defined by the outermost context. Consider this example where we want to include the `user_id` attribute in all log entries. We need to set it on all parent contexts in order to have it extend beyond the current context block. ```ruby logger.context do logger.tag(request_id: "req-123") do logger.info("Processing request") # Includes request_id logger.tag(action: "login") do user_id = login_user(params) logger.tag(login_method: params[:login_method]) logger.tag_all_contexts(user_id: user_id) logger.info("User logged in") # Includes request_id, action, login_method, and user_id end logger.info("Request completed") # Includes request_id and user_id end ``` #### Global Logger Attributes You can add global attributes that apply to all log entries with the `tag!` method. ```ruby logger.tag!( version: "1.2.3", env: Rails.env, request_id: -> { Current.request_id } ) ``` If the value of an attribute is a `Proc`, it will be evaluated at runtime when the log entries are created. So in the above example, `request_id` will be dynamically set to the current request ID whenever a log entry is created. Note that blank attributes are never included in the log output, so if `Current.request_id` is `nil`, the `request_id` attribute will be omitted from the log entry. There is also a global `Lumberjack` context that applies to all Lumberjack loggers. ```ruby Lumberjack.tag(version: "1.2.3") do logger_1.info("Something happened") # Includes version logger_2.info("Something else happened") # Includes version end ``` #### Nested Attributes and Complex Data Attributes support nested structures and complex data types: ```ruby logger.info("API request completed", request: { method: "POST", path: "/api/users", headers: { "Content-Type" => "application/json" } }, response: { status: 201, duration_ms: 45.2, size_bytes: 1024 }, user: { id: 123, role: "admin", permissions: ["read", "write", "delete"] } ) ``` #### Attribute Inheritance and Merging Attributes from different sources are merged together, with more specific attributes taking precedence: ```ruby # Persistent logger attributes logger.tag!(service: "web", datacenter: "us-east-1") logger.tag(request_id: "req-123") do # Block-level attributes override any conflicts logger.tag(datacenter: "us-west-2") do logger.info("Processing request", user_id: 456, datacenter: "eu-central-1" # This takes highest precedence ) # Final attributes: { service: "web", request_id: "req-123", user_id: 456, datacenter: "eu-central-1" } end end ``` Attributes use dot notation for nested structures, so there is no difference between these statements: ```ruby logger.info("User signed in", user: {id: 123}) logger.info("User signed in", "user.id" => 123) ``` #### Working with Array Attributes A common practice is to add an array of values to a specific attribute. You can use the `append_to` method to append values to an array attribute in the current context. Like the `tag` method, this method can be called with a block to create a new context or without a block to update the current context. ```ruby logger.append_to(:tags, "api", "v1") do logger.info("API request started") # Includes tags: ["api", "v1"] logger.append_to(:tags, "users") logger.info("Processing user data") # Includes tags: ["api", "v1", "users"] end # You can also append to other array attributes logger.append_to(:categories, "billing", "premium") do logger.info("Processing premium billing") # Includes categories: ["billing", "premium"] end ``` ### Formatters Lumberjack provides a powerful formatting system that controls how objects are converted to strings in log entries. With this feature you can pass objects to the logger and rely on the formatters to handle formatting thereby simplifying the logging code and improving consistency. There are two types of formatters. #### Message Formatters Message formatters control how different types of objects are converted to strings when logged as messages. Message formatters are registered for specific classes or modules. When you log an object, the formatter will be looked up based on the object's class. ```ruby logger.formatter.format_message(Array) { |arr| arr.join(", ") } logger.info([1, 2, 3]) # Logs "1, 2, 3" ``` For log messages you can use the `Lumberjack::MessageAttributes` class to extract structured data from a log message. This can be used to allow logging objects directly and extracting metadata from the objects in the log attributes. ```ruby logger.formatter.format_message(Exception) do |error| Lumberjack::MessageAttributes.new( error.inspect, # This will be used as the log message error: { # This will be added to the log attributes type: error.class.name, message: error.message, backtrace: error.backtrace } ) end # This will now log the message as `exception.inspect` and pull # out the type, message, and backtrace into attributes. With this # you won't need to figure out how to log each exception and just # just log the object itself and let the formatter deal with it. logger.error(exception) ``` #### Attribute Formatters Attribute formatters control how attribute values (the key-value pairs in structured logging) are formatted. They provide fine-grained control over different attributes and data types. You can specify how to format object types when they are logged as attributes: ```ruby logger.formatter.format_attributes(Time, :date_time, "%Y-%m-%d %H:%M:%S") logger.formatter.format_attributes([Float, BigDecimal], :round, 2) logger.info("Data processed", created_at: Time.now, # → "2025-08-22 14:30:00" price: 29.099, # → 29.10 ) ``` You can also format attributes based on the attribute name: ```ruby # Configure attribute formatting logger.formatter.format_attribute_name("password") { |pwd| "[REDACTED]" } logger.formatter.format_attribute_name("email") { |email| email.downcase } logger.formatter.format_attribute_name("cost", :round, 2) # Now attributes are formatted according to the rules logger.info("User created", email: "JOHN@EXAMPLE.COM", # → "john@example.com" password: "secret123", # → "[REDACTED]" cost: 29.129999 # → 29.13 ) ``` You can remap attributes to other attribute names by returning a `Lumberjack::RemapAttribute` instance. ```ruby # Move the email attribute under the user attributes. logger.formatter.format_attribute_name("email") do |value| Lumberjack::RemapAttribute.new("user.email" => value) end # Transform duration_millis and duration_micros to seconds and move to # the duration attribute. logger.formatter.format_attribute_name("duration_ms") do |value| Lumberjack::RemapAttribute.new("duration" => value.to_f / 1000) end logger.formatter.format_attribute_name("duration_micros") do |value| Lumberjack::RemapAttribute.new("duration" => value.to_f / 1_000_000) end ``` Finally, you can add a default formatter for all other attributes: ```ruby logger.formatter.default_attribute_format { |value| value.to_s.strip[0..100] } ``` #### Built-in Formatters Lumberjack provides several predefined formatters that can be referenced by symbol: ```ruby logger = Lumberjack::Logger.new # Configure the formatter logger.formatter.format_class(Float, :round, 2) # Round floats to 2 decimals logger.formatter.format_class(Time, :date_time, "%H:%M") # Custom time format # Now these objects will be formatted according to the rules logger.info(3.14159) # "3.14" logger.info(Time.now) # "14:30" ``` **Available Built-in Formatters:** | Formatter | Purpose | |-----------|---------| | `:date_time` | Format time/date objects | | `:exception` | Format exceptions with stack traces | | `:id` | Extract object ID or specified field | | `:inspect` | Use Ruby's inspect method | | `:multiply` | Multiply numeric values | | `:object` | Generic object formatter | | `:pretty_print` | Pretty print using PP library | | `:redact` | Redact sensitive information | | `:round` | Round numeric values | | `:string` | Convert to string using to_s | | `:strip` | Strip whitespace from strings | | `:structured` | Recursively format collections | | `:tags` | Format values as tags in the format "[val1] [val2]" for arrays or "[key=value]" for hashes | | `:truncate` | Truncate long strings | #### Custom Formatters You can create custom formatters using blocks, callable objects, or custom classes: ```ruby # Block-based formatters logger.formatter.format_class(User) { |user| "User[#{user.id}:#{user.name}]" } logger.formatter.format_class(BigDecimal) { |decimal| "$#{decimal.round(2)}" } # Callable object formatters class TimeFormatter def call(time) time.strftime("%Y-%m-%d %H:%M:%S") end end logger.formatter.format_class(SecureString, PasswordFormatter.new) ``` Classes can also implement `to_log_format` to define how instances should be serialized for logging. This will apply to both message and attribute formatting. ```ruby class User attr_accessor :id, :name def to_log_format "User[id: #{ id }, name: #{ name }]" end end ``` Primitive classes (`String`, `Integer`, `Float`, `TrueClass`, `FalseClass`, `NilClass`, `BigDecimal`) will not use `to_log_format`. #### Building An Entry Formatter The Entry Formatter coordinates both message and attribute formatting, providing a unified configuration interface: ##### Complete Entry Formatter Setup ```ruby # Build a comprehensive entry formatter entry_formatter = Lumberjack.build_formatter do |formatter| # Format for ActiveRecord models that applies to both messages and attributes. formatter.format_class(ActiveRecord::Base, :id) # Format for the User class when it is logged as the log message. formatter.format_message(User) { |user| "User[#{user.id}:#{user.username}]" } # Attribute formatting formatter.format_attributes(Time, :date_time, "%Y-%m-%d %H:%M:%S") formatter.format_attributes([Float, BigDecimal], :round, 6) formatter.format_attribute_name("email", :redact) end # Use with logger logger = Lumberjack::Logger.new(STDOUT, formatter: entry_formatter) ``` #### Merging Formatters You can merge other formatters into your formatter with the `include` method. Doing so will copy all of the format definitions. ```ruby # Translate the duration tag to microseconds. duration_formatter = Lumberjack::EntryFormatter.build do |formatter| formatter.format_attribute_name(:duration) { |seconds| (seconds.to_f * 1_000_000).round } end entry_formatter = Lumberjack::EntryFormatter.build do |formatter| # Adds the duration attribute formatter formatter.include(duration_formatter) end ``` You can also call `prepend` in which case any formats already defined will take precedence over the formats being included. ### Devices and Templates Devices control where and how log entries are written. Lumberjack provides a variety of built-in devices that can write to files, streams, multiple destinations, or serve special purposes like testing. All devices implement a common interface, making them interchangeable. #### Built-in Devices ##### Writer Device The `Writer` device is the foundation for most logging output, writing formatted log entries to any IO stream. It will be used if you pass an IO object to the logger. ```ruby logger = Lumberjack::Logger.new(STDOUT) ``` ##### LogFile Device The `LogFile` device handles logging to a file. It has the same log rotation capabilities as the `Logger::LogDevice` class in the standard library logger. ```ruby # Daily log rotation logger = Lumberjack::Logger.new("/var/log/app.log", 'daily') ``` ##### Multi Device The `Multi` device broadcasts log entries to multiple devices simultaneously. You can instantiate a multi device by passing in an array of values. ```ruby # Log to both file and STDOUT; the logs to STDOUT will only contain the log message. logger = Lumberjack::Logger.new(["/var/log/app.log", [:stdout, {template: "{{message}}"}]]) logger.info("Application started") # Appears in both file AND STDOUT ``` ##### LoggerWrapper Device The `LoggerWrapper` device forwards entries to another Logger instance. It is most useful when you want to route logs from one logger to another, possibly with different configurations. ```ruby target_logger = Lumberjack::Logger.new("/var/log/target.log") logger = Lumberjack::Logger.new(target_logger) ``` > [!NOTE] > Note that the level of the outer logger will take precedence. So if the outer logger is set to `:warn`, then only warning messages or higher will be forwarded to the target logger. > [!TIP] > You can also pass in a standard Ruby `Logger` instance. This allows you to use the enhanced Lumberjack logging features with a standard library logger. ##### Test Device The `Test` device logs entries in memory and is intended for use in test suites where you want to make assertions that specific log entries are recorded. ```ruby logger = Lumberjack::Logger.new(:test) ``` > [!TIP] > See the [testing utilities](#testing-utilities) section for more information. ##### Null Device The `Null` device discards all output. ```ruby logger = Lumberjack::Logger.new(:null) ``` #### Custom Devices You can create custom devices by implementing the `write` method. The `write` method will receive a `Lumberjack::LogEntry` object and is free to process it in any way. ```ruby class DatabaseDevice < Lumberjack::Device def initialize(database_connection) @db = database_connection end def write(entry) @db.execute( "INSERT INTO logs (timestamp, level, message, attributes, pid) VALUES (?, ?, ?, ?, ?)", entry.time, entry.severity_label, entry.message, JSON.generate(entry.attributes), entry.pid ) end def close @db.close end end # Usage db_device = DatabaseDevice.new(SQLite3::Database.new("logs.db")) logger = Lumberjack::Logger.new(db_device) ``` There are separate gems implementing custom devices for different use cases: - [lumberjack_json_device](https://github.com/bdurand/lumberjack_json_device) - Output logs to JSON - [lumberjack_datadog_device](https://github.com/bdurand/lumberjack_datadog) - Output logs in JSON using the Datadog standard attributes. - [lumberjack_capture_device](https://github.com/bdurand/lumberjack_capture_device) - Device designed for capturing logs in tests to make assertions easier - [lumberjack_syslog_device](https://github.com/bdurand/lumberjack_syslog_device) - Device for logging to a syslog server - [lumberjack_redis_device](https://github.com/bdurand/lumberjack_redis_device) - Device for logging to a Redis database You can register a custom device with Lumberjack using the device registry. This associates the device with the device class and can make using the device easier to setup since the user can pass the symbol and options when instantiating the Logger rather than having to instantiate the device separately. ```ruby Lumberjack::Device.register(:my_device, MyDevice) # Now logger can be instantiated with the name and all options will be passed to # the MyDevice constructor. logger = Lumberjack::Logger.new(:my_device, autoflush: true) ``` #### Templates The output devices writing to a stream or file can define templates that formats how log entries are written. Templates use mustache-style placeholders that are replaced with values from the log entry. ##### Basic Template Usage ```ruby # Simple template with common fields logger = Lumberjack::Logger.new(STDOUT, template: "{{time}} {{severity}} {{message}}") logger.info("Application started") # Output: 2025-09-03T14:30:15.123456 INFO Application started ``` ##### Available Template Variables Templates support the following placeholder variables: | Variable | Description | |----------|-------------| | `time` | Timestamp | | `severity` | Severity level | | `progname` | Program name | | `pid` | Process ID | | `message` | Log message | | `attributes` | Formatted attributes | In addition you can put any attribute name in a placeholder. The attribute will be inserted in the log line where the placeholder is defined and will be removed from the general list of attributes. ```ruby # The user_id attribute will appear separately on the log line from the # rest of the attributes. logger = Lumberjack::Logger.new(STDOUT, template: "[{{time}} {{severity}} {{user_id}}] {{message}} {{attributes}}" ) ``` The severity can also have an optional formatting argument added to it. | Variable | Description | |----------|-------------| | `severity` | Uppercase case severity label (INFO, WARN, etc.) | | `severity(padded)` | Severity label padded to 5 characters | | `severity(char)` | First character of severity label | | `severity(emoji)` | Emoji representation of severity level | | `severity(level)` | Numeric severity level | ##### Template Options You can customize how template variables are formatted using template options: ```ruby logger = Lumberjack::Logger.new(STDOUT, template: "[{{time}} {{severity(padded)}} {{progname}}({{pid}})] [{{http.request_id}}] {{message}} {{attributes}}", time_format: "%Y-%m-%d %H:%M:%S", # Custom time format additional_lines: "\n> [{{http.request_id}}] {{message}}", # Template for additional lines on multiline messages attribute_format: "[%s=%s]", # Format for attributes using printf syntax colorize: true # Colorize log output according to entry severity ) logger.info("Test message", user_id: 123, status: "active") # Output: 2025-09-03 14:30:15 INFO Test message [user_id=123] [status=active] ``` ##### Built-in Templates You can use symbols to refer to built-in templates: - `:default` - The default log template. This is the same as not specifying a template. - `:stdlib` - A template that mimics the default format of the standard library Logger. - `:local` - A simple, human readable template intended for local development and test environments. This template removes the time and pid from the log line since they are generally not needed in these environments. You can also exclude attributes by setting the `exclude_attributes` option to the list of attributes names to exclude. For example, you may want to add a `host` attribute for your production logs, but this just creates clutter in development logs since it is always the same value. ```ruby logger = Lumberjack::Logger.new(STDOUT, template: :local, exclude_attributes: ["host", "env", "version"]) ``` ### Testing Utilities The `Test` device captures log entries in memory for testing and assertions: ```ruby logger = Lumberjack::Logger.new(:test) # Log some entries logger.info("User logged in", user_id: 123) logger.error("Payment failed", amount: 29.99) # You can make assertions against the logs (using rspec in this case) expect(logger.device.entries.size).to eq(2) expect(logger.device.last_entry.message).to eq("Payment failed") # Use pattern matching expect(logger.device).to include( severity: :info, message: "User logged in", attributes: { user_id: 123 } ) expect(logger.device).to include(severity: :error, message: /Payment/) ``` You should make sure to call `logger.device.clear` between tests to clear the captured logs on any global loggers that are using the `Test` device. > [!NOTE] > Log entries are captured after formatters have been applied. This provides a mechanism for including the formatting logic in your tests. > [!TIP] > The [lumberjack_capture_device](https://github.com/bdurand/lumberjack_capture_device) gem provides some additional testing utilities and rspec integration. You can also use the `write_to` method on the `Test` device to write the captured log entries to another logger or device. This can be useful in scenarios where you want to preserve log output for failed tests. ```ruby # Set up test logger (presumably in an initializer) Application.logger = Lumberjack::Logger.new(:test) # Hook into your test framework; in this example using rspec. RSpec.configure do |config| failed_test_logs = Lumberjack::Logger.new("log/test_failures.log") config.around do |example| # Clear the captured logs so we start with a clean slate. Application.logger.clear example.run if example.exception failed_test_logs.error("Test failed: #{example.full_description} @ #{example.location}") Application.logger.device.write_to(failed_test_logs) end end end ``` Lumberjack will catch any errors raised when logging and output the error message and backtrace to STDERR. This prevents logging errors from crashing your application. You should disable this behavior in your test suite so that logging errors are raised and can be fixed. ```ruby Lumberjack.raise_logging_errors = true ``` ### Using As A Stream Lumberjack loggers implement methods necessary for treating them like a stream. You can use this to augment output from components that write output to a stream with metadata like a timestamp and attributes. ```ruby logger = Lumberjack::Logger.new($stderr, progname: "MyApp") $stderr = logger # These statements will now do the same thing logger.unknown("Something went wrong") $stderr.puts "Something went wrong" # You can set the default level to set the level when using the I/O methods logger.default_level = :warn $stderr.puts "This is a warning message" # logged as a warning ``` ### Integrations #### Rails > [!WARNING] > If you are using Rails, you must use the [lumberjack_rails](https://github.com/bdurand/lumberjack_rails) gem. > > Rails does its own monkey patching to the standard library Logger to support tagged logging, silencing logs, and broadcast logging. #### Other Integrations - [lumberjack_sidekiq](https://github.com/bdurand/lumberjack_sidekiq) - Integrates Lumberjack with Sidekiq for background job logging. - [lumberjack_datadog](https://github.com/bdurand/lumberjack_datadog) - Integrates Lumberjack with Datadog by outputting logs in JSON using Datadog's standard attributes. - [lumberjack_local_logger](https://github.com/bdurand/lumberjack_local_logger) - Lightweight wrapper around Lumberjack::Logger that allows contextual logging with custom levels, prognames, and attributes without affecting the parent logger. - Check [RubyGems](https://rubygems.org/gems) for other integrations ## Installation Add this line to your application's Gemfile: ```ruby gem "lumberjack" ``` And then execute: ```bash $ bundle install ``` Or install it yourself as: ```bash $ gem install lumberjack ``` ## Contributing Open a pull request on GitHub. Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). bdurand-lumberjack-ac97435/Rakefile000066400000000000000000000046371515437321200172710ustar00rootroot00000000000000begin require "bundler/setup" rescue LoadError puts "You must `gem install bundler` and `bundle install` to run rake tasks" end require "bundler/gem_tasks" task :verify_release_branch do unless `git rev-parse --abbrev-ref HEAD`.chomp == "main" warn "Gem can only be released from the main branch" exit 1 end end Rake::Task[:release].enhance([:verify_release_branch]) require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task default: :spec desc "run the specs using appraisal" task :appraisals do exec "bundle exec appraisal rake spec" end namespace :appraisals do desc "install all the appraisal gemspecs" task :install do exec "bundle exec appraisal install" end end namespace :profile do desc "Profile logger CPU usage. Set LOGGER=Logger to profile the standard libary logger. Set LOG_LEVEL=warn to profile with a higher log level." task :cpu do |t, args| require "ruby-prof" require_relative "lib/lumberjack" out = StringIO.new logger_class = (ENV["LOGGER"] == "Logger") ? Logger : Lumberjack::Logger log_level = ENV.fetch("LOG_LEVEL", "debug") logger = logger_class.new(out, level: log_level) message = "foobar" result = RubyProf::Profile.profile do 1000.times { logger.info(message) } end printer = RubyProf::FlatPrinter.new(result) printer.print($stdout) end desc "Profile logger memory usage. Set LOGGER=Logger to profile the standard libary logger. Set LOG_LEVEL=warn to profile with a higher log level." task :memory do |t, args| require "memory_profiler" require_relative "lib/lumberjack" out = StringIO.new logger_class = (ENV["LOGGER"] == "Logger") ? Logger : Lumberjack::Logger log_level = ENV.fetch("LOG_LEVEL", "debug") logger = logger_class.new(out, level: log_level) message = "foobar" MemoryProfiler.report do 1000.times { logger.info(message) } end.pretty_print end end namespace :colors do desc "Print the color codes for each severity level" task :print do require_relative "lib/lumberjack" logger = Lumberjack::Logger.new($stdout, level: :trace, template: "{{severity(emoji)}} {{severity(padded)}} {{message}}", colorize: true) logger.trace("Test message") logger.debug("Test message") logger.info("Test message") logger.warn("Test message") logger.error("Test message") logger.fatal("Test message") logger.unknown("Test message") end end bdurand-lumberjack-ac97435/UPGRADE_GUIDE.md000066400000000000000000000100101515437321200201300ustar00rootroot00000000000000# Lumberjack Upgrade Guide Version 2.0 is a major update to the framework with several changes to the public API. ## Constructor `Lumberjack::Logger` now takes keyword arguments instead of an options hash in order to be compatible with the standard library `Logger` class. If you were previously using an options hash, you will need to double splat the hash to convert them to keyword arguments. ```ruby logger = Lumberjack::Logger.new(stream, **options) ``` The default log level is now DEBUG instead of INFO. ## Log Files One of the original goals of Lumberjack was to properly handle rotating log files in a multi-process, production environment. The standard library `Logger` class in modern versions of Ruby now does this properly, so log rotation devices have been removed from Lumberjack. The `:roll` and `:max_size` constructor options are no longer used. Log file rotation is specified with the same constructor arguments that standard library `Logger` class uses. ```ruby # Rotate the logs daily logger = Lumberjack::Logger.new(stream, :daily) # Rotate the logs when they reach 10MB and keeping the last 4 files logger = Lumberjack::Logger.new(stream, 4, 10 * 1024 * 1024) ``` These devices have been removed: - `Lumberjack::Device::LogFile` - `Lumberjack::Device::RollingLogFile` - `Lumberjack::Device::SizeRollingLogFile` - `Lumberjack::Device::DateRollingLogFile` ## Attributes Tags have been renamed "attributes" to keep inline with terminology used in other logging frameworks. The method name `tag` is still used as the main interface as verb (i.e. "to tag logs with attributes"). ```ruby logger.tag(attributes) do logger.info("Something happened") end ``` Internal uses of the word "tag" have all been updated to use "attribute" instead. The "tag" versions of the methods will still work, but they have been [marked as deprecated](CHANGELOG.md#deprecated) and will be removed in a future version. Global attributes are now set with the `tag!` method instead of `tag_globally` or calling `tag` outside of a context. ```ruby logger.tag!(host: Lumberjack::Utils.hostname) ``` ## Rails Integration Rails has it's own extensions for logging. The support for these has been removed from the main `lumberjack` gem and moved to the `lumberjack_rails` gem. This change allows for much better support for integrating Lumberjack into the Rails ecosystem. Using the `tagged` method in Rails will now add the tags to the `"tags"` attribute. Previously it had added it to the `"tagged"` attribute. ## Templates Templates used for writing to streams now use mustache syntax (i.e. `{{message}}` instead of `:message`). The field names are all the same except `:tags` should be replace with `{{attributes}}`. There are also options for how to format the severity on a log entry (see `Lumberjack::Template` for details). ## Formatters Message formatters and attribute formatters have been unified under a single `Lumberjack::EntryFormatter` class. This class supports a builder pattern so it is much easier to define custom formats for classes or attributes in the logs. You should now pass an entry formatter in the `Lumberjack::Logger` constructor `:formatter` option instead of a `Lumberjack::Formatter`. The default formatter has also been removed. This created problems when creating custom formats. You can use the old default formatter by passing `formatter: :default` to the logger constructor. ## Deprecation Warnings Lumberjack will print deprecation warnings to standard error when deprecated methods are used. If you want to suppress these warnings, set `Lumberjack.deprecation_mode` to `:silent`. For performance reasons, deprecation warnings will only be shown the first time a deprecated method is called. You can show all instances where a method is called by setting `Lumberjack.deprecation_mode` to `:verbose`. You can also raise an exception when a deprecated method is called by setting `Lumberjack.deprecation_mode` to `:raise` in your test suite. You can also set the deprecation mode with the `LUMBERJACK_DEPRECATION_WARNINGS` environment variable.bdurand-lumberjack-ac97435/VERSION000066400000000000000000000000061515437321200166560ustar00rootroot000000000000002.0.5 bdurand-lumberjack-ac97435/lib/000077500000000000000000000000001515437321200163605ustar00rootroot00000000000000bdurand-lumberjack-ac97435/lib/lumberjack.rb000066400000000000000000000174401515437321200210320ustar00rootroot00000000000000# frozen_string_literal: true require "rbconfig" require "time" require "logger" require "fiber" require "pathname" # Lumberjack is a flexible logging framework for Ruby that extends the standard # Logger functionality with structured logging, context isolation, and advanced # formatting capabilities. # # The main features include: # - Structured logging with attributes for machine-readable metadata # - Context isolation for scoping logging behavior to specific code blocks # - Flexible formatters for customizing log output # - Multiple output devices and templates # - Built-in testing utilities # # @example Basic usage # logger = Lumberjack::Logger.new(STDOUT) # logger.info("Hello world") # # @example Using contexts # Lumberjack.context do # Lumberjack.tag(user_id: 123) # logger.info("User action") # Will include user_id: 123 # end # # @see Lumberjack::Logger # @see Lumberjack::ContextLogger module Lumberjack VERSION = File.read(File.join(__dir__, "..", "VERSION")).strip.freeze LINE_SEPARATOR = ((RbConfig::CONFIG["host_os"] =~ /mswin/i) ? "\r\n" : "\n") require_relative "lumberjack/device_registry" require_relative "lumberjack/template_registry" require_relative "lumberjack/formatter_registry" require_relative "lumberjack/attribute_formatter" require_relative "lumberjack/attributes_helper" require_relative "lumberjack/context" require_relative "lumberjack/context_logger" require_relative "lumberjack/context_locals" require_relative "lumberjack/io_compatibility" require_relative "lumberjack/log_entry" require_relative "lumberjack/log_entry_matcher" require_relative "lumberjack/device" require_relative "lumberjack/entry_formatter" require_relative "lumberjack/formatter" require_relative "lumberjack/forked_logger" require_relative "lumberjack/logger" require_relative "lumberjack/local_log_template" require_relative "lumberjack/message_attributes" require_relative "lumberjack/remap_attribute" require_relative "lumberjack/rack" require_relative "lumberjack/severity" require_relative "lumberjack/template" require_relative "lumberjack/utils" # Deprecated require_relative "lumberjack/tag_context" require_relative "lumberjack/tag_formatter" require_relative "lumberjack/tags" @global_contexts = {} @global_contexts_mutex = Mutex.new @deprecation_mode = nil @raise_logger_errors = false @isolation_level = :fiber extend ContextLocals class << self # Contexts can be used to store attributes that will be attached to all log entries in the block. # The context will apply to all Lumberjack loggers that are used within the block. # # If this method is called with a block, it will set a logging context for the scope of a block. # If there is already a context in scope, a new one will be created that inherits # all the attributes of the parent context. # # Otherwise, it will return the current context. If one doesn't exist, it will return a new one # but that context will not be in any scope. # # @return [Object] The result # of the block def context(&block) use_context(Context.new(current_context), &block) end # Ensure that the block of code is wrapped by a global context. If there is not already # a context in scope, one will be created. # # @return [Object] The result of the block. def ensure_context(&block) if in_context? yield else context(&block) end end # Set the context to use within a block. # # @param context [Lumberjack::Context] The context to use within the block. # @return [Object] The result of the block. # @api private def use_context(context, &block) unless block_given? raise ArgumentError, "A block must be provided to the context method" end new_context = Context.new(context) new_context.parent = current_context new_context_locals do |locals| locals.context = new_context yield end end # Return true if inside a context block. # # @return [Boolean] def in_context? !current_context.nil? end def context? Utils.deprecated("Lumberjack.context?", "Lumberjack.context? is deprecated and will be removed in version 2.1; use in_context? instead.") do in_context? end end # Return attributes that will be applied to all Lumberjack loggers. # # @return [Hash, nil] def context_attributes current_context&.attributes end # Set the isolation level for global contexts to be either per fiber or per thread. Default is :fiber. # # @param value [Symbol] The isolation level, either :fiber or :thread. # @return [void] def isolation_level=(value) value = value&.to_sym value = :fiber unless [:fiber, :thread].include?(value) @isolation_level = value end # @return [Symbol] The current isolation level. attr_reader :isolation_level # Alias for context_attributes to provide API compatibility with version 1.x. # This method will eventually be removed. # # @return [Hash, nil] # @deprecated Use {.context_attributes} def context_tags Utils.deprecated("Lumberjack.context_tags", "Lumberjack.context_tags is deprecated and will be removed in version 2.1; use context_attributes instead.") do context_attributes end end # Tag all loggers with attributes on the current context. # # @param attributes [Hash] The attributes to set. # @param block [Proc] optional context block in which to set the attributes. # @return [void] def tag(attributes, &block) if block context do current_context.assign_attributes(attributes) block.call end else current_context&.assign_attributes(attributes) end end # Helper method to build an entry formatter. # # @param block [Proc] The block to use for building the entry formatter. # @return [Lumberjack::EntryFormatter] The built entry formatter. # @see Lumberjack::EntryFormatter.build def build_formatter(&block) EntryFormatter.build(&block) end # Control how use of deprecated methods is handled. The default is to print a warning # the first time a deprecated method is called. Setting this to :verbose will print # a warning every time a deprecated method is called. Setting this to :silent will # suppress all deprecation warnings. Setting this to :raise will raise an exception # when a deprecated method is called. # # The default value can be set with the +LUMBERJACK_DEPRECATION_WARNINGS+ environment variable. # # @param value [Symbol, String, nil] The deprecation mode to set. Valid values are :normal, # :verbose, :silent, and :raise. def deprecation_mode=(value) @deprecation_mode = value&.to_sym end # @return [Symbol] The current deprecation mode. # @api private def deprecation_mode @deprecation_mode ||= ENV.fetch("LUMBERJACK_DEPRECATION_WARNINGS", "normal").to_sym end # Set whether errors encountered while logging entries should be raised. The default behavior # is to rescue these errors and print them to standard error. Otherwise there can be no way # to record the error since it cannot be logged. # # You can set this to true in you test and development environments to catch logging errors # before they make it to production. # # @param value [Boolean] Whether to raise logger errors. # @return [void] def raise_logger_errors=(value) @raise_logger_errors = !!value end # @return [Boolean] Whether logger errors should be raised. # @api private def raise_logger_errors? @raise_logger_errors end private def current_context current_context_locals&.context end end end bdurand-lumberjack-ac97435/lib/lumberjack/000077500000000000000000000000001515437321200204775ustar00rootroot00000000000000bdurand-lumberjack-ac97435/lib/lumberjack/attribute_formatter.rb000066400000000000000000000456421515437321200251250ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # AttributeFormatter provides flexible formatting control for log entry attributes (key-value pairs). # It allows you to specify different formatting rules for attribute names, object classes, or # provide a default formatter for all attributes. # # The formatter system works in a hierarchical manner: # 1. Attribute-specific formatters - Applied to specific attribute names (highest priority) # 2. Class-specific formatters - Applied based on the attribute value's class # 3. Default formatter - Applied to all other attributes (lowest priority) # # Formatters can be specified as: # # - Lumberjack::Formatter objects: Full formatter instances with complex logic # - Callable objects: Any object responding to +#call(value)+ # - Blocks: Inline formatting logic # - Symbols: References to predefined formatter classes (e.g., +:strip+, +:truncate+) # # @example Basic usage with build # formatter = Lumberjack::AttributeFormatter.build do |config| # config.add_attribute(["password", "secret", "token"]) { |value| "[REDACTED]" } # config.add_attribute("user.email") { |email| email.downcase } # config.add_class(Time, :date_time, "%Y-%m-%d %H:%M:%S") # end # # If the value returned by a formatter is a +Lumberjack::RemapAttributes+ instance, then # the attributes will be remapped to the new attributes. # # @example # formatter = Lumberjack::AttributeFormatter.new # formatter.add_attribute("duration_ms") { |value| Lumberjack::RemapAttributes.new(duration: value.to_f / 1000) } # formatter.format({ "duration_ms" => 1234 }) # => { "duration" => 1.234 } # # @see Lumberjack::Formatter # @see Lumberjack::EntryFormatter class AttributeFormatter class << self # Build a new attribute formatter using a configuration block. The block receives the # new formatter as a parameter, allowing you to configure it with methods like +add_attribute+, # +add_class+, +default+, etc. # # @yield [formatter] A block that configures the attribute formatter. # @return [Lumberjack::AttributeFormatter] A new configured attribute formatter. # # @example # formatter = Lumberjack::AttributeFormatter.build do |config| # config.default { |value| value.to_s.strip } # config.add_attribute(["password", "secret"]) { |value| "[REDACTED]" } # config.add_attribute("email") { |email| email.downcase } # config.add_class(Time, :date_time, "%Y-%m-%d %H:%M:%S") # end def build(&block) formatter = new block&.call(formatter) formatter end end # Create a new attribute formatter with no default formatters configured. # You'll need to add specific formatters using {#add_class}, {#add_attribute}, or {#default}. # # @return [Lumberjack::AttributeFormatter] A new empty attribute formatter. def initialize @attribute_formatter = {} @class_formatter = Formatter.new @default_formatter = nil end # Set a default formatter applied to all attribute values that don't have specific formatters. # This serves as the fallback formatting behavior for any attributes not covered by # attribute-specific or class-specific formatters. # # @param formatter [Lumberjack::Formatter, #call, Class, nil] The formatter to use. # If nil, the block will be used as the formatter. If a class is passed, it will be # instantiated with the args passed in. # @param args [Array] The arguments to pass to the constructor if formatter is a Class. # @yield [value] Block-based formatter that receives the attribute value. # @yieldparam value [Object] The attribute value to format. # @yieldreturn [Object] The formatted attribute value. # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. def default(formatter = nil, *args, &block) formatter ||= block formatter = dereference_formatter(formatter, args) @default_formatter = formatter self end # Remove the default formatter. After calling this, attributes without specific formatters # will be passed through unchanged. # # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. def remove_default @default_formatter = nil self end # Add formatters for specific attribute names or object classes. This is a convenience method # that automatically delegates to {#add_class} or {#add_attribute} based on the input type. # # When you pass a Module/Class, it creates a class-based formatter that applies to all # attribute values of that type. When you pass a String, it creates an attribute-specific # formatter for that exact attribute name. # # Class formatters are applied recursively to nested hashes and arrays, making them # powerful for formatting complex nested structures. # # @param names_or_classes [String, Module, Array] Attribute names or object classes. # @param formatter [Lumberjack::Formatter, #call, Symbol, nil] The formatter to use. # @yield [value] Block-based formatter that receives the attribute value. # @yieldparam value [Object] The attribute value to format. # @yieldreturn [Object] The formatted attribute value. # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. # @deprecated Use {#add_class} or {#add_attribute} instead. def add(names_or_classes, formatter = nil, *args, &block) Utils.deprecated("AttributeFormatter#add", "AttributeFormatter#add is deprecated and will be removed in version 2.1; use #add_class or #add_attribute instead.") do Array(names_or_classes).each do |obj| if obj.is_a?(Module) add_class(obj, formatter, *args, &block) else add_attribute(obj, formatter, *args, &block) end end end self end # Add formatters for specific object classes. The formatter will be applied to any attribute # value that is an instance of the registered class. This is particularly useful for formatting # all instances of specific data types consistently across your logs. # # Class formatters are recursive - they will be applied to matching objects found within # nested hashes and arrays. # # @param classes_or_names [String, Module, Array] Class names or modules. # @param formatter [Lumberjack::Formatter, #call, Symbol, Class, nil] The formatter to use. # If a Class is provided, it will be instantiated with the provided args. # @param args [Array] The arguments to pass to the constructor if formatter is a Class. # @yield [value] Block-based formatter that receives the attribute value. # @yieldparam value [Object] The attribute value to format. # @yieldreturn [Object] The formatted attribute value. # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. # # @example Time formatting # formatter.add_class(Time, :date_time, "%Y-%m-%d %H:%M:%S") # formatter.add_class([Date, DateTime]) { |dt| dt.strftime("%Y-%m-%d") } def add_class(classes_or_names, formatter = nil, *args, &block) formatter ||= block formatter = dereference_formatter(formatter, args) Array(classes_or_names).each do |class_or_name| class_name = class_or_name.to_s if formatter.nil? @class_formatter.remove(class_name) else @class_formatter.add(class_name, formatter) end end self end # Add formatters for specific attribute names. These formatters take precedence over # class formatters and the default formatter. # # Supports dot notation for nested attributes (e.g., "user.profile.email"). This allows # you to format specific values deep within nested hash structures. # # @param attribute_names [String, Symbol, Array] The attribute names to format. # @param formatter [Lumberjack::Formatter, #call, Symbol, nil] The formatter to use. # @yield [value] Block-based formatter that receives the attribute value. # @yieldparam value [Object] The attribute value to format. # @yieldreturn [Object] The formatted attribute value. # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. # # @example Basic attribute formatting # formatter.add_attribute("password") { |pwd| "[REDACTED]" } # formatter.add_attribute("email") { |email| email.downcase } # # @example Nested attribute formatting # formatter.add_attribute("user.profile.email") { |email| email.downcase } # formatter.add_attribute("config.database.password") { "[HIDDEN]" } # # @example Multiple attributes # formatter.add_attribute(["secret", "token", "api_key"]) { "[REDACTED]" } def add_attribute(attribute_names, formatter = nil, *args, &block) formatter ||= block formatter = dereference_formatter(formatter, args) Array(attribute_names).collect(&:to_s).each do |attribute_name| if attribute_name.is_a?(Module) raise ArgumentError.new("attribute_name cannot be a Module/Class; use #add_class to add class-based formatters") end if formatter.nil? @attribute_formatter.delete(attribute_name) else @attribute_formatter[attribute_name] = formatter end end self end # Remove formatters for specific attribute names or classes. This reverts the specified # attributes or classes to use the default formatter (if configured) or no formatting. # # @param names_or_classes [String, Module, Array] Attribute names or classes # to remove formatters for. # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. # @deprecated Use {#remove_class} or {#remove_attribute} instead. def remove(names_or_classes) Utils.deprecated("AttributeFormatter#remove", "AttributeFormatter#remove is deprecated and will be removed in version 2.1; use #remove_class or #remove_attribute instead.") do Array(names_or_classes).each do |key| if key.is_a?(Module) @class_formatter.remove(key) else @attribute_formatter.delete(key.to_s) end end end self end # Remove formatters for specific object classes. This reverts the specified classes # to use the default formatter (if configured) or no formatting. # # @param classes_or_names [String, Module, Array] The classes or names to remove. # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. def remove_class(classes_or_names) Array(classes_or_names).each do |class_or_name| @class_formatter.remove(class_or_name) end self end # Remove formatters for specific attribute names. This reverts the specified attributes # to use the default formatter (if configured) or no formatting. # # @param attribute_names [String, Symbol, Array] The attribute names to remove. # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. def remove_attribute(attribute_names) Array(attribute_names).collect(&:to_s).each do |attribute_name| @attribute_formatter.delete(attribute_name) end self end # Extend this formatter by merging the formats defined in the provided formatter into this one. # # @param formatter [Lumberjack::AttributeFormatter] The formatter to merge. # @return [self] Returns self for method chaining. def include(formatter) unless formatter.is_a?(Lumberjack::AttributeFormatter) raise ArgumentError.new("formatter must be a Lumberjack::AttributeFormatter") end @class_formatter.include(formatter.instance_variable_get(:@class_formatter)) @attribute_formatter.merge!(formatter.instance_variable_get(:@attribute_formatter)) default_formatter = formatter.instance_variable_get(:@default_formatter) @default_formatter = default_formatter if default_formatter self end # Extend this formatter by merging the formats defined in the provided formatter into this one. # Formats defined in this formatter will take precedence and not be overridden. # # @param formatter [Lumberjack::AttributeFormatter] The formatter to merge. # @return [self] Returns self for method chaining. def prepend(formatter) unless formatter.is_a?(Lumberjack::AttributeFormatter) raise ArgumentError.new("formatter must be a Lumberjack::AttributeFormatter") end @class_formatter.prepend(formatter.instance_variable_get(:@class_formatter)) formatter.instance_variable_get(:@attribute_formatter).each do |key, value| @attribute_formatter[key] = value unless @attribute_formatter.include?(key) end @default_formatter ||= formatter.instance_variable_get(:@default_formatter) self end # Remove all configured formatters, including the default formatter. This resets the # formatter to a completely empty state where all attributes pass through unchanged. # # @return [Lumberjack::AttributeFormatter] Returns self for method chaining. def clear @default_formatter = nil @attribute_formatter.clear @class_formatter.clear self end # Check if the formatter has any configured formatters (attribute, class, or default). # # @return [Boolean] true if no formatters are configured, false otherwise. def empty? @attribute_formatter.empty? && @class_formatter.empty? && @default_formatter.nil? end # Format a hash of attributes using the configured formatters. This is the main # method that applies all formatting rules to transform attribute values. # # The formatting process follows this precedence: # 1. Attribute-specific formatters (highest priority) # 2. Class-specific formatters # 3. Default formatter (lowest priority) # # Nested hashes and arrays are processed recursively, and dot notation attribute # formatters are applied to nested structures. # # @param attributes [Hash, nil] The attributes hash to format. # @return [Hash, nil] The formatted attributes hash, or nil if input was nil. def format(attributes) return nil if attributes.nil? return attributes if empty? formated_attributes(attributes) end # Get the formatter for a specific class or class name. # # @param klass [String, Module] The class or class name to get the formatter for. # @return [#call, nil] The formatter for the class, or nil if not found. def formatter_for_class(klass) @class_formatter.formatter_for(klass) end # Get the formatter for a specific attribute. # # @param name [String, Symbol] The attribute name to get the formatter for. # @return [#call, nil] The formatter for the attribute, or nil if not found. def formatter_for_attribute(name) @attribute_formatter[name.to_s] end # Check if a formatter exists for a specific class or class name. # # @param class_or_name [Class, Module, String] The class or class name to check. # @return [Boolean] true if a formatter exists, false otherwise. def include_class?(class_or_name) @class_formatter.include?(class_or_name.to_s) end # Check if a formatter exists for a specific attribute name. # # @param name [String, Symbol] The attribute name to check. # @return [Boolean] true if a formatter exists, false otherwise. def include_attribute?(name) @attribute_formatter.include?(name.to_s) end private # Recursively format all attributes in a hash, handling nested structures. # # @param attributes [Hash] The attributes to format. # @param skip_classes [Array, nil] Classes to skip during recursive formatting. # @param prefix [String, nil] Dot notation prefix for nested attribute names. # @return [Hash] The formatted attributes hash. def formated_attributes(attributes, skip_classes: nil, prefix: nil) formatted = {} attributes.each do |name, value| name = name.to_s value = formatted_attribute_value(name, value, skip_classes: skip_classes, prefix: prefix) if value.is_a?(RemapAttribute) formatted.merge!(value.attributes) else formatted[name] = value end end formatted end # Format a single attribute value using the appropriate formatter. # # @param name [String] The attribute name. # @param value [Object] The attribute value to format. # @param skip_classes [Array, nil] Classes to skip during recursive formatting. # @param prefix [String, nil] Dot notation prefix for nested attribute names. # @return [Object] The formatted attribute value. def formatted_attribute_value(name, value, skip_classes: nil, prefix: nil) prefixed_name = prefix ? "#{prefix}#{name}" : name using_class_formatter = false formatter = @attribute_formatter[prefixed_name] if formatter.nil? && (skip_classes.nil? || !skip_classes.include?(value.class)) formatter = @class_formatter.formatter_for(value.class) using_class_formatter = true if formatter end formatter ||= @default_formatter formatted_value = begin if formatter.is_a?(Lumberjack::Formatter) formatter.format(value) elsif formatter.respond_to?(:call) formatter.call(value) else value end rescue SystemStackError, StandardError => e error_message = e.class.name error_message = "#{error_message} #{e.message}" if e.message && e.message != "" warn("") "" end if formatted_value.is_a?(MessageAttributes) formatted_value = formatted_value.attributes end if formatted_value.is_a?(Enumerable) skip_classes ||= [] skip_classes << value.class if using_class_formatter sub_prefix = "#{prefixed_name}." formatted_value = if formatted_value.is_a?(Hash) formated_attributes(formatted_value, skip_classes: skip_classes, prefix: sub_prefix) else formatted_value.collect do |item| formatted_attribute_value(nil, item, skip_classes: skip_classes, prefix: sub_prefix) end end end formatted_value end # Convert symbol formatter references to actual formatter instances. # # @param formatter [Symbol, Class, #call] The formatter to dereference. # @param args [Array] The arguments to pass to the constructor if formatter is a Class. # @return [#call] The actual formatter instance. def dereference_formatter(formatter, args) if formatter.is_a?(Symbol) FormatterRegistry.formatter(formatter, *args) else formatter end end end end bdurand-lumberjack-ac97435/lib/lumberjack/attributes_helper.rb000066400000000000000000000057231515437321200245600ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # This class provides an interface for manipulating a attribute hash in a consistent manner. class AttributesHelper class << self # Expand any values in a hash that are Proc's by calling them and replacing # the value with the result. This allows setting global tags with runtime values. # # @param hash [Hash] The hash to transform. # @return [Hash] The hash with string keys and expanded values. def expand_runtime_values(hash) return nil if hash.nil? return hash if hash.all? { |key, value| key.is_a?(String) && !value.is_a?(Proc) } copy = {} hash.each do |key, value| if value.is_a?(Proc) && (value.arity == 0 || value.arity == -1) value = value.call end copy[key.to_s] = value end copy end end def initialize(attributes) @attributes = attributes end # Merge new attributes into the context attributes. Attribute values will be flattened using dot notation # on the keys. So +{ a: { b: 'c' } }+ will become +{ 'a.b' => 'c' }+. # # If a block is given, then the attributes will only be added for the duration of the block. # # @param attributes [Hash] The attributes to set. # @return [void] def update(attributes) @attributes.merge!(Utils.flatten_attributes(attributes)) end # Get a attribute value. # # @param name [String, Symbol] The attribute key. # @return [Object] The attribute value. def [](name) return nil if @attributes.empty? name = name.to_s return @attributes[name] if @attributes.include?(name) # Check for partial matches in dot notation and return the hash representing the partial match. prefix_key = "#{name}." matching_attributes = {} @attributes.each do |key, value| if key.start_with?(prefix_key) # Remove the prefix to get the relative key relative_key = key[prefix_key.length..] matching_attributes[relative_key] = value end end return nil if matching_attributes.empty? matching_attributes end # Set a attribute value. # # @param name [String, Symbol] The attribute name. # @param value [Object] The attribute value. # @return [void] def []=(name, value) if value.is_a?(Hash) @attributes.merge!(Utils.flatten_attributes(name => value)) else @attributes[name.to_s] = value end end # Remove attributes from the context. # # @param names [Array] The attribute names to remove. # @return [void] def delete(*names) names.each do |name| prefix_key = "#{name}." @attributes.delete_if { |k, _| k == name.to_s || k.start_with?(prefix_key) } end nil end # Return a copy of the attributes as a hash. # # @return [Hash] def to_h @attributes.dup end end end bdurand-lumberjack-ac97435/lib/lumberjack/context.rb000066400000000000000000000134271515437321200225170ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # Context stores logging settings and attributes that can be scoped to specific code blocks # or inherited between loggers. It provides a hierarchical system for managing logging state # including level, progname, default severity, and custom attributes. # # Child contexts inherit all configuration from their parent but can override any values. # Changes to child contexts don't affect parent contexts, providing true isolation. # # @see Lumberjack::ContextLogger # @see Lumberjack::AttributesHelper class Context # The attributes hash containing key-value pairs to include in log entries. # @return [Hash, nil] The attributes hash, or nil if no attributes are set. attr_reader :attributes # The logging level for this context. # @return [Integer, nil] The logging level, or nil if not set (inherits from parent or default). attr_reader :level # The program name for this context. # @return [String, nil] The program name, or nil if not set (inherits from parent or default). attr_reader :progname # The default severity used when writing log messages directly to a stream. # @return [Integer, nil] The default severity level, or nil if not set. attr_reader :default_severity # The parent context from which this context inherited its initial attributes. # @return [Lumberjack::Context, nil] The parent context, or nil if this is a top-level context. # @api private attr_accessor :parent # Create a new context, optionally inheriting configuration from a parent context. # # When a parent context is provided, the new context inherits all configuration # (level, progname, default_severity) and a copy of all attributes. Changes to the # new context won't affect the parent context, providing true isolation. # # @param parent_context [Lumberjack::Context, nil] The parent context to inherit from. def initialize(parent_context = nil) @attributes = nil @level = nil @progname = nil @default_severity = nil if parent_context @attributes = parent_context.attributes.dup if parent_context.attributes self.level = parent_context.level self.progname = parent_context.progname end end # Set the logging level for this context. The level determines which log entries # will be processed when this context is active. # # @param value [Integer, Symbol, String, nil] The logging level. Can be a numeric level, # symbol (:debug, :info, :warn, :error, :fatal), string, or nil to unset. # @return [void] def level=(value) value = Severity.coerce(value) unless value.nil? @level = value end # Set the program name for this context. The progname identifies the component # or program that is generating log entries. # # @param value [String, Symbol, nil] The program name. Will be converted to a frozen string. # @return [void] def progname=(value) @progname = value&.to_s&.freeze end # Assign multiple attributes to this context from a hash. This method allows # bulk assignment of context attributes and supports nested attribute names # using dot notation. # # @param attributes [Hash] A hash of attribute names to values. Keys can be strings # or symbols, and support dot notation for nested attributes. # @return [void] # @see #[]= for setting individual attributes def assign_attributes(attributes) attributes_helper.update(attributes) end # Get a context attribute by key. Supports both string and symbol keys, # and can access nested attributes using dot notation. # # @param key [String, Symbol] The attribute key. Supports dot notation for nested access. # @return [Object] The attribute value, or nil if the key doesn't exist. def [](key) attributes_helper[key] end # Set a context attribute by key. Supports both string and symbol keys, # and can set nested attributes using dot notation. # # @param key [String, Symbol] The attribute key. Supports dot notation for nested assignment. # @param value [Object] The attribute value to set. # @return [void] def []=(key, value) attributes_helper[key] = value end # Remove all attributes from this context. This only affects attributes # directly set on this context, not those inherited from parent contexts. def clear_attributes @attributes&.clear end # Remove specific attributes from this context. This only affects attributes # directly set on this context, not those inherited from parent contexts. # Supports dot notation for nested attribute removal. # # @param keys [Array] The attribute keys to remove. Can use # dot notation for nested attributes. # @return [void] def delete(*keys) attributes_helper.delete(*keys) end # Set the default severity level for this context. This determines the minimum # severity level for log entries when no explicit level is specified. # # @param value [Integer, Symbol, String, nil] The default severity level. Can be a numeric level, # symbol (:debug, :info, :warn, :error, :fatal), string, or nil to unset. # @return [void] def default_severity=(value) value = Severity.coerce(value) unless value.nil? @default_severity = value end # Clear all context data including attributes, level, and progname. # This resets the context to its initial state while preserving the # parent context relationship. # # @return [void] def reset @attributes&.clear @level = nil @progname = nil end private def attributes_helper @attributes ||= {} AttributesHelper.new(@attributes) end end end bdurand-lumberjack-ac97435/lib/lumberjack/context_locals.rb000066400000000000000000000061721515437321200240530ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # Provides isolated fiber or thread local storage for thread-safe data access. module ContextLocals # Lightweight structure to hold context-local data. # # @api private class Data attr_accessor :context, :logging, :cleared def initialize(copy = nil) @context = copy&.context @logging = copy&.logging @cleared = copy&.cleared end end # Set the isolation level for context locals. # # @param value [Symbol] The isolation level, either :fiber or :thread. # @return [void] def isolation_level=(value) value = value&.to_sym value = :fiber unless [:fiber, :thread].include?(value) @isolation_level = value end # Get the isolation level for context locals. # # @return [Symbol] The isolation level, either :fiber or :thread. def isolation_level @isolation_level ||= :fiber end private def new_context_locals(&block) init_context_locals! unless defined?(@context_locals) if isolation_level == :fiber set_context_locals(&block) else set_context_locals_thread_id do set_context_locals(&block) end end end def set_context_locals(&block) scope_id = context_locals_scope_id current = nil data = nil begin @context_locals_mutex.synchronize do current = @context_locals[scope_id] if scope_id data = Data.new(current) @context_locals[scope_id] = data end yield data ensure @context_locals_mutex.synchronize do if current.nil? @context_locals.delete(scope_id) else @context_locals[scope_id] = current end end end end def current_context_locals return nil unless defined?(@context_locals) scope_id = context_locals_scope_id @context_locals_mutex.synchronize do @context_locals[scope_id] end end # Initialize the context locals storage and mutex. def init_context_locals! @context_locals ||= {} @context_locals_mutex ||= Mutex.new @isolation_level ||= Lumberjack.isolation_level end def context_locals_scope_id if isolation_level == :fiber Fiber.current.object_id else Thread.current.thread_variable_get(:lumberjack_context_locals_thread_id) end end # Create a consistent thread ID for context locals. We can't use Thread.current.object_id # directly because it may change during execution (e.g., in JRuby when threads are # migrated between native threads). Instead we store a unique ID in a thread variable. def set_context_locals_thread_id thread_id = Thread.current.thread_variable_get(:lumberjack_context_locals_thread_id) return yield if thread_id thread_id = Object.new.object_id begin Thread.current.thread_variable_set(:lumberjack_context_locals_thread_id, thread_id) yield ensure Thread.current.thread_variable_set(:lumberjack_context_locals_thread_id, nil) end end end end bdurand-lumberjack-ac97435/lib/lumberjack/context_logger.rb000066400000000000000000000536211515437321200240560ustar00rootroot00000000000000# frozen_string_literal: true require_relative "context_locals" require_relative "io_compatibility" require_relative "severity" module Lumberjack # ContextLogger provides a logging interface with support for contextual attributes, # level management, and program name scoping. This module is included by Logger # and ForkedLogger to provide a common API for structured logging. # # Key features include: # - Context-aware attribute management with tag/untag methods # - Scoped logging levels and program names # - Compatibility with Ruby's standard Logger API # - Support for forking isolated logger contexts # # @see Lumberjack::Logger # @see Lumberjack::ForkedLogger # @see Lumberjack::Context module ContextLogger # Constant used for setting trace log level. TRACE = Severity::TRACE LEADING_OR_TRAILING_WHITESPACE = /(?:\A\s)|(?:\s\z)/ class << self def included(base) base.include(ContextLocals) unless base.include?(ContextLocals) base.include(IOCompatibility) unless base.include?(IOCompatibility) end end # Get the level of severity of entries that are logged. Entries with a lower # severity level will be ignored. # # @return [Integer] The severity level. def level current_context&.level || default_context&.level end alias_method :sev_threshold, :level # Set the log level using either an integer level like Logger::INFO or a label like # :info or "info" # # @param value [Integer, Symbol, String] The severity level. # @return [void] def level=(value) value = Severity.coerce(value) unless value.nil? ctx = current_context ctx.level = value if ctx end alias_method :sev_threshold=, :level= # Adjust the log level during the block execution for the current Fiber only. # # @param severity [Integer, Symbol, String] The severity level. # @return [Object] The result of the block. def with_level(severity, &block) context do |ctx| ctx.level = severity block.call(ctx) end end # Set the logger progname for the current context. This is the name of the program that is logging. # # @param value [String, nil] # @return [void] def progname=(value) value = value&.to_s&.freeze ctx = current_context ctx.progname = value if ctx end # Get the current progname. # # @return [String, nil] def progname current_context&.progname || default_context&.progname end # Set the logger progname for the duration of the block. # # @yield [Object] The block to execute with the program name set. # @param value [String] The program name to use. # @return [Object] The result of the block. def with_progname(value, &block) context do |ctx| ctx.progname = value block.call(ctx) end end # Get the default severity used when writing log messages directly to a stream. # # @return [Integer] The default severity level. def default_severity current_context&.default_severity || default_context&.default_severity || Logger::UNKNOWN end # Set the default severity used when writing log messages directly to a stream # for the current context. # # @param value [Integer, Symbol, String] The default severity level. # @return [void] def default_severity=(value) ctx = current_context ctx.default_severity = value if ctx end # ::Logger compatible method to add a log entry. # # @param severity [Integer, Symbol, String] The severity of the message. # @param message_or_progname_or_attributes [Object] The message to log, progname, or attributes. # @param progname_or_attributes [String, Hash] The name of the program or attributes. # @return [true] def add(severity, message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block) # This convoluted logic is to have API compatibility with ::Logger#add. severity ||= Logger::UNKNOWN if message_or_progname_or_attributes.nil? && !progname_or_attributes.is_a?(Hash) message_or_progname_or_attributes = progname_or_attributes progname_or_attributes = nil end call_add_entry(severity, message_or_progname_or_attributes, progname_or_attributes, &block) end alias_method :log, :add # Log a +FATAL+ message. The message can be passed in either the +message+ argument or in a block. # # @param message_or_progname_or_attributes [Object] The message to log or progname # if the message is passed in a block. # @param progname_or_attributes [String, Hash] The name of the program that is logging the message or attributes # if the message is passed in a block. # @return [true] def fatal(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block) call_add_entry(Logger::FATAL, message_or_progname_or_attributes, progname_or_attributes, &block) end # Return +true+ if +FATAL+ messages are being logged. # # @return [Boolean] def fatal? level <= Logger::FATAL end # Set the log level to fatal. # # @return [void] def fatal! self.level = Logger::FATAL end # Log an +ERROR+ message. The message can be passed in either the +message+ argument or in a block. # # @param message_or_progname_or_attributes [Object] The message to log or progname # if the message is passed in a block. # @param progname_or_attributes [String, Hash] The name of the program that is logging the message or attributes # if the message is passed in a block. # @return [true] def error(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block) call_add_entry(Logger::ERROR, message_or_progname_or_attributes, progname_or_attributes, &block) end # Return +true+ if +ERROR+ messages are being logged. # # @return [Boolean] def error? level <= Logger::ERROR end # Set the log level to error. # # @return [void] def error! self.level = Logger::ERROR end # Log a +WARN+ message. The message can be passed in either the +message+ argument or in a block. # # @param message_or_progname_or_attributes [Object] The message to log or progname # if the message is passed in a block. # @param progname_or_attributes [String, Hash] The name of the program that is logging the message or attributes # if the message is passed in a block. # @return [true] def warn(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block) call_add_entry(Logger::WARN, message_or_progname_or_attributes, progname_or_attributes, &block) end # Return +true+ if +WARN+ messages are being logged. # # @return [Boolean] def warn? level <= Logger::WARN end # Set the log level to warn. # # @return [void] def warn! self.level = Logger::WARN end # Log an +INFO+ message. The message can be passed in either the +message+ argument or in a block. # # @param message_or_progname_or_attributes [Object] The message to log or progname # if the message is passed in a block. # @param progname_or_attributes [String, Hash] The name of the program that is logging the message or attributes # if the message is passed in a block. # @return [true] def info(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block) call_add_entry(Logger::INFO, message_or_progname_or_attributes, progname_or_attributes, &block) end # Return +true+ if +INFO+ messages are being logged. # # @return [Boolean] def info? level <= Logger::INFO end # Set the log level to info. # # @return [void] def info! self.level = Logger::INFO end # Log a +DEBUG+ message. The message can be passed in either the +message+ argument or in a block. # # @param message_or_progname_or_attributes [Object] The message to log or progname # if the message is passed in a block. # @param progname_or_attributes [String, Hash] The name of the program that is logging the message or attributes # if the message is passed in a block. # @return [true] def debug(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block) call_add_entry(Logger::DEBUG, message_or_progname_or_attributes, progname_or_attributes, &block) end # Return +true+ if +DEBUG+ messages are being logged. # # @return [Boolean] def debug? level <= Logger::DEBUG end # Set the log level to debug. # # @return [void] def debug! self.level = Logger::DEBUG end # Log a +TRACE+ message. The message can be passed in either the +message+ argument or in a block. # Trace logs are a level lower than debug and are generally used to log code execution paths for # low level debugging. # # @param message_or_progname_or_attributes [Object] The message to log or progname # if the message is passed in a block. # @param progname_or_attributes [String, Hash] The name of the program that is logging the message or attributes # if the message is passed in a block. # @return [true] def trace(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block) call_add_entry(TRACE, message_or_progname_or_attributes, progname_or_attributes, &block) end # Return +true+ if +TRACE+ messages are being logged. # # @return [Boolean] def trace? level <= TRACE end # Set the log level to trace. # # @return [void] def trace! self.level = TRACE end # Log a message when the severity is not known. Unknown messages will always appear in the log. # The message can be passed in either the +message+ argument or in a block. # # @param message_or_progname_or_attributes [Object] The message to log or progname # if the message is passed in a block. # @param progname_or_attributes [String, Hash] The name of the program that is logging the message or attributes # if the message is passed in a block. # @return [void] def unknown(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block) call_add_entry(Logger::UNKNOWN, message_or_progname_or_attributes, progname_or_attributes, &block) end # Add a message when the severity is not known. # # @param msg [Object] The message to log. # @return [void] def <<(msg) add_entry(default_severity, msg) end # Tag the logger with a set of attributes. If a block is given, the attributes will only be set # for the duration of the block. Otherwise the attributes will be applied on the current # logger context for the duration of the current context. If there is no current context, # then a new logger object will be returned with those attributes set on it. # # @param attributes [Hash] The attributes to set. # @return [Object, Lumberjack::ContextLogger] If a block is given then the result of the block is returned. # Otherwise it returns the logger itself so you can chain methods. # # @example # # Only applies the attributes inside the block # logger.tag(foo: "bar") do # logger.info("message") # end # # @example # # Only applies the attributes inside the context block # logger.context do # logger.tag(foo: "bar") # logger.info("message") # end def tag(attributes, &block) if block context do |ctx| ctx.assign_attributes(attributes) block.call(ctx) end else local_context&.assign_attributes(attributes) self end end # Tags the logger with a set of persistent attributes. These attributes will be included on every log # entry and are not tied to a context block. If the logger does not have a default context, then # these will be ignored. # # @param attributes [Hash] The attributes to set persistently on the logger. # @return [nil] # @example # logger.tag!(version: "1.2.3", environment: "production") # logger.info("Server started") # Will include version and environment attributes def tag!(attributes) default_context&.assign_attributes(attributes) nil end # Tags the outermost context with a set of attributes. If there is no outermost context, then # nothing will happen. This method can be used to bubble attributes up to the top level context. # It can be used in situations where you want to ensure a set of attributes are set for the rest # of the request or operation defined by the outmermost context. # # @param attributes [Hash] The attributes to set on the outermost context. # @return [nil] # # @example # logger.tag(request_id: "12345") do # logger.tag(action: "login") do # # Add the user_id attribute to the outermost context along with request_id so that # # it doesn't fall out of scope after this tag block ends. # logger.tag_all_contexts(user_id: "67890") # end # end def tag_all_contexts(attributes) parent_context = local_context while parent_context parent_context.assign_attributes(attributes) parent_context = parent_context.parent end nil end # Append a value to an attribute. This method can be used to add "tags" to a logger by appending # values to the same attribute. The tag values will be appended to any value that is already # in the attribute. If a block is passed, then a new context will be opened as well. If no # block is passed, then the values will be appended to the attribute in the current context. # If there is no current context, then nothing will happen. # # @param attribute_name [String, Symbol] The name of the attribute to append values to. # @param tags [Array] The tags to add. # @return [Object, Lumberjack::Logger] If a block is passed then returns the result of the block. # Otherwise returns self so that calls can be chained. def append_to(attribute_name, *tags, &block) return self unless block || in_context? current_tags = attribute_value(attribute_name) || [] current_tags = [current_tags] unless current_tags.is_a?(Array) new_tags = current_tags + tags.flatten tag(attribute_name => new_tags, &block) end # Set up a context block for the logger. All attributes added within the block will be cleared when # the block exits. # # @param block [Proc] The block to execute with the context. # @return [Object] The result of the block. # @yield [Context] def context(&block) unless block_given? raise ArgumentError, "A block must be provided to the context method" end new_context = Context.new(current_context) new_context.parent = local_context new_context_locals do |locals| locals.context = new_context block.call(new_context) end end # Ensure that the block of code is wrapped by a context. If there is not already # a context in scope for this logger, one will be created. # # @return [Object] The result of the block. def ensure_context(&block) if in_context? yield else context(&block) end end # Forks a new logger with a new context that will send output through this logger. # The new logger will inherit the level, progname, and attributes of the current logger # context. Any changes to those values, though, will be isolated to just the forked logger. # Any calls to log messages will be forwarded to the parent logger for output to the # logging device. # # @param level [Integer, String, Symbol, nil] The level to set on the new logger. If this # is not specified, then the level on the parent logger will be used. # @param progname [String, nil] The progname to set on the new logger. If this is not specified, # then the progname on the parent logger will be used. # @param attributes [Hash, nil] The attributes to set on the new logger. The forked logger will # inherit all attributes from the current logging context. # @return [ForkedLogger] # # @example Creating a forked logger # child_logger = logger.fork(level: :debug, progname: "Child") # child_logger.debug("This goes to the parent logger's device") def fork(level: nil, progname: nil, attributes: nil) logger = ForkedLogger.new(self) logger.level = level if level logger.progname = progname if progname logger.tag!(attributes) if attributes logger.isolation_level = isolation_level logger end # Remove attributes from the current context block. # # @param attribute_names [Array] The attributes to remove. # @return [void] def untag(*attribute_names) attributes = local_context&.attributes AttributesHelper.new(attributes).delete(*attribute_names) if attributes nil end # Remove attributes from the default context for the logger. # # @param attribute_names [Array] The attributes to remove. # @return [void] def untag!(*attribute_names) attributes = default_context&.attributes AttributesHelper.new(attributes).delete(*attribute_names) if attributes nil end # Return all attributes in scope on the logger including global attributes set on the Lumberjack # context, attributes set on the logger, and attributes set on the current block for the logger. # # @return [Hash] def attributes merge_all_attributes || {} end # Get the value of an attribute by name from the current context. # # @param name [String, Symbol] The name of the attribute to get. # @return [Object, nil] The value of the attribute or nil if the attribute does not exist. def attribute_value(name) name = name.join(".") if name.is_a?(Array) AttributesHelper.new(attributes)[name] end # Remove all attributes on the current logger and logging context within a block. # You can still set new block scoped attributes within the block and provide # attributes on individual log methods. # # @return [void] def clear_attributes(&block) new_context_locals do |locals| locals.cleared = true context do |ctx| ctx.clear_attributes block.call end end end # Return true if the thread is currently in a context block with a local context. # # @return [Boolean] def in_context? !!local_context end # Add an entry to the log. This method must be implemented by the class that includes this module. # # @param severity [Integer, Symbol, String] The severity of the message. # @param message [Object] The message to log. # @param progname [String] The name of the program that is logging the message. # @param attributes [Hash] The attributes to add to the log entry. # @return [void] # @api private def add_entry(severity, message, progname = nil, attributes = nil) raise NotImplementedError end private def current_context local_context || default_context end def local_context current_context_locals&.context end def default_context nil end # Write a log entry to the logging device. # # @param entry [Lumberjack::LogEntry] The log entry to write. # @return [void] # @api private def write_to_device(entry) raise NotImplementedError end # Dereference arguments to log calls so we can have methods with compatibility with ::Logger def call_add_entry(severity, message_or_progname_or_attributes, progname_or_attributes, &block) # :nodoc: severity = Severity.coerce(severity) unless severity.is_a?(Integer) return true unless level.nil? || severity >= level message = nil progname = nil attributes = nil if block message = block if message_or_progname_or_attributes.is_a?(Hash) attributes = message_or_progname_or_attributes progname = progname_or_attributes else progname = message_or_progname_or_attributes attributes = progname_or_attributes if progname_or_attributes.is_a?(Hash) end else message = message_or_progname_or_attributes if progname_or_attributes.is_a?(Hash) attributes = progname_or_attributes else progname = progname_or_attributes end end message = message.call if message.is_a?(Proc) message = message.strip if message.is_a?(String) && message.match?(LEADING_OR_TRAILING_WHITESPACE) return if (message.nil? || message == "") && (attributes.nil? || attributes.empty?) add_entry(severity, message, progname, attributes) true end # Merge a attributes hash into an existing attributes hash. def merge_attributes(current_attributes, attributes) if current_attributes.nil? || current_attributes.empty? attributes elsif attributes.nil? current_attributes else current_attributes.merge(attributes) end end def merge_all_attributes attributes = nil unless current_context_locals&.cleared global_context_attributes = Lumberjack.context_attributes if global_context_attributes && !global_context_attributes.empty? attributes ||= {} attributes.merge!(global_context_attributes) end default_attributes = default_context&.attributes if default_attributes && !default_attributes.empty? attributes ||= {} attributes.merge!(default_attributes) end end context_attributes = current_context&.attributes if context_attributes && !context_attributes.empty? attributes ||= {} attributes.merge!(context_attributes) end attributes end end end bdurand-lumberjack-ac97435/lib/lumberjack/device.rb000066400000000000000000000162211515437321200222650ustar00rootroot00000000000000# frozen_string_literal: true require_relative "device_registry" module Lumberjack # Abstract base class defining the interface for logging output devices. # Devices are responsible for the final output of log entries to various # destinations such as files, streams, databases, or external services. # # This class establishes the contract that all concrete device implementations # must follow, with the +write+ method being the only required implementation. # Additional lifecycle methods (+close+, +flush+, +reopen+) and configuration # methods (+datetime_format+) are optional but provide standardized interfaces # for device management. # # The device architecture allows for flexible log output handling while # maintaining consistent behavior across different output destinations. # Devices receive formatted LogEntry objects and are responsible for their # final serialization and delivery. # # @abstract Subclass and implement {#write} to create a concrete device # @see Lumberjack::Device::Writer File-based output device # @see Lumberjack::Device::LoggerWrapper Ruby Logger compatibility device # @see Lumberjack::Device::Multi Multiple device routing # @see Lumberjack::Device::Null Silent device for testing # @see Lumberjack::Device::Test In-memory device for testing class Device require_relative "device/writer" require_relative "device/log_file" require_relative "device/logger_wrapper" require_relative "device/multi" require_relative "device/null" require_relative "device/test" require_relative "device/buffer" require_relative "device/size_rolling_log_file" require_relative "device/date_rolling_log_file" class << self # Open a logging device with the given options. # # @param device [nil, Symbol, String, File, IO, Array, Lumberjack::Device, ContextLogger] The device to open. # The device can be: # - +nil+: returns a +Device::Null+ instance that discards all log entries. # - +Symbol+: looks up the device in the +DeviceRegistry+ and creates a new instance with the provided options. # - +String+ or +Pathname+: treated as a file path and opens a +Device::LogFile+. # - +File+: opens a +Device::LogFile+ for the given file stream. # - +IO+: opens a +Device::Writer+ wrapping the given IO stream. # - +Lumberjack::Device+: returns the device instance as-is. # - +ContextLogger+: wraps the logger in a +Device::LoggerWrapper+. # - +Array+: each element is treated as a device specification and opened recursively, # returning a +Device::Multi+ that routes log entries to all specified devices. Each # device can have its own options hash if passed as a two-element array +[device, options]+. # @param options [Hash] Options to pass to the device constructor. # @return [Lumberjack::Device] The opened device instance. # # @example Open a file-based device # device = Lumberjack::Device.open_device("/var/log/myapp.log", shift_age: "daily") # # @example Open a stream-based device # device = Lumberjack::Device.open_device($stdout) # # @example Open a device from the registry # device = Lumberjack::Device.open_device(:syslog) # # @example Open multiple devices # device = Lumberjack::Device.open_device([["/var/log/app.log", {shift_age: "daily"}], $stdout]) # # @example Wrap another logger # device = Lumberjack::Device.open_device(Lumberjack::Logger.new($stdout)) def open_device(device, options = {}) device = device.to_s if device.is_a?(Pathname) if device.nil? Device::Null.new elsif device.is_a?(Device) device elsif device.is_a?(Symbol) DeviceRegistry.new_device(device, options) elsif device.is_a?(ContextLogger) || device.is_a?(::Logger) Device::LoggerWrapper.new(device) elsif device.is_a?(Array) devices = device.collect do |dev, dev_options| dev_options = dev_options.is_a?(Hash) ? options.merge(dev_options) : options open_device(dev, dev_options) end Device::Multi.new(devices) elsif io_but_not_file_stream?(device) Device::Writer.new(device, options) else Device::LogFile.new(device, options) end end private def io_but_not_file_stream?(object) return false if object.is_a?(File) return false unless object.respond_to?(:write) return true if object.respond_to?(:tty?) && object.tty? return false if object.respond_to?(:path) && object.path true end end # Write a log entry to the device. This is the core method that all device # implementations must provide. The method receives a fully formatted # LogEntry object and is responsible for outputting it to the target # destination. # # @param entry [Lumberjack::LogEntry] The log entry to write to the device # @return [void] # @abstract Subclasses must implement this method # @raise [NotImplementedError] If called on the abstract base class def write(entry) raise NotImplementedError end # Close the device and release any resources. The default implementation # calls flush to ensure any buffered data is written before closing. # Subclasses should override this method if they need to perform specific # cleanup operations such as closing file handles or network connections. # # @return [void] def close flush end # Reopen the device, optionally with a new log destination. The default # implementation calls flush to ensure data consistency. This method is # typically used for log rotation scenarios or when changing output # destinations dynamically. # # @param logdev [Object, nil] Optional new log device or destination # @return [void] def reopen(logdev = nil) flush end # Flush any buffered data to the output destination. The default # implementation is a no-op since not all devices use buffering. # Subclasses that implement buffering should override this method # to ensure data is written to the final destination. # # @return [void] def flush end # Get the current datetime format string used for timestamp formatting. # The default implementation returns nil, indicating no specific format # is set. Subclasses may override this to provide device-specific # timestamp formatting. # # @return [String, nil] The datetime format string, or nil if not set def datetime_format end # Set the datetime format string for timestamp formatting. The default # implementation is a no-op. Subclasses that support configurable # timestamp formatting should override this method to store and apply # the specified format. # # @param format [String, nil] The datetime format string to use for timestamps # @return [void] def datetime_format=(format) end # Expose the underlying stream if any. # # @return [IO, Lumberjacke::Device, nil] # @api private def dev self end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/000077500000000000000000000000001515437321200217365ustar00rootroot00000000000000bdurand-lumberjack-ac97435/lib/lumberjack/device/buffer.rb000066400000000000000000000135651515437321200235460ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A buffered logging device that wraps another logging device. Entries are buffered in memory # until the buffer size is reached or the device is flushed. # # @example Create a buffered device that flushes every 5 entries # device = Lumberjack::Device::Buffer.new(Lumberjack::Device::LogFile.new("logfile.log"), buffer_size: 5) # # @example Create a buffered device that automatically flushes every 10 seconds # device = Lumberjack::Device::Buffer.new("/var/log/app.log", buffer_size: 10, flush_seconds: 10) # # @example Create a buffered device with a before_flush callback # before_flush = -> { puts "Flushing log buffer" } # device = Lumberjack::Device::Buffer.new(device, buffer_size: 10, before_flush: before_flush) class Device::Buffer < Device # Internal class that manages the entry buffer and flushing logic. class EntryBuffer attr_accessor :size attr_reader :device, :last_flushed_at def initialize(device, size, before_flush) @device = device @size = size @before_flush = before_flush if before_flush.respond_to?(:call) @lock = Mutex.new @entries = [] @last_flushed_at = Time.now @closed = false end def <<(entry) return if closed? @lock.synchronize do @entries << entry end flush if @entries.size >= @size end def flush entries = nil if closed? @before_flush&.call entries = @entries @entries = [] else @lock.synchronize do @before_flush&.call entries = @entries @entries = [] end end @last_flushed_at = Time.now return if entries.nil? entries.each do |entry| @device.write(entry) rescue => e warn("Error writing log entry from buffer: #{e.inspect}") end end def close @closed = true flush end def closed? @closed end def reopen @closed = false end def empty? @entries.empty? end end class << self private def create_finalizer(buffer) # :nodoc: lambda { |object_id| buffer.close } end def create_flusher_thread(flush_seconds, buffer) # :nodoc: Thread.new do until buffer.closed? sleep(flush_seconds) buffer.flush if Time.now - buffer.last_flushed_at >= flush_seconds end end end end # Initialize a new buffered logging device that wraps another device. # # @param wrapped_device [Lumberjack::Device, String, Symbol, IO] The underlying device to wrap. # This can be any valid device specification that +Lumberjack::Device.open_device+ accepts. # Options not related to buffering will be passed to the underlying device constructor. # @param options [Hash] Options for the buffer and the underlying device. # @option options [Integer] :buffer_size The number of entries to buffer before flushing. Default is 0 (no buffering). # @option options [Integer] :flush_seconds If specified, a background thread will flush the buffer every N seconds. # @option options [Proc] :before_flush A callback that will be called before each flush. The callback should # respond to +call+ and take no arguments. def initialize(wrapped_device, options = {}) buffer_options = [:buffer_size, :flush_seconds, :before_flush] device_options = options.reject { |k, _| buffer_options.include?(k) } device = Device.open_device(wrapped_device, device_options) @buffer = EntryBuffer.new(device, options[:buffer_size] || 0, options[:before_flush]) flush_seconds = options[:flush_seconds] self.class.send(:create_flusher_thread, flush_seconds, @buffer) if flush_seconds.is_a?(Numeric) && flush_seconds > 0 # Add a finalizer to ensure flush is called before the object is destroyed ObjectSpace.define_finalizer(self, self.class.send(:create_finalizer, @buffer)) end def buffer_size @buffer.size end # Set the buffer size. The underlying device will only be written to when the buffer size # is exceeded. # # @param [Integer] value The size of the buffer in bytes. # @return [void] def buffer_size=(value) @buffer.size = value @buffer.flush end # Write an entry to the underlying device. # # @param [LogEntry, String] entry The entry to write. # @return [void] def write(entry) @buffer << entry end # Close the device. # # @return [void] def close @buffer.close @buffer.device.close # Remove the finalizer since we've already flushed ObjectSpace.undefine_finalizer(self) end # Return true if the buffer has been closed. def closed? @buffer.closed? end # Flush the buffer to the underlying device. # # @return [void] def flush @buffer.flush end # Reopen the underlying device, optionally with a new log destination. def reopen(logdev = nil) flush @buffer.device.reopen(logdev) @buffer.reopen ObjectSpace.define_finalizer(self, self.class.send(:create_finalizer, @buffer)) end # Return the underlying stream. Provided for API compatibility with Logger devices. # # @return [IO] The underlying stream. def dev @buffer.device.dev end # @api private def last_flushed_at @buffer.last_flushed_at end # @api private def empty? @buffer.empty? end private def create_flusher_thread(flush_seconds, buffer) # :nodoc: Thread.new do until buffer.closed? sleep(flush_seconds) buffer.flush if Time.now - buffer.last_flushed_at >= flush_seconds end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/date_rolling_log_file.rb000066400000000000000000000014101515437321200265620ustar00rootroot00000000000000# frozen_string_literal: true require "date" module Lumberjack # Deprecated device. Use LogFile instead. # # @deprecated Use Lumberjack::Device::LogFile class Device::DateRollingLogFile < Device::LogFile def initialize(path, options = {}) Utils.deprecated("Lumberjack::Device::DateRollingLogFile", "Lumberjack::Device::DateRollingLogFile is deprecated and will be removed in version 2.1; use Lumberjack::Device::LogFile instead.") unless options[:roll]&.to_s&.match(/(daily)|(weekly)|(monthly)/i) raise ArgumentError.new("illegal value for :roll (#{options[:roll].inspect})") end new_options = options.reject { |k, _| k == :roll }.merge(shift_age: options[:roll].to_s.downcase) super(path, new_options) end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/log_file.rb000066400000000000000000000064601515437321200240510ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A file-based logging device that extends the Writer device with automatic # log rotation capabilities. This device wraps Ruby's standard Logger::LogDevice # to provide file size-based and time-based log rotation while maintaining # compatibility with the Lumberjack device interface. # # The device supports all the rotation features available in Ruby's Logger, # including maximum file size limits, automatic rotation based on age, and # automatic cleanup of old log files. This makes it suitable for production # environments where log management is crucial. # # @example Basic file logging # device = Lumberjack::Device::LogFile.new("/var/log/app.log") # # @example With size-based rotation (10MB files, keep 5 old files) # device = Lumberjack::Device::LogFile.new( # "/var/log/app.log", # shift_size: 10 * 1024 * 1024, # 10MB # shift_age: 5 # Keep 5 old files # ) # # @example With daily rotation # device = Lumberjack::Device::LogFile.new( # "/var/log/app.log", # shift_age: "daily" # ) # # @example With weekly rotation # device = Lumberjack::Device::LogFile.new( # "/var/log/app.log", # shift_age: "weekly" # ) # # @see Device::Writer # @see Logger::LogDevice class Device::LogFile < Device::Writer # Initialize a new LogFile device with automatic log rotation capabilities. # This constructor wraps Ruby's Logger::LogDevice while filtering options to # only pass supported parameters, ensuring compatibility across Ruby versions. # # @param stream [String, IO] The log destination. Can be a file path string # or an IO object. When a string path is provided, the file will be created # if it doesn't exist, and parent directories will be created as needed. # @param options [Hash] Configuration options for the log device. All options # supported by Logger::LogDevice are accepted, including: # - +:shift_age+ - Number of old files to keep, or rotation frequency # ("daily", "weekly", "monthly") # - +:shift_size+ - Maximum file size in bytes before rotation # - +:shift_period_suffix+ - Suffix to add to rotated log files # - +:binmode+ - Whether to open the log file in binary mode def initialize(stream, options = {}) # Filter options to only include keyword arguments supported by Logger::LogDevice#initialize supported_kwargs = ::Logger::LogDevice.instance_method(:initialize).parameters .select { |type, _| type == :key || type == :keyreq } .map { |_, name| name } filtered_options = options.slice(*supported_kwargs) logdev = ::Logger::LogDevice.new(stream, **filtered_options) super(logdev, options) end # Get the file system path of the current log file. This method provides # access to the actual file path being written to, which is useful for # monitoring, log analysis tools, or other file-based operations. # # @return [String] The absolute file system path of the current log file def path stream.filename end # Expose the underlying stream. # # @return [IO] # @api private def dev stream.dev end def reopen(logdev = nil) stream.reopen(logdev) end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/logger_wrapper.rb000066400000000000000000000125151515437321200253060ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A logging device that forwards log entries to another logger instance. # This device enables hierarchical logging architectures and broadcasting scenarios # where log entries need to be distributed to multiple loggers or processed through # different logging pipelines. # # The device is particularly useful when combined with Device::Multi to create # master loggers that can simultaneously write to multiple destinations (files, # databases, external services) while maintaining consistent formatting and # attribute handling across all targets. # # Unlike other devices that write directly to output streams, this device delegates # to another logger's processing pipeline, allowing for complex logging topologies # and reuse of existing logger configurations. # # @example Basic logger forwarding # file_logger = Lumberjack::Logger.new("/var/log/app.log") # logger_device = Lumberjack::Device::LoggerWrapper.new(file_logger) # # @example Broadcasting with Multi device # main_logger = Lumberjack::Logger.new("/var/log/main.log") # error_logger = Lumberjack::Logger.new("/var/log/errors.log") # # broadcast_device = Lumberjack::Device::Multi.new([ # Lumberjack::Device::LoggerWrapper.new(main_logger), # Lumberjack::Device::LoggerWrapper.new(error_logger) # ]) # # master_logger = Lumberjack::Logger.new(broadcast_device) # # @example Hierarchical logging with filtering # # Main application logger # app_logger = Lumberjack::Logger.new("/var/log/app.log") # # # Focused error logs # error_logger = Lumberjack::Logger.new("/var/log/error.log") # error_logger.level = Logger::WARN # # # Route all logs to app, error logs to error logger # multi_device = Lumberjack::Device::Multi.new([ # Lumberjack::Device::LoggerWrapper.new(app_logger), # Lumberjack::Device::LoggerWrapper.new(error_logger) # ]) # # @see Device::Multi # @see Lumberjack::Logger # @see Lumberjack::ContextLogger class Device::LoggerWrapper < Device # @!attribute [r] logger # @return [Lumberjack::ContextLogger] The target logger that will receive forwarded log entries attr_reader :logger # Initialize a new Logger device that forwards entries to the specified logger. # The target logger must be a Lumberjack logger that supports the ContextLogger # interface to ensure proper entry handling and attribute processing. # # @param logger [Lumberjack::ContextLogger, ::Logger] The target logger to receive forwarded entries. # Must be a Lumberjack logger instance (Logger, ForkedLogger, etc.) that includes # the ContextLogger mixin for proper entry processing. # # @raise [ArgumentError] If the provided logger is not a Lumberjack::ContextLogger def initialize(logger) unless logger.is_a?(Lumberjack::ContextLogger) || logger.is_a?(::Logger) raise ArgumentError.new("Logger must be a Lumberjack logger") end @logger = logger end # Forward a log entry to the target logger for processing. This method extracts # the entry components and delegates to the target logger's add_entry method, # ensuring that all attributes, formatting, and processing logic of the target # logger are properly applied. # # The forwarded entry maintains all original metadata including severity, # timestamp, program name, and custom attributes, allowing the target logger # to process it as if it were generated directly. # # @param entry [Lumberjack::LogEntry] The log entry to forward to the target logger # @return [void] def write(entry) if @logger.is_a?(Lumberjack::ContextLogger) @logger.add_entry(entry.severity, entry.message, entry.progname, entry.attributes) else message = entry.message if entry.attributes && !entry.attributes.empty? message_attributes = [] entry.attributes.each do |key, value| next if value.nil? || (value.respond_to?(:empty?) && value.empty?) value = value.join(",") if value.is_a?(Enumerable) message_attributes << "[#{key}=#{value}]" end message = "#{message} #{message_attributes.join(" ")}" unless message_attributes.empty? end @logger.add(entry.severity, message, entry.progname) end end # Closes the target logger to release any resources or finalize log output. # This method delegates to the target logger's +close+ method. # # @return [void] def close @logger.close end # Reopen the underlying logger device. This is typically used to reopen log files # after log rotation or to refresh the logger's output stream. # # Delegates to the target logger's +reopen+ method. # # @return [void] def reopen @logger.reopen end # Flushes the target logger, ensuring that any buffered log entries are written out. # This delegates to the target logger's flush method, which may flush buffers to disk, # external services, or other destinations depending on the logger's configuration. # # @return [void] def flush @logger.flush end # Expose the underlying stream if any. # # @return [IO, Lumberjack::Device, nil] # @api private def dev @logger.device&.dev end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/multi.rb000066400000000000000000000105461515437321200234230ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A multiplexing logging device that broadcasts log entries to multiple target # devices simultaneously. This device enables sophisticated logging architectures # where a single log entry needs to be processed by multiple output destinations, # each potentially with different formatting, filtering, or storage mechanisms. # # The Multi device acts as a fan-out mechanism, ensuring that all configured # devices receive every log entry while maintaining independent processing # pipelines. This is particularly useful for creating redundant logging systems, # separating log streams by concern, or implementing complex routing logic. # # All device lifecycle methods (flush, close, reopen) are propagated to all # child devices, ensuring consistent state management across the entire # logging topology. # # @example Basic multi-device setup # file_device = Lumberjack::Device::Writer.new("/var/log/app.log") # console_device = Lumberjack::Device::Writer.new(STDOUT, template: "{{message}}") # multi_device = Lumberjack::Device::Multi.new(file_device, console_device) class Device::Multi < Device attr_reader :devices # Initialize a new Multi device with the specified target devices. The device # accepts multiple devices either as individual arguments or as arrays, # automatically flattening nested arrays for convenient configuration. # # @param devices [Array] The target devices to receive # log entries. Can be passed as individual arguments or arrays. All devices # must implement the standard Lumberjack::Device interface. # # @example Individual device arguments # multi = Multi.new(file_device, console_device, database_device) def initialize(*devices) @devices = devices.flatten end # Broadcast a log entry to all configured devices. Each device receives the # same LogEntry object and processes it according to its own configuration, # formatting, and output logic. Devices are processed sequentially in the # order they were configured. # # @param entry [Lumberjack::LogEntry] The log entry to broadcast to all devices # @return [void] def write(entry) devices.each do |device| device.write(entry) end end # Flush all configured devices to ensure buffered data is written to their # respective destinations. This method calls flush on each device in sequence, # ensuring consistent state across all output destinations. # # @return [void] def flush devices.each do |device| device.flush end end # Close all configured devices and release their resources. This method calls # close on each device in sequence, ensuring proper cleanup of file handles, # network connections, and other resources across all output destinations. # # @return [void] def close devices.each do |device| device.close end end # Reopen all configured devices, optionally with a new log destination. # This method calls reopen on each device in sequence, which is typically # used for log rotation scenarios or when changing output destinations. # # @param logdev [Object, nil] Optional new log device or destination to pass # to each device's reopen method # @return [void] def reopen(logdev = nil) devices.each do |device| device.reopen(logdev = nil) end end # Get the datetime format from the first device that has one configured. # This method searches through the configured devices and returns the # datetime format from the first device that provides one. # # @return [String, nil] The datetime format string from the first device # that has one configured, or nil if no devices have a format set def datetime_format devices.detect(&:datetime_format).datetime_format end # Set the datetime format on all configured devices that support it. # This method propagates the format setting to each device, allowing # coordinated timestamp formatting across all output destinations. # # @param format [String] The datetime format string to apply to all devices # @return [void] def datetime_format=(format) devices.each do |device| device.datetime_format = format end end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/null.rb000066400000000000000000000022051515437321200232340ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A logging device that discards all output. This device provides a silent # logging implementation useful for testing environments, performance benchmarks, # or production scenarios where logging needs to be temporarily disabled without # changing logger configuration. # # The Null device implements the complete Device interface but performs no # actual operations, making it both efficient and transparent. It accepts # any constructor arguments for compatibility but ignores them all. # # @example Creating a silent logger # logger = Lumberjack::Logger.new(Lumberjack::Device::Null.new) # logger.info("This message is discarded") # # @example Using the convenience constructor # logger = Lumberjack::Logger.new(:null) # logger.error("This error is also discarded") class Device::Null < Device DeviceRegistry.add(:null, self) def initialize(*args) end # Discard the log entry without performing any operation. # # @param entry [Lumberjack::LogEntry] The log entry to discard. # @return [void] def write(entry) end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/size_rolling_log_file.rb000066400000000000000000000016601515437321200266260ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # Deprecated device. Use LogFile instead. # # @deprecated Use Lumberjack::Device::LogFile class Device::SizeRollingLogFile < Device::LogFile attr_reader :max_size # Create an new log device to the specified file. The maximum size of the log file is specified with # the :max_size option. The unit can also be specified: "32K", "100M", "2G" are all valid. def initialize(path, options = {}) Utils.deprecated("Lumberjack::Device::SizeRollingLogFile", "Lumberjack::Device::SizeRollingLogFile is deprecated and will be removed in version 2.1; use Lumberjack::Device::LogFile instead.") @max_size = options[:max_size] new_options = options.reject { |k, _| k == :max_size }.merge(shift_size: max_size) new_options[:shift_age] = 10 unless options[:shift_age].is_a?(Integer) && options[:shift_age] >= 0 super(path, new_options) end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/test.rb000066400000000000000000000354271515437321200232550ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # An in-memory logging device designed specifically for testing and debugging # scenarios. This device captures log entries in a thread-safe buffer, allowing # test code to make assertions about logged content, verify logging behavior, # and inspect log entry details without writing to external outputs. # # The device provides matching capabilities through integration # with LogEntryMatcher, supporting pattern matching on messages, severity levels, # attributes, and program names. This makes it ideal for comprehensive logging # verification in test suites. # # The buffer is automatically managed with configurable size limits to prevent # memory issues during long-running tests, and provides both individual entry # access and bulk matching operations. # # @example Basic test setup # logger = Lumberjack::Logger.new(Lumberjack::Device::Test.new) # logger.info("User logged in", user_id: 123) # # expect(logger.device.entries.size).to eq(1) # expect(logger.device.last_entry.message).to eq("User logged in") # # @example Using convenience constructor # logger = Lumberjack::Logger.new(:test) # logger.warn("Something suspicious", ip: "192.168.1.100") # # expect(logger.device).to include(severity: :warn, message: /suspicious/) # expect(logger.device).to include(attributes: {ip: "192.168.1.100"}) # # @example Advanced pattern matching # logger = Lumberjack::Logger.new(:test) # logger.error("Database error: connection timeout", # database: "users", timeout: 30.5, retry_count: 3) # # expect(logger.device).to include( # severity: :error, # message: /Database error/, # attributes: { # database: "users", # timeout: Float, # retry_count: be > 0 # } # ) # # @example Nested attribute matching # logger.info("Request completed", request: {method: "POST", path: "/users"}) # # expect(logger.device).to include( # attributes: {"request.method" => "POST", "request.path" => "/users"} # ) # # @example Capturing logs to a file only for failed rspec tests # # Set up test logger (presumably in an initializer) # Application.logger = Lumberjack::Logger.new(:test) # # # In your spec_helper or rails_helper.rb # RSpec.configure do |config| # failed_test_logs = Lumberjack::Logger.new("log/test.log") # # config.around do |example| # Application.logger.device.clear # # example.run # # if example.exception # failed_test_logs.error("Test failed: #{example.full_description}") # Application.logger.device.write_to(failed_test_logs) # end # end # end # # @see LogEntryMatcher class Device::Test < Device DeviceRegistry.add(:test, self) # @!attribute [rw] max_entries # @return [Integer] The maximum number of entries to retain in the buffer attr_accessor :max_entries # Configuration options passed to the constructor. While these don't affect # device behavior, they can be useful in tests to verify that options are # correctly passed through device creation and configuration pipelines. # # @return [Hash] A copy of the options hash passed during initialization attr_reader :options class << self # Format a log entry or expectation hash into a more human readable format. This is # intended for use in test failure messages to help diagnose why a match failed when # calling +include?+ or +match+. # # @param expectation [Hash, Lumberjack::LogEntry] The expectation or log entry to format. # @option severity [String, Symbol, Integer] The severity level to match. # @option message [String, Regexp, Object] Pattern to match against log entry messages. # @option attributes [Hash] Hash of attribute patterns to match against log entry attributes. # @option progname [String, Regexp, Object] Pattern to match against the program name that generated the log entry. # @param indent [Integer] The number of spaces to indent each line. # @return [String] A formatted string representation of the expectation or log entry. def formatted_expectation(expectation, indent: 0) if expectation.is_a?(Lumberjack::LogEntry) expectation = { "severity" => expectation.severity_label, "message" => expectation.message, "progname" => expectation.progname, "attributes" => expectation.attributes } end expectation = expectation.transform_keys(&:to_s).compact severity = Lumberjack::Severity.coerce(expectation["severity"]) if expectation.include?("severity") message = [] indent_str = " " * indent message << "#{indent_str}severity: #{Lumberjack::Severity.level_to_label(severity)}" if severity message << "#{indent_str}message: #{expectation["message"]}" if expectation.include?("message") message << "#{indent_str}progname: #{expectation["progname"]}" if expectation.include?("progname") if expectation["attributes"].is_a?(Hash) && !expectation["attributes"].empty? attributes = Lumberjack::Utils.flatten_attributes(expectation["attributes"]) label = "attributes:" prefix = "#{indent_str}#{label}" attributes.sort_by(&:first).each do |name, value| message << "#{prefix} #{name}: #{value.inspect}" prefix = "#{indent_str}#{" " * label.length}" end end message.join(Lumberjack::LINE_SEPARATOR) end end # Initialize a new Test device with configurable buffer management. # The device creates a thread-safe in-memory buffer for capturing log # entries with automatic size management to prevent memory issues. # # @param options [Hash] Configuration options for the test device # @option options [Integer] :max_entries (1000) The maximum number of entries # to retain in the buffer. When this limit is exceeded, the oldest entries # are automatically removed to maintain the size limit. def initialize(options = {}) @buffer = [] @max_entries = options[:max_entries] || 1000 @lock = Mutex.new @options = options.dup end # Write a log entry to the in-memory buffer. The method is thread-safe and # automatically manages buffer size by removing the oldest entries when # the maximum capacity is exceeded. Entries are ignored if max_entries is # set to less than 1. # # @param entry [Lumberjack::LogEntry] The log entry to store in the buffer # @return [void] def write(entry) return if max_entries < 1 @lock.synchronize do @buffer << entry while @buffer.size > max_entries @buffer.shift end end end # Return a thread-safe copy of all captured log entries. The returned array # is a snapshot of the current buffer state and can be safely modified # without affecting the internal buffer. # # @return [Array] A copy of all captured log entries # in chronological order (oldest first) def entries @lock.synchronize { @buffer.dup } end # Return the most recently captured log entry. This provides quick access # to the latest logged information without needing to access the full # entries array. # # @return [Lumberjack::LogEntry, nil] The most recent log entry, or nil # if no entries have been captured yet def last_entry @buffer.last end # Clear all captured log entries from the buffer. This method is useful # for resetting the device state between tests or when you want to start # fresh log capture without creating a new device instance. # # @return [void] def clear @buffer = [] nil end # Write the captured log entries out to another logger or device. This can be useful # in testing scenarios where you want to preserve log output for failed tests. # # @param logger [Lumberjack::Logger, Lumberjack::Device] The target logger or device # to which captured entries should be written # @return [void] def write_to(logger) device = (logger.is_a?(Lumberjack::Device) ? logger : logger.device) entries.each do |entry| device.write(entry) end nil end # Test whether any captured log entries match the specified criteria. # This method provides a convenient interface for making assertions about # logged content using flexible pattern matching capabilities. # # Severity can be specified as a numeric constant (Logger::WARN), symbol # (:warn), or string ("warn"). Messages support exact string matching or # regular expression patterns. Attributes support nested matching using # dot notation and can use any matcher values supported by your test # framework (e.g., RSpec's +anything+, +instance_of+, etc.). # # @param options [Hash] The matching criteria to test against captured entries # @option options [String, Regexp, Object] :message Pattern to match against # log entry messages. Supports exact strings, regular expressions, or any # object that responds to case equality (===) # @option options [String, Symbol, Integer] :severity The severity level to # match. Accepts symbols (:debug, :info, :warn, :error, :fatal), strings, # or numeric Logger constants # @option options [Hash] :attributes Hash of attribute patterns to match. # Supports nested attributes using dot notation (e.g., "user.id" matches # { user: { id: value } }). Values can be exact matches or test framework matchers # @option options [String, Regexp, Object] :progname Pattern to match against # the program name that generated the log entry # # @return [Boolean] True if any captured entries match all specified criteria, # false otherwise # # @example Basic message and severity matching # expect(device).to include(severity: :error, message: "Database connection failed") # # @example Regular expression message matching # expect(device).to include(severity: :info, message: /User \d+ logged in/) # # @example Attribute matching with exact values # expect(device).to include(attributes: {user_id: 123, action: "login"}) # # @example Nested attribute matching # expect(device).to include(attributes: {"request.method" => "POST", "response.status" => 200}) # # @example Using test framework matchers (RSpec example) # expect(device).to include( # severity: :warn, # message: start_with("Warning:"), # attributes: {duration: be_a(Float), retries: be > 0} # ) # # @example Multiple criteria matching # expect(device).to include( # severity: :error, # message: /timeout/i, # progname: "DatabaseWorker", # attributes: {database: "users", timeout_seconds: be > 30} # ) def include?(options) options = options.transform_keys(&:to_sym) !!match(**options) end # Find and return the first captured log entry that matches the specified # criteria. This method is useful when you need to inspect specific entry # details or perform more complex assertions on individual entries. # # Uses the same flexible matching capabilities as include? but returns # the actual LogEntry object instead of a boolean result. # # @param message [String, Regexp, Object, nil] Pattern to match against # log entry messages. Supports exact strings, regular expressions, or # any object that responds to case equality (===) # @param severity [String, Symbol, Integer, nil] The severity level to match. # Accepts symbols, strings, or numeric Logger constants # @param attributes [Hash, nil] Hash of attribute patterns to match against # log entry attributes. Supports nested matching using dot notation # @param progname [String, Regexp, Object, nil] Pattern to match against # the program name that generated the log entry # # @return [Lumberjack::LogEntry, nil] The first matching log entry, or nil # if no entries match the specified criteria # # @example Finding a specific error entry # error_entry = device.match(severity: :error, message: /database/i) # expect(error_entry.attributes[:table_name]).to eq("users") # expect(error_entry.time).to be_within(1.second).of(Time.now) # # @example Finding entries with specific attributes # auth_entry = device.match(attributes: {user_id: 123, action: "login"}) # expect(auth_entry.severity_label).to eq("INFO") # expect(auth_entry.progname).to eq("AuthService") # # @example Handling no matches # missing_entry = device.match(severity: :fatal) # expect(missing_entry).to be_nil # # @example Complex attribute matching # api_entry = device.match( # message: /API request/, # attributes: {"request.endpoint" => "/users", "response.status" => 200} # ) # expect(api_entry.attributes["request.endpoint"]).to eq("/users") def match(message: nil, severity: nil, attributes: nil, progname: nil) matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname) entries.detect { |entry| matcher.match?(entry) } end # Get the closest matching log entry from the captured entries based on a scoring system. # This method evaluates how well each entry matches the specified criteria and # returns the entry with the highest score, provided it meets a minimum threshold. # If no entries meet the threshold, nil is returned. # # This method can be used in tests to return the best match when an assertion fails # to aid in diagnosing why no entries met the criteria. # # @param message [String, Regexp, Object, nil] Pattern to match against # log entry messages. Supports exact strings, regular expressions, or # any object that responds to case equality (===) # @param severity [String, Symbol, Integer, nil] The severity level to match. # Accepts symbols, strings, or numeric Logger constants # @param attributes [Hash, nil] Hash of attribute patterns to match against # log entry attributes. Supports nested matching using dot notation # @param progname [String, Regexp, Object, nil] Pattern to match against # the program name that generated the log entry # @return [Lumberjack::LogEntry, nil] The closest matching log entry, or nil # if no entries meet the minimum score threshold def closest_match(message: nil, severity: nil, attributes: nil, progname: nil) matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname) matcher.closest(entries) end end end bdurand-lumberjack-ac97435/lib/lumberjack/device/writer.rb000066400000000000000000000206551515437321200236070ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A versatile logging device that writes formatted log entries to IO streams. # This device serves as the foundation for most output-based logging, converting # LogEntry objects into formatted strings using configurable templates and # writing them to any IO-compatible stream. # # The Writer device supports extensive customization through templates, encoding # options, stream management, and error handling. It can write to files, console # output, network streams, or any object that implements the IO interface. # # Templates can be either string-based (compiled into Template objects) or # callable objects (Procs, lambdas) for maximum flexibility. The device handles # character encoding, whitespace normalization, and provides robust error # recovery when stream operations fail. # # @see Template class Device::Writer < Device EDGE_WHITESPACE_PATTERN = /\A\s|[ \t\f\v][\r\n]*\z/ # Initialize a new Writer device with configurable formatting and stream options. # The device supports multiple template types, encoding control, and stream # behavior configuration for flexible output handling. # # @param stream [IO, #write] The target stream for log output. Can be any object # that responds to write(), including File objects, STDOUT/STDERR, StringIO, # network streams, or custom IO-like objects # @param options [Hash] Configuration options for the writer device # # @option options [String, Proc, nil] :template The formatting template for log entries. # - String: Compiled into a Template object (default: "[:time :severity :progname(:pid)] :message") # - Proc: Called with LogEntry, should return formatted string # - nil: Uses default template # # @option options [Logger::Formatter] :standard_logger_formatter Use a Ruby Logger # formatter for compatibility with existing logging code # # @option options [String, nil] :additional_lines Template for formatting additional # lines in multi-line messages (default: "\n :message") # # @option options [String, Symbol] :time_format Format for timestamps in templates. # Accepts strftime patterns or :milliseconds/:microseconds shortcuts # # @option options [String] :attribute_format Printf-style format for attributes # with exactly two %s placeholders for name and value (default: "[%s:%s]") # # @option options [Boolean] :autoflush (true) Whether to automatically flush # the stream after each write for immediate output # # @option options [Boolean] :binmode (false) Whether to treat the stream as # binary, skipping UTF-8 encoding conversion # # @option options [Boolean] :colorize (false) Whether to colorize log output def initialize(stream, options = {}) @stream = stream @stream.sync = true if @stream.respond_to?(:sync=) && options[:autoflush] != false @binmode = options[:binmode] if options[:standard_logger_formatter] @template = Template::StandardFormatterTemplate.new(options[:standard_logger_formatter]) else template = options[:template] template = TemplateRegistry.template(template, options) if template.is_a?(Symbol) @template = if template.respond_to?(:call) template else Template.new( template, additional_lines: options[:additional_lines], time_format: options[:time_format], attribute_format: options[:attribute_format], colorize: options[:colorize] ) end end end # Write a log entry to the stream with automatic formatting and error handling. # The entry is converted to a string using the configured template, processed # for encoding and whitespace, and written to the stream with robust error recovery. # # @param entry [LogEntry, String] The log entry to write. LogEntry objects are # formatted using the template, while strings are written directly after # encoding and whitespace processing # @return [void] def write(entry) string = (entry.is_a?(LogEntry) ? @template.call(entry) : entry) return if string.nil? if !@binmode && string.encoding != Encoding::UTF_8 string = string.encode("UTF-8", invalid: :replace, undef: :replace) end string = string.strip if string.match?(EDGE_WHITESPACE_PATTERN) return if string.length == 0 || string == Lumberjack::LINE_SEPARATOR write_to_stream(string) end # Close the underlying stream and release any associated resources. This method # ensures all buffered data is flushed before closing the stream, providing # clean shutdown behavior for file handles and network connections. # # @return [void] def close flush stream.close end # Flush the underlying stream to ensure all buffered data is written to the # destination. This method is safe to call on streams that don't support # flushing, making it suitable for various IO types. # # @return [void] def flush stream.flush if stream.respond_to?(:flush) end # Get the current datetime format from the template if supported. Returns the # format string used for timestamp formatting in log entries. # # @return [String, nil] The datetime format string if the template supports it, # or nil if the template doesn't provide datetime formatting def datetime_format @template.datetime_format if @template.respond_to?(:datetime_format) end # Set the datetime format on the template if supported. This allows dynamic # reconfiguration of timestamp formatting without recreating the device. # # @param format [String] The datetime format string (strftime pattern) to # apply to the template for timestamp formatting # @return [void] def datetime_format=(format) if @template.respond_to?(:datetime_format=) @template.datetime_format = format end end # Access the underlying IO stream for direct manipulation or compatibility # with code expecting Logger device interface. This method provides the # raw stream object for advanced use cases. # # @return [IO] The underlying stream object used for output # @api private def dev stream end # Get the file system path of the underlying stream if available. This method # is useful for monitoring, log rotation, or any operations that need to # work with the actual file path. # # @return [String, nil] The file system path if the stream is file-based, # or nil for non-file streams (STDOUT, StringIO, network streams, etc.) def path stream.path if stream.respond_to?(:path) end # The underlying stream object that is being written to. # # @return [IO] The current stream object attr_accessor :stream private # Write a formatted line to the stream with robust error handling. This method # ensures proper line termination, handles IO errors gracefully, and provides # fallback error reporting to STDERR when the primary stream fails. # # @param line [String] The formatted log line to write # @return [void] def write_to_stream(line) out = line.end_with?(Lumberjack::LINE_SEPARATOR) ? line : "#{line}#{Lumberjack::LINE_SEPARATOR}" begin begin stream.write(out) rescue IOError => e raise e if stream.closed? stream.write(out) end rescue => e $stderr.write(error_message(e)) $stderr.write(out) end end # Generate a detailed error message for logging failures. This method creates # informative error messages that include exception details and backtrace # information for debugging stream write failures. # # @param e [Exception] The exception that occurred during stream operations # @return [String] A formatted error message with exception details def error_message(e) "#{e.class.name}: #{e.message}#{" at " + e.backtrace.first if e.backtrace}#{Lumberjack::LINE_SEPARATOR}" end # Create a test log template. def test_log_template(options) kwargs = { exclude_attributes: options[:exclude_attributes], exclude_progname: options[:exclude_progname], exclude_pid: options[:exclude_pid], exclude_time: options[:exclude_time] } TestLogTemplate.new(**kwargs.compact) end end end bdurand-lumberjack-ac97435/lib/lumberjack/device_registry.rb000066400000000000000000000056501515437321200242210ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # The device registry is used for setting up names to represent Device classes. It is used # in the constructor for Lumberjack::Logger and allows passing in a symbol to reference a # device. # # Devices must have a constructor that accepts the options hash as its sole argument in order # to use the device registry. # # The values :stdout and :stderr are registered by default and map to the standard output # and standard error streams, respectively. # # @example # # Lumberjack::Device.register(:my_device, MyDevice) # logger = Lumberjack::Logger.new(:my_device) module DeviceRegistry @registry = {stdout: :stdout, stderr: :stderr} class << self # Register a device name. Device names can be used to associate a symbol with a device # class. The symbol can then be passed to Logger as the device argument. # # Registered devices must take only one argument and that is the options hash for the # device options. # # @param name [Symbol] The name of the device # @param klass [Class] The device class to register # @return [void] def add(name, klass) raise ArgumentError.new("name must be a symbol") unless name.is_a?(Symbol) @registry[name] = klass end # Remove a device from the registry. # # @param name [Symbol] The name of the device to remove # @return [void] def remove(name) @registry.delete(name) end # Check if a device is registered. # # @param name [Symbol] The name of the device # @return [Boolean] True if the device is registered, false otherwise def registered?(name) @registry.include?(name) end # Instantiate a new device with the specified options from the device registry. # # @param name [Symbol] The name of the device # @param options [Hash] The device options # @return [Lumberjack::Device] def new_device(name, options) klass = device_class(name) unless klass valid_names = @registry.keys.map(&:inspect).join(", ") raise ArgumentError.new("#{name.inspect} is not registered as a device name; valid names are: #{valid_names}") end if klass == :stdout Device::Writer.new($stdout, options) elsif klass == :stderr Device::Writer.new($stderr, options) else klass.new(options) end end # Retrieve the class registered with the given name or nil if the name is not defined. # # @param name [Symbol] The name of the device # @return [Class, nil] The registered device class or nil if not found def device_class(name) @registry[name] end # Return the map of registered device class names. # # @return [Hash] def registered_devices @registry.dup end end end end bdurand-lumberjack-ac97435/lib/lumberjack/entry_formatter.rb000066400000000000000000000375161515437321200242640ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # EntryFormatter provides a unified interface for formatting complete log entries by combining # message formatting and attribute formatting into a single, coordinated system. # # This class serves as the central formatting coordinator in the Lumberjack logging pipeline, # bringing together two specialized formatters: # 1. Message Formatter ({Lumberjack::Formatter}) - Formats the main log message content # 2. Attribute Formatter ({Lumberjack::AttributeFormatter}) - Formats key-value attribute pairs # # @example Complete entry formatting setup # entry_formatter = Lumberjack::EntryFormatter.build do |formatter| # # Formatter will be used in both log messages and attributes # formatter.format_class(ActiveRecord::Base, :id) # # # Message specific formatters # formatter.format_message(Exception, :exception) # formatter.format_message(Time, :date_time, "%Y-%m-%d %H:%M:%S") # # # Attribute specific formatters # formatter.format_attributes(Time, :date_time, "%Y-%m-%d") # formatter.format_attribute_name("password") { |value| "[REDACTED]" } # formatter.format_attribute_name("user_id", :id) # formatter.default_attribute_format { |value| value.to_s.strip } # end # # @example Using with a logger # logger = Lumberjack::Logger.new(STDOUT, formatter: entry_formatter) # logger.info("User login", user: user_object, timestamp: Time.now) # # Both the message and attributes are formatted according to the rules # # @see Lumberjack::Formatter # @see Lumberjack::AttributeFormatter # @see Lumberjack::Logger class EntryFormatter # The message formatter used to format log message content. # @return [Lumberjack::Formatter] The message formatter instance. attr_reader :message_formatter # The attribute formatter used to format log entry attributes. # @return [Lumberjack::AttributeFormatter] The attribute formatter instance. attr_reader :attribute_formatter class << self # Build a new entry formatter using a configuration block. The block receives the new formatter # as a parameter, allowing you to configure it with various configuration methods. # # @param message_formatter [Lumberjack::Formatter, Symbol, nil] The message formatter to use. # Can be a Formatter instance, :default for standard formatter, :none for empty formatter, or nil. # @param attribute_formatter [Lumberjack::AttributeFormatter, nil] The attribute formatter to use. # @yield [formatter] A block that configures the entry formatter. # @return [Lumberjack::EntryFormatter] A new configured entry formatter. # # @example # formatter = Lumberjack::EntryFormatter.build do |config| # config.add(User, :id) # Message formatting # config.add(Time, :date_time, "%Y-%m-%d") # # config.add_attribute("password") { "[REDACTED]" } # ensure passwords are not logged # config.add_attribute_class(Exception) { |e| {error: e.class.name, message: e.message} } # end def build(message_formatter: nil, attribute_formatter: nil, &block) formatter = new(message_formatter: message_formatter, attribute_formatter: attribute_formatter) block&.call(formatter) formatter end end # Create a new entry formatter with the specified message and attribute formatters. # # @param message_formatter [Lumberjack::Formatter, Symbol, nil] The message formatter to use: # - Formatter instance: Used directly # - :default or nil: Creates a new Formatter with default mappings # - :none: Creates an empty Formatter with no default mappings # @param attribute_formatter [Lumberjack::AttributeFormatter, nil] The attribute formatter to use. # If nil, no attribute formatting will be performed unless configured later. def initialize(message_formatter: nil, attribute_formatter: nil) self.message_formatter = message_formatter self.attribute_formatter = attribute_formatter end # Set the message formatter used to format log message content. # # @param value [Lumberjack::Formatter, Symbol, nil] The message formatter to use. If the value # is :default, a standard formatter with default mappings is created. If nil, a new empty # formatter is created. # @return [void] def message_formatter=(value) @message_formatter = if value == :default Lumberjack::Formatter.default elsif value.nil? Lumberjack::Formatter.new else value end end # Set the attribute formatter used to format log entry attributes. # # @param value [Lumberjack::AttributeFormatter, nil] The attribute formatter to use. # If nil, a new empty AttributeFormatter is created. # @return [void] def attribute_formatter=(value) @attribute_formatter = value || AttributeFormatter.new end # Add a formatter for specific classes or modules. This method adds the formatter # for both log messages and attributes. # # @param classes_or_names [Class, Module, String, Array] The class(es) to format. # @param formatter [Symbol, Class, #call, nil] The formatter to use. # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class). # @yield [obj] Block-based formatter that receives the object to format. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @example Adding formatters # formatter.format_class(User, :id) # Use ID formatter for User objects # formatter.format_class(Array) { |vals| vals.join(", ") } # Handle arrays # formatter.format_class(Time, :date_time, "%Y-%m-%d") # Custom time format only # # @see Lumberjack::Formatter#add def format_class(classes_or_names, formatter = nil, *args, &block) Array(classes_or_names).each do |class_or_name| unless @message_formatter.include?(class_or_name) @message_formatter.add(class_or_name, formatter, *args, &block) end unless @attribute_formatter.include_class?(class_or_name) @attribute_formatter.add_class(class_or_name, formatter, *args, &block) end end self end # Remove a formatter for specific classes or modules. This method removes the formatter # for both log messages and attributes. # # @param classes_or_names [Class, Module, String, Array] The class(es) to remove formatters for. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @see Lumberjack::Formatter#remove def remove_class(classes_or_names) @message_formatter.remove(classes_or_names) @attribute_formatter.remove_class(classes_or_names) self end # Add a message formatter for specific classes or modules. # # @param classes_or_names [String, Module, Array] Class names or modules. # @param formatter [Symbol, Class, #call, nil] The formatter to use # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class). # @yield [obj] Block-based formatter that receives the object to format. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @see Lumberjack::Formatter#add def format_message(classes_or_names, formatter = nil, *args, &block) @message_formatter.add(classes_or_names, formatter, *args, &block) self end # Remove a message formatter for specific classes or modules. # # @param classes_or_names [String, Module, Array] Class names or modules. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @see Lumberjack::Formatter#remove def remove_message_formatter(classes_or_names) @message_formatter.remove(classes_or_names) self end # Add a formatter for the named attribute. # # @param names [String, Symbol, Array] The attribute names to format. # @param formatter [Symbol, Class, #call, nil] The formatter to use # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class). # @yield [value] Block-based formatter that receives the attribute value. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @see Lumberjack::AttributeFormatter#add_attribute def format_attribute_name(names, formatter = nil, *args, &block) @attribute_formatter.add_attribute(names, formatter, *args, &block) self end # Add a formatter for the specified class or module when it appears as an attribute value. # # @param classes_or_names [String, Module, Array] Class names or modules. # @param formatter [Symbol, Class, #call, nil] The formatter to use # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class). # @yield [value] Block-based formatter that receives the attribute value. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @see Lumberjack::AttributeFormatter#add_attribute def format_attributes(classes_or_names, formatter = nil, *args, &block) @attribute_formatter.add_class(classes_or_names, formatter, *args, &block) self end # Set the default attribute formatter to apply to any attributes that do not # have a specific formatter defined. # # @param formatter [Symbol, Class, #call, nil] The default formatter to use. # If nil, removes any existing default formatter. # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class). # @yield [value] Block-based formatter that receives the attribute value. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @see Lumberjack::AttributeFormatter#default def default_attribute_format(formatter = nil, *args, &block) @attribute_formatter.default(formatter, *args, &block) self end # Remove an attribute formatter for the specified attribute names. # # @param names [String, Symbol, Array] The attribute names to remove formatters for. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @see Lumberjack::AttributeFormatter#remove_attribute def remove_attribute_name(names) @attribute_formatter.remove_attribute(names) self end # Remove an attribute formatter for the specified classes or modules. # # @param classes_or_names [String, Module, Array] Class names or modules. # @return [Lumberjack::EntryFormatter] Returns self for method chaining. # # @see Lumberjack::AttributeFormatter#remove_class def remove_attribute_class(classes_or_names) @attribute_formatter.remove_class(classes_or_names) self end # Extend this formatter by adding the formats defined in the provided formatter into this one. # # @param formatter [Lumberjack::EntryFormatter] The formatter to merge. # @return [self] Returns self for method chaining. def include(formatter) unless formatter.is_a?(Lumberjack::EntryFormatter) raise ArgumentError.new("formatter must be a Lumberjack::EntryFormatter") end @message_formatter ||= Lumberjack::Formatter.new @message_formatter.include(formatter.message_formatter) @attribute_formatter ||= Lumberjack::AttributeFormatter.new @attribute_formatter.include(formatter.attribute_formatter) self end # Extend this formatter by adding the formats defined in the provided formatter into this one. # Formats defined in this formatter will take precedence and not be overridden. # # @param formatter [Lumberjack::EntryFormatter] The formatter to merge. # @return [self] Returns self for method chaining. def prepend(formatter) unless formatter.is_a?(Lumberjack::EntryFormatter) raise ArgumentError.new("formatter must be a Lumberjack::EntryFormatter") end @message_formatter ||= Lumberjack::Formatter.new @message_formatter.prepend(formatter.message_formatter) @attribute_formatter ||= Lumberjack::AttributeFormatter.new @attribute_formatter.prepend(formatter.attribute_formatter) self end # Format a complete log entry by applying both message and attribute formatting. # This is the main method that coordinates the formatting of both the message content # and any associated attributes. # # @param message [Object, Proc, nil] The log message to format. Can be any object, a Proc that returns the message, or nil. # @param attributes [Hash, nil] The log entry attributes to format. # @return [Array] A two-element array containing [formatted_message, formatted_attributes]. def format(message, attributes) message = message.call if message.is_a?(Proc) message = message_formatter.format(message) if message_formatter.respond_to?(:format) message_attributes = nil if message.is_a?(MessageAttributes) message_attributes = message.attributes message = message.message end message_attributes = Utils.flatten_attributes(message_attributes) if message_attributes attributes = merge_attributes(attributes, message_attributes) if message_attributes attributes = AttributesHelper.expand_runtime_values(attributes) attributes = attribute_formatter.format(attributes) if attributes && attribute_formatter [message, attributes] end # Compatibility method for Ruby's standard Logger::Formatter interface. This delegates # to the message formatter's call method for basic Logger compatibility. # # @param severity [Integer, String, Symbol] The log severity (passed to message formatter). # @param timestamp [Time] The log timestamp (passed to message formatter). # @param progname [String] The program name (passed to message formatter). # @param msg [Object] The message object to format (passed to message formatter). # @return [String, nil] The formatted message string, or nil if no message formatter. # # @see Lumberjack::Formatter#call def call(severity, timestamp, progname, msg) message_formatter&.call(severity, timestamp, progname, msg) end private # Merge two attribute hashes, handling nil values gracefully. # Used to combine explicit log attributes with attributes embedded in MessageAttributes objects. # # @param current_attributes [Hash, nil] The primary attributes hash. # @param attributes [Hash, nil] Additional attributes to merge in. # @return [Hash, nil] The merged attributes hash, or nil if both inputs are nil/empty. # @api private def merge_attributes(current_attributes, attributes) if current_attributes.nil? || current_attributes.empty? attributes elsif attributes.nil? current_attributes else current_attributes.merge(attributes) end end # Check if a formatter accepts an attributes parameter in its call method. # This is used for determining formatter compatibility but is currently unused (TODO). # # @param formatter [#call] The formatter to check. # @return [Boolean] true if the formatter accepts 5+ parameters or has a splat parameter. # @api private # @todo This method needs to be integrated into the logger functionality. def accepts_attributes_parameter?(formatter) method_obj = if formatter.is_a?(Proc) formatter elsif formatter.respond_to?(:call) formatter.method(:call) end return false unless method_obj params = method_obj.parameters positional = params.slice(:req, :opt) has_splat = params.any? { |type, _| type == :rest } positional_count = positional.size positional_count >= 5 || has_splat end end end bdurand-lumberjack-ac97435/lib/lumberjack/forked_logger.rb000066400000000000000000000140671515437321200236450ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # ForkedLogger provides an isolated logging context that forwards all log entries to a parent logger # while maintaining its own independent configuration (level, progname, attributes). # # This class allows you to create specialized logger instances that: # - Inherit initial configuration from a parent logger # - Maintain isolated settings (level, progname, attributes) that don't affect the parent # - Forward all log entries to the parent logger's output device # - Combine their own attributes with the parent logger's attributes # - Provide scoped logging behavior without duplicating output infrastructure # # ForkedLogger is particularly useful for: # # - Component isolation: Give each component its own logger with specific attributes # - Request tracing: Create request-specific loggers with request IDs # - Temporary debugging: Create debug-level loggers for specific code paths # - Library integration: Allow libraries to have their own logging configuration # - Multi-tenant logging: Isolate tenant-specific logging configuration # # The forked logger inherits the parent's initial state but changes are isolated: # # - Inherited: Initial level, progname, and device (through forwarding) # - Isolated: subsequent changes to level, progname, and attributes do not affect the parent logger # - Combined: attributes from the parent and the forked loggers are merged when logging # # @example Basic forked logger # parent = Lumberjack::Logger.new(STDOUT, level: :info) # forked = Lumberjack::ForkedLogger.new(parent) # forked.level = :debug # Only affects the forked logger # forked.debug("Debug message") # Logged because forked logger is debug level # # @example Component-specific logging # main_logger = Lumberjack::Logger.new("/var/log/app.log") # db_logger = Lumberjack::ForkedLogger.new(main_logger) # db_logger.progname = "Database" # db_logger.tag!(component: "database", version: "1.2.3") # db_logger.info("Connection established") # Includes component attributes and different progname # # @example Request-scoped logging # def handle_request(request_id) # request_logger = Lumberjack::ForkedLogger.new(@logger) # request_logger.tag!(request_id: request_id, user_id: current_user.id) # # request_logger.info("Processing request") # Includes request context # # ... process request ... # request_logger.info("Request completed") # All logs tagged with request info # end # # @see Lumberjack::Logger # @see Lumberjack::ContextLogger # @see Lumberjack::Context class ForkedLogger < Logger include ContextLogger # The parent logger that receives all log entries from this forked logger. # @return [Lumberjack::Logger, #add_entry] The parent logger instance. attr_reader :parent_logger # Create a new forked logger that forwards all log entries to the specified parent logger. # The forked logger inherits the parent's initial level and progname but maintains # its own independent context for future changes. # # @param logger [Lumberjack::ContextLogger, #add_entry] The parent logger to forward entries to. # Must respond to either +add_entry+ (for Lumberjack loggers) or standard Logger methods. def initialize(logger) init_context_locals! self.isolation_level = logger.isolation_level if logger.respond_to?(:isolation_level) @parent_logger = logger @context = Context.new @context.level ||= logger.level @context.progname ||= logger.progname end # Forward a log entry to the parent logger with the forked logger's configuration applied. # This method coordinates between the forked logger's settings and the parent logger's # output capabilities. # # @param severity [Integer, Symbol, String] The severity level of the log entry. # @param message [Object] The message to log. # @param progname [String, nil] The program name (defaults to this logger's progname). # @param attributes [Hash, nil] Additional attributes to include with the log entry. # @return [Boolean] Returns the result of the parent logger's logging operation. # # @api private def add_entry(severity, message, progname = nil, attributes = nil) parent_logger.with_level(level || Logger::DEBUG) do attributes = merge_attributes(local_attributes, attributes) progname ||= self.progname if parent_logger.is_a?(ContextLogger) parent_logger.add_entry(severity, message, progname, attributes) else parent_logger.tag(attributes) do parent_logger.add(severity, message, progname) end end end end # Flush any buffered log entries in the parent logger's device. # # @return [void] def flush parent_logger.flush end # Return the log device of the parent logger, if available. # # @return [Lumberjack::Device] The parent logger's output device. def device parent_logger.device if parent_logger.respond_to?(:device) end # Return the formatter of the parent logger, if available. # # @return [Lumberjack::EntryFormatter] The parent logger's formatter. def formatter parent_logger.formatter if parent_logger.respond_to?(:formatter) end private # Return the default context for this forked logger. This provides the isolated # configuration context that doesn't affect the parent logger. # # @return [Lumberjack::Context] The forked logger's private context. # @api private def default_context @context end # Merge all attributes that should be included with log entries from this forked logger. # This combines the default context attributes (set with tag!) and any local context # attributes (set within context blocks). # # @return [Hash, nil] The merged attributes hash, or nil if no attributes are set. # @api private def local_attributes merge_attributes(default_context&.attributes, local_context&.attributes) end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter.rb000066400000000000000000000342021515437321200230300ustar00rootroot00000000000000# frozen_string_literal: true require_relative "formatter_registry" module Lumberjack # Formatter controls the conversion of log entry messages into a loggable format, allowing you # to log any object type and have the logging system handle the string conversion automatically. # # The formatter system works by associating formatting rules with specific classes using the {#add} method. # When an object is logged, the formatter finds the most specific formatter for that object's class # hierarchy and applies it to convert the object into a string representation. # # Formatters can be: # # - Predefined formatters: Accessed by symbol (e.g., +:pretty_print+, +:truncate+) # - Custom objects: Any object responding to +#call(object)+ # - Blocks: Inline formatting logic # - Classes: Instantiated automatically with optional arguments # # The formatter includes optimizations for common primitive types (String, Integer, Float, Boolean) # to avoid unnecessary formatting overhead when custom formatters aren't defined for these types. # # @example Basic formatter usage # formatter = Lumberjack::Formatter.new # formatter.add(MyClass, :pretty_print) # formatter.add(Array) { |vals| vals.join(", ") } # result = formatter.format(my_object) # # @example Building a custom formatter # formatter = Lumberjack::Formatter.build do |config| # config.add(User, :id) # Only log user IDs # config.add(BigDecimal, :round, 2) # Round decimals to 2 places # end class Formatter require_relative "formatter/date_time_formatter" require_relative "formatter/exception_formatter" require_relative "formatter/id_formatter" require_relative "formatter/inspect_formatter" require_relative "formatter/multiply_formatter" require_relative "formatter/object_formatter" require_relative "formatter/pretty_print_formatter" require_relative "formatter/redact_formatter" require_relative "formatter/round_formatter" require_relative "formatter/string_formatter" require_relative "formatter/strip_formatter" require_relative "formatter/structured_formatter" require_relative "formatter/tags_formatter" require_relative "formatter/truncate_formatter" require_relative "formatter/tagged_message" class << self # Build a new formatter using a configuration block. The block receives the new formatter # as a parameter, allowing you to configure it with methods like +add+, +remove+, etc. # # @yield [formatter] A block that configures the formatter. # @return [Lumberjack::Formatter] A new configured formatter. # # @example # formatter = Lumberjack::Formatter.build do |config| # config.add(User, :id) # Only show user IDs # config.add(SecretToken) { |token| "[REDACTED]" } # config.remove(Exception) # Don't format exceptions specially # end def build(&block) formatter = new block&.call(formatter) formatter end # Create a new empty formatter with no mappings. This is an alias for #new. # # @return [Lumberjack::Formatter] A new formatter with no default mappings. # @deprecated Use #new instead. def empty Utils.deprecated("Formatter.empty", "Lumberjack::Formatter.empty is deprecated and will be removed in version 2.1; use new instead.") do new end end # Create a new formatter with default mappings. # # Object: inspect formatter # Exception: exception formatter # Enumerable: structured formatter # # @return [Lumberjack::Formatter] A new formatter with default mappings. def default build do |config| config.add(Object, :inspect) config.add(Exception, :exception) config.add(Enumerable, :structured) end end end # Create a new formatter with default mappings for common Ruby types. # The default configuration provides sensible formatting for most use cases: # - Object: Uses inspect for debugging-friendly output # - Exception: Formats with stack trace details # - Enumerable: Recursively formats collections (Arrays, Hashes, etc.) # # @return [Lumberjack::Formatter] A new formatter with default mappings. def initialize @class_formatters = {} @has_string_formatter = false @has_numeric_formatter = false @has_boolean_formatter = false end # Add a formatter for a specific class or classes. The formatter determines how objects # of that class will be converted to strings when logged. # # The formatter can be specified in several ways: # - Symbol: References a predefined formatter (see list below) # - Class: Will be instantiated with optional arguments # - Object: Must respond to +#call(object)+ method # - Block: Inline formatting logic # # Formatters can be referenced by name from the formatter registry. These formatters # are available out of the box. Some of them require an argument to be provided as well. # # - +:date_time+ - Formats time objects with a customizable format (takes the format string as an argument) # - +:exception+ - Formats exceptions with stack trace details # - +:id+ - Extracts object ID or specified ID field # - +:inspect+ - Uses Ruby's inspect method for debugging output # - +:multiply+ - Multiplies numeric values by a factor (requires the factor as an argument) # - +:object+ - Generic object formatter with custom methods # - +:pretty_print+ - Pretty-prints objects using PP library # - +:redact+ - Redacts sensitive information from objects # - +:round+ - Rounds numeric values to specified precision (takes the precision as an argument; defaults to 3 decimal places) # - +:string+ - Converts objects to strings using to_s # - +:strip+ - Strips whitespace from string representations # - +:structured+ - Recursively formats structured data (Arrays, Hashes) # - +:tags+ - Formats an array or hash of values in the format "[a] [b] [c=d]" # - +:truncate+ - Truncates long strings to specified length (takes the length as an argument) # # Classes can be specified as: # # - Class objects: Direct class references # - Arrays: Multiple classes at once # - Strings: Class names to avoid loading dependencies # # @param klass [Class, Module, String, Array] The class(es) to format. # @param formatter [Symbol, Class, #call, nil] The formatter to use. # @param args [Array] Arguments passed to formatter constructor (when formatter is a Class). # @yield [obj] Block-based formatter that receives the object to format. # @yieldparam obj [Object] The object to format. # @yieldreturn [String] The formatted string representation. # @return [self] Returns self for method chaining. # # @example Using predefined formatters # formatter.add(Float, :round, 2) # Round floats to 2 decimal places # formatter.add(Time, :date_time, "%Y-%m-%d") # Custom time format # formatter.add([User, Admin], :id) # Show only IDs for user objects # # @example Using custom formatters # formatter.add(MyClass, MyFormatter.new) # Custom formatter object # formatter.add("BigDecimal", RoundFormatter, 4) # Class with arguments # # @example Method chaining # formatter.add(User, :id) # .add(BigDecimal, :round, 2) def add(klass, formatter = nil, *args, &block) formatter ||= block return remove(klass) if formatter.nil? if formatter.is_a?(Symbol) formatter = FormatterRegistry.formatter(formatter, *args) elsif formatter.is_a?(Class) formatter = formatter.new(*args) end raise ArgumentError.new("formatter must respond to call") unless formatter.respond_to?(:call) Array(klass).each do |k| @class_formatters[k.to_s] = formatter end set_optimized_flags! self end # Remove formatter associations for one or more classes. This reverts the classes # to use the default Object formatter (inspect method) or no formatting if no default exists. # # @param klass [Class, Module, String, Array] The class(es) to remove formatters for. # @return [self] Returns self for method chaining. def remove(klass) Array(klass).each do |k| @class_formatters.delete(k.to_s) end set_optimized_flags! self end # Extend this formatter by merging the formats defined in the provided formatter into this one. # # @param formatter [Lumberjack::Formatter] The formatter to merge. # @return [self] Returns self for method chaining. def include(formatter) unless formatter.is_a?(Lumberjack::Formatter) raise ArgumentError.new("formatter must be a Lumberjack::Formatter") end formatter.instance_variable_get(:@class_formatters).each do |class_name, fmttr| add(class_name, fmttr) end self end # Extend this formatter by adding the formats defined in the provided formatter into this one. # Formats defined in this formatter will take precedence and not be overridden. # # @param formatter [Lumberjack::Formatter] The formatter to merge. # @return [self] Returns self for method chaining. def prepend(formatter) unless formatter.is_a?(Lumberjack::Formatter) raise ArgumentError.new("formatter must be a Lumberjack::Formatter") end formatter.instance_variable_get(:@class_formatters).each do |class_name, fmttr| add(class_name, fmttr) unless @class_formatters.include?(class_name) end self end # Remove all formatter associations, including defaults. This creates a completely # empty formatter where all objects will be passed through unchanged. # # @return [self] Returns self for method chaining. def clear @class_formatters.clear set_optimized_flags! self end # Check if the formatter has any registered formatters. # # @return [Boolean] true if no formatters are registered, false otherwise. def empty? @class_formatters.empty? end # Format an object by applying the appropriate formatter based on its class hierarchy. # The formatter searches up the class hierarchy to find the most specific formatter available. # # @param value [Object] The object to format. # @return [Object] The formatted representation (usually a String). def format(value) # These primitive types are the most common in logs and so are optimized here # for the normal case where a custom formatter has not been defined. case value when String return value unless @has_string_formatter when Integer, Float return value unless @has_numeric_formatter when Numeric if defined?(BigDecimal) && value.is_a?(BigDecimal) return value unless @has_numeric_formatter end when true, false return value unless @has_boolean_formatter end if value.respond_to?(:to_log_format) && !@class_formatters.include?(value.class.name) return value.to_log_format end formatter = formatter_for(value.class) value = formatter.call(value) if formatter&.respond_to?(:call) value rescue SystemStackError, StandardError => e error_message = e.class.name error_message = "#{error_message} #{e.message}" if e.message && e.message != "" warn("") "" end # Compatibility method for Ruby's standard Logger::Formatter interface. This allows # the Formatter to be used directly as a logger formatter, though it only uses the # message parameter and ignores severity, timestamp, and progname. # # @param severity [Integer, String, Symbol] The log severity (ignored). # @param timestamp [Time] The log timestamp (ignored). # @param progname [String] The program name (ignored). # @param msg [Object] The message object to format. # @return [String] The formatted message with line separator. def call(severity, timestamp, progname, msg) formatted_message = format(msg) formatted_message = formatted_message.message if formatted_message.is_a?(MessageAttributes) "#{formatted_message}#{Lumberjack::LINE_SEPARATOR}" end # Find the most appropriate formatter for a class by searching up the class hierarchy. # Returns the first formatter found by walking through the class's ancestors. # # @param klass [Class] The class to find a formatter for. # @return [#call, nil] The formatter object, or nil if no formatter is found. # @api private def formatter_for(klass) return nil if @class_formatters.empty? unless klass.is_a?(Module) begin klass = Object.const_get(klass.to_s) rescue NameError return @class_formatters[klass.to_s] end end formatter = nil has_to_log_format = klass.public_method_defined?(:to_log_format) if klass.is_a?(Module) klass.ancestors.detect do |ancestor| break if has_to_log_format && ancestor == Object formatter = @class_formatters[ancestor.name] break if formatter end formatter end # Check if a formatter exists for a specific class or class name. # # @param class_or_name [Class, Module, String] The class or class name to check. # @return [Boolean] true if a formatter exists, false otherwise. def include?(class_or_name) @class_formatters.include?(class_or_name.to_s) end private # Update internal optimization flags based on currently registered formatters. # This enables fast-path optimization for common primitive types. # # @return [void] # @api private def set_optimized_flags! @has_string_formatter = @class_formatters.include?("String") @has_numeric_formatter = @class_formatters.slice("Integer", "Float", "BigDecimal", "Numeric").any? @has_boolean_formatter = @class_formatters.include?("TrueClass") || @class_formatters.include?("FalseClass") end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/000077500000000000000000000000001515437321200225025ustar00rootroot00000000000000bdurand-lumberjack-ac97435/lib/lumberjack/formatter/date_time_formatter.rb000066400000000000000000000024751515437321200270550ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Format a Date, Time, or DateTime object. If you don't specify a format in the constructor, it will use # the ISO-8601 format with microsecond precision. This formatter provides consistent date/time representation # across your application logs. class DateTimeFormatter FormatterRegistry.add(:date_time, self) # @!attribute [r] format # @return [String, nil] The strftime format string, or nil for ISO-8601 default. attr_reader :format # @param format [String, nil] The format to use when formatting the date/time object. # If nil, uses ISO-8601 format with microsecond precision. def initialize(format = nil) @format = format.dup.to_s.freeze unless format.nil? end # Format a date/time object using the configured format. # # @param obj [Date, Time, DateTime, Object] The object to format. Should respond to # strftime (for custom format) or iso8601 (for default format). # @return [String] The formatted date/time string. def call(obj) if @format && obj.respond_to?(:strftime) obj.strftime(@format) elsif obj.respond_to?(:iso8601) obj.iso8601(6) else obj.to_s end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/exception_formatter.rb000066400000000000000000000033021515437321200271060ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Format an exception including the backtrace. You can specify an object that # responds to +call+ as a backtrace cleaner. The exception backtrace will be # passed to this object and the returned array is what will be logged. You can # use this to clean out superfluous lines. class ExceptionFormatter FormatterRegistry.add(:exception, self) # @!attribute [rw] backtrace_cleaner # @return [#call, nil] An object that responds to +call+ and takes # an array of strings (the backtrace) and returns an array of strings. attr_accessor :backtrace_cleaner # @param backtrace_cleaner [#call, nil] An object that responds to +call+ and takes # an array of strings (the backtrace) and returns an array of strings (the # cleaned backtrace). def initialize(backtrace_cleaner = nil) self.backtrace_cleaner = backtrace_cleaner end # Format an exception with its message and backtrace. # # @param exception [Exception] The exception to format. # @return [String] The formatted exception with class name, message, and backtrace. def call(exception) message = +"#{exception.class.name}: #{exception.message}" trace = exception.backtrace if trace trace = clean_backtrace(trace) message << "#{Lumberjack::LINE_SEPARATOR} #{trace.join("#{Lumberjack::LINE_SEPARATOR} ")}" end message end private def clean_backtrace(trace) if trace && backtrace_cleaner backtrace_cleaner.call(trace) else trace end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/id_formatter.rb000066400000000000000000000026411515437321200255110ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Format an object that has an id as a hash with keys for class and id. This formatter is useful # as a default formatter for objects pulled from a data store. By default it will use :id as the # id attribute. # # The formatter creates a standardized representation of objects by extracting their class name # and identifier, making it easy to identify objects in logs without exposing all their data. # This is particularly useful for ActiveRecord models, database objects, or any objects that # have a unique identifier attribute. class IdFormatter FormatterRegistry.add(:id, self) # @param id_attribute [Symbol, String] The attribute to use as the id (defaults to :id). def initialize(id_attribute = :id) @id_attribute = id_attribute end # Format an object by extracting its class name and ID attribute. # # @param obj [Object] The object to format. Must respond to the configured ID attribute. # @return [Hash, String] A hash with "class" and "id" keys if the object has the ID attribute, # otherwise returns the object's string representation. def call(obj) if obj.respond_to?(@id_attribute) id = obj.send(@id_attribute) {"class" => obj.class.name, "id" => id} else obj.to_s end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/inspect_formatter.rb000066400000000000000000000015541515437321200265640ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Format an object by calling +inspect+ on it. This formatter provides # a debugging-friendly representation of objects, showing their internal # structure and contents in a readable format. # # The InspectFormatter is particularly useful for logging complex objects # where you need to see their complete state, such as arrays, hashes, # or custom objects. It relies on Ruby's built-in +inspect+ method, # which provides detailed object representations. class InspectFormatter FormatterRegistry.add(:inspect, self) # Convert an object to its inspect representation. # # @param obj [Object] The object to format. # @return [String] The inspect representation of the object. def call(obj) obj.inspect end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/multiply_formatter.rb000066400000000000000000000024671515437321200270020ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # This formatter can be used to multiply a numeric value by a specified multiplier and # optionally round to a specified number of decimal places. # # This is useful for unit conversions (e.g., converting seconds to milliseconds) # or scaling values for display purposes. Non-numeric values are passed through unchanged. class MultiplyFormatter FormatterRegistry.add(:multiply, self) # @param multiplier [Numeric] The multiplier to apply to the value. # @param decimals [Integer, nil] The number of decimal places to round the result to. # If nil, no rounding is applied. def initialize(multiplier, decimals = nil) @multiplier = multiplier @decimals = decimals end # Multiply a numeric value by the configured multiplier and optionally round. # # @param value [Object] The value to format. Only numeric values are processed. # @return [Numeric, Object] The multiplied (and optionally rounded) value if numeric, # otherwise returns the original value unchanged. def call(value) return value unless value.is_a?(Numeric) value *= @multiplier value = value.round(@decimals) if @decimals value end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/object_formatter.rb000066400000000000000000000014651515437321200263660ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # No-op formatter that returns the object unchanged. This formatter is useful # as a default or fallback formatter when you want to preserve the original # object without any transformation. # # The ObjectFormatter is commonly used in scenarios where you want to maintain # the original data structure and let downstream components handle the actual # formatting, or when you need a placeholder formatter in the formatter chain. class ObjectFormatter FormatterRegistry.add(:object, self) # Return the object unchanged. # # @param obj [Object] The object to format. # @return [Object] The original object without any modifications. def call(obj) obj end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/pretty_print_formatter.rb000066400000000000000000000024531515437321200276610ustar00rootroot00000000000000# frozen_string_literal: true require "pp" require "stringio" module Lumberjack class Formatter # Format an object with its pretty print method. This formatter provides multi-line, # indented output that makes complex data structures easier to read and debug. # It's particularly useful for logging hashes, arrays, and other nested objects. # # The formatter uses Ruby's built-in PP (Pretty Print) library to generate # well-formatted output with appropriate indentation and line breaks. class PrettyPrintFormatter FormatterRegistry.add(:pretty_print, self) # @!attribute [rw] width # @return [Integer] The maximum width of the message. attr_accessor :width # Create a new formatter. The maximum width of the message can be specified with the width # parameter (defaults to 79 characters). # # @param width [Integer] The maximum width of the message. def initialize(width = 79) @width = width end # Format an object using pretty print with the configured width. # # @param obj [Object] The object to format. # @return [String] The pretty-printed representation of the object. def call(obj) s = StringIO.new PP.pp(obj, s) s.string.chomp end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/redact_formatter.rb000066400000000000000000000026601515437321200263600ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Log sensitive information in a redacted format showing the first and last # characters of the value, with the rest replaced by asterisks. The number of # characters shown is dependent on the length of the value; short values will # not show any characters in order to avoid revealing too much information. # # This formatter is useful for logging sensitive data while still providing # enough context to distinguish between different values during debugging. class RedactFormatter FormatterRegistry.add(:redact, self) # Redact a string value by showing only the first and last characters. # # @param obj [Object] The object to format. Only strings are redacted. # @return [String, Object] The redacted string if the object is a string, # otherwise returns the object unchanged. # # @example Different string lengths # formatter.call("password123") # => "pa*******23" # formatter.call("secret") # => "s****t" # formatter.call("abc") # => "*****" def call(obj) return obj unless obj.is_a?(String) if obj.length > 8 "#{obj[0..1]}#{"*" * (obj.length - 4)}#{obj[-2..]}" elsif obj.length > 5 "#{obj[0]}#{"*" * (obj.length - 2)}#{obj[-1]}" else "*****" end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/round_formatter.rb000066400000000000000000000021361515437321200262430ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Round numeric values to a set number of decimal places. This is useful when logging # floating point numbers to reduce noise and rounding errors in the logs. # # The formatter only affects numeric values, leaving other object types unchanged. # This makes it safe to use as a general-purpose formatter for attributes that # might contain various data types. class RoundFormatter FormatterRegistry.add(:round, self) # @param precision [Integer] The number of decimal places to round to (defaults to 3). def initialize(precision = 3) @precision = precision end # Round a numeric value to the configured precision. # # @param obj [Object] The object to format. Only numeric values are rounded. # @return [Numeric, Object] The rounded number if the object is numeric, # otherwise returns the object unchanged. def call(obj) if obj.is_a?(Numeric) obj.round(@precision) else obj end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/string_formatter.rb000066400000000000000000000010771515437321200264250ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Format an object by calling +to_s+ on it. This is the simplest formatter # implementation and is commonly used as a fallback for objects that don't # have specialized formatters. class StringFormatter FormatterRegistry.add(:string, self) # Convert an object to its string representation. # # @param obj [Object] The object to format. # @return [String] The string representation of the object. def call(obj) obj.to_s end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/strip_formatter.rb000066400000000000000000000015331515437321200262550ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Format an object by calling +to_s+ on it and stripping leading and trailing whitespace. # This formatter is useful for cleaning up string values that may have unwanted whitespace # from user input, file processing, or other sources. # # The StripFormatter combines string conversion with whitespace normalization, # making it ideal for attribute values that should be clean and consistent # in log output. class StripFormatter FormatterRegistry.add(:strip, self) # Convert an object to a string and remove leading and trailing whitespace. # # @param obj [Object] The object to format. # @return [String] The string representation with whitespace stripped. def call(obj) obj.to_s.strip end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/structured_formatter.rb000066400000000000000000000053561515437321200273270ustar00rootroot00000000000000# frozen_string_literal: true require "set" module Lumberjack class Formatter # Dereference arrays and hashes and recursively call formatters on each element. # This formatter provides deep traversal of nested data structures, applying # formatting to all contained elements while handling circular references safely. # # The StructuredFormatter is essential for formatting complex nested objects # like configuration hashes, API responses, or any hierarchical data structures # that need consistent formatting throughout their entire structure. class StructuredFormatter FormatterRegistry.add(:structured, self) # Exception raised when a circular reference is detected during traversal. # This prevents infinite recursion when formatting objects that reference themselves. class RecusiveReferenceError < StandardError end # @param formatter [Formatter, nil] The formatter to call on each element # in the structure. If nil, elements are returned unchanged. def initialize(formatter = nil) @formatter = formatter end # Format a structured object by recursively processing all nested elements. # # @param obj [Object] The object to format. Arrays and hashes are traversed # recursively, while other objects are passed to the configured formatter. # @return [Object] The formatted structure with all nested elements processed. def call(obj) call_with_references(obj, Set.new) end private def call_with_references(obj, references) if obj.is_a?(Hash) with_object_reference(obj, references) do hash = {} obj.each do |name, value| value = call_with_references(value, references) hash[name.to_s] = value unless value.is_a?(RecusiveReferenceError) end hash end elsif obj.is_a?(Enumerable) && obj.respond_to?(:size) && obj.size != Float::INFINITY with_object_reference(obj, references) do array = [] obj.each do |value| value = call_with_references(value, references) array << value unless value.is_a?(RecusiveReferenceError) end array end elsif @formatter @formatter.format(obj) else obj end end def with_object_reference(obj, references) if obj.is_a?(Enumerable) return RecusiveReferenceError.new if references.include?(obj.object_id) references << obj.object_id begin yield ensure references.delete(obj.object_id) end else yield end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/tagged_message.rb000066400000000000000000000007561515437321200257760ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../message_attributes" module Lumberjack # This is a deprecated alias for Lumberjack::MessageAttributes. # # @deprecated Use Lumberjack::MessageAttributes instead. # @see MessageAttributes class Formatter::TaggedMessage < MessageAttributes def initialize(message, attributes) Utils.deprecated("Lumberjack::Formatter::TaggedMessage", "Use Lumberjack::MessageAttributes instead.") do super end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/tags_formatter.rb000066400000000000000000000015671515437321200260610ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # This formatter is designed to output tags in a specific format. # # - Simple values will be formatted as "[value]" # - Arrays will be formatted as "[value1] [value2] [value3]" # - Hashes will be formatted as "[key1=value1] [key2=value2]" # - Hashes in arrays will be formatted as "[key=value]" class Formatter::TagsFormatter FormatterRegistry.add(:tags, self) def call(tags) tags = tags.collect { |key, value| "#{key}=#{value}" } if tags.is_a?(Hash) if tags.is_a?(Array) tags.collect { |tag| format_tag(tag) }.join(" ") unless tags.empty? else format_tag(tags) end end private def format_tag(tag) if tag.is_a?(Hash) tag.collect { |key, value| "[#{key}=#{value.strip}]" }.join(" ") else "[#{tag.strip}]" end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter/truncate_formatter.rb000066400000000000000000000022501515437321200267360ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Formatter # Truncate a string object to a specific length. This is useful # for formatting messages when there is a limit on the number of # characters that can be logged per message. This formatter should # only be used when necessary since it is a lossy formatter. # # When a string is truncated, it will have a unicode ellipsis # character (U+2026) appended to the end of the string. class TruncateFormatter FormatterRegistry.add(:truncate, self) # @param length [Integer] The maximum length of the string (defaults to 32K). def initialize(length = 32768) @length = length end # Truncate a string if it exceeds the maximum length. # # @param obj [Object] The object to format. If it's a string longer than the maximum # length, it will be truncated. Other objects are returned unchanged. # @return [String, Object] The truncated string or original object. def call(obj) if obj.is_a?(String) && obj.length > @length "#{obj[0, @length - 1]}…" else obj end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/formatter_registry.rb000066400000000000000000000057231515437321200247660ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # The formatter registry is used for setting up names to represent Formatter classes. It is used # in the constructor for Lumberjack::Logger and allows passing in a symbol to reference a # formatter. # # Formatters must respond to the +call+ method. # # @example # # Lumberjack::FormatterRegistry.add(:upcase) { |value| value.to_s.upcase } # Lumberjack::FormatterRegistry.add(:currency, Lumberjack::Formatter::RoundFormatter, 2) # # Lumberjack::EntryFormatter.build do |config| # config.add_attribute :status, :upcase # config.add_attribute :amount, :currency # end module FormatterRegistry @registry = {} class << self # Register a formatter name. Formatter names can be used to associate a symbol with a formatter # class. The symbol can then be passed to Logger as the formatter argument. # # Registered formatters must take only one argument and that is the options hash for the # formatter options. # # @param name [Symbol] The name of the formatter # @param formatter [Class, #call] The formatter or formatter class to register.. # @return [void] def add(name, formatter = nil, &block) raise ArgumentError.new("name must be a symbol") unless name.is_a?(Symbol) raise ArgumentError.new("formatter or block must be provided") if formatter.nil? && block.nil? raise ArgumentError.new("cannot have both formatter and a block") if !formatter.nil? && !block.nil? formatter ||= block raise ArgumentError.new("formatter must be a class or respond to call") unless formatter.is_a?(Class) || formatter.respond_to?(:call) @registry[name] = formatter end # Remove a formatter from the registry. # # @param name [Symbol] The name of the formatter to remove # @return [void] def remove(name) @registry.delete(name) end # Check if a formatter is registered. # # @param name [Symbol] The name of the formatter # @return [Boolean] True if the formatter is registered, false otherwise def registered?(name) @registry.include?(name) end # Retrieve the formatter registered with the given name or nil if the name is not defined. # # @param name [Symbol] The name of the formatter # @return [#call, nil] The registered formatter class or nil if not found def formatter(name, *args) instance = @registry[name] if instance.nil? valid_names = @registry.keys.map(&:inspect).join(", ") raise ArgumentError.new("#{name.inspect} is not registered as a formatter name; valid names are: #{valid_names}") end instance = instance.new(*args) if instance.is_a?(Class) instance end # Return the map of registered formatters. # # @return [Hash] def registered_formatters @registry.dup end end end end bdurand-lumberjack-ac97435/lib/lumberjack/io_compatibility.rb000066400000000000000000000120361515437321200243660ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # IOCompatibility provides methods that allow a logger to be used as an IO-like stream. # This enables loggers to be used anywhere an IO object is expected, such as for # redirecting standard output/error or integrating with libraries that expect stream objects. # # When used as a stream, all written values are logged with UNKNOWN severity and include # timestamps and other standard log entry metadata. This is particularly useful for: # - Capturing output from external libraries or subprocesses # - Redirecting STDOUT/STDERR to logs # - Providing a logging destination that conforms to IO interface expectations # # The module implements the essential IO methods like write, puts, print, printf, flush, # and close to provide broad compatibility with Ruby's IO ecosystem. # # @example Basic stream usage # logger = Lumberjack::Logger.new(STDOUT) # logger.puts("Hello, world!") # Logs with UNKNOWN severity # logger.write("Direct write") # Also logs with UNKNOWN severity # # @example Setting the log entry severity # logger = Lumberjack::Logger.new(STDOUT) # logger.default_severity = :info # logger.puts("This is an info message") # Logs with INFO severity # # @example Using as STDOUT replacement # logger = Lumberjack::Logger.new("/var/log/app.log") # $stdout = logger # Redirect all puts/print calls to the logger # puts "This goes to the log file" module IOCompatibility # Write a value to the log as a log entry. The value will be recorded with UNKNOWN severity, # ensuring it always appears in the log regardless of the current log level. # # @param value [Object] The message to write. Will be converted to a string for logging. # @return [Integer] Returns 1 if a log entry was written, or 0 if the value was nil or empty. def write(value) return 0 if value.nil? || value == "" self << value 1 end # Write multiple values to the log, each as a separate log entry with UNKNOWN severity. # This method mimics the behavior of IO#puts by writing each argument on a separate line. # # @param args [Array] The messages to write. Each will be converted to a string. # @return [nil] def puts(*args) args.each do |arg| write(arg) end nil end # Concatentate strings into a single log entry. This mimics IO#print behavior # by writing arguments without separators. If no arguments are given, writes the # value of the global $_ variable. # # @param args [Array] The messages to write. If empty, uses $_ (last input record). # @return [nil] # # @example # logger.print("Hello", " ", "World") # Single log entry: "Hello World" def print(*args) if args.empty? write($_) else write(args.join("")) end nil end # Write a formatted string to the log using sprintf-style formatting. The formatted # result is logged as a single entry with UNKNOWN severity. # # @param format [String] The format string (printf-style format specifiers). # @param args [Array] The values to substitute into the format string. # @return [nil] # # @example # logger.printf("User %s logged in at %s", "alice", Time.now) # # Logs: "User alice logged in at 2025-08-21 10:30:00 UTC" def printf(format, *args) write(format % args) nil end # Flush any buffered output. This method is provided for IO compatibility but # is a no-op since log entries are typically written immediately to the underlying device. # The actual flushing behavior depends on the logging device being used. # # @return [nil] def flush end # Close the stream. This method is provided for IO compatibility but is a no-op. # To actually close a logger, call close on the logger object itself, which will # close the underlying logging device. # # @return [nil] def close end # Check if the stream is closed. Always returns false since loggers using this # module don't maintain a closed state through this interface. # # @return [Boolean] Always returns false. def closed? false end # Check if the stream is connected to a terminal (TTY). Always returns false # since loggers are not terminal devices, even when they write to STDOUT/STDERR. # This method is required for complete IO compatibility. # # @return [Boolean] Always returns false. # @api private def tty? false end # Alias for tty? to provide complete IO compatibility. # # @return [Boolean] # @api private def isatty tty? end # Set the encoding for the stream. This method is provided for IO compatibility # but is a no-op since loggers handle encoding internally through their devices # and formatters. # # @param _encoding [String, Encoding] The encoding to set (ignored). # @return [nil] # @api private def set_encoding(_encoding) end end end bdurand-lumberjack-ac97435/lib/lumberjack/local_log_template.rb000066400000000000000000000155541515437321200246640ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # This is a log template designed for local environments. It provides a simple, # human-readable format that includes key information about log entries while # omitting extraneous details. The template can be configured to include or # exclude certain components such as the times, process ID, program name, # and attributes. # # It is registered with the TemplateRegistry as :local. # # @see Template class LocalLogTemplate TemplateRegistry.add(:local, self) # Create a new LocalLogTemplate instance. # # @param options [Hash] Options for configuring the template. # @option options [Boolean, Array, nil] :exclude_attributes If true, all attributes are excluded. # If an array of strings is provided, those attributes (and their sub-attributes) are excluded. # Defaults to nil (include all attributes). # @option options [Boolean] :exclude_progname If true, the progname is excluded. Defaults to false. # @option options [Boolean] :exclude_pid If true, the process ID is excluded. Defaults to true. # @option options [Boolean] :exclude_time If true, the time is excluded. Defaults to true. # @option options [Boolean] :colorize If true, colorize the output based on severity. Defaults to false. # @option options [Symbol, Formatter, #call] :exception_formatter The formatter to use for exceptions in messages. # Can be a symbol registered in the FormatterRegistry, a Formatter instance, or any object that responds to #call. # Defaults to nil (use default exception formatting). If the logger does not have an exception formatter # configured, then the device will use this to format exceptions. # @option options [String, Symbol] :severity_format The optional format for severity labels (padded, char, emoji). def initialize(options = {}) self.exclude_progname = options.fetch(:exclude_progname, false) self.exclude_pid = options.fetch(:exclude_pid, true) self.exclude_time = options.fetch(:exclude_time, true) self.exclude_attributes = options.fetch(:exclude_attributes, nil) self.colorize = options.fetch(:colorize, false) self.severity_format = options.fetch(:severity_format, nil) self.exception_formatter = options.fetch(:exception_formatter, :exception) end # Format a log entry according to the template. # # @param entry [LogEntry] The log entry to format. # @return [String] The formatted log entry. def call(entry) message = entry.message if message.is_a?(Exception) && exception_formatter message = exception_formatter.call(message) end formatted = +"" formatted << entry.time.strftime("%Y-%m-%d %H:%M:%S.%6N ") unless exclude_time? formatted << "#{severity_label(entry)} #{message}" formatted << "#{Lumberjack::LINE_SEPARATOR} progname: #{entry.progname}" if entry.progname.to_s != "" && !exclude_progname? formatted << "#{Lumberjack::LINE_SEPARATOR} pid: #{entry.pid}" unless exclude_pid? if entry.attributes && !entry.attributes.empty? && !exclude_attributes? Lumberjack::Utils.flatten_attributes(entry.attributes).to_a.sort_by(&:first).each do |name, value| next if @attribute_filter.any? do |filter_name| if name.start_with?(filter_name) next_char = name[filter_name.length] next_char.nil? || next_char == "." end end formatted << "#{Lumberjack::LINE_SEPARATOR} #{name}: #{value}" end end formatted = Template.colorize_entry(formatted, entry) if colorize? formatted << Lumberjack::LINE_SEPARATOR end # Return true if all attributes are excluded, false otherwise. # # @return [Boolean] def exclude_attributes? @exclude_attributes end # Return the list of excluded attribute names. # # @return [Array] def excluded_attributes @attribute_filter.dup end # Set the attributes to exclude. If set to true, all attributes are excluded. # If set to an array of strings, those attributes (and their sub-attributes) # are excluded. If set to false or nil, no attributes are excluded. # # @param value [Boolean, Array, nil] # @return [void] def exclude_attributes=(value) @exclude_attributes = false @attribute_filter = [] if value == true @exclude_attributes = true elsif value @attribute_filter = Array(value).map(&:to_s) end end # Return true if the progname is excluded, false otherwise. # # @return [Boolean] def exclude_progname? @exclude_progname end # Set whether to exclude the progname. # # @param value [Boolean] # @return [void] def exclude_progname=(value) @exclude_progname = !!value end # Return true if the pid is excluded, false otherwise. # # @return [Boolean] def exclude_pid? @exclude_pid end # Set whether to exclude the pid. # # @param value [Boolean] # @return [void] def exclude_pid=(value) @exclude_pid = !!value end # Return true if the time is excluded, false otherwise. # # @return [Boolean] def exclude_time? @exclude_time end # Set whether to exclude the time. # # @param value [Boolean] # @return [void] def exclude_time=(value) @exclude_time = !!value end # Return true if colorization is enabled, false otherwise. # # @return [Boolean] def colorize? @colorize end # Set whether to enable colorization. # # @param value [Boolean] # @return [void] def colorize=(value) @colorize = !!value end # Set the severity format. # # @param value [String, Symbol] The severity format (:padded, :char, :emoji, :level, nil). # @return [void] def severity_format=(value) @severity_format = value.to_s end # Return the current severity format. # # @return [String] attr_reader :severity_format # Set the exception formatter. Can be a symbol registered in the FormatterRegistry, # a Formatter instance, or any object that responds to #call. # # @param value [Symbol, Formatter, #call] The exception formatter to use. # @return [void] def exception_formatter=(value) @exception_formatter = value.is_a?(Symbol) ? FormatterRegistry.formatter(value) : value end # Return the exception formatter. # # @return [#call, nil] attr_reader :exception_formatter private def severity_label(entry) severity = entry.severity_data case severity_format when "padded" severity.padded_label when "char" severity.char when "emoji" severity.emoji when "level" severity.level.to_s else severity.label end end end end bdurand-lumberjack-ac97435/lib/lumberjack/log_entry.rb000066400000000000000000000201151515437321200230250ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A structured representation of a single log entry containing the message, # metadata, and contextual information. LogEntry objects are immutable data # structures that capture all relevant information about a logging event, # including timing, severity, source identification, and custom attributes. # # This class serves as the fundamental data structure passed between loggers, # formatters, and output devices throughout the Lumberjack logging pipeline. # Each entry maintains consistent structure while supporting flexible attribute # attachment for contextual logging scenarios. class LogEntry # @!attribute [rw] time # @return [Time] The timestamp when the log entry was created # @!attribute [rw] message # @return [String] The primary log message content # @!attribute [rw] severity # @return [Integer] The numeric severity level of the log entry # @!attribute [rw] progname # @return [String] The name of the program or component that generated the entry # @!attribute [rw] pid # @return [Integer] The process ID of the logging process # @!attribute [rw] attributes # @return [Hash] Custom attributes associated with the log entry attr_accessor :time, :message, :severity, :progname, :pid, :attributes TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%3N" # Create a new log entry with the specified components. The entry captures # all relevant information about a logging event in a structured format. # # @param time [Time] The timestamp when the log entry was created # @param severity [Integer, String, Symbol] The severity level, accepts numeric levels or labels # @param message [String] The primary log message content # @param progname [String, nil] The name of the program or component generating the entry # @param pid [Integer] The process ID of the logging process # @param attributes [Hash, nil] Custom attributes to associate with the entry def initialize(time, severity, message, progname, pid, attributes) @time = time @severity = (severity.is_a?(Integer) ? severity : Severity.label_to_level(severity)) @message = message @progname = progname @pid = pid @attributes = flatten_attributes(attributes) if attributes.is_a?(Hash) end # Get the human-readable severity label corresponding to the numeric severity level. # # @return [String] The severity label (DEBUG, INFO, WARN, ERROR, FATAL, or UNKNOWN) def severity_label severity_data.label end # Get the data corresponding to the severity. This include variations on the severity label. def severity_data Severity.data(severity) end # Generate a formatted string representation of the log entry suitable for # human consumption. Includes timestamp, severity, program name, process ID, # attributes, and the main message. # # @return [String] A formatted string representation of the complete log entry def to_s msg = +"[#{time.strftime(TIME_FORMAT)} #{severity_label} #{progname}(#{pid})] #{message}" attributes&.each do |key, value| msg << " [#{key}:#{value}]" end msg end # Return a string representation suitable for debugging and inspection. # # @return [String] The same as {#to_s} def inspect to_s end # Compare this log entry with another for equality. Two log entries are # considered equal if all their components match exactly. # # @param other [Object] The object to compare against # @return [Boolean] True if the entries are identical, false otherwise def ==(other) return true if equal?(other) return false unless other.is_a?(LogEntry) time == other.time && severity == other.severity && message == other.message && progname == other.progname && pid == other.pid && attributes == other.attributes end # Alias for tags to provide backward compatibility with version 1.x API. This method # will eventually be removed. # # @return [Hash, nil] The attributes of the log entry. # @deprecated Use {#attributes} instead. def tags Utils.deprecated("LogEntry#tags", "Lumberjack::LogEntry#tags is deprecated and will be removed in version 2.1; use attributes instead.") do attributes end end # Access an attribute value by name. Supports both simple and nested attribute # access using dot notation for hierarchical data structures. # # @param name [String, Symbol] The attribute name, supports dot notation for nested access # @return [Object, nil] The attribute value or nil if the attribute does not exist def [](name) return nil if attributes.nil? AttributesHelper.new(attributes)[name] end # Alias method for #[] to provide backward compatibility with version 1.x API. This # method will eventually be removed. # # @return [Hash] # @deprecated Use {#[]} instead. def tag(name) Utils.deprecated("LogEntry#tag", "Lumberjack::LogEntry#tag is deprecated and will be removed in version 2.1; use [] instead.") do self[name] end end # Expand flat attributes with dot notation into a nested hash structure. # Attributes containing dots in their names are converted into hierarchical # nested hashes for structured data representation. # # @return [Hash] The attributes expanded into a nested structure def nested_attributes Utils.expand_attributes(attributes) end # Alias for nested_attributes to provide API compatibility with version 1.x. # This method will eventually be removed. # # @return [Hash] # @deprecated Use {#nested_attributes} instead. def nested_tags Utils.deprecated("LogEntry#nested_tags", "Lumberjack::LogEntry#nested_tags is deprecated and will be removed in version 2.1; use nested_attributes instead.") do nested_attributes end end # Determine if the log entry contains no meaningful content. An entry is # considered empty if it has no message content and no attributes. # # @return [Boolean] True if the entry is empty, false otherwise def empty? (message.nil? || message == "") && (attributes.nil? || attributes.empty?) end # Convert the log entry into a hash suitable for JSON serialization. Attributes will be expanded # into a nested structure (i.e. { "user.id" => 123 } becomes `{ "user" => { "id" => 123 } }). # Severities will be converted to their string labels. # # @return [Hash] The JSON representation of the log entry def as_json { "time" => time, "severity" => severity_label, "message" => message, "progname" => progname, "pid" => pid, "attributes" => Utils.expand_attributes(attributes) } end private # Generate a string representation of all attributes for inclusion in the # formatted output. Each attribute is formatted as key:value pairs. # # @return [String] A formatted string of all attributes def attributes_to_s attributes_string = +"" attributes&.each { |name, value| attributes_string << " #{name}:#{value.inspect}" } attributes_string end # Flatten nested attributes and remove empty values. # # @param attributes [Hash] The attributes hash to compact # @return [Hash] The flattened attributes with empty values removed def flatten_attributes(attributes) unless attributes.all? { |key, value| key.is_a?(String) && !value.is_a?(Hash) } attributes = Utils.flatten_attributes(attributes) end delete_keys = nil attributes.each do |key, value| if value.nil? || value == "" delete_keys ||= [] delete_keys << key elsif value.is_a?(Array) && value.empty? delete_keys ||= [] delete_keys << key end end return attributes if delete_keys.nil? attributes = attributes.dup delete_keys&.each { |key| attributes.delete(key) } attributes end end end bdurand-lumberjack-ac97435/lib/lumberjack/log_entry_matcher.rb000066400000000000000000000130201515437321200245250ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A flexible matching utility for testing and filtering log entries based on # multiple criteria. This class provides pattern-based matching against log # entry components including message content, severity levels, program names, # and custom attributes with support for nested attribute structures. # # The matcher uses Ruby's case equality operator (===) for flexible matching, # supporting exact values, regular expressions, ranges, classes, and other # pattern matching constructs. It's primarily designed for use with the Test # device in testing scenarios but can be used anywhere log entry filtering # is needed. # # @see Lumberjack::Device::Test class LogEntryMatcher require_relative "log_entry_matcher/score" # Create a new log entry matcher with optional filtering criteria. All # parameters are optional and nil values indicate no filtering for that # component. The matcher uses case equality (===) for flexible pattern # matching against each specified criterion. # # @param message [Object, nil] Pattern to match against log entry messages. # Supports strings, regular expressions, or any object responding to === # @param severity [Integer, String, Symbol, nil] Severity level to match. # Accepts numeric levels or symbolic names (:debug, :info, etc.) # @param progname [Object, nil] Pattern to match against program names. # Supports strings, regular expressions, or any object responding to === # @param attributes [Hash, nil] Hash of attribute patterns to match against # log entry attributes. Supports nested attribute matching and dot notation def initialize(message: nil, severity: nil, progname: nil, attributes: nil) message = message.strip if message.is_a?(String) @message_filter = message @severity_filter = Severity.coerce(severity) if severity @progname_filter = progname @attributes_filter = Utils.expand_attributes(attributes) if attributes end # Test whether a log entry matches all specified criteria. The entry must # satisfy all non-nil filter conditions to be considered a match. Uses # case equality (===) for flexible pattern matching. # # @param entry [Lumberjack::LogEntry] The log entry to test against the matcher # @return [Boolean] True if the entry matches all specified criteria, false otherwise def match?(entry) return false unless match_filter?(entry.message, @message_filter) return false unless match_filter?(entry.severity, @severity_filter) return false unless match_filter?(entry.progname, @progname_filter) if @attributes_filter attributes = Utils.expand_attributes(entry.attributes) return false unless match_attributes?(attributes, @attributes_filter) end true end # Find the closest matching log entry from a list of candidates. This method # scores each entry based on how well it matches the specified criteria and # returns the entry with the highest score, provided it meets a minimum # threshold. If no entries meet the threshold, nil is returned. # # @param entries [Array] The list of log entries to evaluate # @return [Lumberjack::LogEntry, nil] The closest matching log entry or nil if none match def closest(entries) scored_entries = entries.map { |entry| [entry, entry_score(entry)] } best_score = scored_entries.max_by { |_, score| score } (best_score&.last.to_f >= Score::MIN_SCORE_THRESHOLD) ? best_score.first : nil end private def entry_score(entry) Score.calculate_match_score( entry, message: @message_filter, severity: @severity_filter, attributes: @attributes_filter, progname: @progname_filter ) end # Apply a filter pattern against a value using case equality. Returns true # if no filter is specified (nil) or if the filter matches the value. # # @param value [Object] The value to test against the filter # @param filter [Object, nil] The filter pattern, nil means no filtering # @return [Boolean] True if the filter matches or is nil, false otherwise def match_filter?(value, filter) return true if filter.nil? filter === value end # Recursively match nested attribute structures against filter patterns. # Handles both simple attribute matching and complex nested hash structures # with support for partial matching and empty value detection. # # @param attributes [Hash] The expanded attributes hash from the log entry # @param filter [Hash] The filter patterns to match against attributes # @return [Boolean] True if all filter patterns match their corresponding attributes def match_attributes?(attributes, filter) return true unless filter return false unless attributes filter.all? do |name, value_filter| name = name.to_s attribute_values = attributes[name] if attribute_values.is_a?(Hash) if value_filter.is_a?(Hash) match_attributes?(attribute_values, value_filter) else match_filter?(attribute_values, value_filter) end elsif value_filter.nil? || (value_filter.is_a?(Enumerable) && value_filter.empty?) attribute_values.nil? || (attribute_values.is_a?(Array) && attribute_values.empty?) elsif attributes.include?(name) match_filter?(attribute_values, value_filter) else false end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/log_entry_matcher/000077500000000000000000000000001515437321200242045ustar00rootroot00000000000000bdurand-lumberjack-ac97435/lib/lumberjack/log_entry_matcher/score.rb000066400000000000000000000240601515437321200256460ustar00rootroot00000000000000# frozen_string_literal: true # Class responsible for scoring and matching log entries against filters. # This class provides fuzzy matching capabilities to find the best matching # log entry when exact matches are not available. class Lumberjack::LogEntryMatcher::Score # Minimum score threshold for considering a match (30% match) MIN_SCORE_THRESHOLD = 0.3 class << self # Calculate the overall match score for an entry against all provided filters. # Returns a score between 0.0 and 1.0, where 1.0 represents a perfect match. # # @param entry [Lumberjack::LogEntry] The log entry to score. # @param message [String, Regexp, nil] The message filter to match against. # @param severity [Integer, nil] The severity level to match against. # @param attributes [Hash, nil] The attributes hash to match against. # @param progname [String, nil] The program name to match against. # @return [Float] A score between 0.0 and 1.0 indicating match quality. def calculate_match_score(entry, message: nil, severity: nil, attributes: nil, progname: nil) scores = [] weights = [] # Check message match if message message_score = calculate_field_score(entry.message, message) scores << message_score weights << 0.5 # Weight message matching highly end # Check severity match if severity severity_score = if entry.severity == severity 1.0 # Exact severity match else severity_proximity_score(entry.severity, severity) # Partial severity match end scores << severity_score weights << 0.2 end # Check progname match if progname progname_score = calculate_field_score(entry.progname, progname) scores << progname_score weights << 0.2 end # Check attributes match if attributes.is_a?(Hash) && !attributes.empty? attributes_score = calculate_attributes_score(entry.attributes, attributes) scores << attributes_score weights << 0.3 end # Return 0 if no criteria were provided return 0.0 if scores.empty? # Calculate weighted average, but apply a penalty if any score is 0 # This ensures that completely failed criteria significantly impact the result total_weighted_score = scores.zip(weights).map { |score, weight| score * weight }.sum total_weight = weights.sum base_score = total_weighted_score / total_weight # Apply penalty for zero scores: reduce the score based on how many criteria completely failed zero_scores = scores.count(0.0) if zero_scores > 0 penalty_factor = 1.0 - (zero_scores.to_f / scores.length * 0.5) # Up to 50% penalty base_score *= penalty_factor end base_score end # Calculate score for any field value against a filter. # Returns a score between 0.0 and 1.0 based on how well the value matches the filter. # # @param value [Object] The value to match against the filter. # @param filter [String, Regexp, Object] The filter to match the value against. # @return [Float] A score between 0.0 and 1.0 indicating match quality. def calculate_field_score(value, filter) return 0.0 unless value && filter case filter when String value_str = value.to_s if value_str == filter 1.0 elsif value_str.include?(filter) 0.7 else # Use string similarity for partial matching similarity = string_similarity(value_str, filter) (similarity > 0.5) ? similarity * 0.6 : 0.0 end when Regexp filter.match?(value.to_s) ? 1.0 : 0.0 else # For other matchers (like RSpec matchers), try to use === operator begin (filter === value) ? 1.0 : 0.0 rescue 0.0 end end end # Calculate proximity score based on log severity distance. # Provides partial scoring for severities that are close to the target. # # @param entry_severity [Integer] The severity level of the log entry. # @param filter_severity [Integer] The target severity level to match. # @return [Float] A score between 0.0 and 1.0 based on severity proximity. def severity_proximity_score(entry_severity, filter_severity) severity_diff = (entry_severity - filter_severity).abs case severity_diff when 0 then 1.0 when 1 then 0.7 when 2 then 0.4 else 0.0 end end # Calculate score for attribute matching. # Compares entry attributes against filter attributes and returns a score # based on how many attributes match. # # @param entry_attributes [Hash] The attributes from the log entry. # @param attributes_filter [Hash] The attributes filter to match against. # @return [Float] A score between 0.0 and 1.0 based on attribute matches. def calculate_attributes_score(entry_attributes, attributes_filter) return 0.0 unless entry_attributes && attributes_filter.is_a?(Hash) attributes_filter = deep_stringify_keys(Lumberjack::Utils.expand_attributes(attributes_filter)) attributes = deep_stringify_keys(Lumberjack::Utils.expand_attributes(entry_attributes)) total_attribute_filters = count_attribute_filters(attributes_filter) return 0.0 if total_attribute_filters == 0 matched_attributes = count_matched_attributes(attributes, attributes_filter) matched_attributes.to_f / total_attribute_filters end private # Calculate string similarity using a simple Levenshtein distance-based approach. # Returns a score between 0.0 and 1.0 where 1.0 is an exact match. # # @param str1 [String] The first string to compare. # @param str2 [String] The second string to compare. # @return [Float] A similarity score between 0.0 and 1.0. def string_similarity(str1, str2) return 1.0 if str1 == str2 return 0.0 if str1.nil? || str2.nil? || str1.empty? || str2.empty? # Convert to lowercase for case-insensitive comparison s1 = str1.downcase s2 = str2.downcase # If one string contains the other, give it a good score if s1.include?(s2) || s2.include?(s1) shorter = [s1.length, s2.length].min longer = [s1.length, s2.length].max return shorter.to_f / longer * 0.8 + 0.2 # Boost score for containment end # Calculate Levenshtein distance distance = levenshtein_distance(s1, s2) max_length = [s1.length, s2.length].max # Convert distance to similarity score return 0.0 if max_length == 0 1.0 - (distance.to_f / max_length) end # Simple Levenshtein distance implementation. # Calculates the minimum number of single-character edits needed # to change one string into another. # # @param str1 [String] The first string. # @param str2 [String] The second string. # @return [Integer] The Levenshtein distance between the strings. def levenshtein_distance(str1, str2) return str2.length if str1.empty? return str1.length if str2.empty? matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1, 0) } # Initialize first row and column (0..str1.length).each { |i| matrix[i][0] = i } (0..str2.length).each { |j| matrix[0][j] = j } # Fill the matrix (1..str1.length).each do |i| (1..str2.length).each do |j| cost = (str1[i - 1] == str2[j - 1]) ? 0 : 1 matrix[i][j] = [ matrix[i - 1][j] + 1, # deletion matrix[i][j - 1] + 1, # insertion matrix[i - 1][j - 1] + cost # substitution ].min end end matrix[str1.length][str2.length] end # Count the total number of attribute filters in a nested hash structure. # # @param attributes_filter [Hash] The attributes filter hash to count. # @param count [Integer] The current count (used for recursion). # @return [Integer] The total number of filters. def count_attribute_filters(attributes_filter, count = 0) attributes_filter.each do |_name, value_filter| if value_filter.is_a?(Hash) count = count_attribute_filters(value_filter, count) else count += 1 end end count end # Count the number of matched attributes in a nested structure. # # @param attributes [Hash] The log entry attributes to check. # @param attributes_filter [Hash] The filter attributes to match against. # @param count [Integer] The current count (used for recursion). # @return [Integer] The number of matched attributes. def count_matched_attributes(attributes, attributes_filter, count = 0) return count unless attributes && attributes_filter attributes_filter.each do |name, value_filter| name = name.to_s attribute_values = attributes[name] if value_filter.is_a?(Hash) && attribute_values.is_a?(Hash) count = count_matched_attributes(attribute_values, value_filter, count) elsif attributes.include?(name) && exact_match?(attribute_values, value_filter) count += 1 end end count end # Check if a value exactly matches the filter using the === operator. # # @param value [Object] The value to match. # @param filter [Object] The filter to match against. # @return [Boolean] True if the value matches the filter. def exact_match?(value, filter) return true unless filter filter === value end # Recursively convert all keys in a hash structure to strings. # # @param hash [Hash, Object] The hash to stringify or other object to return as-is. # @return [Hash, Object] The hash with string keys or the original object. def deep_stringify_keys(hash) if hash.is_a?(Hash) hash.each_with_object({}) do |(key, value), result| new_key = key.to_s new_value = deep_stringify_keys(value) result[new_key] = new_value end elsif hash.is_a?(Enumerable) hash.collect { |item| deep_stringify_keys(item) } else hash end end end end bdurand-lumberjack-ac97435/lib/lumberjack/logger.rb000066400000000000000000000526161515437321200223150ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # Lumberjack::Logger is a thread-safe, feature-rich logging implementation that extends Ruby's standard # library Logger class with advanced capabilities for structured logging. # # Key features include: # - Structured logging with attributes (key-value pairs) attached to log entries # - Context isolation for scoping logging behavior to specific code blocks # - Flexible output devices supporting files, streams, and custom destinations # - Customizable formatters for messages and attributes # # The Logger maintains full API compatibility with Ruby's standard Logger while adding # powerful extensions for modern logging needs. # # @example Basic usage # logger = Lumberjack::Logger.new(STDOUT) # logger.info("Starting processing") # logger.debug("Processing options #{options.inspect}") # logger.fatal("OMG the application is on fire!") # # @example Structured logging with attributes # logger = Lumberjack::Logger.new("/var/log/app.log") # logger.tag(request_id: "abc123") do # logger.info("User logged in", user_id: 123, ip: "192.168.1.1") # logger.info("Processing request") # Will include request_id: "abc123" # end # # @example Log rotation # # Keep 10 files, rotate when each reaches 10MB # logger = Lumberjack::Logger.new("/var/log/app.log", 10, 10 * 1024 * 1024) # # @example Using different devices # logger = Lumberjack::Logger.new("logs/application.log") # Log to file # logger = Lumberjack::Logger.new(STDOUT, template: "{{severity}} - {{message}}") # Log to a stream with a template # logger = Lumberjack::Logger.new(:test) # Log to an in memory buffer for testing # logger = Lumberjack::Logger.new(another_logger) # Proxy logs to another logger # logger = Lumberjack::Logger.new(MyDevice.new) # Log to a custom Lumberjack::Device # # @example Logging to multiple devices with an array # logger = Lumberjack::Logger.new(["/var/log/app.log", [:stdout, {template: "{{message}}"}]]) # # Log entries are written to a logging Device if their severity meets or exceeds the log level. # Each log entry records the log message and severity along with the time it was logged, the # program name, process id, and an optional hash of attributes. Messages are converted to strings # using a Formatter associated with the logger. # # @see Lumberjack::ContextLogger # @see Lumberjack::Device # @see Lumberjack::Template # @see Lumberjack::EntryFormatter class Logger < ::Logger include ContextLogger # Create a new logger to log to a Device. # # The +device+ argument can be in any one of several formats: # - A symbol for a device name (e.g. :null, :test). You can call +Lumberjack::DeviceRegistry.registered_devices+ for a list. # - A stream # - A file path string or +Pathname+ # - A +Lumberjack::Device+ object # - An object with a +write+ method will be wrapped in a Device::Writer # - An array of any of the above will open a Multi device that will send output to all devices. # # @param logdev [Lumberjack::Device, IO, Symbol, String, Pathname] The device to log to. # If this is a symbol, the device will be looked up from the DeviceRegistry. If it is # a string or a Pathname, the logs will be sent to the corresponding file path. # @param shift_age [Integer, String, Symbol] If this is an integer greater than zero, then # log files will be rolled when they get to the size specified in shift_size and the number of # files to keep will be determined by this value. Otherwise it will be interpreted as a date # rolling value and must be one of "daily", "weekly", or "monthly". This parameter has no # effect unless the device parameter is a file path or file stream. This can also be # specified with the :roll keyword argument. # @param shift_size [Integer] The size in bytes of the log files before rolling them. This can # be passed as a string with a unit suffix of K, M, or G (e.g. "10M" for 10 megabytes). # This can also be specified with the :max_size keyword argument. # @param level [Integer, Symbol, String] The logging level below which messages will be ignored. # @param progname [String] The name of the program that will be recorded with each log entry. # @param formatter [Lumberjack::EntryFormatter, Lumberjack::Formatter, ::Logger::Formatter, :default, #call] # The formatter to use for outputting messages to the log. If this is a Lumberjack::EntryFormatter # or a Lumberjack::Formatter, it will be used to format structured log entries. # You can also pass the value +:default+ to use the default message formatter which formats # non-primitive objects with +inspect+ and includes the backtrace in exceptions. # # For compatibility with the standard library Logger when writing to a stream, you can also # pass in a +::Logger::Formatter+ object or a callable object that takes exactly 4 arguments # (severity, time, progname, msg). # @param datetime_format [String] The format to use for log timestamps. # @param binmode [Boolean] Whether to open the log file in binary mode. # @param shift_period_suffix [String] The suffix to use for the shifted log file names. # @param kwargs [Hash] Additional device-specific options. These will be passed through when creating # a device from the logdev argument. # @return [Lumberjack::Logger] A new logger instance. def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG, progname: nil, formatter: nil, datetime_format: nil, binmode: false, shift_period_suffix: "%Y%m%d", **kwargs) init_context_locals! if shift_age.is_a?(Hash) Lumberjack::Utils.deprecated("Logger.new(options)", "Passing a Hash as the second argument to Logger.new is deprecated and will be removed in version 2.1; use keyword arguments instead.") options = shift_age level = options[:level] if options.include?(:level) progname = options[:progname] if options.include?(:progname) formatter = options[:formatter] if options.include?(:formatter) datetime_format = options[:datetime_format] if options.include?(:datetime_format) kwargs = options.merge(kwargs) end self.isolation_level = kwargs.delete(:isolation_level) || Lumberjack.isolation_level # Include standard args that affect devices with the optional kwargs which may # contain device specific options. device_options = kwargs.merge(shift_age: shift_age, shift_size: size_with_units(shift_size), binmode: binmode, shift_period_suffix: shift_period_suffix) device_options[:standard_logger_formatter] = formatter if standard_logger_formatter?(formatter) if device_options.include?(:roll) Utils.deprecated("Logger.options(:roll)", "Lumberjack::Logger :roll option is deprecated and will be removed in version 2.1; use the shift_age argument instead.") device_options[:shift_age] = device_options.delete(:roll) unless shift_age != 0 end if device_options.include?(:max_size) Utils.deprecated("Logger.options(:max_size)", "Lumberjack::Logger :max_size option is deprecated and will be removed in version 2.1; use the shift_size argument instead.") device_options[:shift_age] = 10 if shift_age == 0 device_options[:shift_size] = device_options.delete(:max_size) end message_formatter = nil if device_options.include?(:message_formatter) Utils.deprecated("Logger.options(:message_formatter)", "Lumberjack::Logger :message_formatter option is deprecated and will be removed in version 2.1; use the formatter argument instead to specify an EntryFormatter.") message_formatter = device_options.delete(:message_formatter) end attribute_formatter = nil if device_options.include?(:tag_formatter) Utils.deprecated("Logger.options(:tag_formatter)", "Lumberjack::Logger :tag_formatter option is deprecated and will be removed in version 2.1; use the formatter argument instead to specify an EntryFormatter.") attribute_formatter = device_options.delete(:tag_formatter) end @logdev = Device.open_device(logdev, device_options) @context = Context.new self.level = level || DEBUG self.progname = progname self.formatter = build_entry_formatter(formatter, message_formatter, attribute_formatter) self.datetime_format = datetime_format if datetime_format @closed = false end # Get the logging device that is used to write log entries. # # @return [Lumberjack::Device] The logging device. def device @logdev end # Set the logging device to a new device. # # @param device [Lumberjack::Device] The new logging device. # @return [void] def device=(device) @logdev = Device.open_device(device, {}) end # Set the formatter used for log entries. This can be an EntryFormatter, a standard Logger::Formatter, # or any callable object that formats log entries. # # @param value [Lumberjack::EntryFormatter, ::Logger::Formatter, #call] The formatter to use. # @return [void] def formatter=(value) @formatter = build_entry_formatter(value, nil, nil) end # Get the timestamp format on the device if it has one. # # @return [String, nil] The timestamp format or nil if the device doesn't support it. def datetime_format device.datetime_format if device.respond_to?(:datetime_format) end # Set the timestamp format on the device if it is supported. # # @param format [String] The timestamp format. # @return [void] def datetime_format=(format) if device.respond_to?(:datetime_format=) device.datetime_format = format end end # Get the message formatter used to format log messages. # # @return [Lumberjack::Formatter] The message formatter. def message_formatter formatter.message_formatter end # Set the message formatter used to format log messages. # # @param value [Lumberjack::Formatter] The message formatter to use. # @return [void] def message_formatter=(value) formatter.message_formatter = value end # Get the attribute formatter used to format log entry attributes. # # @return [Lumberjack::AttributeFormatter] The attribute formatter. def attribute_formatter formatter.attribute_formatter end # Set the attribute formatter used to format log entry attributes. # # @param value [Lumberjack::AttributeFormatter] The attribute formatter to use. # @return [void] def attribute_formatter=(value) formatter.attribute_formatter = value end # @deprecated Use {#attribute_formatter} instead. def tag_formatter Utils.deprecated("Logger#tag_formatter", "Lumberjack::Logger#tag_formatter is deprecated and will be removed in version 2.1; use attribute_formatter instead.") do formatter.attributes.attribute_formatter end end # @deprecated Use {#attribute_formatter=} instead. def tag_formatter=(value) Utils.deprecated("Logger#tag_formatter=", "Lumberjack::Logger#tag_formatter= is deprecated and will be removed in version 2.1; use attribute_formatter= instead.") do formatter.attributes.attribute_formatter = value end end # Flush the logging device. Messages are not guaranteed to be written until this method is called. # # @return [void] def flush device.flush nil end # Close the logging device. # # @return [void] def close flush device.close if device.respond_to?(:close) @closed = true end # Returns +true+ if the logging device is closed. # # @return [Boolean] +true+ if the logging device is closed. def closed? return true if @closed device.respond_to?(:closed?) && device.closed? end # Reopen the logging device. # # @param logdev [Object] passed through to the logging device. # @return [Lumberjack::Logger] self def reopen(logdev = nil) @closed = false device.reopen(logdev) if device.respond_to?(:reopen) self end # Set the program name that is associated with log messages. If a block # is given, the program name will be valid only within the block. # # @param value [String] The program name to use. # @return [void] # @deprecated Use with_progname or progname= instead. def set_progname(value, &block) Utils.deprecated("Logger#set_progname", "Lumberjack::Logger#set_progname is deprecated and will be removed in version 2.1; use with_progname or progname= instead.") do if block with_progname(value, &block) else self.progname = value end end end # Alias method for #attributes to provide backward compatibility with version 1.x API. This # method will eventually be removed. # # @return [Hash] # @deprecated Use {#attributes} instead def tags Utils.deprecated("Logger#tags", "Lumberjack::Logger#tags is deprecated and will be removed in version 2.1; use attributes instead.") do attributes end end # Alias method for #attribute_value to provide backward compatibility with version 1.x API. This # method will eventually be removed. # # @return [Hash] # @deprecated Use {#attribute_value} instead def tag_value(name) Utils.deprecated("Logger#tag_value", "Lumberjack::Logger#tag_value is deprecated and will be removed in version 2.1; use attribute_value instead.") do attribute_value(name) end end # Use tag! instead # # @return [void] # @deprecated Use {#tag!} instead. def tag_globally(tags) Utils.deprecated("Logger#tag_globally", "Lumberjack::Logger#tag_globally is deprecated and will be removed in version 2.1; use tag! instead.") do tag!(tags) end end # Use context? instead # # @return [Boolean] # @deprecated Use {#in_context?} instead. def in_tag_context? Utils.deprecated("Logger#in_tag_context?", "Lumberjack::Logger#in_tag_context? is deprecated and will be removed in version 2.1; use in_context? instead.") do context? end end # Remove a tag from the current context block. If this is called inside a context block, # the attributes will only be removed for the duration of that block. Otherwise they will be removed # from the global attributes. # # @param tag_names [Array] The attributes to remove. # @return [void] # @deprecated Use untag or untag! instead. def remove_tag(*tag_names) Utils.deprecated("Logger#remove_tag", "Lumberjack::Logger#remove_tag is deprecated and will be removed in version 2.1; use untag or untag! instead.") do attributes = current_context&.attributes AttributesHelper.new(attributes).delete(*tag_names) if attributes end end # Alias for append_to(:tagged) for compatibility with ActiveSupport support in Lumberjack 1.x. # This functionality has been moved to the lumberjack_rails gem. Note that in that gem the # tags are added to the :tags attribute instead of the :tagged attribute. # # @see append_to # @deprecated This implementation is deprecated. Install the lumberjack_rails gem for full support. def tagged(*tags, &block) deprecation_message = "Install the lumberjack_rails gem for full support of the tagged method." Utils.deprecated("Logger#tagged", deprecation_message) do append_to(:tagged, *tags, &block) end end # Alias for clear_attributes. # # @see clear_attributes # @deprecated Use clear_attributes instead. def untagged(&block) Utils.deprecated("Logger#untagged", "Lumberjack::Logger#untagged is deprecated and will be removed in version 2.1; use clear_attributes instead.") do clear_attributes(&block) end end # Alias for with_level for compatibility with ActiveSupport loggers. This functionality # has been moved to the lumberjack_rails gem. # # @see with_level # @deprecated This implementation is deprecated. Install the lumberjack_rails gem for full support. def log_at(level, &block) deprecation_message = "Install the lumberjack_rails gem for full support of the log_at method." Utils.deprecated("Logger#log_at", deprecation_message) do with_level(level, &block) end end # Alias for with_level for compatibilty with ActiveSupport loggers. This functionality # has been moved to the lumberjack_rails gem. # # @see with_level # @deprecated This implementation is deprecated. Install the lumberjack_rails gem for full support. def silence(level = Logger::ERROR, &block) deprecation_message = "Install the lumberjack_rails gem for full support of the silence method." Utils.deprecated("Logger#silence", deprecation_message) do with_level(level, &block) end end # Add an entry to the log. # # @param severity [Integer, Symbol, String] The severity of the message. # @param message [Object] The message to log. # @param progname [String] The name of the program that is logging the message. # @param attributes [Hash] The attributes to add to the log entry. # @return [void] # @api private def add_entry(severity, message, progname = nil, attributes = nil) return false unless device # Prevent infinite recursion if logging is attempted from within a logging call. if current_context_locals&.logging log_to_stderr(severity, message) return false end severity = Severity.label_to_level(severity) unless severity.is_a?(Integer) new_context_locals do |locals| locals.logging = true # protection from infinite loops time = Time.now progname ||= self.progname attributes = nil unless attributes.is_a?(Hash) attributes = merge_attributes(merge_all_attributes, attributes) message, attributes = formatter.format(message, attributes) if formatter entry = Lumberjack::LogEntry.new(time, severity, message, progname, Process.pid, attributes) write_to_device(entry) end true end # Return a human-readable representation of the logger showing its key configuration. # # @return [String] A string representation of the logger. def inspect formatted_object_id = object_id.to_s(16).rjust(16, "0") "#" end private def default_context @context end def write_to_device(entry) # :nodoc: device.write(entry) rescue => e err = e.class.name.dup err << ": #{e.message}" unless e.message.to_s.empty? err << " at #{e.backtrace.first}" if e.backtrace $stderr.write("#{err}#{Lumberjack::LINE_SEPARATOR}#{entry}#{Lumberjack::LINE_SEPARATOR}") # rubocop:disable Style/StderrPuts raise e if Lumberjack.raise_logger_errors? end def build_entry_formatter(formatter, message_formatter, attribute_formatter) # :nodoc: entry_formatter = formatter if formatter.is_a?(Lumberjack::EntryFormatter) unless entry_formatter message_formatter ||= formatter if formatter.is_a?(Lumberjack::Formatter) || formatter == :default entry_formatter = Lumberjack::EntryFormatter.new end message_formatter = Lumberjack::Formatter.default if message_formatter == :default entry_formatter.message_formatter = message_formatter if message_formatter entry_formatter.attribute_formatter = attribute_formatter if attribute_formatter entry_formatter end def standard_logger_formatter?(formatter) return false if formatter.is_a?(Lumberjack::EntryFormatter) return false if formatter.is_a?(Lumberjack::Formatter) return true if formatter.is_a?(::Logger::Formatter) takes_exactly_n_call_args?(formatter, 4) end # Convert a size string with optional unit suffix to an integer size in bytes. # Allowed suffixes are K, M, and G (case insensitive) for kilobytes, megabytes, and gigabytes. # # @param size [String, Integer] The size string to convert. # @return [Integer] The size in bytes. def size_with_units(size) return size unless size.is_a?(String) && size.match?(/\A\d+(\.\d+)?[KMG]?\z/i) multiplier = case size[-1].upcase when "K" then 1024 when "M" then 1024 * 1024 when "G" then 1024 * 1024 * 1024 else 1 end (size.to_f * multiplier).round end def takes_exactly_n_call_args?(callable, count) params = if callable.is_a?(Proc) callable.parameters elsif callable.respond_to?(:call) callable.method(:call).parameters end return false unless params positional_arg_count = params.count do |type, _name| type == :req || type == :opt end has_forbidden_args = params.any? do |type, _name| [:rest, :keyreq, :key, :keyrest].include?(type) end positional_arg_count == 4 && !has_forbidden_args end def log_to_stderr(severity, message) severity = Severity.coerce(severity) severity_label = Severity.level_to_label(severity) $stderr.write("Recursive logging detected; you cannot write new log entries while logging other entries: #{severity_label} #{message}\n") end end end bdurand-lumberjack-ac97435/lib/lumberjack/message_attributes.rb000066400000000000000000000021271515437321200247200ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # This class can be used as the return value from a formatter +call+ method to # extract additional attributes from an object being logged. This can be useful when there # using structured logging to include important metadata in the log entry in addition # to the message. # # @example # # Automatically add attributes with error details when logging an exception. # logger.add_formatter(Exception, ->(e) { # Lumberjack::MessageAttributes.new(e.inspect, { # error: { # message: e.message, # class: e.class.name, # stack: e.backtrace # } # }) # }) class MessageAttributes attr_reader :message, :attributes # @param message [Object] The message to be logged. # @param attributes [Hash] The attributes to be associated with the message. def initialize(message, attributes) @message = message @attributes = attributes || {} end def to_s inspect end def inspect {message: @message, attributes: @attributes}.inspect end end end bdurand-lumberjack-ac97435/lib/lumberjack/rack.rb000066400000000000000000000001551515437321200217450ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack module Rack require_relative "rack/context" end end bdurand-lumberjack-ac97435/lib/lumberjack/rack/000077500000000000000000000000001515437321200214175ustar00rootroot00000000000000bdurand-lumberjack-ac97435/lib/lumberjack/rack/context.rb000066400000000000000000000067501515437321200234400ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack module Rack # Rack middleware ensures that a global Lumberjack context exists for # the duration of each HTTP request. This middleware creates an isolated # logging context that automatically cleans up after the request completes, # ensuring that request-specific attributes don't leak between requests. # # The middleware supports dynamic attribute extraction from the Rack environment, # allowing automatic tagging of log entries with request-specific information # such as request IDs, user agents, IP addresses, or any other data available # in the Rack environment. # # This is particularly useful in web applications where you want to correlate # all log entries within a single request with common identifying information, # making it easier to trace request flows and debug issues. # # @example Basic usage in a Rack application # use Lumberjack::Rack::Context # # @example With static attributes # use Lumberjack::Rack::Context, { # app_name: "MyWebApp", # version: "1.2.3" # } # # @example With dynamic attributes from request environment # use Lumberjack::Rack::Context, { # request_id: ->(env) { env["HTTP_X_REQUEST_ID"] }, # user_agent: ->(env) { env["HTTP_USER_AGENT"] }, # remote_ip: ->(env) { env["REMOTE_ADDR"] }, # method: ->(env) { env["REQUEST_METHOD"] }, # path: ->(env) { env["PATH_INFO"] } # } # # @example Rails integration # # In config/application.rb # config.middleware.use Lumberjack::Rack::Context, { # request_id: ->(env) { env["action_dispatch.request_id"] }, # session_id: ->(env) { env["rack.session"]&.id }, # user_id: ->(env) { env["warden"]&.user&.id } # } # # @see Lumberjack.context # @see Lumberjack.tag class Context # Initialize the middleware with the Rack application and optional environment # attribute configuration. The middleware will create a scoped logging context # for each request and automatically apply the specified attributes. # # @param app [Object] The next Rack application in the middleware stack # @param env_attributes [Hash, nil] Optional hash defining attributes to extract # from the request environment. Values can be: # - Static values: Applied directly to all requests # - Proc objects: Called with the Rack env hash to generate dynamic values # - Any callable: Invoked with env to produce request-specific attributes def initialize(app, env_attributes = nil) @app = app @env_attributes = env_attributes end # Process a Rack request within a scoped Lumberjack logging context. # # @param env [Hash] The Rack environment hash containing request information # @return [Array] The standard Rack response array [status, headers, body] def call(env) Lumberjack.ensure_context do apply_attributes(env) if @env_attributes @app.call(env) end end private # Apply configured environment attributes to the current logging context. # # @param env [Hash] The Rack environment hash # @return [void] def apply_attributes(env) attributes = @env_attributes.transform_values do |value| value.is_a?(Proc) ? value.call(env) : value end Lumberjack.tag(attributes) end end end end bdurand-lumberjack-ac97435/lib/lumberjack/remap_attribute.rb000066400000000000000000000016141515437321200242150ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # This class can be used as a return value from an AttributeFormatter to indicate that the # value should be remapped to a new attribute name. # # @example # # Transform duration_millis and duration_micros to seconds and move to # # the duration attribute. # logger.formatter.format_attribute_name("duration_ms") do |value| # Lumberjack::RemapAttribute.new("duration" => value.to_f / 1000) # end # logger.formatter.format_attribute_name("duration_micros") do |value| # Lumberjack::RemapAttribute.new("duration" => value.to_f / 1_000_000) # end class RemapAttribute attr_reader :attributes # @param remapped_attributes [Hash] The remapped attribute with the new names. def initialize(remapped_attributes) @attributes = Lumberjack::Utils.flatten_attributes(remapped_attributes) end end end bdurand-lumberjack-ac97435/lib/lumberjack/severity.rb000066400000000000000000000051301515437321200226750ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # The standard severity levels for logging messages. module Severity # Custom severity level for trace messages, lower than DEBUG. TRACE = -1 DEBUG = Logger::Severity::DEBUG INFO = Logger::Severity::INFO WARN = Logger::Severity::WARN ERROR = Logger::Severity::ERROR FATAL = Logger::Severity::FATAL UNKNOWN = Logger::Severity::UNKNOWN SEVERITY_LABELS = %w[TRACE DEBUG INFO WARN ERROR FATAL ANY].freeze private_constant :SEVERITY_LABELS # Data object for severity levels that includes variations on the label. class Data attr_reader :level, :label, :padded_label, :char, :emoji, :terminal_color def initialize(level, label, emoji, terminal_color) @level = level @label = label.freeze @padded_label = label.ljust(5).freeze @char = label[0].freeze @emoji = emoji.freeze @terminal_color = "\e[38;5;#{terminal_color}m" end end SEVERITIES = [ Data.new(TRACE, "TRACE", "🔍", 247).freeze, Data.new(DEBUG, "DEBUG", "🔧", 244).freeze, Data.new(INFO, "INFO", "🔵", 33).freeze, Data.new(WARN, "WARN", "🟡", 208).freeze, Data.new(ERROR, "ERROR", "❌", 9).freeze, Data.new(FATAL, "FATAL", "🔥", 160).freeze, Data.new(UNKNOWN, "ANY", "❓", 129).freeze ].freeze private_constant :SEVERITIES class << self # Convert a severity level to a label. # # @param severity [Integer] The severity level to convert. # @return [String] The severity label. def level_to_label(severity) SEVERITIES[severity + 1]&.label || SEVERITIES.last.label end # Convert a severity label to a level. # # @param label [String, Symbol] The severity label to convert. # @return [Integer] The severity level. def label_to_level(label) label = label.to_s.upcase (SEVERITY_LABELS.index(label) || UNKNOWN + 1) - 1 end # Coerce a value to a severity level. # # @param value [Integer, String, Symbol] The value to coerce. # @return [Integer] The severity level. def coerce(value) if value.is_a?(Numeric) value.to_i else label_to_level(value) end end # Return a data object that maps the severity level to variations on the label. # # @param level [Integer, String, Symbol] The severity level. # @return [SeverityData] The severity data object. def data(level) SEVERITIES[coerce(level) + 1] || SEVERITIES.last end end end end bdurand-lumberjack-ac97435/lib/lumberjack/tag_context.rb000066400000000000000000000007361515437321200233510ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # This class was renamed to Lumberjack::AttributesHelper. This class is provided for # backward compatibility with the version 1.x API and will eventually be removed. # # @deprecated Use Lumberjack::AttributesHelper instead. class TagContext < AttributesHelper def initialize Utils.deprecated("Lumberjack::TagContext", "Use Lumberjack::AttributesHelper instead.") do super end end end end bdurand-lumberjack-ac97435/lib/lumberjack/tag_formatter.rb000066400000000000000000000022031515437321200236570ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # TagFormatter has been renamed to {AttributeFormatter} as part of the transition from # "tags" to "attributes" terminology in Lumberjack 2.0. This class exists solely for # backward compatibility with the 1.x API and will be removed in a future version. # # All functionality has been moved to {AttributeFormatter} with no changes to the API. # Simply replace +TagFormatter+ with +AttributeFormatter+ in your code. # # @deprecated Use {Lumberjack::AttributeFormatter} instead. # @see Lumberjack::AttributeFormatter # # @example Migration # # Old code (deprecated) # formatter = Lumberjack::TagFormatter.new # # # New code # formatter = Lumberjack::AttributeFormatter.new class TagFormatter < AttributeFormatter # Create a new TagFormatter instance. Issues a deprecation warning and delegates # to {AttributeFormatter}. # # @deprecated Use {Lumberjack::AttributeFormatter.new} instead. def initialize Utils.deprecated("Lumberjack::TagFormatter", "Use Lumberjack::AttributeFormatter instead.") do super end end end end bdurand-lumberjack-ac97435/lib/lumberjack/tags.rb000066400000000000000000000026121515437321200217630ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class Tags class << self # Transform hash keys to strings. This method exists for optimization and backward compatibility. # If a hash already has string keys, it will be returned as is. # # @param hash [Hash] The hash to transform. # @return [Hash] The hash with string keys. # @deprecated No longer supported def stringify_keys(hash) Utils.deprecated("Lumberjack::Tags.stringify_keys", "Lumberjack::Tags.stringify_keys is no longer supported and will be removed in version 2.1") do return nil if hash.nil? if hash.keys.all? { |key| key.is_a?(String) } hash else hash.transform_keys(&:to_s) end end end # Alias to AttributesHelper.expand_runtime_values # # @param hash [Hash] The hash to transform. # @return [Hash] The hash with string keys and expanded values. # @deprecated Use {Lumberjack::AttributesHelper.expand_runtime_values} instead. def expand_runtime_values(hash) Utils.deprecated("Lumberjack::Tags.expand_runtime_values", "Lumberjack::Tags.expand_runtime_values is deprecated and will be removed in version 2.1; use Lumberjack::AttributesHelper.expand_runtime_values instead.") do AttributesHelper.expand_runtime_values(hash) end end end end end bdurand-lumberjack-ac97435/lib/lumberjack/template.rb000066400000000000000000000333511515437321200226440ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack # A flexible template system for converting log entries into formatted strings. # Templates use mustache style placeholders to create customizable log output formats. # # The template system supports the following built-in placeholders: # # - {{time}} - The log entry timestamp # - {{severity}} - The severity level (DEBUG, INFO, WARN, ERROR, FATAL). The severity # can also be formatted in a variety of ways with an optional format specifier. # Supported formats include: # - {{severity(padded)}} - Right padded so that all values are five characters # - {{severity(char)}} - Single character representation (D, I, W, E, F) # - {{severity(emoji)}} - Emoji representation # - {{severity(level)}} - Numeric level representation # - {{progname}} - The program name that generated the entry # - {{pid}} - The process ID # - {{message}} - The main log message content # - {{attributes}} - All custom attributes formatted as key:value pairs # # Custom attribute placeholders can also be put in the double bracket placeholders. # Any attributes explicitly added to the template in their own placeholder will be removed # from the general list of attributes. # # @example Basic template usage # template = Lumberjack::Template.new("[{{time}} {{severity}}] {{message}}") # # Output: [2023-08-21T10:30:15.123 INFO] User logged in # # @example Multi-line message formatting # template = Lumberjack::Template.new( # "[{{time}} {{severity}}] {{message}}", # additional_lines: "\n | {{message}}" # ) # # Output: # # [2023-08-21T10:30:15.123 INFO] First line # # | Second line # # | Third line # # @example Custom attribute placeholders # # The user_id attribute will be put before the message instead of with the rest of the attributes. # template = Lumberjack::Template.new("[{{time}} {{severity}}] (usr:{{user_id}} {{message}} -- {{attributes}})") class Template DEFAULT_FIRST_LINE_TEMPLATE = "[{{time}} {{severity(padded)}} {{progname}}({{pid}})] {{message}} {{attributes}}" STDLIB_FIRST_LINE_TEMPLATE = "{{severity(char)}}, [{{time}} {{pid}}] {{severity(padded)}} -- {{progname}}: {{message}} {{attributes}}" DEFAULT_ADDITIONAL_LINES_TEMPLATE = "#{Lumberjack::LINE_SEPARATOR}> {{message}}" DEFAULT_ATTRIBUTE_FORMAT = "[%s:%s]" TemplateRegistry.add(:default, DEFAULT_FIRST_LINE_TEMPLATE) TemplateRegistry.add(:stdlib, STDLIB_FIRST_LINE_TEMPLATE) TemplateRegistry.add(:message, "{{message}}") # A wrapper template that delegates formatting to a standard Ruby Logger formatter. # This provides compatibility with existing Logger::Formatter implementations while # maintaining the Template interface for consistent usage within Lumberjack. class StandardFormatterTemplate < Template # Create a new wrapper for a standard Ruby Logger formatter. # # @param formatter [Logger::Formatter] The formatter to wrap def initialize(formatter) @formatter = formatter end # Format a log entry using the wrapped formatter. # # @param entry [Lumberjack::LogEntry] The log entry to format # @return [String] The formatted log entry def call(entry) @formatter.call(entry.severity_label, entry.time, entry.progname, entry.message) end # Set the datetime format on the wrapped formatter if supported. # # @param value [String] The datetime format string # @return [void] def datetime_format=(value) @formatter.datetime_format = value if @formatter.respond_to?(:datetime_format=) end # Get the datetime format from the wrapped formatter if supported. # # @return [String, nil] The datetime format string, or nil if not supported def datetime_format @formatter.datetime_format if @formatter.respond_to?(:datetime_format) end end TEMPLATE_ARGUMENT_ORDER = %w[ time severity severity(padded) severity(char) severity(emoji) severity(level) progname pid message attributes ].freeze MILLISECOND_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%3N" MICROSECOND_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N" PLACEHOLDER_PATTERN = /{{ *((?:[^}]|}(?!}))*) *}}/i V1_PLACEHOLDER_PATTERN = /:[a-z0-9_.-]+/i RESET_CHAR = "\e[0m" private_constant :TEMPLATE_ARGUMENT_ORDER, :MILLISECOND_TIME_FORMAT, :MICROSECOND_TIME_FORMAT, :PLACEHOLDER_PATTERN, :V1_PLACEHOLDER_PATTERN, :RESET_CHAR class << self def colorize_entry(formatted_string, entry) color_start = entry.severity_data.terminal_color formatted_string.split(Lumberjack::LINE_SEPARATOR).collect do |line| "\e7#{color_start}#{line}\e8" end.join(Lumberjack::LINE_SEPARATOR) end end # Create a new template with customizable formatting options. The template # supports different formatting for single-line and multi-line messages, # custom time formatting, and configurable attribute display. # # @param first_line [String, nil] Template for formatting the first line of messages. # Defaults to [{{ time }} {{ severity(padded) }} {{ progname }}({{ pid }})] {{ message }} {{ attributes }} # @param additional_lines [String, nil] Template for formatting additional lines # in multi-line messages. Defaults to \\n> {{ message }} # @param time_format [String, Symbol, nil] Time formatting specification. Can be: # - A strftime format string (e.g., "%Y-%m-%d %H:%M:%S") # - +:milliseconds+ for ISO format with millisecond precision (default) # - +:microseconds+ for ISO format with microsecond precision # @param attribute_format [String, nil] Printf-style format for individual attributes. # Must contain exactly two %s placeholders for name and value. Defaults to "[%s:%s]" # @param colorize [Boolean] Whether to colorize the log entry based on severity (default: false) # @raise [ArgumentError] If attribute_format doesn't contain exactly two %s placeholders def initialize(first_line = nil, additional_lines: nil, time_format: nil, attribute_format: nil, colorize: false) first_line ||= DEFAULT_FIRST_LINE_TEMPLATE first_line = "#{first_line.chomp}#{Lumberjack::LINE_SEPARATOR}" if !first_line.include?("{{") && first_line.match?(V1_PLACEHOLDER_PATTERN) Utils.deprecated("Template.v1", "Templates now use {{placeholder}} instead of :placeholder and :tags has been replaced with {{attributes}}.") do @first_line_template, @first_line_attributes = compile_v1(first_line) end else @first_line_template, @first_line_attributes = compile(first_line) end additional_lines ||= DEFAULT_ADDITIONAL_LINES_TEMPLATE if !additional_lines.include?("{{") && additional_lines.match?(V1_PLACEHOLDER_PATTERN) Utils.deprecated("Template.v1", "Templates now use {{placeholder}} instead of :placeholder and :tags has been replaced with {{attributes}}.") do @additional_line_template, @additional_line_attributes = compile_v1(additional_lines) end else @additional_line_template, @additional_line_attributes = compile(additional_lines) end @attribute_template = attribute_format || DEFAULT_ATTRIBUTE_FORMAT unless @attribute_template.scan("%s").size == 2 raise ArgumentError.new("attribute_format must be a printf template with exactly two '%s' placeholders") end # Formatting the time is relatively expensive, so only do it if it will be used @template_include_time = "#{@first_line_template} #{@additional_line_template}".include?("%1$s") self.datetime_format = (time_format || :milliseconds) @colorize = colorize end # Set the datetime format used for timestamp formatting in the template. # This method accepts both strftime format strings and symbolic shortcuts. # # @param format [String, Symbol] The datetime format specification: # - String: A strftime format pattern (e.g., "%Y-%m-%d %H:%M:%S") # - +:milliseconds+: ISO format with millisecond precision (YYYY-MM-DDTHH:MM:SS.sss) # - +:microseconds+: ISO format with microsecond precision (YYYY-MM-DDTHH:MM:SS.ssssss) # @return [void] def datetime_format=(format) if format == :milliseconds format = MILLISECOND_TIME_FORMAT elsif format == :microseconds format = MICROSECOND_TIME_FORMAT end @time_formatter = Formatter::DateTimeFormatter.new(format) end # Get the current datetime format string used for timestamp formatting. # # @return [String] The strftime format string currently in use def datetime_format @time_formatter.format end # Convert a log entry into a formatted string using the template. This method # handles both single-line and multi-line messages, applying the appropriate # templates and performing placeholder substitution. # # @param entry [Lumberjack::LogEntry] The log entry to format # @return [String] The formatted log entry string def call(entry) return entry unless entry.is_a?(LogEntry) first_line = entry.message.to_s additional_lines = nil if first_line.include?(Lumberjack::LINE_SEPARATOR) additional_lines = first_line.split(Lumberjack::LINE_SEPARATOR) first_line = additional_lines.shift end formatted_time = @time_formatter.call(entry.time) if @template_include_time severity = entry.severity_data format_args = [ formatted_time, severity.label, severity.padded_label, severity.char, severity.emoji, severity.level, entry.progname, entry.pid, first_line ] append_attribute_args!(format_args, entry.attributes, @first_line_attributes) message = (@first_line_template % format_args) if additional_lines && !additional_lines.empty? format_args.slice!(9, format_args.size) append_attribute_args!(format_args, entry.attributes, @additional_line_attributes) message_length = message.length message.chomp!(Lumberjack::LINE_SEPARATOR) chomped = message.length != message_length additional_lines.each do |line| format_args[8] = line line_message = @additional_line_template % format_args message << line_message end message << Lumberjack::LINE_SEPARATOR if chomped end message = Template.colorize_entry(message, entry) if @colorize message end private # Build the arguments array for sprintf formatting by appending attribute values. # This method handles both the general :attributes placeholder and specific # attribute placeholders defined in the template. # # @param args [Array] The existing format arguments array to modify # @param attributes [Hash, nil] The log entry attributes hash # @param attribute_vars [Array] List of specific attribute names used in template # @return [void] def append_attribute_args!(args, attributes, attribute_vars) if attributes.nil? || attributes.size == 0 (attribute_vars.length + 1).times { args << nil } return end attributes_string = +"" attributes.each do |name, value| unless value.nil? || attribute_vars.include?(name) value = value.to_s value = value.gsub(Lumberjack::LINE_SEPARATOR, " ") if value.include?(Lumberjack::LINE_SEPARATOR) attributes_string << " " attributes_string << @attribute_template % [name, value] end end args << attributes_string attribute_vars.each do |name| args << attributes[name] end end # Parse and compile a template string into a sprintf-compatible format string # and extract attribute variable names. This method handles placeholder # substitution and escape sequence processing. # # @param template [String] The raw template string with placeholders # @return [Array>] A tuple of [compiled_template, attribute_vars] def compile(template) # :nodoc: template = template.gsub(/ ({{ *)attributes( *}})/, "\\1attributes\\2") template = template.gsub(/%(?!%)/, "%%") attribute_vars = [] template = template.gsub(PLACEHOLDER_PATTERN) do |match| var_name = match.sub(/{{ */, "").sub(/ *}}/, "") position = TEMPLATE_ARGUMENT_ORDER.index(var_name) if position "%#{position + 1}$s" else attribute_vars << var_name "%#{TEMPLATE_ARGUMENT_ORDER.size + attribute_vars.size}$s" end end [template, attribute_vars] end # Parse and compile a template string into a sprintf-compatible format string # and extract attribute variable names. This method handles placeholder # substitution and escape sequence processing. # # @param template [String] The raw template string with placeholders # @return [Array>] A tuple of [compiled_template, attribute_vars] def compile_v1(template) # :nodoc: template = template.gsub(":tags", ":attributes").gsub(/ ?:attributes/, ":attributes") template = template.gsub(/%(?!%)/, "%%") attribute_vars = [] template = template.gsub(V1_PLACEHOLDER_PATTERN) do |match| var_name = match[1, match.length] position = TEMPLATE_ARGUMENT_ORDER.index(var_name) if position "%#{position + 1}$s" else attribute_vars << var_name "%#{TEMPLATE_ARGUMENT_ORDER.size + attribute_vars.size}$s" end end [template, attribute_vars] end end end bdurand-lumberjack-ac97435/lib/lumberjack/template_registry.rb000066400000000000000000000037231515437321200245740ustar00rootroot00000000000000# frozen_string_literal: true module Lumberjack class TemplateRegistry @templates = {} class << self # Register a log template class with a symbol. # # @param name [Symbol] The name of the template. # @param template [String, Class, #call] The log template to register. def add(name, template) unless template.is_a?(String) || template.is_a?(Class) || template.respond_to?(:call) raise ArgumentError.new("template must be a String, Class, or respond to :call") end @templates[name.to_sym] = template end # Remove a template from the registry. raise ArgumentError.new("template must be a String, Class, or respond to :call") # # @param name [Symbol] The name of the template to remove. # @return [void] def remove(name) @templates.delete(name.to_sym) end # Check if a template is registered. # # @param name [Symbol] The name of the template. # @return [Boolean] True if the template is registered, false otherwise. def registered?(name) @templates.include(name.to_sym) end # Get a registered log template class by its symbol. # # @param name [Symbol] The symbol of the registered log template class. # @return [Class, nil] The registered log template class, or nil if not found. def template(name, options = {}) template = @templates[name.to_sym] if template.is_a?(Class) template.new(options) elsif template.is_a?(String) template_options = options.slice(:additional_lines, :time_format, :attribute_format, :colorize) Template.new(template, **template_options) else template end end # List all registered log template symbols. # # @return [Array] An array of all registered log template symbols. def registered_templates @templates.dup end end end end bdurand-lumberjack-ac97435/lib/lumberjack/utils.rb000066400000000000000000000326341515437321200221740ustar00rootroot00000000000000# frozen_string_literal: true require "socket" module Lumberjack # Error raised when a deprecated method is called and the deprecation mode is set to :raise. class DeprecationError < StandardError end # Utils provides utility methods and helper functions used throughout the Lumberjack logging framework. module Utils UNDEFINED = Object.new.freeze private_constant :UNDEFINED NON_SLUGGABLE_PATTERN = /[^A-Za-z0-9_.-]+/.freeze private_constant :NON_SLUGGABLE_PATTERN @deprecations = nil @deprecations_lock = nil @hostname = UNDEFINED class << self # Print warning when deprecated methods are called the first time. This can be disabled # by setting +Lumberjack.deprecation_mode+ to +:silent+. # # In order to cut down on noise, each deprecated method will only print a warning once per process. # You can change this by setting +Lumberjack.deprecation_mode+ to +:verbose+. # # @param method [String, Symbol] The name of the deprecated method. # @param message [String] The deprecation message explaining what to use instead. # @yield The block containing the deprecated functionality to execute. # @return [Object] The result of the yielded block. # # @example # def old_method # Utils.deprecated(:old_method, "Use new_method instead.") do # # deprecated implementation # end # end def deprecated(method, message) if Lumberjack.deprecation_mode != :silent && !@deprecations&.include?(method) @deprecations_lock ||= Mutex.new @deprecations_lock.synchronize do @deprecations ||= {} unless @deprecations.include?(method) trace = ($VERBOSE && Lumberjack.deprecation_mode != :raise) ? caller[3..] : caller[3, 1] if trace.first.start_with?(__dir__) && !$VERBOSE non_lumberjack_caller = caller[4..].detect { |line| !line.start_with?(__dir__) } trace = [non_lumberjack_caller] if non_lumberjack_caller end message = "DEPRECATION WARNING: #{message} Called from #{trace.join(Lumberjack::LINE_SEPARATOR)}" if Lumberjack.deprecation_mode == :raise raise DeprecationError, message end unless Lumberjack.deprecation_mode == :verbose @deprecations[method] = true end warn(message) end end end yield if block_given? end # Helper method for tests to silence deprecation warnings within a block. You should # not use this in production code since it will silence all deprecation warnings # globally across all threads. # # @param mode [Symbol, String] The deprecation mode to set within the block. Valid values are # :normal, :verbose, :silent, and :raise. # @yield The block in which to silence deprecation warnings. # @return [Object] The result of the yielded block. def with_deprecation_mode(mode) save_mode = Lumberjack.deprecation_mode begin Lumberjack.deprecation_mode = mode yield ensure Lumberjack.deprecation_mode = save_mode end end # Get the hostname of the machine. The returned value will be in UTF-8 encoding. # The hostname is cached after the first call for performance. # # @return [String] The hostname of the machine in UTF-8 encoding. def hostname if @hostname.equal?(UNDEFINED) @hostname = force_utf8(Socket.gethostname) end @hostname end # Get the current line of code that calls this method. This is useful for debugging # purposes to record the exact location in your code that generated a log entry. # # @param root_path [String, Pathname, nil] An optional root path to strip from the file path. # @return [String] A string representation of the caller location (file:line:method). # # @example Adding source location to log entries # logger.info("Something happened", source: Lumberjack::Utils.current_line) # # Logs: "Something happened" with source: "/path/to/file.rb:123:in `method_name'" def current_line(root_path = nil) location = caller_locations(1, 1)[0] path = location.path if root_path root_path = root_path.to_s root_path = "#{root_path}#{File::SEPARATOR}" unless root_path.end_with?(File::SEPARATOR) path = path.delete_prefix(root_path) end "#{path}:#{location.lineno}:in `#{location.label}'" end # Set the hostname to a specific value. This overrides the system hostname. # Useful for testing or when you want to use a specific identifier. # # @param hostname [String] The hostname to use. # @return [void] def hostname=(hostname) @hostname = force_utf8(hostname) end # Generate a global process identifier that includes the hostname and process ID. # This creates a unique identifier that can distinguish processes across different machines. # # @return [String] The global process ID in the format "hostname-pid". # # @example # Lumberjack::Utils.global_pid # # => "server1-12345" def global_pid(pid = Process.pid) if hostname "#{hostname}-#{pid}" else pid.to_s end end # Generate a global thread identifier that includes the global process ID and thread name. # This creates a unique identifier for threads across processes and machines. # # @return [String] The global thread ID in the format "hostname-pid-threadname". # # @example # Lumberjack::Utils.global_thread_id # # => "server1-12345-main" or "server1-12345-worker-1" def global_thread_id "#{global_pid}-#{thread_name}" end # Get a safe name for a thread. Uses the thread's assigned name if available, # otherwise generates a unique identifier based on the thread's object ID. # Non-alphanumeric characters (except underscores, dashes, and periods) are replaced # with dashes to create URL-safe identifiers. # # @param thread [Thread] The thread to get the name for. Defaults to the current thread. # @return [String] A safe string identifier for the thread. # # @example # Thread.current.name = "worker-thread" # Lumberjack::Utils.thread_name # => "worker-thread" # # # For unnamed threads # Lumberjack::Utils.thread_name # => "2c001a80c" (based on object_id) def thread_name(thread = Thread.current) thread.name ? slugify(thread.name) : thread.object_id.to_s(36) end # Force encode a string to UTF-8, handling invalid byte sequences gracefully. # Any invalid or undefined byte sequences will be replaced with an empty string, # ensuring the result is always valid UTF-8. # # @param str [String, nil] The string to encode. Returns nil if input is nil. # @return [String, nil] The UTF-8 encoded string, or nil if input was nil. # # @example # # Handles strings with invalid encoding # bad_string = "Hello\xff\xfeWorld".force_encoding("ASCII-8BIT") # Lumberjack::Utils.force_utf8(bad_string) # => "HelloWorld" def force_utf8(str) return nil if str.nil? str.dup.force_encoding("ASCII-8BIT").encode("UTF-8", invalid: :replace, undef: :replace, replace: "") end # Flatten a nested attribute hash into a single-level hash using dot notation for nested keys. # This is useful for converting structured data into a flat format suitable for logging systems # that don't support nested structures. # # @param attr_hash [Hash] The hash to flatten. Non-hash values are ignored. # @return [Hash] A flattened hash with dot-notation keys. # # @example Basic flattening # hash = {user: {id: 123, profile: {name: "Alice"}}, action: "login"} # Lumberjack::Utils.flatten_attributes(hash) # # => {"user.id" => 123, "user.profile.name" => "Alice", "action" => "login"} # # @example With mixed types # hash = {config: {db: {host: "localhost", port: 5432}}, debug: true} # Lumberjack::Utils.flatten_attributes(hash) # # => {"config.db.host" => "localhost", "config.db.port" => 5432, "debug" => true} def flatten_attributes(attr_hash) return {} unless attr_hash.is_a?(Hash) flatten_hash_recursive(attr_hash) end # Alias for {.flatten_attributes} to provide compatibility with the 1.x API. # This method will eventually be removed in a future version. # # @param tag_hash [Hash] The hash to flatten. # @return [Hash] The flattened hash. # @deprecated Use {.flatten_attributes} instead. def flatten_tags(tag_hash) Utils.deprecated("Lumberjack::Utils.flatten_tags", "Lumberjack::Utils.flatten_tags is deprecated and will be removed in version 2.1; use flatten_attributes instead.") do flatten_attributes(tag_hash) end end # Expand a hash containing dot notation keys into a nested hash structure. # This is the inverse operation of {.flatten_attributes} and is useful for converting # flat attribute structures back into nested hashes. # # @param attributes [Hash] The hash with dot notation keys to expand. Non-hash values are ignored. # @return [Hash] A nested hash with dot notation keys expanded into nested structures. # # @example Basic expansion # flat = {"user.id" => 123, "user.name" => "Alice", "action" => "login"} # Lumberjack::Utils.expand_attributes(flat) # # => {"user" => {"id" => 123, "name" => "Alice"}, "action" => "login"} # # @example Deep nesting # flat = {"app.db.host" => "localhost", "app.db.port" => 5432, "app.debug" => true} # Lumberjack::Utils.expand_attributes(flat) # # => {"app" => {"db" => {"host" => "localhost", "port" => 5432}, "debug" => true}} # # @example Mixed with existing nested structures # mixed = {"user.id" => 123, "settings" => {"theme" => "dark"}} # Lumberjack::Utils.expand_attributes(mixed) # # => {"user" => {"id" => 123}, "settings" => {"theme" => "dark"}} def expand_attributes(attributes) return {} unless attributes.is_a?(Hash) expand_dot_notation_hash(attributes) end # Alias for {.expand_attributes} to provide compatibility with the 1.x API. # This method will eventually be removed in a future version. # # @param tags [Hash] The hash to expand. # @return [Hash] The expanded hash. # @deprecated Use {.expand_attributes} instead. def expand_tags(tags) Utils.deprecated("Lumberjack::Utils.expand_tags", "Lumberjack::Utils.expand_tags is deprecated and will be removed in version 2.1; use expand_attributes instead.") do expand_attributes(tags) end end private # Recursively flatten a hash, building dot notation keys for nested structures. # # @param hash [Hash] The hash to flatten. # @param prefix [String, nil] The current key prefix for nested structures. # @return [Hash] The flattened hash. # @api private def flatten_hash_recursive(hash, prefix = nil) hash.each_with_object({}) do |(key, value), result| full_key = prefix ? "#{prefix}.#{key}" : key.to_s if value.is_a?(Hash) result.merge!(flatten_hash_recursive(value, full_key)) else result[full_key] = value end end end # Convert a string to a URL-safe slug by replacing non-alphanumeric characters # (except underscores, dashes, and periods) with dashes, and removing leading/trailing dashes. # # @param str [String, nil] The string to slugify. # @return [String, nil] The slugified string, or nil if input was nil. # @api private def slugify(str) return nil if str.nil? str = str.gsub(NON_SLUGGABLE_PATTERN, "-") str.delete_prefix!("-") str.chomp!("-") str end # Recursively expand dot notation keys in a hash into nested structures. # # @param hash [Hash] The hash containing dot notation keys to expand. # @param expanded [Hash] The target hash to store expanded results. # @return [Hash] The expanded hash with nested structures. # @api private def expand_dot_notation_hash(hash, expanded = {}) return hash unless hash.is_a?(Hash) hash.each do |key, value| key = key.to_s if key.include?(".") main_key, sub_key = key.split(".", 2) main_key_hash = expanded[main_key] unless main_key_hash.is_a?(Hash) main_key_hash = {} expanded[main_key] = main_key_hash end expand_dot_notation_hash({sub_key => value}, main_key_hash) elsif value.is_a?(Hash) key_hash = expanded[key] unless key_hash.is_a?(Hash) key_hash = {} expanded[key] = key_hash end expand_dot_notation_hash(value, key_hash) else expanded[key] = value end end expanded end end end end bdurand-lumberjack-ac97435/lumberjack.gemspec000066400000000000000000000023201515437321200212730ustar00rootroot00000000000000Gem::Specification.new do |spec| spec.name = "lumberjack" spec.version = File.read(File.join(__dir__, "VERSION")).strip spec.authors = ["Brian Durand"] spec.email = ["bbdurand@gmail.com"] spec.summary = "Extension of Ruby’s standard Logger for advanced, structured logging. Includes log entry attributes, context isolation, customizable formatters, flexible output devices, and testing tools." spec.homepage = "https://github.com/bdurand/lumberjack" spec.license = "MIT" spec.metadata = { "homepage_uri" => spec.homepage, "source_code_uri" => spec.homepage, "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md" } # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. ignore_files = %w[ . Appraisals Gemfile Gemfile.lock Rakefile gemfiles/ spec/ ] spec.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } } end spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.7" spec.add_runtime_dependency "logger" spec.add_development_dependency "bundler" end bdurand-lumberjack-ac97435/spec/000077500000000000000000000000001515437321200165445ustar00rootroot00000000000000bdurand-lumberjack-ac97435/spec/lumberjack/000077500000000000000000000000001515437321200206635ustar00rootroot00000000000000bdurand-lumberjack-ac97435/spec/lumberjack/attribute_formatter_spec.rb000066400000000000000000000317111515437321200263130ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::AttributeFormatter do let(:attributes) { {"foo" => "bar", "baz" => "boo", "count" => 1} } describe "#build" do it "builds an attribute formatter in a block" do attribute_formatter = Lumberjack::AttributeFormatter.build do |config| config.add_attribute(:foo) { |val| val.to_s.upcase } end expect(attribute_formatter.format(attributes)).to eq({"foo" => "BAR", "baz" => "boo", "count" => 1}) end end describe "#add", deprecation_mode: :silent do it "adds an attribute formatter for a specific attribute" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add(:foo) { |val| val.to_s.upcase } expect(attribute_formatter.format(foo: "bar")).to eq({"foo" => "BAR"}) end it "adds an attribute for a class" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add(String) { |val| val.to_s.upcase } expect(attribute_formatter.format(foo: "bar")).to eq({"foo" => "BAR"}) end end describe "#remove", deprecation_mode: :silent do it "removes an attribute formatter for a specific attribute" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add(:foo) { |val| val.to_s.upcase } expect(attribute_formatter.format(foo: "bar")).to eq({"foo" => "BAR"}) attribute_formatter.remove(:foo) expect(attribute_formatter.format(foo: "bar")).to eq({foo: "bar"}) end it "removes an attribute formatter for a class" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add(String) { |val| val.to_s.upcase } expect(attribute_formatter.format(foo: "bar")).to eq({"foo" => "BAR"}) attribute_formatter.remove(String) expect(attribute_formatter.format(foo: "bar")).to eq({foo: "bar"}) end end describe "#format" do it "should do nothing by default" do attribute_formatter = Lumberjack::AttributeFormatter.new expect(attribute_formatter.format(attributes)).to eq attributes end it "should have a default formatter as a Formatter" do formatter = Lumberjack::Formatter.new.add(String, :inspect) attribute_formatter = Lumberjack::AttributeFormatter.new.default(formatter) expect(attribute_formatter.format(attributes)).to eq({"foo" => '"bar"', "baz" => '"boo"', "count" => 1}) end it "should be able to add tag name specific formatters" do formatter = Lumberjack::Formatter.new.clear.add(String, :inspect) attribute_formatter = Lumberjack::AttributeFormatter.new.add_attribute(:foo, formatter) expect(attribute_formatter.format(attributes)).to eq({"foo" => '"bar"', "baz" => "boo", "count" => 1}) attribute_formatter.remove_attribute(:foo).add_attribute(["baz", "count"]) { |val| "#{val}!" } expect(attribute_formatter.format(attributes)).to eq({"foo" => "bar", "baz" => "boo!", "count" => "1!"}) attribute_formatter.remove_attribute(:foo).add_attribute("foo", :inspect) expect(attribute_formatter.format(attributes)).to eq({"foo" => '"bar"', "baz" => "boo!", "count" => "1!"}) end it "should be able to add attribute formatters with add_attribute" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_attribute("foo") { |val| val * 2 } expect(attribute_formatter.format({"foo" => 12})).to eq({"foo" => 24}) end it "should be able to add and remove class formatters" do attribute_formatter = Lumberjack::AttributeFormatter.new.add_class(Integer) { |val| val * 2 } attribute_formatter.add_class(String, :redact) expect(attribute_formatter.format(attributes)).to eq({"foo" => "*****", "baz" => "*****", "count" => 2}) attribute_formatter.remove_class(String) expect(attribute_formatter.format(attributes)).to eq({"foo" => "bar", "baz" => "boo", "count" => 2}) end it "should be able to add class formatters by class name" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_class("String") { |val| val.reverse } expect(attribute_formatter.format({"foo" => "bar", "baz" => "boo", "count" => 1})).to eq({"foo" => "rab", "baz" => "oob", "count" => 1}) end it "should use a class formatter on child classes" do attribute_formatter = Lumberjack::AttributeFormatter.new.add_class(Numeric) { |val| val * 2 } expect(attribute_formatter.format({"foo" => 2.5, "bar" => 3})).to eq({"foo" => 5.0, "bar" => 6}) end it "should use class formatters for modules" do attribute_formatter = Lumberjack::AttributeFormatter.new.add_class(Enumerable) { |val| val.to_a.join(", ") } expect(attribute_formatter.format({"foo" => [1, 2, 3], "bar" => "baz"})).to eq({"foo" => "1, 2, 3", "bar" => "baz"}) end it "can mix and match tag and class formatters" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_attribute(:foo, &:reverse) attribute_formatter.add_class(Integer, &:even?) expect(attribute_formatter.format(attributes)).to eq({"foo" => "rab", "baz" => "boo", "count" => false}) end it "applies class formatters inside arrays and hashes" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_class(Integer, &:even?) attribute_formatter.add_class(String, &:reverse) expect(attribute_formatter.format({"foo" => [1, 2, 3], "bar" => {"baz" => "boo"}})).to eq({ "foo" => [false, true, false], "bar" => {"baz" => "oob"} }) end it "can pass arguments to class formatters" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_class(String, :truncate, 3) attribute_formatter.add_attribute(:foo, :truncate, 4) expect(attribute_formatter.format({"foo" => "foobar", "bar" => "bazqux"})).to eq({"foo" => "foo…", "bar" => "ba…"}) end it "applies name formatters inside hashes using dot syntax" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_attribute("foo.bar", &:reverse) expect(attribute_formatter.format({"foo" => {"bar" => "baz"}})).to eq({"foo" => {"bar" => "zab"}}) end it "recursively applies class formatters to nested hashes" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_attribute("foo") { |val| {"bar" => val.to_s} } attribute_formatter.add_class(String, &:reverse) expect(attribute_formatter.format({"foo" => 12})).to eq({"foo" => {"bar" => "21"}}) end it "recursively applies class formatters to nested arrays" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_attribute("foo") { |val| [val, val] } attribute_formatter.add_class(String, &:reverse) expect(attribute_formatter.format({"foo" => "bar"})).to eq({"foo" => ["rab", "rab"]}) end it "short circuits recursive formatting for already formatted classes" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_class(String) { |val| "#{val},#{val}".split(",") } expect(attribute_formatter.format({"foo" => "bar"})).to eq({"foo" => ["bar", "bar"]}) end it "should be able to clear all formatters" do attribute_formatter = Lumberjack::AttributeFormatter.new.default(&:to_s).add_attribute(:foo, &:reverse) expect(attribute_formatter.format(attributes)).to eq({"foo" => "rab", "baz" => "boo", "count" => "1"}) attribute_formatter.clear expect(attribute_formatter.format(attributes)).to eq attributes end it "should return the attributes themselves if not formatting is necessary" do attribute_formatter = Lumberjack::AttributeFormatter.new expect(attribute_formatter.format(attributes).object_id).to eq attributes.object_id end it "uses the attributes from a MessageAttributes" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_attribute("foo") { |val| Lumberjack::MessageAttributes.new(val.upcase, "attr" => val) } expect(attribute_formatter.format({"foo" => "bar"})).to eq({"foo" => {"attr" => "bar"}}) end it "remaps attributes if the value is a Lumberjack::RemapAttribute instance" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_attribute("duration_ms") { |value| Lumberjack::RemapAttribute.new(duration: value.to_f / 1000) } expect(attribute_formatter.format({"duration_ms" => 1500})).to eq({"duration" => 1.5}) end it "remaps structured attributes correctly" do attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_attribute(:email) { |value| Lumberjack::RemapAttribute.new(user: {email: value}) } attributes = {"user.id" => 42, "email" => "user@example.com"} expect(attribute_formatter.format(attributes)).to eq({"user.id" => 42, "user.email" => "user@example.com"}) end it "returns an error string if there was an error formatting the value" do save_stderr = $stderr begin $stderr = StringIO.new attribute_formatter = Lumberjack::AttributeFormatter.new attribute_formatter.add_class(String, lambda { |obj| raise "error" }) expect(attribute_formatter.format(attributes)["foo"]).to eq("") ensure $stderr = save_stderr end end end describe "#include_class?" do it "returns true if a formatter exists for a specific class" do formatter = Lumberjack::AttributeFormatter.build do |config| config.add_class(Array, :inspect) end expect(formatter.include_class?(Array)).to be true expect(formatter.include_class?("Array")).to be true expect(formatter.include_class?(String)).to be false end end describe "#include_attribute?" do it "returns true if a formatter exists for a specific attribute" do formatter = Lumberjack::AttributeFormatter.build do |config| config.add_attribute(:foo, :inspect) end expect(formatter.include_attribute?(:foo)).to be true expect(formatter.include_attribute?("foo")).to be true expect(formatter.include_attribute?(:bar)).to be false end end describe "#formatter_for_class" do let(:formatter) do Lumberjack::AttributeFormatter.build do |config| config.add_class(Array, :inspect) end end it "returns the formatter for a specific class" do expect(formatter.formatter_for_class(Array)).to be_a(Lumberjack::Formatter::InspectFormatter) expect(formatter.formatter_for_class("Array")).to be_a(Lumberjack::Formatter::InspectFormatter) expect(formatter.formatter_for_class(String)).to be_nil end end describe "#formatter_for_attribute" do let(:formatter) do Lumberjack::AttributeFormatter.build do |config| config.add_attribute(:foo, :inspect) end end it "returns the formatter for a specific attribute" do expect(formatter.formatter_for_attribute(:foo)).to be_a(Lumberjack::Formatter::InspectFormatter) expect(formatter.formatter_for_attribute("foo")).to be_a(Lumberjack::Formatter::InspectFormatter) expect(formatter.formatter_for_attribute(:bar)).to be_nil end end describe "#include" do it "merges the formats from the formatter" do formatter_1 = Lumberjack::AttributeFormatter.new formatter_1.add_class(String) { |val| val.to_s.upcase } formatter_1.add_class(Float, :round, 1) formatter_1.add_attribute(:tags) { |val| val.join(", ") } formatter_2 = Lumberjack::AttributeFormatter.new formatter_2.add_class(String) { |val| val.to_s.downcase } formatter_2.add_attribute(:foo) { |val| val.to_s.downcase } expect(formatter_2.include(formatter_1)).to eq formatter_2 expect(formatter_2.format("test" => "Test")).to eq("test" => "TEST") expect(formatter_2.format("pi" => 3.14)).to eq("pi" => 3.1) expect(formatter_2.format("tags" => ["foo", "bar"])).to eq("tags" => "foo, bar") expect(formatter_2.format("foo" => "FOO")).to eq("foo" => "foo") end end describe "#prepend" do it "prepends the formats from the formatter" do formatter_1 = Lumberjack::AttributeFormatter.new formatter_1.add_class(String) { |val| val.to_s.upcase } formatter_1.add_class(Float, :round, 1) formatter_1.add_attribute(:tags) { |val| val.join(", ") } formatter_2 = Lumberjack::AttributeFormatter.new formatter_2.add_class(String) { |val| val.to_s.downcase } formatter_2.add_attribute(:foo) { |val| val.to_s.downcase } expect(formatter_2.prepend(formatter_1)).to eq formatter_2 expect(formatter_2.format("test" => "Test")).to eq("test" => "test") expect(formatter_2.format("pi" => 3.14)).to eq("pi" => 3.1) expect(formatter_2.format("tags" => ["foo", "bar"])).to eq("tags" => "foo, bar") expect(formatter_2.format("foo" => "FOO")).to eq("foo" => "foo") end end end bdurand-lumberjack-ac97435/spec/lumberjack/attributes_helper_spec.rb000066400000000000000000000066341515437321200257600ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::AttributesHelper do let(:attributes) { {} } let(:tag_context) { Lumberjack::AttributesHelper.new(attributes) } describe "expand_runtime_values" do it "should return the identical hash as is if there are no Procs" do hash = {"foo" => 1, "bar" => 2} expect(Lumberjack::AttributesHelper.expand_runtime_values(hash)).to equal(hash) end it "should replace all keys with strings" do hash = {foo: 1, bar: 2} expect(Lumberjack::AttributesHelper.expand_runtime_values(hash)).to eq({"foo" => 1, "bar" => 2}) end it "should replace Procs that take no arguments with the runtime value" do p1 = lambda { "stuff" } p2 = lambda { |x| x.upcase } hash = {foo: 1, bar: p1, baz: p2} expect(Lumberjack::AttributesHelper.expand_runtime_values(hash)).to eq({"foo" => 1, "bar" => "stuff", "baz" => p2}) end end describe "#to_h" do it "returns a copy of the attributes" do attributes["a"] = 1 hash = tag_context.to_h expect(hash).to eq({"a" => 1}) expect(hash.object_id).not_to eq(tag_context.to_h.object_id) attributes["b"] = 2 expect(hash).to eq({"a" => 1}) end end describe "#tag" do it "should have attributes" do expect(tag_context.to_h).to eq({}) tag_context.update(foo: "bar", baz: "boo") expect(tag_context.to_h).to eq({"foo" => "bar", "baz" => "boo"}) tag_context[:stuff] = "nonsense" expect(tag_context.to_h).to eq({"foo" => "bar", "baz" => "boo", "stuff" => "nonsense"}) expect(tag_context[:stuff]).to eq("nonsense") end it "should flatten attributes" do tag_context.update(foo: {bar: "baz", far: "qux"}) expect(tag_context.to_h).to eq({"foo.bar" => "baz", "foo.far" => "qux"}) tag_context.update("foo.bip" => "bop", "foo.far" => "foe") expect(tag_context.to_h).to eq({"foo.bar" => "baz", "foo.bip" => "bop", "foo.far" => "foe"}) end end describe "#[]" do it "sets and gets a tag value" do tag_context[:foo] = "bar" expect(tag_context[:foo]).to eq("bar") expect(tag_context.to_h).to eq({"foo" => "bar"}) end it "flattens nested attributes" do tag_context[:foo] = {bar: "baz", far: "qux"} expect(tag_context.to_h).to eq({"foo.bar" => "baz", "foo.far" => "qux"}) end it "returns a hash with subattributes" do tag_context.update(foo: {bar: "baz", far: "qux"}) expect(tag_context[:foo]).to eq({"bar" => "baz", "far" => "qux"}) end it "returns has deeply nested attributes" do tag_context.update(a: {b: {c: {d: 4, e: 5}, f: 6}, g: 7}) expect(tag_context[:a]).to eq({"b.c.d" => 4, "b.c.e" => 5, "b.f" => 6, "g" => 7}) expect(tag_context["a.b"]).to eq({"c.d" => 4, "c.e" => 5, "f" => 6}) end end describe "#delete" do it "removes specified attributes" do tag_context[:foo] = "bar" tag_context[:baz] = "boo" tag_context[:qux] = "quux" expect(tag_context.to_h).to eq({"foo" => "bar", "baz" => "boo", "qux" => "quux"}) tag_context.delete(:foo, :baz) expect(tag_context.to_h).to eq({"qux" => "quux"}) end it "removes subattributes" do tag_context.update(foo: {bar: "baz", far: "qux"}) expect(tag_context.to_h).to eq({"foo.bar" => "baz", "foo.far" => "qux"}) tag_context.delete(:foo) expect(tag_context.to_h).to eq({}) end end end bdurand-lumberjack-ac97435/spec/lumberjack/context_logger_spec.rb000066400000000000000000000674561515437321200252670ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::ContextLogger do let(:logger) { TestContextLogger.new } let(:logger_with_default_context) { TestContextLogger.new(default_context) } let(:default_context) { Lumberjack::Context.new } describe "#level" do it "can be nil" do expect(logger.level).to be_nil logger.level = nil expect(logger.level).to be_nil end it "sets the default level if the logger has a default context" do logger_with_default_context.level = Logger::INFO expect(logger_with_default_context.level).to eq(Logger::INFO) end it "has no effect outside a context block if the logger does not have a default context" do logger.level = Logger::INFO expect(logger.level).to be_nil end it "sets a temporary level within a block" do logger.context do logger.level = Logger::DEBUG expect(logger.level).to eq(Logger::DEBUG) end expect(logger.level).to be_nil end it "can set the value with a symbol" do logger_with_default_context.level = :info expect(logger_with_default_context.level).to eq(Logger::INFO) end it "can set the value with a string" do logger_with_default_context.level = "INFO" expect(logger_with_default_context.level).to eq(Logger::INFO) end end describe "#with_level" do it "sets a temporary level within the block" do logger.with_level(Logger::ERROR) do expect(logger.level).to eq(Logger::ERROR) end expect(logger.level).to be_nil end it "is only affects the level for the current fiber" do fiber_1 = Fiber.new do logger.with_level(Logger::ERROR) do expect(logger.level).to eq(Logger::ERROR) Fiber.yield end expect(logger.level).to be_nil end fiber_2 = Fiber.new do logger.with_level(Logger::WARN) do expect(logger.level).to eq(Logger::WARN) fiber_1.resume end expect(logger.level).to be_nil end fiber_2.resume end end describe "#progname" do it "returns the progname from the default context" do logger_with_default_context.progname = "TestProgname" expect(logger_with_default_context.progname).to eq("TestProgname") end it "returns nil if the logger does not have a default context" do expect(logger.progname).to be_nil end it "returns the progname from the current context" do logger.context do logger.progname = "TestProgname" expect(logger.progname).to eq("TestProgname") end expect(logger.progname).to be_nil end end describe "#with_progname" do it "sets a temporary progname within the block" do logger.with_progname("TestProgname") do expect(logger.progname).to eq("TestProgname") end expect(logger.progname).to be_nil end it "is only affects the progname for the current fiber" do fiber_1 = Fiber.new do logger.with_progname("TestProgname") do expect(logger.progname).to eq("TestProgname") Fiber.yield end expect(logger.progname).to be_nil end fiber_2 = Fiber.new do logger.with_progname("AnotherProgname") do expect(logger.progname).to eq("AnotherProgname") fiber_1.resume end expect(logger.progname).to be_nil end fiber_2.resume end end describe "#add" do it "returns true" do result = logger.add(Logger::INFO, "Test message") expect(result).to be true end it "adds a message to the log" do logger.add(Logger::INFO, "Test message") expect(logger.entries.last).to eq({ severity: Logger::INFO, message: "Test message", progname: nil, attributes: nil }) end it "adds a message with the progname" do logger.add(:info, "Test", "MyApp") expect(logger.entries.last).to eq( severity: Logger::INFO, message: "Test", progname: "MyApp", attributes: nil ) end it "adds a message with attributes" do logger.add(:info, "Test", foo: "bar") expect(logger.entries.last).to eq( severity: Logger::INFO, message: "Test", progname: nil, attributes: {foo: "bar"} ) end it "adds a message with the message in a block" do logger.add(:info) { "Test Message" } expect(logger.entries.last).to eq({ severity: Logger::INFO, message: "Test Message", progname: nil, attributes: nil }) end it "adds a message with attributes with the message in a block" do logger.add(:info, foo: "bar") { "Test Message" } expect(logger.entries.last).to eq({ severity: Logger::INFO, message: "Test Message", progname: nil, attributes: {foo: "bar"} }) end it "adds a message with a progname with the message in a block" do logger.add(:info, "MyApp") { "Test Message" } expect(logger.entries.last).to eq({ severity: Logger::INFO, message: "Test Message", progname: "MyApp", attributes: nil }) end it "does not add a message if the severity is less than the level" do logger.with_level(:warn) do logger.add(Logger::INFO, "Test message") end expect(logger.entries).to be_empty end it "adds the entry as UNKNOWN if the severity is not specified" do logger.add(nil, "Test message") expect(logger.entries.last).to eq({ severity: Logger::UNKNOWN, message: "Test message", progname: nil, attributes: nil }) end it "adds the entry as UNKNOWN if severity is not specified and message is in a block" do logger.add(nil) { "Test message" } expect(logger.entries.last).to eq({ severity: Logger::UNKNOWN, message: "Test message", progname: nil, attributes: nil }) end it "strips whitespace from the message" do logger.add(Logger::INFO, " Test message ") expect(logger.entries.last[:message]).to eq("Test message") logger.add("\nTest message") expect(logger.entries.last[:message]).to eq("Test message") logger.add("Test message\n") expect(logger.entries.last[:message]).to eq("Test message") end it "is aliased as #log" do logger.add(Logger::INFO, "Test message") logger.log(Logger::INFO, "Test message") expect(logger.entries[0]).to eq(logger.entries[1]) end end describe "#<<" do it "appends a message to the log" do logger << "Test message" expect(logger.entries.last).to eq({ severity: Logger::UNKNOWN, message: "Test message", progname: nil, attributes: nil }) end it "will use the default severity to log the message" do logger = TestContextLogger.new(Lumberjack::Context.new) logger.default_severity = :info logger << "Test message" expect(logger.entries.last).to eq({ severity: Logger::INFO, message: "Test message", progname: nil, attributes: nil }) end end describe "#context" do it "creates isolated contexts in nested block" do logger.context do logger.level = :debug logger.progname = "Temp" logger.tag(foo: "bar") expect(logger.level).to eq(Logger::DEBUG) expect(logger.progname).to eq("Temp") expect(logger.attributes).to eq({"foo" => "bar"}) logger.context do expect(logger.level).to eq(Logger::DEBUG) expect(logger.progname).to eq("Temp") expect(logger.attributes).to eq({"foo" => "bar"}) logger.level = :info logger.progname = "Inner" logger.tag(baz: "qux") expect(logger.level).to eq(Logger::INFO) expect(logger.progname).to eq("Inner") expect(logger.attributes).to eq({"foo" => "bar", "baz" => "qux"}) end expect(logger.level).to eq(Logger::DEBUG) expect(logger.progname).to eq("Temp") expect(logger.attributes).to eq({"foo" => "bar"}) end expect(logger.level).to be_nil expect(logger.progname).to be_nil expect(logger.attributes).to be_empty end it "returns the result of the block" do result = logger.context { :foobar } expect(result).to eq(:foobar) end it "yields the context" do logger.context do |ctx| expect(ctx).to be_a(Lumberjack::Context) end end end describe "#in_context?" do it "returns true inside a context block" do expect(logger.in_context?).to be false logger.context do expect(logger.in_context?).to be true end expect(logger.in_context?).to be false end end describe "#ensure_context" do it "should create a context if one does not exist" do expect(logger.in_context?).to eq false value = logger.ensure_context do expect(logger.in_context?).to eq true :foo end expect(logger.in_context?).to eq false expect(value).to eq :foo end it "does not create a new context if one already exists" do logger.context do value = logger.ensure_context do logger.tag(baz: "bap") :foo end expect(logger.attributes).to eq({"baz" => "bap"}) expect(value).to eq :foo end end end describe "#tag" do it "adds attributes inside of a block" do logger.tag(foo: "bar") do expect(logger.attributes).to eq({"foo" => "bar"}) end expect(logger.attributes).to be_empty end it "merges attributes in nested blocks" do logger.tag(foo: "bar") do logger.tag(baz: "qux") do expect(logger.attributes).to eq({"foo" => "bar", "baz" => "qux"}) end expect(logger.attributes).to eq({"foo" => "bar"}) end expect(logger.attributes).to be_empty end it "flattens nested attributes" do logger.tag(foo: {bar: "baz", far: "qux"}) do expect(logger.attributes).to eq({"foo.bar" => "baz", "foo.far" => "qux"}) end end it "adds new attributes to the current context without a block" do logger.context do logger.tag(foo: "bar") logger.tag(baz: "qux") expect(logger.attributes).to eq({"foo" => "bar", "baz" => "qux"}) end expect(logger.attributes).to be_empty end it "works with a frozen hash" do logger.tag({foo: "bar"}.freeze) do logger.tag(baz: "qux") expect(logger.attributes).to eq({"foo" => "bar", "baz" => "qux"}) end end it "returns the result of the block" do result = logger.tag(foo: "bar") { :foobar } expect(result).to eq(:foobar) end it "returns self when called inside a context without a block" do logger.context do expect(logger.tag(foo: "bar")).to eq(logger) end end it "returns self without applying attributes if there is no current context" do expect(logger.tag(foo: "bar")).to equal(logger) expect(logger.attributes).to be_empty end end describe "#tag_all_contexts" do it "adds attributes to the parent contexts in the hierarchy" do logger.tag(foo: "bar") do logger.tag(baz: "qux") do logger.tag_all_contexts(bip: "bap") expect(logger.attributes).to eq({"foo" => "bar", "baz" => "qux", "bip" => "bap"}) end expect(logger.attributes).to eq({"foo" => "bar", "bip" => "bap"}) end expect(logger.attributes).to be_empty end it "does not attributes to the default context if there is no current context" do logger.tag_all_contexts(bip: "bap") expect(logger.attributes).to be_empty end end describe "#tag!" do it "adds attributes to the default context" do logger_with_default_context.tag!(foo: "bar") expect(logger_with_default_context.attributes).to eq({"foo" => "bar"}) logger_with_default_context.tag!(baz: "qux") expect(logger_with_default_context.attributes).to eq({"foo" => "bar", "baz" => "qux"}) end it "does nothing if there is no default context" do logger.tag!(foo: "bar") expect(logger.attributes).to be_empty end end describe "#untag" do it "removes attributes from the current context block" do logger.tag(foo: "bar", baz: "qux", bip: "bap") do logger.untag(:foo, "baz") expect(logger.attributes).to eq({"bip" => "bap"}) end end it "does nothing outside of a context block" do logger_with_default_context.tag!(foo: "bar") logger_with_default_context.untag(:foo) expect(logger_with_default_context.attributes).to eq({"foo" => "bar"}) end end describe "#untag!" do it "removes attributes from the default context" do logger_with_default_context.tag!(foo: "bar", baz: "qux", bip: "bap") logger_with_default_context.untag!(:foo, "baz") expect(logger_with_default_context.attributes).to eq({"bip" => "bap"}) end it "does nothing if the logger does not have a default context" do logger.untag!(:foo) expect(logger.attributes).to be_empty end end describe "#attributes" do it "returns an empty hash by default" do expect(logger.attributes).to eq({}) expect(logger_with_default_context.attributes).to eq({}) end it "returns attributes from the default context" do logger_with_default_context.tag!(foo: "bar") expect(logger_with_default_context.attributes).to eq({"foo" => "bar"}) end it "returns attributes from the current context" do logger.tag(foo: "bar") do expect(logger.attributes).to eq({"foo" => "bar"}) end end it "merges attributes from the current context with the default context attributes" do logger_with_default_context.tag!(foo: "bar") logger_with_default_context.tag(baz: "qux") do expect(logger_with_default_context.attributes).to eq({"foo" => "bar", "baz" => "qux"}) end expect(logger_with_default_context.attributes).to eq({"foo" => "bar"}) end it "includes attributes from the global context" do Lumberjack.tag(foo: "bar") do logger.tag(baz: "qux") do expect(logger.attributes).to eq({"foo" => "bar", "baz" => "qux"}) end expect(logger.attributes).to eq({"foo" => "bar"}) end end it "prefers attributes from the most local context" do Lumberjack.tag(foo: "one") do expect(logger.attributes["foo"]).to eq("one") logger_with_default_context.tag!(foo: "two") expect(logger_with_default_context.attributes["foo"]).to eq("two") logger_with_default_context.tag(foo: "three") do expect(logger_with_default_context.attribute_value("foo")).to eq("three") end end end end describe "#attribute_value" do it "returns the value of a tag" do Lumberjack.tag(foo: "bar") do logger_with_default_context.tag!(baz: "qux") logger_with_default_context.tag(bip: "bap") do expect(logger_with_default_context.attribute_value("foo")).to eq("bar") expect(logger_with_default_context.attribute_value("baz")).to eq("qux") expect(logger_with_default_context.attribute_value("bip")).to eq("bap") end end end it "expands dot notation in tag names" do logger.tag(foo: {"bar.baz": "boo"}) do expect(logger.attribute_value("foo.bar.baz")).to eq("boo") expect(logger.attribute_value("foo.bar")).to eq("baz" => "boo") end end it "should expand tag name as a array to dot notation" do logger.tag("foo.bar" => "baz") do expect(logger.attribute_value([:foo, :bar])).to eq("baz") end end it "should return nil for a non-existent tag" do expect(logger.attribute_value(:non_existent)).to be_nil end end describe "#append_to" do it "does nothing if there is no context" do expect(logger.append_to(:tags, :foo)).to eq(logger) expect(logger.attributes).to be_empty end it "appends tags to the tags attribute in the current context" do logger.context do logger.append_to(:tags, :foo, :bar) expect(logger.attributes["tags"]).to eq([:foo, :bar]) logger.append_to(:tags, [:baz]) expect(logger.attributes["tags"]).to eq([:foo, :bar, :baz]) logger.context do logger.append_to(:tags, :qux) expect(logger.attributes["tags"]).to eq([:foo, :bar, :baz, :qux]) end expect(logger.attributes["tags"]).to eq([:foo, :bar, :baz]) end end it "appends to the tags attribute inside a block" do logger.append_to(:tags, [:foo, :bar]) do expect(logger.attributes["tags"]).to eq([:foo, :bar]) logger.append_to(:tags, [:baz]) do expect(logger.attributes["tags"]).to eq([:foo, :bar, :baz]) end expect(logger.attributes["tags"]).to eq([:foo, :bar]) end end it "returns the logger instance when called without a block" do logger.context do expect(logger.append_to(:tags, [:foo])).to eq(logger) end end it "returns the result of the block" do result = logger.append_to(:tags, [:foo]) { :foobar } expect(result).to eq(:foobar) end end describe "#clear_attributes" do it "removes all attributes from the current, default, and global contexts for the duration of the block" do Lumberjack.tag(foo: "bar") do logger_with_default_context.tag!(baz: "qux") logger_with_default_context.tag(bip: "bap") do expect(logger_with_default_context.attributes.length).to eq(3) logger_with_default_context.clear_attributes do expect(logger_with_default_context.attributes).to be_empty logger_with_default_context.tag(moo: "mip") do expect(logger_with_default_context.attributes).to eq({"moo" => "mip"}) end end end end end it "returns the result of the block" do result = logger.clear_attributes { :foobar } expect(result).to eq(:foobar) end end describe "#fork" do it "returns a local logger that has an isolated context from the current logger" do logger = Lumberjack::Logger.new(:test) logger.tag!(one: 1, two: 2) forked_logger = logger.fork expect(forked_logger.level).to eq logger.level forked_logger.level = :warn expect(forked_logger.level).to_not eq logger.level expect(forked_logger.progname).to eq logger.progname forked_logger.progname = "ForkedLogger" expect(forked_logger.progname).to_not eq logger.progname expect(forked_logger.attributes).to eq({}) forked_logger.tag!(foo: "bar", two: 22) expect(forked_logger.attributes).to eq({"two" => 22, "foo" => "bar"}) logger.tag!(three: 3) expect(forked_logger.attributes).to eq({"two" => 22, "foo" => "bar"}) end it "can set the level on the local logger" do forked_logger = logger.fork(level: :warn) expect(forked_logger.level).to eq(Logger::WARN) end it "can set the progname on the local logger" do forked_logger = logger.fork(progname: "ForkedLogger") expect(forked_logger.progname).to eq("ForkedLogger") end it "can set attributes on the local logger" do forked_logger = logger.fork(attributes: {foo: "bar"}) expect(forked_logger.attributes).to eq({"foo" => "bar"}) end it "maintains the same isolation level as the parent logger" do logger.isolation_level = :thread forked_logger = logger.fork expect(forked_logger.isolation_level).to eq(:thread) logger.isolation_level = :fiber forked_logger = logger.fork expect(forked_logger.isolation_level).to eq(:fiber) end end describe "#isolation_level" do it "isolates the context to the current fiber by default" do expect(logger.isolation_level).to eq(:fiber) inner_attributes = nil logger.tag(foo: "bar") do fiber = Fiber.new do inner_attributes = logger.attributes end fiber.resume end expect(inner_attributes).to eq({}) end it "can isolate the context to the current thread" do logger.isolation_level = :thread expect(logger.isolation_level).to eq(:thread) inner_attributes = nil other_thread_attributes = nil logger.tag(foo: "bar") do thread = Thread.new do other_thread_attributes = logger.attributes end fiber = Fiber.new do inner_attributes = logger.attributes end thread.join fiber.resume end expect(other_thread_attributes).to eq({}) expect(inner_attributes).to eq("foo" => "bar") end end [:fatal, :error, :warn, :info, :debug, :unknown, :trace].each do |severity| describe "##{severity}" do it "returns true" do result = logger.public_send(severity, "Message") expect(result).to be true end it "logs an entry as #{severity}" do logger.public_send(severity, "Message") expect(logger.entries.first).to eq({ severity: Lumberjack::Severity.coerce(severity), message: "Message", progname: nil, attributes: nil }) end it "logs an entry as #{severity} with the message in a block" do logger.public_send(severity) { "Message" } expect(logger.entries.first).to eq({ severity: Lumberjack::Severity.coerce(severity), message: "Message", progname: nil, attributes: nil }) end it "logs an entry as #{severity} with a progname" do logger.public_send(severity, "Message", "myApp") expect(logger.entries.first).to eq({ severity: Lumberjack::Severity.coerce(severity), message: "Message", progname: "myApp", attributes: nil }) end it "logs an entry as #{severity} with attributes" do logger.public_send(severity, "Message", foo: "bar") expect(logger.entries.first).to eq({ severity: Lumberjack::Severity.coerce(severity), message: "Message", progname: nil, attributes: {foo: "bar"} }) end it "logs an entry as #{severity} with attributes and the message in a block" do logger.public_send(severity, foo: "bar") { "Message" } expect(logger.entries.first).to eq({ severity: Lumberjack::Severity.coerce(severity), message: "Message", progname: nil, attributes: {foo: "bar"} }) end it "logs an entry as #{severity} with a progname and the message in a block" do logger.public_send(severity, "myApp") { "Message" } expect(logger.entries.first).to eq({ severity: Lumberjack::Severity.coerce(severity), message: "Message", progname: "myApp", attributes: nil }) end it "does not log an entry if the level is greater than #{severity}" do logger.with_level(Lumberjack::Severity.coerce(severity) + 1) do logger.public_send(severity) { raise NotImplementedError } expect(logger.entries).to be_empty end end it "does not log nil entries even if there are context attributes" do logger.tag(foo: "bar") do logger.public_send(severity, nil) logger.public_send(severity, "", {}) end expect(logger.entries).to be_empty end it "does log nil if there are explicit attributes" do logger.public_send(severity, nil, {foo: "bar"}) expect(logger.entries).to include({ severity: Lumberjack::Severity.coerce(severity), message: nil, progname: nil, attributes: {foo: "bar"} }) end end end describe "#fatal?" do it "is true if the level is fatal or less" do logger_with_default_context.level = :fatal expect(logger_with_default_context.fatal?).to be true logger_with_default_context.level = Logger::UNKNOWN expect(logger_with_default_context.fatal?).to be false end end describe "#error?" do it "is true if the level is error or less" do logger_with_default_context.level = :error expect(logger_with_default_context.error?).to be true logger_with_default_context.level = :fatal expect(logger_with_default_context.error?).to be false end end describe "#warn?" do it "is true if the level is warn or less" do logger_with_default_context.level = :warn expect(logger_with_default_context.warn?).to be true logger_with_default_context.level = :error expect(logger_with_default_context.warn?).to be false end end describe "#info?" do it "is true if the level is info or less" do logger_with_default_context.level = :info expect(logger_with_default_context.info?).to be true logger_with_default_context.level = :warn expect(logger_with_default_context.info?).to be false end end describe "#debug?" do it "is true if the level is debug or less" do logger_with_default_context.level = :debug expect(logger_with_default_context.debug?).to be true logger_with_default_context.level = :info expect(logger_with_default_context.debug?).to be false end end describe "#fatal!" do it "sets the level to fatal" do logger_with_default_context.fatal! expect(logger_with_default_context.level).to eq(Logger::FATAL) end it "temporarily sets the level to fatal in a context block" do logger do logger.fatal! expect(logger.level).to eq(Logger::FATAL) end expect(logger.level).to be_nil end it "has no effect outside the context block without a default context" do logger.fatal! expect(logger.level).to be_nil end end describe "#error!" do it "sets the level to error" do logger_with_default_context.error! expect(logger_with_default_context.level).to eq(Logger::ERROR) end it "temporarily sets the level to error in a context block" do logger do logger.error! expect(logger.level).to eq(Logger::ERROR) end expect(logger.level).to be_nil end it "has no effect outside the context block without a default context" do logger.error! expect(logger.level).to be_nil end end describe "#warn!" do it "sets the level to warn" do logger_with_default_context.warn! expect(logger_with_default_context.level).to eq(Logger::WARN) end it "temporarily sets the level to warn in a context block" do logger do logger.warn! expect(logger.level).to eq(Logger::WARN) end expect(logger.level).to be_nil end it "has no effect outside the context block without a default context" do logger.warn! expect(logger.level).to be_nil end end describe "#info!" do it "sets the level to info" do logger_with_default_context.info! expect(logger_with_default_context.level).to eq(Logger::INFO) end it "temporarily sets the level to info in a context block" do logger do logger.info! expect(logger.level).to eq(Logger::INFO) end expect(logger.level).to be_nil end it "has no effect outside the context block without a default context" do logger.info! expect(logger.level).to be_nil end end describe "#debug!" do it "sets the level to debug" do logger_with_default_context.debug! expect(logger_with_default_context.level).to eq(Logger::DEBUG) end it "temporarily sets the level to debug in a context block" do logger do logger.debug! expect(logger.level).to eq(Logger::DEBUG) end expect(logger.level).to be_nil end it "has no effect outside the context block without a default context" do logger.debug! expect(logger.level).to be_nil end end end bdurand-lumberjack-ac97435/spec/lumberjack/context_spec.rb000066400000000000000000000077171515437321200237220ustar00rootroot00000000000000require "spec_helper" RSpec.describe Lumberjack::Context do describe "#level" do it "should have a level" do context = Lumberjack::Context.new expect(context.level).to be_nil context.level = :info expect(context.level).to eq(Logger::INFO) context.level = nil expect(context.level).to be_nil end it "should inherit the parent context's level" do parent = Lumberjack::Context.new parent.level = Logger::WARN context = Lumberjack::Context.new(parent) expect(context.level).to eq(Logger::WARN) end end describe "#progname" do it "should have a progname" do context = Lumberjack::Context.new expect(context.progname).to be_nil context.progname = :test expect(context.progname).to eq("test") context.progname = nil expect(context.progname).to be_nil end it "should inherit the parent context's progname" do parent = Lumberjack::Context.new parent.progname = "parent" context = Lumberjack::Context.new(parent) expect(context.progname).to eq("parent") end end describe "#default_severity" do it "should have a default severity" do context = Lumberjack::Context.new expect(context.default_severity).to be_nil context.default_severity = :info expect(context.default_severity).to eq(Logger::INFO) context.default_severity = nil expect(context.default_severity).to be_nil end end describe "#assign_attributes" do it "should have attributes" do context = Lumberjack::Context.new expect(context.attributes).to be_nil context.assign_attributes(foo: "bar", baz: "boo") expect(context.attributes).to eq({"foo" => "bar", "baz" => "boo"}) context[:stuff] = "nonsense" expect(context.attributes).to eq({"foo" => "bar", "baz" => "boo", "stuff" => "nonsense"}) expect(context[:stuff]).to eq("nonsense") end it "should inherit attributes from a parent context" do parent = Lumberjack::Context.new parent.assign_attributes(foo: "bar", baz: "boo") context = Lumberjack::Context.new(parent) context.assign_attributes(foo: "other", stuff: "nonsense") expect(context.attributes).to eq({"foo" => "other", "baz" => "boo", "stuff" => "nonsense"}) expect(parent.attributes).to eq({"foo" => "bar", "baz" => "boo"}) end it "should flatten attributes" do context = Lumberjack::Context.new context.assign_attributes(foo: {bar: "baz", far: "qux"}) expect(context.attributes).to eq({"foo.bar" => "baz", "foo.far" => "qux"}) context.assign_attributes("foo.bip" => "bop", "foo.far" => "foe") expect(context.attributes).to eq({"foo.bar" => "baz", "foo.bip" => "bop", "foo.far" => "foe"}) end end describe "#[]" do it "sets and gets an attribute value" do context = Lumberjack::Context.new context[:foo] = "bar" expect(context[:foo]).to eq("bar") expect(context.attributes).to eq({"foo" => "bar"}) end it "flattens nested attributes" do context = Lumberjack::Context.new context[:foo] = {bar: "baz", far: "qux"} expect(context.attributes).to eq({"foo.bar" => "baz", "foo.far" => "qux"}) end end describe "#delete" do it "removes specified attributes" do context = Lumberjack::Context.new context[:foo] = "bar" context[:baz] = "boo" context[:qux] = "quux" expect(context.attributes).to eq({"foo" => "bar", "baz" => "boo", "qux" => "quux"}) context.delete(:foo, :baz) expect(context.attributes).to eq({"qux" => "quux"}) end end describe "#reset" do it "clears all attributes and context data" do context = Lumberjack::Context.new context.assign_attributes(foo: "bar", baz: "boo") context.level = :info context.progname = "test" context.reset expect(context.attributes).to eq({}) expect(context.level).to be_nil expect(context.progname).to be_nil end end end bdurand-lumberjack-ac97435/spec/lumberjack/device/000077500000000000000000000000001515437321200221225ustar00rootroot00000000000000bdurand-lumberjack-ac97435/spec/lumberjack/device/buffer_spec.rb000066400000000000000000000100721515437321200247320ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::Buffer do let(:device) { Lumberjack::Device::Test.new } let(:entry_1) { Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Message 1", nil, Process.pid, nil) } let(:entry_2) { Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Message 2", nil, Process.pid, nil) } describe "#write" do it "writes entries immediately when buffer_size is 0" do buffer = Lumberjack::Device::Buffer.new(device, buffer_size: 0) begin buffer.write(entry_1) expect(device.entries.size).to eq(1) expect(device.entries.first).to eq(entry_1) ensure buffer.close unless buffer.closed? end end it "buffers entries and flushes when buffer_size is reached" do buffer = Lumberjack::Device::Buffer.new(device, buffer_size: 2) begin buffer.write(entry_1) expect(device.entries.size).to eq(0) # Not flushed yet buffer.write(entry_2) expect(device.entries).to eq([entry_1, entry_2]) buffer.write(entry_2) buffer.write(entry_1) expect(device.entries).to eq([entry_1, entry_2, entry_2, entry_1]) ensure buffer.close unless buffer.closed? end end end describe "#flush" do it "flushes entries when called" do buffer = Lumberjack::Device::Buffer.new(device, buffer_size: 5) begin buffer.write(entry_1) buffer.write(entry_2) buffer.flush expect(device.entries).to eq([entry_1, entry_2]) ensure buffer.close unless buffer.closed? end end it "calls before_flush callback if provided" do before_flush_called = false before_flush = proc { before_flush_called = true } buffer = Lumberjack::Device::Buffer.new(device, buffer_size: 5, before_flush: before_flush) begin buffer.write(entry_1) buffer.flush expect(before_flush_called).to be true expect(device.entries).to eq([entry_1]) ensure buffer.close unless buffer.closed? end end it "sets last_flushed_at timestamp" do buffer = Lumberjack::Device::Buffer.new(device, buffer_size: 5) begin initial_time = buffer.last_flushed_at buffer.write(entry_1) buffer.flush flushed_time = buffer.last_flushed_at expect(flushed_time).to be > initial_time ensure buffer.close unless buffer.closed? end end end describe "#close" do it "flushes entries and closes the wrapped device" do buffer = Lumberjack::Device::Buffer.new(device, buffer_size: 5) begin buffer.write(entry_1) expect(device).to receive(:close) buffer.close expect(device.entries).to eq([entry_1]) expect { buffer.write(entry_2) }.not_to raise_error expect(buffer).to be_empty ensure buffer.close unless buffer.closed? end end end describe "#reopen" do it "flushes entries and reopens the wrapped device" do buffer = Lumberjack::Device::Buffer.new(device, buffer_size: 5) begin buffer.write(entry_1) expect(device).to receive(:reopen).with(nil) buffer.reopen expect(device.entries).to eq([entry_1]) ensure buffer.close unless buffer.closed? end end end describe "#buffer_size" do it "returns the configured buffer size" do buffer = Lumberjack::Device::Buffer.new(device) begin expect(buffer.buffer_size).to eq(0) buffer.buffer_size = 3 expect(buffer.buffer_size).to eq(3) ensure buffer.close unless buffer.closed? end end end describe "flusher thread" do it "automatically flushes entries after flush_seconds interval" do buffer = Lumberjack::Device::Buffer.new(device, buffer_size: 5, flush_seconds: 0.15) begin buffer.write(entry_1) expect(device.entries.size).to eq(0) sleep 0.2 expect(device.entries).to eq([entry_1]) ensure buffer.close unless buffer.closed? end end end end bdurand-lumberjack-ac97435/spec/lumberjack/device/date_rolling_log_file_spec.rb000066400000000000000000000010411515437321200277600ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::DateRollingLogFile do it "is a deprecated alias for Lumberjack::Device::LogFile", deprecation_mode: :silent do file = Tempfile.new("lumberjack_test") begin device = Lumberjack::Device::DateRollingLogFile.new(file.path, roll: :daily) expect(device).to be_a(Lumberjack::Device::LogFile) expect(device.send(:stream).instance_variable_get(:@shift_age)).to eq("daily") ensure file.close file.unlink end end end bdurand-lumberjack-ac97435/spec/lumberjack/device/log_file_spec.rb000066400000000000000000000031441515437321200252430ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::LogFile do let(:out) { StringIO.new } it "wraps a ::Logger::LogDevice" do device = Lumberjack::Device::LogFile.new(out, template: "{{severity}} {{message}}") expect(device.class).to eq(Lumberjack::Device::LogFile) device.write(Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Test message", nil, Process.pid, nil)) expect(out.string.chomp).to eq("INFO Test message") end it "passes supported device options through to the underlying device" do expect(Logger::LogDevice).to receive(:new).with(out, shift_age: 10).and_call_original Lumberjack::Device::LogFile.new(out, template: "{{severity}} {{message}}", shift_age: 10) end it "exposes the file path for the underlying stream" do file = Tempfile.new("lumberjack_test") file.close begin device = Lumberjack::Device::LogFile.new(file) expect(device.path).to eq(file.path) ensure file.unlink end end describe "#dev" do it "returns the underlying stream" do stream = StringIO.new device = Lumberjack::Device::LogFile.new(stream) expect(device.dev).to eq(stream) end end describe "#reopen" do it "calls reopen on the underlying stream with the correct parameters" do file = Tempfile.new("lumberjack_test") file.close begin device = Lumberjack::Device::LogFile.new(file.path) expect_any_instance_of(::Logger::LogDevice).to receive(:reopen).with(nil).and_call_original device.reopen ensure file.unlink end end end end bdurand-lumberjack-ac97435/spec/lumberjack/device/logger_wrapper_spec.rb000066400000000000000000000040541515437321200265030ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::LoggerWrapper do it "wraps another Lumberjack logger as a device" do logger = Lumberjack::Logger.new(:test) outer_logger = Lumberjack::Logger.new(Lumberjack::Device::LoggerWrapper.new(logger), progname: "MyApp") outer_logger.info("Test log message", foo: "bar") expect(logger.device).to( include(severity: :info, message: "Test log message", progname: "MyApp", attributes: {"foo" => "bar"}) ) end it "automatically wraps a logger set as the device" do logger = Lumberjack::Logger.new(:test) outer_logger = Lumberjack::Logger.new(logger, progname: "MyApp") outer_logger.info("Test log message", foo: "bar") expect(logger.device).to( include(severity: :info, message: "Test log message", progname: "MyApp", attributes: {"foo" => "bar"}) ) end it "wraps a forked logger as a device" do logger = Lumberjack::Logger.new(:test) forked_logger = logger.fork outer_logger = Lumberjack::Logger.new(forked_logger, progname: "MyApp") outer_logger.info("Test log message", foo: "bar") expect(logger.device).to( include(severity: :info, message: "Test log message", progname: "MyApp", attributes: {"foo" => "bar"}) ) end it "wraps a standard Ruby Logger as a device" do stream = StringIO.new ruby_logger = ::Logger.new(stream) outer_logger = Lumberjack::Logger.new(Lumberjack::Device::LoggerWrapper.new(ruby_logger), progname: "MyApp") outer_logger.warn("No attributes") outer_logger.info("With attributes", foo: "bar", baz: [1, 2, 3]) log_output = stream.string expect(log_output).to include("WARN -- MyApp: No attributes") expect(log_output).to include("INFO -- MyApp: With attributes [foo=bar] [baz=1,2,3]") end describe "#dev" do it "returns the logger deviceunderlying stream" do stream = StringIO.new logger = Lumberjack::Logger.new(stream) device = Lumberjack::Device::LoggerWrapper.new(logger) expect(device.dev).to eq(stream) end end end bdurand-lumberjack-ac97435/spec/lumberjack/device/multi_spec.rb000066400000000000000000000027421515437321200246200ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::Multi do let(:output_1) { StringIO.new } let(:output_2) { StringIO.new } let(:device_1) { Lumberjack::Device::Writer.new(output_1, template: "{{message}}") } let(:device_2) { Lumberjack::Device::Writer.new(output_2, template: "{{severity}} - {{message}}") } let(:device) { Lumberjack::Device::Multi.new(device_1, device_2) } let(:entry) { Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 100, {}) } it "should write an entry to each device" do device.write(entry) expect(output_1.string.chomp).to eq "test" expect(output_2.string.chomp).to eq "INFO - test" end it "should flush each device" do expect(device_1).to receive(:flush).and_call_original expect(device_2).to receive(:flush).and_call_original device.flush end it "should close each device" do expect(device_1).to receive(:close).and_call_original expect(device_2).to receive(:close).and_call_original device.close end it "should reopen each device" do expect(device_1).to receive(:reopen).with(nil).and_call_original expect(device_2).to receive(:reopen).with(nil).and_call_original device.reopen end it "should set the dateformat on each device" do device.datetime_format = "%Y-%m-%d" expect(device.datetime_format).to eq "%Y-%m-%d" expect(device_1.datetime_format).to eq "%Y-%m-%d" expect(device_2.datetime_format).to eq "%Y-%m-%d" end end bdurand-lumberjack-ac97435/spec/lumberjack/device/null_spec.rb000066400000000000000000000007031515437321200244330ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::Null do it "is registered as :null" do expect(Lumberjack::DeviceRegistry.device_class(:null)).to eq(Lumberjack::Device::Null) end it "should not generate any output" do device = Lumberjack::Device::Null.new device.write(Lumberjack::LogEntry.new(Time.now, 1, "New log entry", nil, Process.pid, nil)) device.flush device.close end end bdurand-lumberjack-ac97435/spec/lumberjack/device/size_rolling_log_file_spec.rb000066400000000000000000000010571515437321200300240ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::SizeRollingLogFile do it "is a deprecated alias for Lumberjack::Device::LogFile", deprecation_mode: :silent do file = Tempfile.new("lumberjack_test") begin device = Lumberjack::Device::SizeRollingLogFile.new(file.path, max_size: 1024 * 1024) expect(device).to be_a(Lumberjack::Device::LogFile) expect(device.send(:stream).instance_variable_get(:@shift_size)).to eq(1024 * 1024) ensure file.close file.unlink end end end bdurand-lumberjack-ac97435/spec/lumberjack/device/test_spec.rb000066400000000000000000000176521515437321200244530ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::Test do let(:device) { Lumberjack::Device::Test.new } it "is registered as :test" do expect(Lumberjack::DeviceRegistry.device_class(:test)).to eq(Lumberjack::Device::Test) end describe "#max_entries" do it "is 1000 by default" do expect(device.max_entries).to eq(1000) end end describe "#write" do it "captures log entries" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Test message", nil, nil, nil) device.write(entry) expect(device.entries).to eq([entry]) end it "only keeps the last n entries" do device.max_entries = 3 entry_1 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Entry 1", nil, nil, nil) entry_2 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Entry 2", nil, nil, nil) entry_3 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Entry 3", nil, nil, nil) entry_4 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Entry 4", nil, nil, nil) device.write(entry_1) device.write(entry_2) device.write(entry_3) device.write(entry_4) expect(device.entries).to eq([entry_2, entry_3, entry_4]) end end describe "#clear" do it "clears all captured log entries" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Test message", nil, nil, nil) device.write(entry) expect(device.entries).to eq([entry]) device.clear expect(device.entries).to be_empty end end describe "#write_to" do it "can write captured log entries to another logger" do entry_1 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Entry 1", nil, nil, nil) entry_2 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Entry 2", nil, nil, nil) device.write(entry_1) device.write(entry_2) target_logger = Lumberjack::Logger.new(:test) device.write_to(target_logger) expect(target_logger.device.entries).to eq([entry_1, entry_2]) expect(device.entries).to_not be_empty end it "can write captured log entries to another device" do entry_1 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Entry 1", nil, nil, nil) entry_2 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Entry 2", nil, nil, nil) device.write(entry_1) device.write(entry_2) target_device = Lumberjack::Device::Test.new device.write_to(target_device) expect(target_device.entries).to eq([entry_1, entry_2]) expect(device.entries).to_not be_empty end end describe "#entries" do it "returns all captured log entries" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Test message", nil, nil, nil) device.write(entry) expect(device.entries).to eq([entry]) end end describe "#include?" do it "is true if the entry matches one in the buffer" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Test message", nil, nil, nil) device.write(entry) expect(device.include?(message: "Test message")).to be true expect(device.include?(message: "Different message")).to be false end end describe "#match" do it "returns the first match" do entry_1 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Message 1", nil, nil, nil) entry_2 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Message 2", nil, nil, nil) entry_3 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Message 3", nil, nil, nil) device.write(entry_1) device.write(entry_2) device.write(entry_3) expect(device.match(message: "Message 2")).to eq(entry_2) expect(device.match(message: "Different message")).to be_nil end it "can match by severity" do entry_1 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Message 1", nil, nil, nil) entry_2 = Lumberjack::LogEntry.new(Time.now, Logger::WARN, "Message 2", nil, nil, nil) device.write(entry_1) device.write(entry_2) expect(device.match(severity: Logger::INFO)).to eq(entry_1) expect(device.match(severity: Logger::WARN)).to eq(entry_2) expect(device.match(severity: Logger::ERROR)).to be_nil end it "can match by progname" do entry_1 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Message 1", "progname1", nil, nil) entry_2 = Lumberjack::LogEntry.new(Time.now, Logger::WARN, "Message 2", "progname2", nil, nil) device.write(entry_1) device.write(entry_2) expect(device.match(progname: "progname1")).to eq(entry_1) expect(device.match(progname: "progname2")).to eq(entry_2) expect(device.match(progname: "different_progname")).to be_nil end it "can match by attributes" do entry_1 = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Message 1", "progname1", nil, {"foo" => "bar"}) entry_2 = Lumberjack::LogEntry.new(Time.now, Logger::WARN, "Message 2", "progname2", nil, {"foo" => "baz"}) device.write(entry_1) device.write(entry_2) expect(device.match(attributes: {foo: "bar"})).to eq(entry_1) expect(device.match(attributes: {foo: "baz"})).to eq(entry_2) expect(device.match(attributes: {foo: "qux"})).to be_nil end end describe "#closest_match" do let(:entry_1) { Lumberjack::LogEntry.new(Time.now, Logger::INFO, "User logged in successfully", nil, nil, {"user" => "alice"}) } let(:entry_2) { Lumberjack::LogEntry.new(Time.now, Logger::WARN, "User failed to login", nil, nil, {"component" => "database"}) } let(:entry_3) { Lumberjack::LogEntry.new(Time.now, Logger::ERROR, "Failed to authenticate user", nil, nil, {"user" => "bob"}) } before do device.write(entry_1) device.write(entry_2) device.write(entry_3) end it "returns the entry with an exact match" do expect(device.closest_match(message: "User logged in successfully")).to eq(entry_1) end it "returns the closest matching entry based on message similarity" do expect(device.closest_match(message: "User fail login", severity: Logger::ERROR)).to eq(entry_2) end it "returns nil if no entries meet the minimum score threshold" do expect(device.closest_match(message: "Completely different message")).to be_nil end end describe "#dev" do it "returns self underlying stream" do device = Lumberjack::Device::Test.new expect(device.dev).to eq(device) end end describe ".formatted_expectation" do it "should format a log entry expectation into a string" do expectation = { severity: Logger::INFO, message: "Test message", progname: "TestProgname", attributes: {foo: "bar", baz: {one: 1, two: 2}} } expected = <<~STRING severity: INFO message: Test message progname: TestProgname attributes: baz.one: 1 baz.two: 2 foo: "bar" STRING expect(Lumberjack::Device::Test.formatted_expectation(expectation)).to eq(expected.chomp) end it "should omit nil values" do expectation = { severity: Logger::INFO, message: "Test message", progname: nil, attributes: nil } expected = <<~STRING severity: INFO message: Test message STRING expect(Lumberjack::Device::Test.formatted_expectation(expectation)).to eq(expected.chomp) end it "should indent a specified number of spaces" do expectation = { severity: Logger::INFO, message: "Test message", progname: "TestProgname", attributes: {foo: "bar"} } expected = <<~STRING severity: INFO message: Test message progname: TestProgname attributes: foo: "bar" STRING expected = expected.split(Lumberjack::LINE_SEPARATOR).collect { |line| " #{line}" }.join(Lumberjack::LINE_SEPARATOR) expect(Lumberjack::Device::Test.formatted_expectation(expectation, indent: 4)).to eq(expected.chomp) end end end bdurand-lumberjack-ac97435/spec/lumberjack/device/writer_spec.rb000066400000000000000000000127441515437321200250050ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device::Writer do let(:time_string) { "2011-01-15T14:23:45.123" } let(:time) { Time.parse(time_string) } let(:stream) { StringIO.new } let(:entry) { Lumberjack::LogEntry.new(time, Logger::INFO, "test message", "app", 12345, "foo" => "ABCD") } let(:io_class) do Class.new do attr_reader :string attr_accessor :sync attr_accessor :path def initialize @string = +"" @buffer = +"" @sync = false end def write(string) @buffer << string end def flush @string << @buffer @buffer = +"" end end end it "should sync the stream and flush it when the device is flushed" do # Create an IO like object that require flush to be called io = io_class.new device = Lumberjack::Device::Writer.new(io, template: "{{message}}") device.write(entry) expect(io.string).to eq("") device.flush expect(io.string).to eq("test message#{Lumberjack::LINE_SEPARATOR}") end it "sets io sync by default" do io = io_class.new Lumberjack::Device::Writer.new(io) expect(io.sync).to be true end it "disables io sync if autoflush is false" do io = io_class.new Lumberjack::Device::Writer.new(io, autoflush: false) expect(io.sync).to be false end describe "#path" do it "can get the file path for the underlying stream" do io = io_class.new io.path = "/path/to/logfile.log" device = Lumberjack::Device::Writer.new(io) expect(device.path).to eq("/path/to/logfile.log") end it "returns nil if the stream does not have a path" do device = Lumberjack::Device::Writer.new(StringIO.new) expect(device.path).to be_nil end end it "should write entries out to the stream with a default template" do device = Lumberjack::Device::Writer.new(stream) device.write(entry) device.flush expect(stream.string).to eq("[2011-01-15T14:23:45.123 INFO app(12345)] test message [foo:ABCD]#{Lumberjack::LINE_SEPARATOR}") end it "should write entries out to the stream with a custom template" do device = Lumberjack::Device::Writer.new(stream, template: "{{message}}") device.write(entry) device.flush expect(stream.string).to eq("test message#{Lumberjack::LINE_SEPARATOR}") end it "should be able to specify the time format for the template" do device = Lumberjack::Device::Writer.new(stream, time_format: :microseconds) device.write(entry) device.flush expect(stream.string).to eq("[2011-01-15T14:23:45.123000 INFO app(12345)] test message [foo:ABCD]#{Lumberjack::LINE_SEPARATOR}") end it "should be able to specify a block template for log entries" do device = Lumberjack::Device::Writer.new(stream, template: lambda { |e| e.message.upcase }) device.write(entry) device.flush expect(stream.string).to eq("TEST MESSAGE#{Lumberjack::LINE_SEPARATOR}") end it "can write to a template registry template" do device = Lumberjack::Device::Writer.new(stream, template: :local, exclude_pid: false) device.write(entry) device.flush template = Lumberjack::LocalLogTemplate.new(exclude_pid: false) expect(stream.string).to eq(template.call(entry)) end it "should write to STDERR if an error is raised when flushing to the stream" do stderr = $stderr $stderr = StringIO.new begin device = Lumberjack::Device::Writer.new(stream, template: "{{message}}") expect(stream).to receive(:write).and_raise(StandardError.new("Cannot write to stream")) device.write(entry) device.flush expect($stderr.string).to include("test message#{Lumberjack::LINE_SEPARATOR}") expect($stderr.string).to include("StandardError: Cannot write to stream") ensure $stderr = stderr end end describe "multi line messages" do let(:message) { "line 1#{Lumberjack::LINE_SEPARATOR}line 2#{Lumberjack::LINE_SEPARATOR}line 3" } let(:entry) { Lumberjack::LogEntry.new(time, Logger::INFO, message, "app", 12345, "foo" => "ABCD") } it "should have a default template for multiline messages" do device = Lumberjack::Device::Writer.new(stream) device.write(entry) device.flush expect(stream.string.split(Lumberjack::LINE_SEPARATOR)).to eq(["[2011-01-15T14:23:45.123 INFO app(12345)] line 1 [foo:ABCD]", "> line 2", "> line 3"]) end it "should be able to specify a template for multiple line messages" do device = Lumberjack::Device::Writer.new(stream, additional_lines: " // {{message}}") device.write(entry) device.flush expect(stream.string).to eq("[2011-01-15T14:23:45.123 INFO app(12345)] line 1 [foo:ABCD] // line 2 // line 3#{Lumberjack::LINE_SEPARATOR}") end end describe "colorized entries" do it "should write entries out to the stream with colorized severity" do device = Lumberjack::Device::Writer.new(stream, colorize: true) entry = Lumberjack::LogEntry.new(time, Logger::INFO, "line 1#{Lumberjack::LINE_SEPARATOR}line 2#", "app", 12345, "foo" => "ABCD") device.write(entry) expect(stream.string).to eq("\e7\e[38;5;33m[2011-01-15T14:23:45.123 INFO app(12345)] line 1 [foo:ABCD]\e8#{Lumberjack::LINE_SEPARATOR}\e7\e[38;5;33m> line 2#\e8#{Lumberjack::LINE_SEPARATOR}") end end describe "#dev" do it "returns the underlying stream" do stream = StringIO.new device = Lumberjack::Device::Writer.new(stream) expect(device.dev).to eq(stream) end end end bdurand-lumberjack-ac97435/spec/lumberjack/device_registry_spec.rb000066400000000000000000000031571515437321200254170ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::DeviceRegistry do it "has :stdout, :stderr, :null and :test registered by default" do expect(Lumberjack::DeviceRegistry.registered_devices).to eq({ stdout: :stdout, stderr: :stderr, null: Lumberjack::Device::Null, test: Lumberjack::Device::Test }) end it "can add new devices to the registry" do Lumberjack::DeviceRegistry.add(:foobar, Object) expect(Lumberjack::DeviceRegistry.device_class(:foobar)).to eq Object expect(Lumberjack::DeviceRegistry.device_class(:other)).to be_nil ensure Lumberjack::DeviceRegistry.remove(:foobar) end it "can instantiate a device by name and options" do device = Lumberjack::DeviceRegistry.new_device(:test, max_entries: 15) expect(device).to be_a(Lumberjack::Device::Test) expect(device.max_entries).to eq 15 end it "instatiates a Writer to STDOUT with :stdout" do save_stdout = $stdout stdout = StringIO.new device = nil begin $stdout = stdout device = Lumberjack::DeviceRegistry.new_device(:stdout, {}) ensure $stdout = save_stdout end expect(device).to be_a(Lumberjack::Device::Writer) expect(device.dev).to eq(stdout) end it "instantiates a Writer to STDERR with :stderr" do save_stderr = $stderr stderr = StringIO.new device = nil begin $stderr = stderr device = Lumberjack::DeviceRegistry.new_device(:stderr, {}) ensure $stderr = save_stderr end expect(device).to be_a(Lumberjack::Device::Writer) expect(device.dev).to eq(stderr) end end bdurand-lumberjack-ac97435/spec/lumberjack/device_spec.rb000066400000000000000000000071041515437321200234630ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Device do describe ".open_device" do it "passes the options to the underlying device" do device = Lumberjack::Device.open_device(:test, max_entries: 5) expect(device.max_entries).to eq(5) end it "returns a Null device when given nil" do device = Lumberjack::Device.open_device(nil) expect(device).to be_a(Lumberjack::Device::Null) end it "returns the device when given a Lumberjack::Device" do original_device = Lumberjack::Device::Test.new device = Lumberjack::Device.open_device(original_device) expect(device).to equal(original_device) end it "looks up a device in the registry when given a Symbol" do device = Lumberjack::Device.open_device(:test) expect(device).to be_a(Lumberjack::Device::Test) end it "opens a LogFile when given a String path" do tmp_file = Tempfile.new("lumberjack_test") begin device = Lumberjack::Device.open_device(tmp_file.path) expect(device).to be_a(Lumberjack::Device::LogFile) expect(device.dev.path).to eq(tmp_file.path) ensure tmp_file.unlink end end it "opens a LogFile when given a Pathname" do tmp_file = Tempfile.new("lumberjack_test") begin path = Pathname.new(tmp_file.path) device = Lumberjack::Device.open_device(path) expect(device).to be_a(Lumberjack::Device::LogFile) expect(device.dev.path).to eq(tmp_file.path) ensure tmp_file.unlink end end it "opens a LogFile when given a File" do tmp_file = Tempfile.new("lumberjack_test") file = File.open(tmp_file.path, "a") begin device = Lumberjack::Device.open_device(file) expect(device).to be_a(Lumberjack::Device::LogFile) expect(device.dev.path).to eq(tmp_file.path) ensure file.close tmp_file.unlink end end it "opens a Writer when given an IO that is not a File" do string_io = StringIO.new device = Lumberjack::Device.open_device(string_io) expect(device).to be_a(Lumberjack::Device::Writer) expect(device.dev).to equal(string_io) end it "wraps a ContextLogger in a LoggerWrapper" do context_logger = Lumberjack::Logger.new(:test) device = Lumberjack::Device.open_device(context_logger) expect(device).to be_a(Lumberjack::Device::LoggerWrapper) expect(device.logger).to equal(context_logger) end it "wraps a ::Logger in a LoggerWrapper" do logger = ::Logger.new(File::NULL) device = Lumberjack::Device.open_device(logger) expect(device).to be_a(Lumberjack::Device::LoggerWrapper) expect(device.logger).to equal(logger) end it "opens multiple devices when given an Array and can pass shared and device specific options" do out_1 = StringIO.new out_2 = StringIO.new device = Lumberjack::Device.open_device([out_1, [out_2, {attribute_format: "(%s=%s)"}]], template: "{{message}} {{attributes}}") expect(device).to be_a(Lumberjack::Device::Multi) device_1 = device.devices[0] device_2 = device.devices[1] expect(device_1).to be_a(Lumberjack::Device::Writer) expect(device_2).to be_a(Lumberjack::Device::Writer) entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Test", "test", 123, {"foo" => "bar"}) device.write(entry) expect(out_1.string).to eq("Test [foo:bar]#{Lumberjack::LINE_SEPARATOR}") expect(out_2.string).to eq("Test (foo=bar)#{Lumberjack::LINE_SEPARATOR}") end end end bdurand-lumberjack-ac97435/spec/lumberjack/entry_formatter_spec.rb000066400000000000000000000211231515437321200254450ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::EntryFormatter do describe "building formatters" do it "starts with an empty message formatter and attribute formatter" do entry_formatter = Lumberjack::EntryFormatter.new expect(entry_formatter.message_formatter).to_not be_nil expect(entry_formatter.attribute_formatter).to_not be_nil obj = Object.new expect(entry_formatter.message_formatter.format(obj)).to equal(obj) expect(entry_formatter.attribute_formatter).to be_empty end it "uses the default formatter if message_formatter is :default" do entry_formatter = Lumberjack::EntryFormatter.new(message_formatter: :default) expect(entry_formatter.message_formatter).to_not be_nil obj = Object.new expect(entry_formatter.message_formatter.format(obj)).to eq(obj.inspect) end it "can add new message formatters in a chain" do entry_formatter = Lumberjack::EntryFormatter.new formatter = lambda {} expect(entry_formatter.format_class(Object, formatter)).to eq(entry_formatter) expect(entry_formatter.message_formatter.formatter_for(Object)).to eq(formatter) end it "can remove message formatters in a chain" do entry_formatter = Lumberjack::EntryFormatter.new formatter = lambda {} entry_formatter.format_class(Object, formatter) expect(entry_formatter.remove_class(Object)).to eq(entry_formatter) expect(entry_formatter.message_formatter.formatter_for(Object)).to be_nil end it "can add new attribute formatters in a chain" do entry_formatter = Lumberjack::EntryFormatter.new formatter = lambda {} expect(entry_formatter.format_attributes(Object, formatter)).to eq(entry_formatter) expect(entry_formatter.attribute_formatter).to_not be_empty end it "can remove attribute formatters in a chain" do entry_formatter = Lumberjack::EntryFormatter.new formatter = lambda {} entry_formatter.format_attributes(Object, formatter) expect(entry_formatter.remove_attribute_class(Object)).to eq(entry_formatter) expect(entry_formatter.attribute_formatter).to be_empty end it "can add formatters for both message and attributes" do entry_formatter = Lumberjack::EntryFormatter.new entry_formatter.format_class(String) { |obj| obj.upcase } expect(entry_formatter.message_formatter.format("foo")).to eq("FOO") expect(entry_formatter.attribute_formatter.format("foo" => "bar")).to eq({"foo" => "BAR"}) end it "can add formatters for just the message" do entry_formatter = Lumberjack::EntryFormatter.new entry_formatter.format_message(String) { |obj| obj.upcase } expect(entry_formatter.message_formatter.format("foo")).to eq("FOO") expect(entry_formatter.attribute_formatter.format("foo" => "bar")).to eq({"foo" => "bar"}) end it "does not overwrite message formatters with generic formatters" do entry_formatter = Lumberjack::EntryFormatter.new entry_formatter.format_message(String) { |obj| obj.upcase } entry_formatter.format_class(String) { |obj| obj.downcase } expect(entry_formatter.message_formatter.format("FooBar")).to eq("FOOBAR") expect(entry_formatter.attribute_formatter.format("foo" => "FooBar")).to eq({"foo" => "foobar"}) end it "does not overwrite attribute formatters with generic formatters" do entry_formatter = Lumberjack::EntryFormatter.new entry_formatter.format_attributes(String) { |obj| obj.upcase } entry_formatter.format_class(String) { |obj| obj.downcase } expect(entry_formatter.message_formatter.format("FooBar")).to eq("foobar") expect(entry_formatter.attribute_formatter.format("foo" => "FooBar")).to eq({"foo" => "FOOBAR"}) end it "can set the attribute default formatter in a chain" do entry_formatter = Lumberjack::EntryFormatter.new entry_formatter.default_attribute_format { |obj| obj.to_s.upcase } expect(entry_formatter.attribute_formatter.format(foo: "bar")).to eq({"foo" => "BAR"}) end it "build a formatter with a build block" do entry_formatter = Lumberjack::EntryFormatter.build do |config| config.format_class(String) { |obj| obj.to_s.upcase } config.format_attribute_name("status") { |obj| "[#{obj}]" } config.format_attributes(Array) { |obj| obj.join("|") } end expect(entry_formatter.message_formatter.format("foobar")).to eq("FOOBAR") expect(entry_formatter.attribute_formatter.format("status" => "new")).to eq({"status" => "[new]"}) expect(entry_formatter.attribute_formatter.format("tags" => ["a", "b", "c"])).to eq({"tags" => "a|b|c"}) end end describe "#include" do it "merges the formats from the formatter" do formatter_1 = Lumberjack::EntryFormatter.build do |config| config.format_class(String) { |obj| obj.to_s.upcase } config.format_attribute_name("status") { |obj| "[#{obj}]" } config.format_attribute_name("foo") { |obj| "foo:#{obj}" } end formatter_2 = Lumberjack::EntryFormatter.build do |config| config.format_class(String) { |obj| obj.to_s.downcase } config.format_attribute_name("foo") { |obj| "(#{obj})" } end expect(formatter_2.include(formatter_1)).to eq formatter_2 message, attributes = formatter_2.format("foobar", {"status" => "new", "foo" => "bar"}) expect(message).to eq("FOOBAR") expect(attributes).to eq({"status" => "[new]", "foo" => "foo:bar"}) end end describe "#prepend" do it "prepends the formats from the formatter" do formatter_1 = Lumberjack::EntryFormatter.build do |config| config.format_class(String) { |obj| obj.to_s.upcase } config.format_attribute_name("status") { |obj| "[#{obj}]" } config.format_attribute_name("foo") { |obj| "foo:#{obj}" } end formatter_2 = Lumberjack::EntryFormatter.build do |config| config.format_class(String) { |obj| obj.to_s.downcase } config.format_attribute_name("foo") { |obj| "(#{obj})" } end expect(formatter_2.prepend(formatter_1)).to eq formatter_2 message, attributes = formatter_2.format("Foobar", {"status" => "new", "foo" => "bar"}) expect(message).to eq("foobar") expect(attributes).to eq({"status" => "[new]", "foo" => "(bar)"}) end end describe "#format" do let(:entry_formatter) { Lumberjack::EntryFormatter.new } it "does nothing with no message or attribute formatter" do entry_formatter.message_formatter = nil entry_formatter.attribute_formatter = nil message, attributes = entry_formatter.format("foobar", {"foo" => "bar"}) expect(message).to eq("foobar") expect(attributes).to eq({"foo" => "bar"}) end it "formats the message on the entry" do entry_formatter.format_class(String) { |obj| "String: #{obj}" } message, _ = entry_formatter.format("foobar", {"foo" => "bar"}) expect(message).to eq("String: foobar") end it "calls the message block if it is a Proc" do message, _ = entry_formatter.format(-> { "foobar" }, {"foo" => "bar"}) expect(message).to eq("foobar") end it "splits the message and attributes if the message formatter is a attributed message" do entry_formatter.format_message(String) { |obj| Lumberjack::MessageAttributes.new("attributeged: #{obj}", {"attribute" => obj}) } message, attributes = entry_formatter.format("foobar", {"foo" => "bar"}) expect(message).to eq("attributeged: foobar") expect(attributes).to eq({"attribute" => "foobar", "foo" => "bar"}) end it "applies the attribute formatter to the attributes" do entry_formatter.format_attribute_name("foo") { |obj| "Foo: #{obj}" } message, attributes = entry_formatter.format("foobar", {"foo" => "bar"}) expect(message).to eq("foobar") expect(attributes).to eq({"foo" => "Foo: bar"}) end it "calls Proc values in attributes" do message, attributes = entry_formatter.format("foobar", {"foo" => -> { "bar" }}) expect(message).to eq("foobar") expect(attributes).to eq({"foo" => "bar"}) end it "handles nil messages" do entry_formatter.message_formatter.clear message, attributes = entry_formatter.format(nil, {"foo" => "bar"}) expect(message).to be_nil expect(attributes).to eq({"foo" => "bar"}) end it "handles nil attributes" do entry_formatter.format_attribute_name("foo") { |obj| "Foo: #{obj}" } message, attributes = entry_formatter.format("foobar", nil) expect(message).to eq("foobar") expect(attributes).to be_nil end end end bdurand-lumberjack-ac97435/spec/lumberjack/forked_logger_spec.rb000066400000000000000000000113301515437321200250310ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::ForkedLogger do let(:logger) { TestContextLogger.new(Lumberjack::Context.new) } it "logs to the parent logger" do forked_logger = Lumberjack::ForkedLogger.new(logger) forked_logger.info("Test message") expect(logger.entries.first).to eq({ severity: Logger::INFO, message: "Test message", progname: nil, attributes: nil }) end it "flushes the parent logger's device" do forked_logger = Lumberjack::ForkedLogger.new(logger) expect(logger).to receive(:flush) forked_logger.flush end it "inherits the parent logger's level" do logger.level = Logger::WARN forked_logger = Lumberjack::ForkedLogger.new(logger) expect(forked_logger.level).to eq(Logger::WARN) end it "inherits the parent logger's isolation level" do logger.isolation_level = :thread forked_logger = Lumberjack::ForkedLogger.new(logger) expect(forked_logger.isolation_level).to eq(:thread) end it "will log in the parent logger even if the parent logger has a higher threshold" do logger.level = :warn forked_logger = Lumberjack::ForkedLogger.new(logger) forked_logger.level = :info expect(logger.level).to eq(Logger::WARN) forked_logger.info("Test message") expect(logger.entries.first).to eq({ severity: Logger::INFO, message: "Test message", progname: nil, attributes: nil }) end it "allows setting attributes on the local logger" do logger = Lumberjack::Logger.new(:test) logger.tag!(foo: "bar", baz: "boo") forked_logger = Lumberjack::ForkedLogger.new(logger) forked_logger.tag!(test: "value", baz: "overridden") expect(logger.attributes).to eq({"foo" => "bar", "baz" => "boo"}) forked_logger.info("Test message") entry = logger.device.entries.last expect(entry.attributes).to eq({ "foo" => "bar", "baz" => "overridden", "test" => "value" }) end it "does not bleed attributes to the parent logger contexts" do forked_logger = Lumberjack::ForkedLogger.new(logger) forked_logger.tag_all_contexts(test: "value") expect(logger.attributes).to be_empty forked_logger.tag(foo: "bar") do forked_logger.tag_all_contexts(test: "value") end expect(logger.attributes).to be_empty end it "does not bleed attributes to the default context" do forked_logger = Lumberjack::ForkedLogger.new(logger) forked_logger.tag(test: "value") expect(forked_logger.attributes).to be_empty forked_logger.tag_all_contexts(test: "value") expect(forked_logger.attributes).to be_empty forked_logger.tag(foo: "bar") do forked_logger.tag_all_contexts(test: "value") end expect(forked_logger.attributes).to be_empty end it "allows setting the progname on the local logger" do forked_logger = Lumberjack::ForkedLogger.new(logger) forked_logger.progname = "TestProgname" forked_logger.info("Test message") expect(logger.progname).to be_nil expect(logger.entries.first).to eq({ severity: Logger::INFO, message: "Test message", progname: "TestProgname", attributes: nil }) end it "returns the parent logger's device" do logger = Lumberjack::Logger.new(:test) forked_logger = Lumberjack::ForkedLogger.new(logger) expect(forked_logger.device).to eq(logger.device) end it "returns the parent logger's formatter" do formatter = Lumberjack::EntryFormatter.new logger = Lumberjack::Logger.new(:test, formatter: formatter) forked_logger = Lumberjack::ForkedLogger.new(logger) expect(forked_logger.formatter).to eq(logger.formatter) end context "when forking from a forked logger" do let(:original_logger) { Lumberjack::Logger.new(:test).tap { |logger| logger.tag!(foo: 1) } } let(:parent_forked_logger) { original_logger.fork(level: :warn, progname: "ParentForked", attributes: {bar: 2, baz: 3}) } let(:forked_logger) { parent_forked_logger.fork(level: :info, progname: "ChildForked", attributes: {baz: 4, qux: 5, wip: 7}) } it "uses the forked level" do forked_logger.info("Test message") expect(original_logger.device.entries.size).to eq(1) end it "uses the forked progname" do forked_logger.info("Test message") entry = original_logger.device.entries.last expect(entry.progname).to eq("ChildForked") end it "merges the forked attributes into the parent logger attributes" do forked_logger.info("Test message", tik: 6, wip: 8) entry = original_logger.device.entries.last expect(entry.attributes).to eq({ "foo" => 1, "bar" => 2, "baz" => 4, "qux" => 5, "tik" => 6, "wip" => 8 }) end end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/000077500000000000000000000000001515437321200226665ustar00rootroot00000000000000bdurand-lumberjack-ac97435/spec/lumberjack/formatter/date_time_formatter_spec.rb000066400000000000000000000024461515437321200302510ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::DateTimeFormatter do it "is registered as :date_time" do expect(Lumberjack::FormatterRegistry.formatter(:date_time, "YYYY-mm-dd")).to be_a(Lumberjack::Formatter::DateTimeFormatter) end it "should format a time object" do time = Time.now formatter = Lumberjack::Formatter::DateTimeFormatter.new("%Y-%m-%d %H:%M") expect(formatter.call(time)).to eq time.strftime("%Y-%m-%d %H:%M") end it "should format a date object" do date = Date.today formatter = Lumberjack::Formatter::DateTimeFormatter.new("%Y-%m-%d %H:%M") expect(formatter.call(date)).to eq date.strftime("%Y-%m-%d %H:%M") end it "should format a datetime object" do datetime = DateTime.now formatter = Lumberjack::Formatter::DateTimeFormatter.new("%Y-%m-%d %H:%M") expect(formatter.call(datetime)).to eq datetime.strftime("%Y-%m-%d %H:%M") end it "should not format a non date or time" do formatter = Lumberjack::Formatter::DateTimeFormatter.new("%Y-%m-%d %H:%M") expect(formatter.call("foo")).to eq "foo" end it "should use iso8601 by default" do time = Time.now formatter = Lumberjack::Formatter::DateTimeFormatter.new expect(formatter.call(time)).to eq time.iso8601(6) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/exception_formatter_spec.rb000066400000000000000000000023331515437321200303070ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::ExceptionFormatter do it "is registered as :exception" do expect(Lumberjack::FormatterRegistry.formatter(:exception)).to be_a(Lumberjack::Formatter::ExceptionFormatter) end it "should convert an exception without a backtrace to a string" do e = ArgumentError.new("not expected") formatter = Lumberjack::Formatter::ExceptionFormatter.new expect(formatter.call(e)).to eq("ArgumentError: not expected") end it "should convert an exception with a backtrace to a string" do raise ArgumentError.new("not expected") rescue => e formatter = Lumberjack::Formatter::ExceptionFormatter.new expect(formatter.call(e)).to eq("ArgumentError: not expected#{Lumberjack::LINE_SEPARATOR} #{e.backtrace.join(Lumberjack::LINE_SEPARATOR + " ")}") end it "should clean the backtrace" do raise ArgumentError.new("not expected") rescue => e formatter = Lumberjack::Formatter::ExceptionFormatter.new formatter.backtrace_cleaner = lambda { |lines| ["redacted: #{lines.size}"] } expect(formatter.call(e)).to eq("ArgumentError: not expected#{Lumberjack::LINE_SEPARATOR} redacted: #{e.backtrace.size}") end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/id_formatter_spec.rb000066400000000000000000000007571515437321200267150ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::IdFormatter do it "is registered as :id" do expect(Lumberjack::FormatterRegistry.formatter(:id)).to be_a(Lumberjack::Formatter::IdFormatter) end it "should format an object as a hash of class and id" do obj = Object.new def obj.id 1 end formatter = Lumberjack::Formatter::IdFormatter.new expect(formatter.call(obj)).to eq({"class" => "Object", "id" => 1}) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/inspect_formatter_spec.rb000066400000000000000000000011541515437321200277560ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::InspectFormatter do it "is registered as :inspect" do expect(Lumberjack::FormatterRegistry.formatter(:inspect)).to be_a(Lumberjack::Formatter::InspectFormatter) end it "should format objects as string by calling their inspect method" do formatter = Lumberjack::Formatter::InspectFormatter.new expect(formatter.call("abc")).to eq("\"abc\"") expect(formatter.call(:test)).to eq(":test") expect(formatter.call(1)).to eq("1") expect(formatter.call([:a, 1, "b"])).to eq([:a, 1, "b"].inspect) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/multiply_formatter_spec.rb000066400000000000000000000020341515437321200301660ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::MultiplyFormatter do it "is registered as :multiply" do expect(Lumberjack::FormatterRegistry.formatter(:multiply, 2)).to be_a(Lumberjack::Formatter::MultiplyFormatter) end it "multiplies a numeric value" do formatter = Lumberjack::Formatter::MultiplyFormatter.new(2) expect(formatter.call(5)).to eq(10) end it "rounds the result if decimals are specified" do formatter = Lumberjack::Formatter::MultiplyFormatter.new(2, 1) expect(formatter.call(5.125)).to eq(10.3) end it "can round to integer if decimals is zero" do formatter = Lumberjack::Formatter::MultiplyFormatter.new(2, 0) result = formatter.call(5.9) expect(result).to eq(12) expect(result).to be_a(Integer) end it "returns the original value if not numeric" do formatter = Lumberjack::Formatter::MultiplyFormatter.new(2) expect(formatter.call("not a number")).to eq("not a number") expect(formatter.call(nil)).to eq(nil) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/object_formatter_spec.rb000066400000000000000000000007101515437321200275540ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::ObjectFormatter do it "is registered as :object" do expect(Lumberjack::FormatterRegistry.formatter(:object)).to be_a(Lumberjack::Formatter::ObjectFormatter) end it "should return the object itself" do formatter = Lumberjack::Formatter::ObjectFormatter.new obj = Object.new expect(formatter.call(obj).object_id).to eq obj.object_id end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/pretty_print_formatter_spec.rb000066400000000000000000000010561515437321200310550ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::PrettyPrintFormatter do it "is registered as :pretty_print" do expect(Lumberjack::FormatterRegistry.formatter(:pretty_print)).to be_a(Lumberjack::Formatter::PrettyPrintFormatter) end it "should convert an object to a string using pretty print" do object = Object.new def object.pretty_print(q) q.text "woot!" end formatter = Lumberjack::Formatter::PrettyPrintFormatter.new expect(formatter.call(object)).to eq("woot!") end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/redact_formatter_spec.rb000066400000000000000000000016421515437321200275550ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::RedactFormatter do it "is registered as :redact" do expect(Lumberjack::FormatterRegistry.formatter(:redact)).to be_a(Lumberjack::Formatter::RedactFormatter) end it "should redact long strings" do formatter = Lumberjack::Formatter::RedactFormatter.new expect(formatter.call("1234567890")).to eq("12******90") end it "should redact shorter strings with fewer hints" do formatter = Lumberjack::Formatter::RedactFormatter.new expect(formatter.call("123456")).to eq("1****6") end it "should fully redact very short strings" do formatter = Lumberjack::Formatter::RedactFormatter.new expect(formatter.call("123")).to eq("*****") end it "should return non-string values unchanged" do formatter = Lumberjack::Formatter::RedactFormatter.new expect(formatter.call(123)).to eq(123) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/round_formtter_spec.rb000066400000000000000000000017661515437321200273100ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::RoundFormatter do it "is registered as :round" do expect(Lumberjack::FormatterRegistry.formatter(:round, 1)).to be_a(Lumberjack::Formatter::RoundFormatter) end it "should round a numeric value" do formatter = Lumberjack::Formatter::RoundFormatter.new expect(formatter.call(1.23456789)).to eq(1.235) end it "should work with an integer value" do formatter = Lumberjack::Formatter::RoundFormatter.new expect(formatter.call(10)).to eq(10) end it "should round a numeric value with specified precision" do formatter = Lumberjack::Formatter::RoundFormatter.new(1) expect(formatter.call(1.234)).to eq(1.2) end it "should return non-numeric values unchanged" do formatter = Lumberjack::Formatter::RoundFormatter.new expect(formatter.call("not a number")).to eq("not a number") expect(formatter.call(nil)).to eq(nil) expect(formatter.call([])).to eq([]) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/string_formatter_spec.rb000066400000000000000000000010321515437321200276120ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::StringFormatter do it "is registered as :string" do expect(Lumberjack::FormatterRegistry.formatter(:string)).to be_a(Lumberjack::Formatter::StringFormatter) end it "should format objects as string by calling their to_s method" do formatter = Lumberjack::Formatter::StringFormatter.new expect(formatter.call("abc")).to eq("abc") expect(formatter.call(:test)).to eq("test") expect(formatter.call(1)).to eq("1") end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/strip_formatter_spec.rb000066400000000000000000000010521515437321200274470ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::StripFormatter do it "is registered as :strip" do expect(Lumberjack::FormatterRegistry.formatter(:strip)).to be_a(Lumberjack::Formatter::StripFormatter) end it "should format objects as strings with leading and trailing whitespace removed" do formatter = Lumberjack::Formatter::StripFormatter.new expect(formatter.call(" abc \n")).to eq("abc") expect(formatter.call(:test)).to eq("test") expect(formatter.call(1)).to eq("1") end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/structured_formatter_spec.rb000066400000000000000000000042001515437321200305100ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::StructuredFormatter do it "is registered as :structured" do expect(Lumberjack::FormatterRegistry.formatter(:structured)).to be_a(Lumberjack::Formatter::StructuredFormatter) end it "should recursively format arrays and hashes" do formatter = Lumberjack::Formatter.new.clear formatter.add(Enumerable, Lumberjack::Formatter::StructuredFormatter.new(formatter)) formatter.add(String) { |obj| "#{obj}?" } formatter.add(Object, :object) formatted = formatter.format({foo: "bar", baz: [1, 2, "three", {four: "four"}]}) expect(formatted).to eq({"foo" => "bar?", "baz" => [1, 2, "three?", {"four" => "four?"}]}) end it "should not get into an infinite loop" do formatter = Lumberjack::Formatter.new.clear formatter.add(Enumerable, Lumberjack::Formatter::StructuredFormatter.new(formatter)) formatter.add(Object, :object) object = {name: "object", children: [], v1: true, v2: true} object[:parent] = object child_1 = {name: "child_1", parent: object} child_2 = {name: "child_2", parent: child_1} object[:children] << child_1 object[:children] << child_2 formatted = formatter.format(object) expect(formatted).to eq({"name" => "object", "children" => [{"name" => "child_1"}, {"name" => "child_2", "parent" => {"name" => "child_1"}}], "v1" => true, "v2" => true}) end it "should be able to include an object multiple times if not-recursive" do formatter = Lumberjack::Formatter.new.clear formatter.add(Enumerable, Lumberjack::Formatter::StructuredFormatter.new(formatter)) formatter.add(Object, :object) object = {name: "object", children: []} duplicated = {name: "duplicated"} child_1 = {name: "child_1", parent: duplicated} child_2 = {name: "child_2", parent: duplicated} object[:children] << child_1 object[:children] << child_2 formatted = formatter.format(object) expect(formatted).to eq({"name" => "object", "children" => [{"name" => "child_1", "parent" => {"name" => "duplicated"}}, {"name" => "child_2", "parent" => {"name" => "duplicated"}}]}) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/tagged_message_spec.rb000066400000000000000000000004601515437321200271640ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::TaggedMessage, deprecation_mode: :silent do it "is a MessageAttributes" do obj = Lumberjack::Formatter::TaggedMessage.new("foo", bar: "baz") expect(obj).to be_a(Lumberjack::MessageAttributes) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/tags_formatter_spec.rb000066400000000000000000000024171515437321200272520ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::TagsFormatter do it "is registered as :tags" do expect(Lumberjack::FormatterRegistry.formatter(:tags)).to be_a(Lumberjack::Formatter::TagsFormatter) end describe "#call" do it "formats a hash of tags" do formatter = Lumberjack::Formatter::TagsFormatter.new tags = {foo: "bar", baz: "qux"} expect(formatter.call(tags)).to eq("[foo=bar] [baz=qux]") end it "formats an array of tags" do formatter = Lumberjack::Formatter::TagsFormatter.new tags = ["foo", "bar"] expect(formatter.call(tags)).to eq("[foo] [bar]") end it "returns a string for a single tag" do formatter = Lumberjack::Formatter::TagsFormatter.new tags = "foo" expect(formatter.call(tags)).to eq("[foo]") end it "formats hashes inside arrays" do formatter = Lumberjack::Formatter::TagsFormatter.new tags = [{foo: "bar"}, {baz: "qux"}] expect(formatter.call(tags)).to eq("[foo=bar] [baz=qux]") end it "formats mixed arrays" do formatter = Lumberjack::Formatter::TagsFormatter.new tags = ["foo", {bar: "baz", fip: "fop"}] expect(formatter.call(tags)).to eq("[foo] [bar=baz] [fip=fop]") end end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter/truncate_formatter_spec.rb000066400000000000000000000015361515437321200301420ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter::TruncateFormatter do it "is registered as :truncate" do expect(Lumberjack::FormatterRegistry.formatter(:truncate)).to be_a(Lumberjack::Formatter::TruncateFormatter) end it "should truncate a string longer than the limit" do formatter = Lumberjack::Formatter::TruncateFormatter.new(9) expect(formatter.call("1234567890")).to eq "12345678…" end it "should not truncate a string that is shorter than the length" do formatter = Lumberjack::Formatter::TruncateFormatter.new(9) expect(formatter.call("123456789")).to eq "123456789" end it "should pass through objects that are not strings" do formatter = Lumberjack::Formatter::TruncateFormatter.new(9) expect(formatter.call(:abcdefghijklmnop)).to eq :abcdefghijklmnop end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter_registry_spec.rb000066400000000000000000000022661515437321200261630ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::FormatterRegistry do after do Lumberjack::FormatterRegistry.remove(:test) end it "can add a formatter as a block" do expect(Lumberjack::FormatterRegistry.registered?(:test)).to be false Lumberjack::FormatterRegistry.add(:test) { |value| value.to_s.upcase } expect(Lumberjack::FormatterRegistry.registered?(:test)).to be true formatter = Lumberjack::FormatterRegistry.formatter(:test) expect(formatter.call("Test")).to eq("TEST") end it "can add a formatter with an object" do formatter = lambda { |value| value.to_s.capitalize } Lumberjack::FormatterRegistry.add(:test, formatter) expect(Lumberjack::FormatterRegistry.formatter(:test)).to eq(formatter) end it "can add a formatter as a class" do Lumberjack::FormatterRegistry.add(:test, Lumberjack::Formatter::TruncateFormatter) formatter = Lumberjack::FormatterRegistry.formatter(:test, 3) expect(formatter.call("Test")).to eq("Te…") end it "raises an error if the formatter is not registered" do expect { Lumberjack::FormatterRegistry.formatter(:unknown) }.to raise_error(ArgumentError) end end bdurand-lumberjack-ac97435/spec/lumberjack/formatter_spec.rb000066400000000000000000000214341515437321200242310ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Formatter do let(:formatter) { Lumberjack::Formatter.default } describe "optimized formatters for primitive types" do let(:formatter) { Lumberjack::Formatter.new.add(Object, :inspect) } it "should have an optimized set of formatters that return self for primitive types" do expect(formatter.format("foo")).to eq("foo") expect(formatter.format(1)).to eq(1) expect(formatter.format(2.1)).to eq(2.1) expect(formatter.format(true)).to eq(true) expect(formatter.format(false)).to eq(false) expect(formatter.format(:foo)).to eq(":foo") end it "should be able to override the optimized formatters" do formatter.add(String) { |s| s.upcase } expect(formatter.format("foo")).to eq("FOO") expect(formatter.format(1)).to eq(1) # Still uses optimized formatter end end describe "#build" do it "builds a formatter in a block" do formatter = Lumberjack::Formatter.build do |config| config.add(String) { |s| s.upcase } config.add(Integer) { |obj| "number: #{obj}" } end expect(formatter.format("foo")).to eq("FOO") expect(formatter.format(10)).to eq("number: 10") end end describe "#formatter_for" do let(:formatter) do Lumberjack::Formatter.build do |config| config.add(Numeric, :round) config.add(Lumberjack::LogEntry, :inspect) end end it "returns the formatter for a specific class" do expect(formatter.formatter_for(Integer)).to be_a(Lumberjack::Formatter::RoundFormatter) expect(formatter.formatter_for("Float")).to be_a(Lumberjack::Formatter::RoundFormatter) expect(formatter.formatter_for("Lumberjack::LogEntry")).to be_a(Lumberjack::Formatter::InspectFormatter) end it "returns nil for unknown classes" do expect(formatter.formatter_for("Foo::Bar")).to be_nil end it "returns an exact match even if the class doesn't exist" do formatter = Lumberjack::Formatter.build do |config| config.add("Foo::Bar", :inspect) end expect(formatter.formatter_for("Foo::Bar")).to be_a(Lumberjack::Formatter::InspectFormatter) end end describe "#include?" do it "returns true if a class has been added" do formatter = Lumberjack::Formatter.new expect(formatter.include?(Object)).to be false formatter.add(Object, :inspect) expect(formatter.include?(Object)).to be true expect(formatter.include?("Object")).to be true end it "does not return true for subclasses" do formatter = Lumberjack::Formatter.new formatter.add(Object, :inspect) expect(formatter.include?(Exception)).to be false expect(formatter.include?(Object)).to be true end it "only returns true for one of the default formatters if it's been overridden" do formatter = Lumberjack::Formatter.new expect(formatter.include?(String)).to be false formatter.add(String, :inspect) expect(formatter.include?(String)).to be true end end describe "#format" do it "should have a default set of formatters" do expect(formatter.format("abc")).to eq("abc") expect(formatter.format([1, 2, 3])).to eq([1, 2, 3]) expect(formatter.format(ArgumentError.new("boom"))).to eq("ArgumentError: boom") end it "should be able to add a formatter object for a class" do formatter.add(Numeric, lambda { |obj| "number: #{obj}" }) expect(formatter.format(10)).to eq("number: 10") end it "should be able to add a formatter object for a class name" do formatter.add("Numeric", lambda { |obj| "number: #{obj}" }) expect(formatter.format(10)).to eq("number: 10") end it "should be able to add a formatter object for multiple classes" do formatter.add([Numeric, NilClass], &:to_i) expect(formatter.format(10.1)).to eq(10) expect(formatter.format(nil)).to eq(0) end it "should be able to add a formatter with arguments" do formatter.add(String, :truncate, 9) expect(formatter.format("1234567890")).to eq("12345678…") end it "should be able to add a formatter object for a module" do formatter.add(Enumerable, lambda { |obj| "list: #{obj.inspect}" }) expect(formatter.format([1, 2])).to eq("list: [1, 2]") end it "should be able to add a formatter block for a class" do formatter.add(Numeric) { |obj| "number: #{obj}" } expect(formatter.format(10)).to eq("number: 10") end it "should be able to remove a formatter for a class" do formatter = Lumberjack::Formatter.new formatter.add(Symbol, :inspect) expect(formatter.format(:foo)).to eq(":foo") formatter.remove(Symbol) expect(formatter.format(:foo)).to eq(:foo) end it "should be able to remove a formatter for a class" do formatter = Lumberjack::Formatter.new formatter.add([Symbol, Array], :inspect) expect(formatter.format(:foo)).to eq(":foo") formatter.remove([Symbol, Array]) expect(formatter.format(:foo)).to eq(:foo) end it "should be able to remove multiple formatters" do formatter = Lumberjack::Formatter.new formatter.add([Symbol, Array], :inspect) expect(formatter.format(:foo)).to eq(":foo") expect(formatter.format([1, 2, 3])).to eq([1, 2, 3].inspect) formatter.remove([Symbol, Array]) expect(formatter.format(:foo)).to eq(:foo) expect(formatter.format([1, 2, 3])).to eq([1, 2, 3]) end it "should be able to chain add and remove calls" do expect(formatter.remove(String)).to eq(formatter) expect(formatter.add(String, Lumberjack::Formatter::StringFormatter.new)).to eq(formatter) end it "should format an object based on the class hierarchy" do formatter.add(Numeric) { |obj| "number: #{obj}" } formatter.add(Integer) { |obj| "fixed number: #{obj}" } expect(formatter.format(10)).to eq("fixed number: 10") expect(formatter.format(10.1)).to eq("number: 10.1") end it "should have a default object formatter" do expect(formatter.format(:test)).to eq(":test") formatter.remove(Object) expect(formatter.format(:test)).to eq(:test) end it "applies the to_log_format method if there is no registered formatter" do obj = TestToLogFormat.new("test") expect(formatter.format(obj)).to eq("LOG FORMAT: test") end it "overrides the to_log_format method if there is a registered formatter" do formatter.add(TestToLogFormat) { |obj| obj.value.upcase } obj = TestToLogFormat.new("test") expect(formatter.format(obj)).to eq("TEST") end it "does not override the to_log_format if the registered formatter is on a parent class" do formatter.add(:object) { |obj| "Object:#{obj.object_id}" } obj = TestToLogFormat.new("test") expect(formatter.format(obj)).to eq("LOG FORMAT: test") end it "returns an error string if there was an error formatting the value" do save_stderr = $stderr begin $stderr = StringIO.new formatter.add(String, lambda { |obj| raise "error" }) expect(formatter.format("abc")).to eq("") ensure $stderr = save_stderr end end describe ".clear" do it "should clear all mappings" do expect(formatter.format(:test)).to eq(":test") formatter.clear expect(formatter.format(:test)).to eq(:test) end end end describe ".empty", deprecation_mode: :silent do it "should be able to get an empty formatter" do expect(Lumberjack::Formatter.empty.format(:test)).to eq(:test) end end describe "#include" do it "merges the formats from the formatter" do formatter_1 = Lumberjack::Formatter.new formatter_1.add(String) { |s| s.to_s.upcase } formatter_1.add(Float, :round, 1) formatter_2 = Lumberjack::Formatter.new formatter_2.add(String) { |s| s.to_s.downcase } formatter_2.add(Integer, :multiply, 2) expect(formatter_2.include(formatter_1)).to eq formatter_2 expect(formatter_2.format("Test")).to eq("TEST") expect(formatter_2.format(3.14)).to eq(3.1) expect(formatter_2.format(2)).to eq(4) end end describe "#prepend" do it "prepends the formats from the formatter" do formatter_1 = Lumberjack::Formatter.new formatter_1.add(String) { |s| s.to_s.upcase } formatter_1.add(Float, :round, 1) formatter_2 = Lumberjack::Formatter.new formatter_2.add(String) { |s| s.to_s.downcase } formatter_2.add(Integer, :multiply, 2) expect(formatter_2.prepend(formatter_1)).to eq formatter_2 expect(formatter_2.format("Test")).to eq("test") expect(formatter_2.format(3.14)).to eq(3.1) expect(formatter_2.format(2)).to eq(4) end end end bdurand-lumberjack-ac97435/spec/lumberjack/io_compatibility_spec.rb000066400000000000000000000037521515437321200255710ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::IOCompatibility do let(:logger) { TestContextLogger.new(Lumberjack::Context.new) } it "can write to the log" do logger.write("Hello, world") expect(logger.entries).to eq([ { message: "Hello, world", severity: Logger::UNKNOWN, progname: nil, attributes: nil } ]) end it "writes to the log using the default severity" do logger.default_severity = Logger::INFO logger.write("Hello, world") expect(logger.entries).to eq([ { message: "Hello, world", severity: Logger::INFO, progname: nil, attributes: nil } ]) end it "can puts to the log" do logger.puts("Hello", "world") expect(logger.entries).to eq([ { message: "Hello", severity: Logger::UNKNOWN, progname: nil, attributes: nil }, { message: "world", severity: Logger::UNKNOWN, progname: nil, attributes: nil } ]) end it "can print objects to the log" do logger.print("Hello", "world") expect(logger.entries).to eq([ { message: "Helloworld", severity: Logger::UNKNOWN, progname: nil, attributes: nil } ]) end it "can printf to the log" do logger.printf("Hello %s", "world") expect(logger.entries).to eq([ { message: "Hello world", severity: Logger::UNKNOWN, progname: nil, attributes: nil } ]) end it "responds to flush" do expect { logger.flush }.to_not raise_error end it "responds to close" do expect { logger.close }.to_not raise_error end it "responds to closed?" do expect(logger.closed?).to be false end it "is not a tty?" do expect(logger.tty?).to be false end it "alias isatty to tty?" do expect(logger).to receive(:isatty).and_call_original expect(logger.isatty).to be false end end bdurand-lumberjack-ac97435/spec/lumberjack/local_log_template_spec.rb000066400000000000000000000127561515437321200260630ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::LocalLogTemplate do let(:entry) do Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test message", "myapp", 1234, "foo" => "bar", "baz.bax" => "qux") end it "formats log entries with values pertinent to test environments" do template = Lumberjack::LocalLogTemplate.new formatted = template.call(entry) expected = <<~STRING.chomp INFO test message progname: myapp baz.bax: qux foo: bar STRING expect(formatted).to eq(expected) end it "can add the time" do template = Lumberjack::LocalLogTemplate.new(exclude_time: false) formatted = template.call(entry) expected = <<~STRING.chomp #{entry.time.strftime("%Y-%m-%d %H:%M:%S.%6N")} INFO test message progname: myapp baz.bax: qux foo: bar STRING expect(formatted).to eq(expected) end it "can add the pid" do template = Lumberjack::LocalLogTemplate.new(exclude_pid: false) formatted = template.call(entry) expected = <<~STRING.chomp INFO test message progname: myapp pid: 1234 baz.bax: qux foo: bar STRING expect(formatted).to eq(expected) end it "can exclude the progname" do template = Lumberjack::LocalLogTemplate.new(exclude_progname: true) formatted = template.call(entry) expected = <<~STRING.chomp INFO test message baz.bax: qux foo: bar STRING expect(formatted).to eq(expected) end it "can exclude all attributes" do template = Lumberjack::LocalLogTemplate.new(exclude_attributes: true) formatted = template.call(entry) expected = <<~STRING.chomp INFO test message progname: myapp STRING expect(formatted).to eq(expected) end it "can exclude specific attributes" do template = Lumberjack::LocalLogTemplate.new(exclude_attributes: ["foo"]) formatted = template.call(entry) expected = <<~STRING.chomp INFO test message progname: myapp baz.bax: qux STRING expect(formatted).to eq(expected) end it "can exclude specific nested attributes" do template = Lumberjack::LocalLogTemplate.new(exclude_attributes: ["baz"]) formatted = template.call(entry) expected = <<~STRING.chomp INFO test message progname: myapp foo: bar STRING expect(formatted).to eq(expected) end it "can colorize the output" do template = Lumberjack::LocalLogTemplate.new(colorize: true) formatted = template.call(entry) expected = <<~STRING.chomp \e7#{entry.severity_data.terminal_color}INFO test message\e8 \e7#{entry.severity_data.terminal_color} progname: myapp\e8 \e7#{entry.severity_data.terminal_color} baz.bax: qux\e8 \e7#{entry.severity_data.terminal_color} foo: bar\e8 STRING expect(formatted).to eq(expected) end it "can set the severity format to padded" do template = Lumberjack::LocalLogTemplate.new(severity_format: :padded) formatted = template.call(entry) expected = <<~STRING.chomp INFO test message progname: myapp baz.bax: qux foo: bar STRING expect(formatted).to eq(expected) end it "can set the severity format to emoji" do template = Lumberjack::LocalLogTemplate.new(severity_format: :emoji) formatted = template.call(entry) expected = <<~STRING.chomp 🔵 test message progname: myapp baz.bax: qux foo: bar STRING expect(formatted).to eq(expected) end it "can set the severity format to char" do template = Lumberjack::LocalLogTemplate.new(severity_format: :char) formatted = template.call(entry) expected = <<~STRING.chomp I test message progname: myapp baz.bax: qux foo: bar STRING expect(formatted).to eq(expected) end it "can set the severity format to level" do template = Lumberjack::LocalLogTemplate.new(severity_format: :level) formatted = template.call(entry) expected = <<~STRING.chomp 1 test message progname: myapp baz.bax: qux foo: bar STRING expect(formatted).to eq(expected) end it "can formats exceptions with a backtrace by default" do exception = begin raise StandardError, "Something went wrong" rescue => e e end entry = Lumberjack::LogEntry.new(Time.now, Logger::ERROR, exception, "myapp", 1234, "foo" => "bar") template = Lumberjack::LocalLogTemplate.new formatted = template.call(entry) expected = <<~STRING.chomp ERROR StandardError: Something went wrong #{exception.backtrace.join("#{Lumberjack::LINE_SEPARATOR} ")} progname: myapp foo: bar STRING expect(formatted).to eq(expected) end it "can use a custom exception formatter" do exception = begin raise StandardError, "Something went wrong" rescue => e e end entry = Lumberjack::LogEntry.new(Time.now, Logger::ERROR, exception, "myapp", 1234, "foo" => "bar") custom_formatter = lambda do |ex| "CustomException: #{ex.class}: #{ex.message}" end template = Lumberjack::LocalLogTemplate.new(exception_formatter: custom_formatter) formatted = template.call(entry) expected = <<~STRING.chomp ERROR CustomException: StandardError: Something went wrong progname: myapp foo: bar STRING expect(formatted).to eq(expected) end end bdurand-lumberjack-ac97435/spec/lumberjack/log_entry_matcher/000077500000000000000000000000001515437321200243705ustar00rootroot00000000000000bdurand-lumberjack-ac97435/spec/lumberjack/log_entry_matcher/score_spec.rb000066400000000000000000000205161515437321200270460ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../../spec_helper" RSpec.describe Lumberjack::LogEntryMatcher::Score do let(:logger) { Lumberjack::Logger.new(:test) } # Create real log entries using the capture device let(:entries) do logger.info("User logged in successfully") logger.warn("Database connection slow") logger.error("Failed to authenticate user") logger.debug("Processing request", user_id: 123, action: "login") logger.progname = "TestApp" logger.info("Service started", service: "test") logger.device.entries end let(:entry_info) { entries[0] } # "User logged in successfully" let(:entry_warn) { entries[1] } # "Database connection slow" let(:entry_error) { entries[2] } # "Failed to authenticate user" let(:entry_debug) { entries[3] } # "Processing request" with attributes let(:entry_with_progname) { entries[4] } # "Service started" with progname describe ".calculate_match_score" do it "returns 1.0 for perfect matches" do score = Lumberjack::LogEntryMatcher::Score.calculate_match_score( entry_info, message: "User logged in successfully", severity: Logger::INFO, attributes: {}, progname: nil ) expect(score).to eq 1.0 end it "returns 0.0 when no criteria are provided" do score = Lumberjack::LogEntryMatcher::Score.calculate_match_score(entry_info) expect(score).to eq 0.0 end it "returns partial scores for partial matches" do score = Lumberjack::LogEntryMatcher::Score.calculate_match_score( entry_info, message: "User logged in successfully", severity: Logger::WARN # Different severity ) expect(score).to be > 0.5 expect(score).to be < 1.0 end it "considers severity proximity for nearby severitys" do exact_score = Lumberjack::LogEntryMatcher::Score.calculate_match_score( entry_info, severity: Logger::INFO ) nearby_score = Lumberjack::LogEntryMatcher::Score.calculate_match_score( entry_info, severity: Logger::WARN # One severity away ) distant_score = Lumberjack::LogEntryMatcher::Score.calculate_match_score( entry_info, severity: Logger::FATAL # Far away ) expect(exact_score).to be > nearby_score expect(nearby_score).to be > distant_score end it "scores entries below minimum threshold as 0" do score = Lumberjack::LogEntryMatcher::Score.calculate_match_score( entry_info, message: "completely different message", severity: Logger::FATAL, attributes: {different: "attributes"}, progname: "DifferentApp" ) expect(score).to be < Lumberjack::LogEntryMatcher::Score::MIN_SCORE_THRESHOLD end it "handles attribute matching" do score = Lumberjack::LogEntryMatcher::Score.calculate_match_score( entry_debug, attributes: {user_id: 123} ) expect(score).to be > 0.0 end it "handles progname matching" do score = Lumberjack::LogEntryMatcher::Score.calculate_match_score( entry_with_progname, progname: "TestApp" ) expect(score).to be > 0.0 end end describe ".calculate_field_score" do context "with string filters" do it "returns 1.0 for exact matches" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test message", "test message") expect(score).to eq 1.0 end it "returns 0.7 for substring matches" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test message", "test") expect(score).to eq 0.7 end it "returns similarity score for partial matches" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test message", "test mesage") # typo expect(score).to be > 0.0 expect(score).to be < 0.7 end it "returns 0.0 for completely different strings" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test message", "xyz") expect(score).to eq 0.0 end end context "with regex filters" do it "returns 1.0 for matching regex" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test message", /test/) expect(score).to eq 1.0 end it "returns 0.0 for non-matching regex" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test message", /xyz/) expect(score).to eq 0.0 end end context "with other matchers" do it "returns 1.0 when matcher returns true" do matcher = double("matcher") allow(matcher).to receive(:===).with("test").and_return(true) score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test", matcher) expect(score).to eq 1.0 end it "returns 0.0 when matcher returns false" do matcher = double("matcher") allow(matcher).to receive(:===).with("test").and_return(false) score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test", matcher) expect(score).to eq 0.0 end it "returns 0.0 when matcher raises an exception" do matcher = double("matcher") allow(matcher).to receive(:===).with("test").and_raise(StandardError) score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test", matcher) expect(score).to eq 0.0 end end context "with nil values" do it "returns 0.0 when value is nil" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score(nil, "test") expect(score).to eq 0.0 end it "returns 0.0 when filter is nil" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score("test", nil) expect(score).to eq 0.0 end it "returns 0.0 when both are nil" do score = Lumberjack::LogEntryMatcher::Score.calculate_field_score(nil, nil) expect(score).to eq 0.0 end end end describe ".severity_proximity_score" do it "returns 1.0 for exact severity match" do score = Lumberjack::LogEntryMatcher::Score.severity_proximity_score( Logger::INFO, Logger::INFO ) expect(score).to eq 1.0 end it "returns 0.7 for one severity difference" do score = Lumberjack::LogEntryMatcher::Score.severity_proximity_score( Logger::INFO, Logger::WARN ) expect(score).to eq 0.7 end it "returns 0.4 for two severity difference" do score = Lumberjack::LogEntryMatcher::Score.severity_proximity_score( Logger::DEBUG, Logger::WARN ) expect(score).to eq 0.4 end it "returns 0.0 for three or more severity difference" do score = Lumberjack::LogEntryMatcher::Score.severity_proximity_score( Logger::DEBUG, Logger::FATAL ) expect(score).to eq 0.0 end end describe ".calculate_attributes_score" do let(:entry_attributes) { {user_id: 123, action: "login", metadata: {ip: "192.168.1.1"}} } it "returns 1.0 for exact attribute matches" do score = Lumberjack::LogEntryMatcher::Score.calculate_attributes_score(entry_attributes, {user_id: 123}) expect(score).to eq 1.0 end it "returns partial score for partially matching attributes" do score = Lumberjack::LogEntryMatcher::Score.calculate_attributes_score( entry_attributes, {user_id: 123, action: "logout"} # One matches, one doesn't ) expect(score).to eq 0.5 end it "returns 0.0 for completely non-matching attributes" do score = Lumberjack::LogEntryMatcher::Score.calculate_attributes_score( entry_attributes, {different_attribute: "value"} ) expect(score).to eq 0.0 end it "handles nested attribute matching" do score = Lumberjack::LogEntryMatcher::Score.calculate_attributes_score( entry_attributes, {metadata: {ip: "192.168.1.1"}} ) expect(score).to eq 1.0 end it "returns 0.0 when entry_attributes is nil" do score = Lumberjack::LogEntryMatcher::Score.calculate_attributes_score(nil, {user_id: 123}) expect(score).to eq 0.0 end it "returns 0.0 when attributes_filter is not a hash" do score = Lumberjack::LogEntryMatcher::Score.calculate_attributes_score(entry_attributes, "not a hash") expect(score).to eq 0.0 end end end bdurand-lumberjack-ac97435/spec/lumberjack/log_entry_matcher_spec.rb000066400000000000000000000301021515437321200257230ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::LogEntryMatcher do describe "#match?" do let(:entry) { Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Test message", "AppName", Process.pid, attributes) } let(:attributes) { {} } describe "severity filter" do it "matches if the severity is equal" do matcher = Lumberjack::LogEntryMatcher.new(severity: Logger::INFO) expect(matcher.match?(entry)).to be true end it "matches if the severity is equal using a severity name" do matcher = Lumberjack::LogEntryMatcher.new(severity: :info) expect(matcher.match?(entry)).to be true end it "does not match if the severity is not equal" do matcher = Lumberjack::LogEntryMatcher.new(severity: Logger::ERROR) expect(matcher.match?(entry)).to be false end end describe "message filter" do it "matches if the messages are equal" do matcher = Lumberjack::LogEntryMatcher.new(message: "Test message") expect(matcher.match?(entry)).to be true end it "does not match if the messages are not equal" do matcher = Lumberjack::LogEntryMatcher.new(message: "Different message") expect(matcher.match?(entry)).to be false end it "matches if the message matches a pattern" do matcher = Lumberjack::LogEntryMatcher.new(message: /Test/) expect(matcher.match?(entry)).to be true end it "does not match if the message does not match the pattern" do matcher = Lumberjack::LogEntryMatcher.new(message: /Different/) expect(matcher.match?(entry)).to be false end it "matches if the message matches the class" do matcher = Lumberjack::LogEntryMatcher.new(message: String) expect(matcher.match?(entry)).to be true end it "strips leading and trailing whitespace from string message filters" do matcher = Lumberjack::LogEntryMatcher.new(message: " Test message\n") expect(matcher.match?(entry)).to be true end end describe "progname filter" do it "matches if the progname is equal" do matcher = Lumberjack::LogEntryMatcher.new(progname: "AppName") expect(matcher.match?(entry)).to be true end it "does not match if the progname is not equal" do matcher = Lumberjack::LogEntryMatcher.new(progname: "DifferentApp") expect(matcher.match?(entry)).to be false end it "matches if the progname matches a pattern" do matcher = Lumberjack::LogEntryMatcher.new(progname: /App/) expect(matcher.match?(entry)).to be true end it "does not match if the progname does not match the pattern" do matcher = Lumberjack::LogEntryMatcher.new(progname: /Different/) expect(matcher.match?(entry)).to be false end it "matches if the progname matches the class" do matcher = Lumberjack::LogEntryMatcher.new(progname: String) expect(matcher.match?(entry)).to be true end end describe "attributes filter" do it "matches if the attribute is equal" do attributes["key"] = "value" matcher = Lumberjack::LogEntryMatcher.new(attributes: {key: "value"}) expect(matcher.match?(entry)).to be true end it "does not match if the attribute is not equal" do attributes["key"] = "value" matcher = Lumberjack::LogEntryMatcher.new(attributes: {key: "different"}) expect(matcher.match?(entry)).to be false end it "matches if the attribute matches a pattern" do attributes["key"] = "value" matcher = Lumberjack::LogEntryMatcher.new(attributes: {key: /val/}) expect(matcher.match?(entry)).to be true end it "does not match if the attribute does not match the pattern" do attributes["key"] = "value" matcher = Lumberjack::LogEntryMatcher.new(attributes: {key: /different/}) expect(matcher.match?(entry)).to be false end it "matches if the attribute matches the class" do attributes["key"] = 14 matcher = Lumberjack::LogEntryMatcher.new(attributes: {key: Integer}) expect(matcher.match?(entry)).to be true end it "does not match if the attribute does not match the class" do attributes["key"] = 14 matcher = Lumberjack::LogEntryMatcher.new(attributes: {key: String}) expect(matcher.match?(entry)).to be false end it "does not match if the attribute does not exist" do attributes["key"] = "value" matcher = Lumberjack::LogEntryMatcher.new(attributes: {other_key: "nonexistent"}) expect(matcher.match?(entry)).to be false end it "matches if all attributes match" do attributes["key_1"] = "value 1" attributes["key_2"] = "value 2" matcher = Lumberjack::LogEntryMatcher.new(attributes: {key_1: "value 1", key_2: "value 2"}) expect(matcher.match?(entry)).to be true end it "does not match if any values do not match" do attributes["key_1"] = "value 1" attributes["key_2"] = "value 2" matcher = Lumberjack::LogEntryMatcher.new(attributes: {key_1: "value 1", key_2: "different"}) expect(matcher.match?(entry)).to be false end it "matches a nil only if the attribute does not exist" do attributes["key"] = "value" expect(Lumberjack::LogEntryMatcher.new(attributes: {key: nil}).match?(entry)).to be false expect(Lumberjack::LogEntryMatcher.new(attributes: {other_key: nil}).match?(entry)).to be true end it "matches an empty array only if the attribute does not exist" do attributes["key"] = "value" expect(Lumberjack::LogEntryMatcher.new(attributes: {key: []}).match?(entry)).to be false expect(Lumberjack::LogEntryMatcher.new(attributes: {other_key: []}).match?(entry)).to be true end it "matches dot notation on attribute filters" do attributes["foo.bar.baz"] = "boo" expect(Lumberjack::LogEntryMatcher.new(attributes: {"foo.bar" => {"baz" => "boo"}}).match?(entry)).to be true expect(Lumberjack::LogEntryMatcher.new(attributes: {"foo.bar" => {"baz" => "bip"}}).match?(entry)).to be false expect(Lumberjack::LogEntryMatcher.new(attributes: {"foo.bar" => Hash}).match?(entry)).to be true expect(Lumberjack::LogEntryMatcher.new(attributes: {"foo.bar" => String}).match?(entry)).to be false end it "matches nested attribute filters" do attributes["foo.bar.baz"] = "boo" attributes["foo.bar.bip"] = "bop" expect(Lumberjack::LogEntryMatcher.new(attributes: {foo: {bar: {baz: "boo"}}}).match?(entry)).to be true expect(Lumberjack::LogEntryMatcher.new(attributes: {foo: {bar: {baz: "boo", bip: /b/}}}).match?(entry)).to be true expect(Lumberjack::LogEntryMatcher.new(attributes: {foo: {bar: {baz: "boo", bip: /c/}}}).match?(entry)).to be false expect(Lumberjack::LogEntryMatcher.new(attributes: {foo: {"bar.baz": "boo"}}).match?(entry)).to be true end it "should match arrays of hashes" do attributes["foo"] = [{bar: "baz"}, {bip: "bop"}] expect(Lumberjack::LogEntryMatcher.new(attributes: {foo: [{bar: "baz"}, {bip: "bop"}]}).match?(entry)).to be true expect(Lumberjack::LogEntryMatcher.new(attributes: {foo: [{bar: "baz"}]}).match?(entry)).to be false end it "does not match an entry with no attributes" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Test message", nil, nil, nil) matcher = Lumberjack::LogEntryMatcher.new(attributes: {key: "value"}) expect(matcher.match?(entry)).to be false end end describe "multiple filters" do it "matches if all filters match" do matcher = Lumberjack::LogEntryMatcher.new(message: /Test/, progname: "AppName") expect(matcher.match?(entry)).to be true end it "does not match if any filters do not match" do matcher = Lumberjack::LogEntryMatcher.new(message: /Test/, progname: "DifferentApp") expect(matcher.match?(entry)).to be false end end end describe "#closest" do let(:user_logged_in) { Lumberjack::LogEntry.new(Time.now, Logger::INFO, "User logged in successfully", nil, nil, nil) } let(:database_slow) { Lumberjack::LogEntry.new(Time.now, Logger::WARN, "Database connection slow", nil, nil, nil) } let(:failed_auth) { Lumberjack::LogEntry.new(Time.now, Logger::ERROR, "Failed to authenticate user", nil, nil, nil) } let(:processing_request) { Lumberjack::LogEntry.new(Time.now, Logger::DEBUG, "Processing request", nil, nil, {"user_id" => 123, "action" => "login"}) } let(:service_started) { Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Service started", "TestService", nil, {"service" => "test"}) } let(:entries) do [ user_logged_in, database_slow, failed_auth, processing_request, service_started ] end it "should return nil when there are no entries" do matcher = Lumberjack::LogEntryMatcher.new(severity: :info, message: "test") expect(matcher.closest([])).to be_nil end it "should return the exact match when criteria match perfectly" do matcher = Lumberjack::LogEntryMatcher.new(severity: :info, message: "User logged in successfully") expect(matcher.closest(entries)).to eq user_logged_in end it "should handle regex patterns in message matching" do matcher = Lumberjack::LogEntryMatcher.new(message: /authenticate/) expect(matcher.closest(entries)).to eq failed_auth end it "should return the closest match based on message similarity" do matcher = Lumberjack::LogEntryMatcher.new(severity: :info, message: "User login successful") expect(matcher.closest(entries)).to eq user_logged_in end it "should find matches with the approximate severity when exact severity doesn't match" do matcher = Lumberjack::LogEntryMatcher.new(severity: :info, message: "Database connection") expect(matcher.closest(entries)).to eq database_slow end it "should match based on attributes" do matcher = Lumberjack::LogEntryMatcher.new(attributes: {user_id: 123}) expect(matcher.closest(entries)).to eq processing_request end it "should handle nested attribute matching" do nested_entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "Nested test", nil, nil, {"user.id" => 456, "user.name" => "John"}) entries << nested_entry matcher = Lumberjack::LogEntryMatcher.new(attributes: {user: {id: 456}}) expect(matcher.closest(entries)).to eq nested_entry end it "should match based on progname" do matcher = Lumberjack::LogEntryMatcher.new(progname: "TestService") expect(matcher.closest(entries)).to eq service_started end it "should handle string similarity for progname" do matcher = Lumberjack::LogEntryMatcher.new(progname: "TestServ") expect(matcher.closest(entries)).to eq service_started end it "should return nil when no entry meets minimum criteria" do matcher = Lumberjack::LogEntryMatcher.new(severity: :fatal, message: "Completely different message") expect(matcher.closest(entries)).to be_nil end it "should handle multiple criteria and weight them properly" do matcher = Lumberjack::LogEntryMatcher.new( severity: :debug, message: "Processing", attributes: {action: "login"} ) result = matcher.closest(entries) expect(result).to eq processing_request end it "should return the best match when multiple entries partially match" do entries = [ Lumberjack::LogEntry.new(Time.now, Logger::INFO, "User authentication started", nil, nil, nil), Lumberjack::LogEntry.new(Time.now, Logger::INFO, "User authentication failed", nil, nil, nil), Lumberjack::LogEntry.new(Time.now, Logger::INFO, "User authentication successful", nil, nil, nil) ] matcher = Lumberjack::LogEntryMatcher.new(message: "authentication success") expect(matcher.closest(entries).message).to eq "User authentication successful" end end end bdurand-lumberjack-ac97435/spec/lumberjack/log_entry_spec.rb000066400000000000000000000201461515437321200242270ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::LogEntry do describe "entry values" do it "should have a time" do t = Time.now entry = Lumberjack::LogEntry.new(t, Logger::INFO, "test", "app", 1500, "foo" => "ABCD") expect(entry.time).to eq(t) entry.time = t + 1 expect(entry.time).to eq(t + 1) end it "should have a severity" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo" => "ABCD") expect(entry.severity).to eq(Logger::INFO) entry.severity = Logger::WARN expect(entry.severity).to eq(Logger::WARN) end it "should convert a severity label to a numeric level" do entry = Lumberjack::LogEntry.new(Time.now, "INFO", "test", "app", 1500, "foo" => "ABCD") expect(entry.severity).to eq(Logger::INFO) end it "should get the severity as a string" do expect(Lumberjack::LogEntry.new(Time.now, Logger::DEBUG, "test", "app", 1500, nil).severity_label).to eq("DEBUG") expect(Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, nil).severity_label).to eq("INFO") expect(Lumberjack::LogEntry.new(Time.now, Logger::WARN, "test", "app", 1500, nil).severity_label).to eq("WARN") expect(Lumberjack::LogEntry.new(Time.now, Logger::ERROR, "test", "app", 1500, nil).severity_label).to eq("ERROR") expect(Lumberjack::LogEntry.new(Time.now, Logger::FATAL, "test", "app", 1500, nil).severity_label).to eq("FATAL") expect(Lumberjack::LogEntry.new(Time.now, -100, "test", "app", 1500, nil).severity_label).to eq("ANY") expect(Lumberjack::LogEntry.new(Time.now, 1000, "test", "app", 1500, nil).severity_label).to eq("ANY") end it "should have a message" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo" => "ABCD") expect(entry.message).to eq("test") entry.message = "new message" expect(entry.message).to eq("new message") end it "should have a progname" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo" => "ABCD") expect(entry.progname).to eq("app") entry.progname = "prog" expect(entry.progname).to eq("prog") end it "should have a pid" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo" => "ABCD") expect(entry.pid).to eq(1500) entry.pid = 150 expect(entry.pid).to eq(150) end end describe "#severity_data" do it "returns the severity data object" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo" => "ABCD") expect(entry.severity_data).to eq(Lumberjack::Severity.data(Logger::INFO)) end end describe "#attributes" do it "should have attributes" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo" => "ABCD") expect(entry.attributes).to eq("foo" => "ABCD") end it "should flatten attributes that are set to empty values" do attributes = { "a" => "A", "b" => nil, "c" => "", "d" => {"e" => "E", "f" => nil}, "g" => {"h" => "", "i" => []} } entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, attributes) expect(entry.attributes).to eq("a" => "A", "d.e" => "E") end end describe "#empty?" do it "is empty if the log has no message and no attributes" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, nil, "app", 1500, nil) expect(entry.empty?).to be true entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "", "app", 1500, {}) expect(entry.empty?).to be true entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "message", "app", 1500, nil) expect(entry.empty?).to be false entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, nil, "app", 1500, {attribute: "value"}) expect(entry.empty?).to be false end end describe "#[]]" do it "returns the attribute value for a given name" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "a" => 1, "b" => 2) expect(entry["a"]).to eq(1) expect(entry["b"]).to eq(2) expect(entry["non_existent"]).to be_nil end it "returns a hash when a attribute is a parent of a dot notation key" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo.bar" => "baz", "foo.far" => "qux") expect(entry["foo"]).to eq({"bar" => "baz", "far" => "qux"}) expect(entry["foo.bar"]).to eq("baz") expect(entry["foo.far"]).to eq("qux") end it "return nil if there are no tags" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, nil) expect(entry["a"]).to be_nil end end describe "#nested_attributes" do it "expands attributes into a nested structure" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "a.b.c" => 1, "a.b.d" => 2) expect(entry.nested_attributes).to eq({"a" => {"b" => {"c" => 1, "d" => 2}}}) end it "returns an empty hash if there are no attributes" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, nil) expect(entry.nested_attributes).to eq({}) end end describe "==" do let(:entry) { Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo" => "bar") } it "is equal to a log entry with the same attributes" do other_entry = Lumberjack::LogEntry.new(entry.time, entry.severity, entry.message, entry.progname, entry.pid, entry.attributes) expect(entry).to eq(other_entry) end it "is not equal to another class type" do expect(entry).not_to eq("not a log entry") end it "is not equal to an entry with a different time" do other_entry = Lumberjack::LogEntry.new(entry.time + 1, entry.severity, entry.message, entry.progname, entry.pid, entry.attributes) expect(entry).not_to eq(other_entry) end it "is not equal to an entry with a different severity" do other_entry = Lumberjack::LogEntry.new(entry.time, entry.severity + 1, entry.message, entry.progname, entry.pid, entry.attributes) expect(entry).not_to eq(other_entry) end it "is not equal to an entry with a different message" do other_entry = Lumberjack::LogEntry.new(entry.time, entry.severity, entry.message + " altered", entry.progname, entry.pid, entry.attributes) expect(entry).not_to eq(other_entry) end it "is not equal to an entry with a different progname" do other_entry = Lumberjack::LogEntry.new(entry.time, entry.severity, entry.message, entry.progname + " altered", entry.pid, entry.attributes) expect(entry).not_to eq(other_entry) end it "is not equal to an entry with a different pid" do other_entry = Lumberjack::LogEntry.new(entry.time, entry.severity, entry.message, entry.progname, entry.pid + 1, entry.attributes) expect(entry).not_to eq(other_entry) end it "is not equal to an entry with different attributes" do other_entry = Lumberjack::LogEntry.new(entry.time, entry.severity, entry.message, entry.progname, entry.pid, {"foo" => "baz"}) expect(entry).not_to eq(other_entry) end end describe "#to_s" do it "returns a formatted string representation of the log entry" do time = Time.now entry = Lumberjack::LogEntry.new(time, Logger::INFO, "test message", "myapp", 1234, "foo" => "bar", "baz" => "qux") expected_string = "[#{time.strftime(Lumberjack::LogEntry::TIME_FORMAT)} INFO myapp(1234)] test message [foo:bar] [baz:qux]" expect(entry.to_s).to eq(expected_string) end end describe "#as_json" do it "returns a hash representation of the log entry" do entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "foo.bar" => "baz") expect(entry.as_json).to eq({ "time" => entry.time, "severity" => "INFO", "message" => entry.message, "progname" => entry.progname, "pid" => entry.pid, "attributes" => {"foo" => {"bar" => "baz"}} }) end end end bdurand-lumberjack-ac97435/spec/lumberjack/logger_spec.rb000066400000000000000000000562761515437321200235210ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Logger do describe "compatibility" do it "should implement the same public API as the ::Logger class" do logger = ::Logger.new($stdout) lumberjack = Lumberjack::Logger.new($stdout) (logger.public_methods - Object.public_methods).each do |method_name| logger_method = logger.method(method_name) lumberjack_method = lumberjack.method(method_name) if logger_method.arity != lumberjack_method.arity fail "Lumberjack::Logger.#{method_name} has arity of #{lumberjack_method.arity} instead of #{logger_method.arity}" end end end end describe "initialization" do before :all do create_tmp_dir end after :all do delete_tmp_dir end it "should wrap an IO stream in a Writer device" do out = StringIO.new logger = Lumberjack::Logger.new(out) expect(logger.device.class).to eq(Lumberjack::Device::Writer) end it "should have a formatter" do out = StringIO.new logger = Lumberjack::Logger.new(out) expect(logger.formatter).to be end it "does not apply any formatting by default" do logger = Lumberjack::Logger.new(:test) obj = Object.new expect(logger.message_formatter.format(obj)).to eq(obj) end it "uses the default message formatter if formatter is :default" do logger = Lumberjack::Logger.new(:test, formatter: :default) obj = Object.new expect(logger.message_formatter.format(obj)).to eq(obj.inspect) end it "should have a message formatter" do out = StringIO.new logger = Lumberjack::Logger.new(out) expect(logger.message_formatter).to be end it "should have an attribute formatter" do out = StringIO.new logger = Lumberjack::Logger.new(out) expect(logger.attribute_formatter).to be end it "should open a file path in a LogFile device" do logger = Lumberjack::Logger.new(File.join(tmp_dir, "log_file_1.log")) expect(logger.device.class).to eq(Lumberjack::Device::LogFile) end it "should open a pathname in a LogFile device" do logger = Lumberjack::Logger.new(Pathname.new(File.join(tmp_dir, "log_file_1.log"))) expect(logger.device).to be_a(Lumberjack::Device::LogFile) end it "should open a File in a LogFile device" do file = File.new(File.join(tmp_dir, "log_file_1.log"), "w") logger = Lumberjack::Logger.new(file) expect(logger.device.class).to eq(Lumberjack::Device::LogFile) end it "should open a Lumberjack Logger in a LoggerWrapper device" do logger = Lumberjack::Logger.new(Lumberjack::Logger.new(File::NULL)) expect(logger.device.class).to eq(Lumberjack::Device::LoggerWrapper) end it "should open a tty stream in a Writer device" do out = StringIO.new allow(out).to receive(:tty?).and_return(true) logger = Lumberjack::Logger.new(out) expect(logger.device.class).to eq(Lumberjack::Device::Writer) end it "should use the null device if the stream is :null" do logger = Lumberjack::Logger.new(:null) expect(logger.device.class).to eq(Lumberjack::Device::Null) end it "should use the test device if the stream is :test" do logger = Lumberjack::Logger.new(:test) expect(logger.device.class).to eq(Lumberjack::Device::Test) end it "should create a multi device if the stream is an array" do stream_1 = StringIO.new stream_2 = StringIO.new logger = Lumberjack::Logger.new( [ stream_1, [stream_2, {attribute_format: "(%s:%s)"}] ], template: "{{severity}} - {{message}} {{attributes}}", attribute_format: "[%s=%s]" ) device = logger.device expect(device).to be_a(Lumberjack::Device::Multi) expect(device.devices.collect(&:dev)).to eq [stream_1, stream_2] logger.info("test", foo: "bar") expect(stream_1.string).to eq("INFO - test [foo=bar]#{Lumberjack::LINE_SEPARATOR}") expect(stream_2.string).to eq("INFO - test (foo:bar)#{Lumberjack::LINE_SEPARATOR}") end it "should set the level with a numeric" do logger = Lumberjack::Logger.new(:null, level: Logger::WARN) expect(logger.level).to eq(Logger::WARN) end it "should set the level with a level" do logger = Lumberjack::Logger.new(:null, level: :warn) expect(logger.level).to eq(Logger::WARN) end it "should default the level to DEBUG" do logger = Lumberjack::Logger.new(:null) expect(logger.level).to eq(Logger::DEBUG) end it "should set the level within a block" do logger = Lumberjack::Logger.new(:null, level: :warn) retval = logger.with_level(:info) do expect(logger.level).to eq(Logger::INFO) :foo end expect(retval).to eq(:foo) expect(logger.level).to eq(Logger::WARN) end it "should set the progname" do logger = Lumberjack::Logger.new(:null, progname: "app") expect(logger.progname).to eq("app") end it "allows the deprecated method of passing an options hash", deprecation_mode: :silent do output = StringIO.new logger = Lumberjack::Logger.new(output, {level: :warn, template: "{{message}} {{attributes}}"}, attribute_format: "%s=%s") expect(logger.level).to eq(Logger::WARN) logger.warn("test", foo: "bar") expect(output.string).to eq("test foo=bar#{Lumberjack::LINE_SEPARATOR}") end it "allows using the deprecated :max_size option without blowing up", deprecation_mode: :silent do expect { Lumberjack::Logger.new(File::NULL, max_size: 10) }.to_not raise_error end it "converts the shift_size option to a numeric if given as a string with units" do logger = Lumberjack::Logger.new(File::NULL, 0, "10M") expect(logger.device.send(:stream).instance_variable_get(:@shift_size)).to eq(10 * 1024 * 1024) end it "allows using the deprecated :message_formatter option without blowing up", deprecation_mode: :silent do message_formatter = Lumberjack::Formatter.new.add(String) { |s| s.upcase } logger = Lumberjack::Logger.new(File::NULL, message_formatter: message_formatter) msg, _ = logger.formatter.format("foobar", {}) expect(msg).to eq("FOOBAR") end it "allows using the deprecated :tag_formatter option without blowing up", deprecation_mode: :silent do attribute_formatter = Lumberjack::TagFormatter.new.add_class(String) { |s| s.upcase } logger = Lumberjack::Logger.new(File::NULL, tag_formatter: attribute_formatter) _, attributes = logger.formatter.format(nil, {"test" => "foobar"}) expect(attributes).to eq("test" => "FOOBAR") end it "can set the isolation level" do logger = Lumberjack::Logger.new(:test, isolation_level: :thread) expect(logger.isolation_level).to eq :thread end end describe "#set_progname", deprecation_mode: :silent do it "should be able to set the progname in a block" do logger = Lumberjack::Logger.new(StringIO.new) logger.set_progname("app") expect(logger.progname).to eq("app") block_executed = false logger.set_progname("xxx") do block_executed = true expect(logger.progname).to eq("xxx") end expect(block_executed).to eq(true) expect(logger.progname).to eq("app") end it "should be able to set the local progname in a block" do logger = Lumberjack::Logger.new(StringIO.new) logger.set_progname("app") logger.with_progname("xxx") do expect(logger.progname).to eq("xxx") end expect(logger.progname).to eq("app") end it "should only affect the current fiber when changing the progname in a block" do out = StringIO.new logger = Lumberjack::Logger.new(out, progname: "thread1", template: "{{progname}} {{message}}") Fiber.new do logger.set_progname("fiber1") do expect(logger.progname).to eq("fiber1") end end.resume expect(logger.progname).to eq("thread1") end end describe "#device" do it "should be able to open a new device by setting the device attribute" do logger = Lumberjack::Logger.new(:null) out = StringIO.new logger.device = out expect(logger.device.class).to eq(Lumberjack::Device::Writer) logger.info("foo") logger.flush expect(out.string).to include("foo#{Lumberjack::LINE_SEPARATOR}") end end describe "#datetime_format" do it "should be able to set the datetime format for timestamps on the log device" do out = StringIO.new logger = Lumberjack::Logger.new(out, template: "{{time}} {{message}}", datetime_format: "%Y-%m-%d") expect(logger.datetime_format).to eq "%Y-%m-%d" Timecop.freeze do logger.info("one") logger.datetime_format = "%m-%d-%Y" expect(logger.datetime_format).to eq "%m-%d-%Y" logger.info("two") logger.flush expect(out.string).to eq "#{Time.now.strftime("%Y-%m-%d")} one#{Lumberjack::LINE_SEPARATOR}#{Time.now.strftime("%m-%d-%Y")} two#{Lumberjack::LINE_SEPARATOR}" end end end describe "with a ::Logger::Formatter" do let(:out) { StringIO.new } it "formats output using the standard library formatter" do formatter = Class.new(Logger::Formatter) do def call(severity, time, progname, msg) super.upcase end end.new logger = Lumberjack::Logger.new(out, formatter: formatter) logger.info("test") expect(out.string.chomp).to match(/I, \[.+\] INFO -- : TEST/) end it "formats output using a standard library formatter if it's a Proc that takes 4 args" do formatter = lambda { |severity, time, progname, msg| "#{severity}: #{time.to_i} #{msg}" } logger = Lumberjack::Logger.new(out, formatter: formatter) logger.info("test") expect(out.string.chomp).to match(/INFO: \d+ test/) end it "can set the datetime format" do formatter = Logger::Formatter.new logger = Lumberjack::Logger.new(out, formatter: formatter, datetime_format: "%Y%m%d") logger.info("test") expect(out.string.chomp).to match(/\b\d{8}\b/) end end describe "#close" do it "should close the device" do out = StringIO.new logger = Lumberjack::Logger.new(out, level: Logger::INFO, template: "{{message}}") expect(logger.device).to receive(:flush).at_least(:once) expect(logger.closed?).to eq false logger.close expect(out).to be_closed expect(logger.closed?).to eq true end end describe "#reopen" do it "should reopen the devices" do out = StringIO.new logger = Lumberjack::Logger.new(out, level: Logger::INFO, template: "{{message}}") logger.close expect(logger.device).to receive(:reopen).and_call_original logger.reopen expect(logger.closed?).to eq false end end describe "logging methods" do let(:out) { StringIO.new } let(:device) { Lumberjack::Device::Writer.new(out, template: "[{{time}} {{severity}} {{progname}}({{pid}})] {{message}} {{attributes}}") } let(:logger) { Lumberjack::Logger.new(device, level: Logger::INFO, progname: "app") } let(:n) { Lumberjack::LINE_SEPARATOR } describe "add" do it "should add entries with a numeric severity and a message" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.add(Logger::INFO, "test") expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO app(#{Process.pid})] test") end it "should add entries with a severity label" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.add(:info, "test") expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO app(#{Process.pid})] test") end it "should add entries with a custom progname and message" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.add(Logger::INFO, "test", "spec") expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO spec(#{Process.pid})] test") end it "should add entries with a local progname and message" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.with_progname("block") do logger.add(Logger::INFO, "test") end expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO block(#{Process.pid})] test") end it "should add entries with a progname but no message or block" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.with_progname("default") do logger.add(Logger::INFO, nil, "message") end expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO default(#{Process.pid})] message") end it "should add entries with a block" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.add(Logger::INFO) { "test" } expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO app(#{Process.pid})] test") end it "should log entries (::Logger compatibility)" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.log(Logger::INFO, "test") expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO app(#{Process.pid})] test") end it "should append messages with ANY severity to the log" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger << "test" expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 ANY app(#{Process.pid})] test") end end describe "add_entry" do it "should add entries with attributes" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.add_entry(Logger::INFO, "test", "spec", "tag" => "ABCD") expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO spec(#{Process.pid})] test [tag:ABCD]") end it "should handle malformed attributes" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.add_entry(Logger::INFO, "test", "spec", "ABCD") expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO spec(#{Process.pid})] test") end it "should output entries to STDERR if they can't be written the the device" do stderr = $stderr $stderr = StringIO.new save_raise_logger_errors = Lumberjack.raise_logger_errors? begin Lumberjack.raise_logger_errors = false time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) expect(device).to receive(:write).and_raise(StandardError.new("Cannot write to device")) logger.add_entry(Logger::INFO, "test") expect($stderr.string).to include("[2011-01-30T12:31:56.123 INFO app(#{Process.pid})] test") expect($stderr.string).to include("StandardError: Cannot write to device") ensure Lumberjack.raise_logger_errors = save_raise_logger_errors $stderr = stderr end end it "should raise logging errors if configured to do so" do stderr = $stderr $stderr = StringIO.new save_raise_logger_errors = Lumberjack.raise_logger_errors? begin Lumberjack.raise_logger_errors = true expect(device).to receive(:write).and_raise(StandardError.new("Cannot write to device")) expect { logger.add_entry(Logger::INFO, "test") }.to raise_error(StandardError, /Cannot write to device/) ensure Lumberjack.raise_logger_errors = save_raise_logger_errors $stderr = stderr end end it "should call Proc tag values" do time = Time.parse("2011-01-30T12:31:56.123") allow(Time).to receive_messages(now: time) logger.add_entry(Logger::INFO, "test", "spec", tag: lambda { "foo" }) expect(out.string.chomp).to eq("[2011-01-30T12:31:56.123 INFO spec(#{Process.pid})] test [tag:foo]") end it "should not get into infinite loops by logging entries while logging entries" do time = Time.new(2011, 1, 30, 12, 31, 56, 0) allow(Time).to receive_messages(now: time) tag_proc = lambda do logger.warn("inner logging") "foo" end save_stderr = $stderr captured_stderr = StringIO.new begin $stderr = captured_stderr logger.add_entry(Logger::INFO, "test", "spec", tag: tag_proc) ensure $stderr = save_stderr end expect(out.string.chomp).to eq("[2011-01-30T12:31:56.000 INFO spec(#{Process.pid})] test [tag:foo]") expect(captured_stderr.string).to include("WARN inner logging\n") end end %w[fatal error warn info debug].each do |level| describe level do around :each do |example| Timecop.freeze(time) do example.call end end before :each do logger.level = level end let(:time) { Time.at(1296419516) } let(:timestamp) { time.strftime("%Y-%m-%dT%H:%M:%S.%3N") } it "should log a message string" do logger.send(level, "test") expect(out.string.chomp).to eq("[#{timestamp} #{level.upcase} app(#{Process.pid})] test") end it "should log a message string with a progname" do logger.send(level, "test", "spec") expect(out.string.chomp).to eq("[#{timestamp} #{level.upcase} spec(#{Process.pid})] test") end it "should log a message string with attributes" do logger.send(level, "test", tag: 1) expect(out.string.chomp).to eq("[#{timestamp} #{level.upcase} app(#{Process.pid})] test [tag:1]") end it "should log a message block" do logger.send(level) { "test" } expect(out.string.chomp).to eq("[#{timestamp} #{level.upcase} app(#{Process.pid})] test") end it "should log a message block with a progname" do logger.send(level, "spec") { "test" } expect(out.string.chomp).to eq("[#{timestamp} #{level.upcase} spec(#{Process.pid})] test") end it "should log a message block with attributes" do logger.send(level, tag: 1) { "test" } expect(out.string.chomp).to eq("[#{timestamp} #{level.upcase} app(#{Process.pid})] test [tag:1]") end it "should log a message block with a progname and attributes" do logger.send(level, "spec", tag: 1) { "test" } expect(out.string.chomp).to eq("[#{timestamp} #{level.upcase} spec(#{Process.pid})] test [tag:1]") end it "should log a message block with a progname and attributes" do logger.send(level, {tag: 1}, "spec") { "test" } expect(out.string.chomp).to eq("[#{timestamp} #{level.upcase} spec(#{Process.pid})] test [tag:1]") end end end describe "message" do it "should apply the message formatter to the message" do logger = Lumberjack::Logger.new(out, template: "{{message}}") logger.formatter.format_message(String) { |msg| msg.upcase } logger.info("test") expect(out.string.chomp).to eq "TEST" end it "should copy attributes from the message if the formatter returns a Lumberjack::MessageAttributes" do logger = Lumberjack::Logger.new(out, template: "{{message}} {{attributes}}") logger.formatter.format_message(String) { |msg| Lumberjack::MessageAttributes.new(msg.upcase, tag: msg.downcase) } logger.info("Test") expect(out.string.chomp).to eq "TEST [tag:test]" end end describe "#tag_globally", deprecation_mode: :silent do let(:device) { Lumberjack::Device::Writer.new(out, template: "{{message}} - {{count}} - {{attributes}}") } it "should be able to add global attributes to the logger" do logger.tag_globally(count: 1, foo: "bar") logger.info("one") logger.info("two", count: 2) lines = out.string.split(n) expect(lines[0]).to eq "one - 1 - [foo:bar]" expect(lines[1]).to eq "two - 2 - [foo:bar]" end end describe "#remove_tag", deprecation_mode: :silent do let(:device) { Lumberjack::Device::Writer.new(out, template: "{{message}} - {{count}} - {{attributes}}") } it "should remove context attributes in a context block and global attributes outside of one" do logger.tag!(foo: "bar", wip: "wap") logger.context do logger.tag(baz: "boo", bip: "bap") logger.remove_tag(:baz) logger.remove_tag(:foo) expect(logger.attributes).to eq({"foo" => "bar", "wip" => "wap", "bip" => "bap"}) end logger.remove_tag(:foo) expect(logger.attributes).to eq({"wip" => "wap"}) end it "should be able to extract attributes from an object with a formatter that returns Lumberjack::MessageAttributes" do logger.formatter.format_message(Exception, ->(e) { Lumberjack::MessageAttributes.new(e.inspect, {message: e.message, class: e.class.name}) }) error = StandardError.new("foobar") logger.info(error) line = out.string.chomp expect(line).to eq "#{error.inspect} - - [message:foobar] [class:StandardError]" end it "should apply an attribute formatter to the attributes" do logger.attribute_formatter.add(:foo, &:reverse).add(:count) { |val| val * 100 } logger.info("message", count: 2, foo: "abc") line = out.string.chomp expect(line).to eq "message - 200 - [foo:cba]" end end end describe "#tagged", deprecation_mode: :silent do let(:logger) { Lumberjack::Logger.new(:test) } it "adds tags to the tagged attribute" do logger.tagged("foo", "bar") do expect(logger.attributes).to eq({"tagged" => ["foo", "bar"]}) logger.tagged("baz") expect(logger.attributes).to eq({"tagged" => ["foo", "bar", "baz"]}) end end end describe "#untagged", deprecation_mode: :silent do let(:logger) { Lumberjack::Logger.new(:test) } it "removes tags from the tagged attribute" do logger.tag(foo: "bar") do logger.untagged do expect(logger.attributes).to be_empty end end end end describe "#log_at", deprecation_mode: :silent do let(:logger) { Lumberjack::Logger.new(:test) } it "changes the log level temporarily" do logger.log_at(:info) do expect(logger.level).to eq(Logger::INFO) end expect(logger.level).to eq(Logger::DEBUG) end end describe "#silence", deprecation_mode: :silent do let(:logger) { Lumberjack::Logger.new(:test) } it "temporarily suppresses logging" do logger.silence do expect(logger.level).to eq(Logger::ERROR) end expect(logger.level).to eq(Logger::DEBUG) end it "can specify the log level" do logger.silence(Logger::WARN) do expect(logger.level).to eq(Logger::WARN) end expect(logger.level).to eq(Logger::DEBUG) end end end bdurand-lumberjack-ac97435/spec/lumberjack/rack/000077500000000000000000000000001515437321200216035ustar00rootroot00000000000000bdurand-lumberjack-ac97435/spec/lumberjack/rack/context_spec.rb000066400000000000000000000021151515437321200246250ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Rack::Context do it "should create a context for a request" do app = lambda { |env| [200, {"Context" => Lumberjack.in_context?}, ["OK"]] } handler = Lumberjack::Rack::Context.new(app) response = handler.call("Content-Type" => "text/plain", "action_dispatch.request_id" => "0123-4567-89ab-cdef") expect(response).to eq([200, {"Context" => true}, ["OK"]]) end it "should apply attributes from the request environment" do app = lambda { |env| [200, {"Content-Type" => env["Content-Type"], "Request-ID" => Lumberjack.context_attributes["request_id"]}, ["OK"]] } handler = Lumberjack::Rack::Context.new(app, request_id: ->(env) { env["action_dispatch.request_id"] }) response = handler.call("Content-Type" => "text/plain", "action_dispatch.request_id" => "0123-4567-89ab-cdef") expect(response[0]).to eq(200) expect(response[1]["Content-Type"]).to eq("text/plain") expect(response[1]["Request-ID"]).to eq("0123-4567-89ab-cdef") expect(response[2]).to eq(["OK"]) end end bdurand-lumberjack-ac97435/spec/lumberjack/severity_spec.rb000066400000000000000000000057231515437321200241030ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Severity do describe "#level_to_label" do it "converts a level to a label" do expect(Lumberjack::Severity.level_to_label(Logger::DEBUG)).to eq("DEBUG") expect(Lumberjack::Severity.level_to_label(Logger::INFO)).to eq("INFO") expect(Lumberjack::Severity.level_to_label(Logger::WARN)).to eq("WARN") expect(Lumberjack::Severity.level_to_label(Logger::ERROR)).to eq("ERROR") expect(Lumberjack::Severity.level_to_label(Logger::FATAL)).to eq("FATAL") expect(Lumberjack::Severity.level_to_label(Lumberjack::Logger::TRACE)).to eq("TRACE") expect(Lumberjack::Severity.level_to_label(Logger::UNKNOWN)).to eq("ANY") expect(Lumberjack::Severity.level_to_label(100)).to eq("ANY") end end describe "#label_to_level" do it "converts a label to a level" do expect(Lumberjack::Severity.label_to_level("DEBUG")).to eq(Logger::DEBUG) expect(Lumberjack::Severity.label_to_level(:info)).to eq(Logger::INFO) expect(Lumberjack::Severity.label_to_level(:warn)).to eq(Logger::WARN) expect(Lumberjack::Severity.label_to_level("Error")).to eq(Logger::ERROR) expect(Lumberjack::Severity.label_to_level("FATAL")).to eq(Logger::FATAL) expect(Lumberjack::Severity.label_to_level("TRACE")).to eq(Lumberjack::Logger::TRACE) expect(Lumberjack::Severity.label_to_level("???")).to eq(Logger::UNKNOWN) end end describe "#coerce" do it "coerces integer levels to themselves" do expect(Lumberjack::Severity.coerce(Logger::DEBUG)).to eq(Logger::DEBUG) end it "coerces a symbol to a level" do expect(Lumberjack::Severity.coerce(:debug)).to eq(Logger::DEBUG) end it "coerces a string to a level" do expect(Lumberjack::Severity.coerce("Info")).to eq(Logger::INFO) end it "coerces unknown values to UNKNOWN" do expect(Lumberjack::Severity.coerce("???")).to eq(Logger::UNKNOWN) end it "coerces trace labels to TRACE" do expect(Lumberjack::Severity.coerce("TRACE")).to eq(Lumberjack::Logger::TRACE) end end describe "#severity" do it "returns the severity data for a given level" do expect(Lumberjack::Severity.data(Logger::DEBUG)).to be_a(Lumberjack::Severity::Data) expect(Lumberjack::Severity.data(Logger::INFO).label).to eq("INFO") expect(Lumberjack::Severity.data(:error).emoji).to eq("❌") expect(Lumberjack::Severity.data("WARN").padded_label).to eq("WARN ") expect(Lumberjack::Severity.data("ERROR").padded_label).to eq("ERROR") expect(Lumberjack::Severity.data(Logger::FATAL).char).to eq("F") expect(Lumberjack::Severity.data(Lumberjack::Logger::TRACE).label).to eq("TRACE") expect(Lumberjack::Severity.data(Logger::UNKNOWN).emoji).to eq("❓") expect(Lumberjack::Severity.data(Logger::UNKNOWN).label).to eq("ANY") expect(Lumberjack::Severity.data(100)).to eq(Lumberjack::Severity.data(Logger::UNKNOWN)) end end end bdurand-lumberjack-ac97435/spec/lumberjack/tags_spec.rb000066400000000000000000000010121515437321200231520ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Tags, deprecation_mode: :silent do describe "stringify_keys" do it "transforms hash keys to strings" do hash = {foo: 1, bar: 2} expect(Lumberjack::Tags.stringify_keys(hash)).to eq({"foo" => 1, "bar" => 2}) end it "returns the hash itself if the keys are already strings" do hash = {"foo" => 1, "bar" => 2} expect(Lumberjack::Tags.stringify_keys(hash).object_id).to eq(hash.object_id) end end end bdurand-lumberjack-ac97435/spec/lumberjack/template_registry_spec.rb000066400000000000000000000031121515437321200257620ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::TemplateRegistry do it "has :default, :stdlib, and :local registered by default" do expect(Lumberjack::TemplateRegistry.registered_templates).to eq({ default: Lumberjack::Template::DEFAULT_FIRST_LINE_TEMPLATE, stdlib: Lumberjack::Template::STDLIB_FIRST_LINE_TEMPLATE, message: "{{message}}", local: Lumberjack::LocalLogTemplate }) end it "can add new templates to the registry" do template = lambda { |entry| "foobar" } Lumberjack::TemplateRegistry.add(:foobar, template) expect(Lumberjack::TemplateRegistry.template(:foobar)).to eq template expect(Lumberjack::TemplateRegistry.template(:other)).to be_nil ensure Lumberjack::TemplateRegistry.remove(:foobar) end it "can instantiate a template class by name and options" do template = Lumberjack::TemplateRegistry.template(:local, exclude_pid: false) expect(template).to be_a(Lumberjack::LocalLogTemplate) expect(template.exclude_pid?).to be false end it "can instantiate a template string by name and options" do template = Lumberjack::TemplateRegistry.template(:stdlib) expect(template).to be_a(Lumberjack::Template) entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test message", "myapp", 1234, "foo" => "bar", "baz.bax" => "qux") formatted = template.call(entry) expected = "I, [#{entry.time.strftime("%Y-%m-%dT%H:%M:%S.%3N")} 1234] INFO -- myapp: test message [foo:bar] [baz.bax:qux]#{Lumberjack::LINE_SEPARATOR}" expect(formatted).to eq(expected) end end bdurand-lumberjack-ac97435/spec/lumberjack/template_spec.rb000066400000000000000000000145411515437321200240420ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Template do let(:time_string) { "2011-01-15T14:23:45.123" } let(:time) { Time.parse(time_string) } let(:entry) { Lumberjack::LogEntry.new(time, Logger::INFO, "line 1#{Lumberjack::LINE_SEPARATOR}line 2#{Lumberjack::LINE_SEPARATOR}line 3", "app", 12345, "unit_of_work_id" => "ABCD", "foo" => "bar") } describe ".colorize_entry" do it "wraps each line in terminal save/restore sequences" do colored = Lumberjack::Template.colorize_entry("line 1#{Lumberjack::LINE_SEPARATOR}line 2", entry) expect(colored).to eq("\e7\e[38;5;33mline 1\e8#{Lumberjack::LINE_SEPARATOR}\e7\e[38;5;33mline 2\e8") end end describe "format" do it "has a default format" do template = Lumberjack::Template.new expect(template.call(entry)).to eq("[2011-01-15T14:23:45.123 INFO app(12345)] line 1 [unit_of_work_id:ABCD] [foo:bar]#{Lumberjack::LINE_SEPARATOR}> line 2#{Lumberjack::LINE_SEPARATOR}> line 3#{Lumberjack::LINE_SEPARATOR}") end it "should format a log entry with a template string" do template = Lumberjack::Template.new("{{message}} - {{severity}}, {{time}}, {{progname}}@{{pid}} ({{unit_of_work_id}}) {{attributes}}") expect(template.call(entry)).to eq("line 1 - INFO, 2011-01-15T14:23:45.123, app@12345 (ABCD) [foo:bar]#{Lumberjack::LINE_SEPARATOR}> line 2#{Lumberjack::LINE_SEPARATOR}> line 3#{Lumberjack::LINE_SEPARATOR}") end it "should be able to specify a template for additional lines in a message" do template = Lumberjack::Template.new("{{message}} ({{time}})", additional_lines: " // {{message}}") expect(template.call(entry)).to eq("line 1 (2011-01-15T14:23:45.123) // line 2 // line 3#{Lumberjack::LINE_SEPARATOR}") end it "does not blow up if there is a % in the template" do template = Lumberjack::Template.new("%s {{message}}") expect(template.call(entry)).to eq("%s line 1#{Lumberjack::LINE_SEPARATOR}> line 2#{Lumberjack::LINE_SEPARATOR}> line 3#{Lumberjack::LINE_SEPARATOR}") end it "can pad the severity labels" do template = Lumberjack::Template.new("{{severity(padded)}}-{{message}}") expect(template.call(entry)).to start_with("INFO -line 1") end it "can use a single character for the severity" do template = Lumberjack::Template.new("{{severity(char)}}-{{message}}") expect(template.call(entry)).to start_with("I-line 1") end it "can use an emoji for the severity" do template = Lumberjack::Template.new("{{severity(emoji)}}-{{message}}") expect(template.call(entry)).to start_with("🔵-line 1") end it "can use the level for the severity" do template = Lumberjack::Template.new("{{severity(level)}}-{{message}}") expect(template.call(entry)).to start_with("1-line 1") end end describe "timestamp format" do it "should be able to specify the time format for log entries as microseconds" do template = Lumberjack::Template.new("{{message}} ({{time}})", time_format: :microseconds) expect(template.call(entry)).to eq("line 1 (2011-01-15T14:23:45.123000)#{Lumberjack::LINE_SEPARATOR}> line 2#{Lumberjack::LINE_SEPARATOR}> line 3#{Lumberjack::LINE_SEPARATOR}") end it "should be able to specify the time format for log entries as milliseconds" do template = Lumberjack::Template.new("{{message}} ({{time}})", time_format: :milliseconds) expect(template.call(entry)).to eq("line 1 (2011-01-15T14:23:45.123)#{Lumberjack::LINE_SEPARATOR}> line 2#{Lumberjack::LINE_SEPARATOR}> line 3#{Lumberjack::LINE_SEPARATOR}") end it "should be able to specify the time format for log entries with a custom format" do template = Lumberjack::Template.new("{{message}} ({{time}})", time_format: "%m/%d/%Y, %I:%M:%S %p") expect(template.call(entry)).to eq("line 1 (01/15/2011, 02:23:45 PM)#{Lumberjack::LINE_SEPARATOR}> line 2#{Lumberjack::LINE_SEPARATOR}> line 3#{Lumberjack::LINE_SEPARATOR}") end end describe "attributes" do it "should format named attributes in the template and not in the {{attributes}} placement" do template = Lumberjack::Template.new("{{message}} - {{foo}} - {{attributes}}") entry = Lumberjack::LogEntry.new(time, Logger::INFO, "here", "app", 12345, "foo" => "bar", "tag" => "a") expect(template.call(entry)).to eq("here - bar - [tag:a]#{Lumberjack::LINE_SEPARATOR}") end it "should put nothing in place of missing named attributes" do template = Lumberjack::Template.new("{{message}} - {{foo}} - {{attributes}}") entry = Lumberjack::LogEntry.new(time, Logger::INFO, "here", "app", 12345, "tag" => "a") expect(template.call(entry)).to eq("here - - [tag:a]#{Lumberjack::LINE_SEPARATOR}") end it "should remove line separators in attributes" do template = Lumberjack::Template.new("{{message}} - {{foo}} - {{attributes}}") entry = Lumberjack::LogEntry.new(time, Logger::INFO, "here", "app", 12345, "tag" => "a#{Lumberjack::LINE_SEPARATOR}b") expect(template.call(entry)).to eq("here - - [tag:a b]#{Lumberjack::LINE_SEPARATOR}") end it "should handle attributes with special characters by surrounding with brackets" do template = Lumberjack::Template.new("{{message}} - {{ foo.bar }} - {{@baz!}} - {{ attributes}}") entry = Lumberjack::LogEntry.new(time, Logger::INFO, "here", "app", 12345, "foo.bar" => "test", "@baz!" => 1, "tag" => "a") expect(template.call(entry)).to eq("here - test - 1 - [tag:a]#{Lumberjack::LINE_SEPARATOR}") end it "can customize the attribute format" do template = Lumberjack::Template.new("{{message}} - {{foo}} - {{attributes}}", attribute_format: "(%s=%s)") entry = Lumberjack::LogEntry.new(time, Logger::INFO, "here", "app", 12345, "foo" => "bar", "tag" => "a") expect(template.call(entry)).to eq("here - bar - (tag=a)#{Lumberjack::LINE_SEPARATOR}") end end describe "v1 template format", deprecation_mode: :silent do it "uses :name as placeholders in place of {{name}} and tags instead of attributes" do template = Lumberjack::Template.new(":time :severity :progname, :message: - :foo - :tags") entry = Lumberjack::LogEntry.new(time, Logger::INFO, "here", "app", 12345, "foo" => "bar", "tag" => "a") expect(template.call(entry)).to eq("2011-01-15T14:23:45.123 INFO app, here: - bar - [tag:a]#{Lumberjack::LINE_SEPARATOR}") end end end bdurand-lumberjack-ac97435/spec/lumberjack/utils_spec.rb000066400000000000000000000172711515437321200233720ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack::Utils do describe ".hostname" do it "returns the hostname in UTF-8 encoding" do expect(Lumberjack::Utils.hostname).to be_a(String) expect(Lumberjack::Utils.hostname.encoding).to eq(Encoding::UTF_8) end it "caches the hostname" do expect(Lumberjack::Utils.hostname.object_id).to eq(Lumberjack::Utils.hostname.object_id) end it "returns an explicitly set hostname" do hostname = Lumberjack::Utils.hostname begin Lumberjack::Utils.hostname = "test-host" expect(Lumberjack::Utils.hostname).to eq("test-host") ensure Lumberjack::Utils.hostname = hostname end end end describe ".global_pid" do it "generates a global process ID" do expect(Lumberjack::Utils.global_pid).to eq "#{Lumberjack::Utils.hostname}-#{Process.pid}" end it "can generate a value for a specific pid" do expect(Lumberjack::Utils.global_pid(12345)).to eq "#{Lumberjack::Utils.hostname}-12345" end end describe ".global_thread_id" do it "generates a global thread ID" do expect(Lumberjack::Utils.global_thread_id).to eq "#{Lumberjack::Utils.global_pid}-#{Lumberjack::Utils.thread_name}" end end describe ".thread_name" do it "generates a name based on the object id if there is no thread name" do thread = Thread.new { sleep 0.001 } expect(Lumberjack::Utils.thread_name(thread)).to eq thread.object_id.to_s(36) end it "generates a sluggified name based on the thread name" do thread = Thread.new { sleep 0.001 } thread.name = "Test Thread" expect(Lumberjack::Utils.thread_name(thread)).to eq "Test-Thread" end end describe ".flatten_attributes" do it "flattens a nested attribute hash" do attribute_hash = {"user" => {"id" => 123, "name" => "Alice"}, "action" => "login"} expect(Lumberjack::Utils.flatten_attributes(attribute_hash)).to eq( "user.id" => 123, "user.name" => "Alice", "action" => "login" ) end it "flattens a deeply nested attribute hash" do attribute_hash = {"a" => {"b" => {"c" => 3, "d" => 4}}, "e" => 5} expect(Lumberjack::Utils.flatten_attributes(attribute_hash)).to eq( "a.b.c" => 3, "a.b.d" => 4, "e" => 5 ) end it "returns an empty hash for non-hash input" do expect(Lumberjack::Utils.flatten_attributes("not a hash")).to eq({}) end it "handles mixing dot notation with nested attributes with dot notation attributes first" do attribute_hash = {"user.id" => 123, user: {"name" => "Alice"}, "user.action": "login"} # rubocop:disable Style/HashSyntax expect(Lumberjack::Utils.flatten_attributes(attribute_hash)).to eq( "user.id" => 123, "user.name" => "Alice", "user.action" => "login" ) end it "handles mixing dot notation with structured attributes first" do attribute_hash = {user: {id: 123, name: "Alice"}, "user.action": "login"} expect(Lumberjack::Utils.flatten_attributes(attribute_hash)).to eq( "user.id" => 123, "user.name" => "Alice", "user.action" => "login" ) end end describe ".expand_attributes" do it "expands a hash with nested hashes and dot notation keys" do attribute_hash = {"user.id" => 123, "user.name" => "Alice", "action" => "login"} expect(Lumberjack::Utils.expand_attributes(attribute_hash)).to eq( "user" => {"id" => 123, "name" => "Alice"}, "action" => "login" ) end it "handles mixed dot notation and nested hashes with dot notation attributes first" do attribute_hash = {"user.id" => 123, user: {"name" => "Alice"}, "user.action": "login"} # rubocop:disable Style/HashSyntax expect(Lumberjack::Utils.expand_attributes(attribute_hash)).to eq( "user" => {"id" => 123, "name" => "Alice", "action" => "login"} ) end it "handles mix dot notation with structured attributes first" do attribute_hash = {user: {id: 123, name: "Alice"}, "user.action": "login"} expect(Lumberjack::Utils.expand_attributes(attribute_hash)).to eq( "user" => {"id" => 123, "name" => "Alice", "action" => "login"} ) end end describe ".deprecated" do around do |example| original_verbose = $VERBOSE original_deprecated = ENV["LUMBERJACK_NO_DEPRECATION_WARNINGS"] original_stderr = $stderr begin $stderr = StringIO.new $VERBOSE = false ENV["LUMBERJACK_NO_DEPRECATION_WARNINGS"] = nil Lumberjack::Utils.instance_variable_set(:@deprecations, nil) example.run ensure $stderr = original_stderr $VERBOSE = original_verbose ENV["LUMBERJACK_NO_DEPRECATION_WARNINGS"] = original_deprecated end end it "prints a deprecation warning the first time a deprecated method is called", deprecation_mode: :normal do retval = Lumberjack::Utils.deprecated("test_method_1", "This is deprecated") { :foo } expect($stderr.string).to match(/DEPRECATION WARNING: This is deprecated/) expect(retval).to eq :foo end it "does not print the warning again for subsequent calls", deprecation_mode: :normal do Lumberjack::Utils.deprecated("test_method_2", "This is deprecated") { :foo } expect($stderr.string).to match(/DEPRECATION WARNING: This is deprecated/) $stderr.rewind $stderr.truncate(0) retval = Lumberjack::Utils.deprecated("test_method_2", "This is deprecated") { :bar } expect($stderr.string).to be_empty expect(retval).to eq :bar end it "does print the warning again if deprecation mode is verbose" do Lumberjack::Utils.with_deprecation_mode(:verbose) do Lumberjack::Utils.deprecated("test_method_2", "This is deprecated") { :foo } expect($stderr.string).to match(/DEPRECATION WARNING: This is deprecated/) $stderr.rewind $stderr.truncate(0) retval = Lumberjack::Utils.deprecated("test_method_2", "This is deprecated") { :bar } expect($stderr.string).to match(/DEPRECATION WARNING: This is deprecated/) expect(retval).to eq :bar end end it "raises an exception if deprecation mode is raise" do Lumberjack::Utils.with_deprecation_mode(:raise) do expect { Lumberjack::Utils.deprecated("test_method_3", "This is deprecated") { :foo } }.to raise_error(Lumberjack::DeprecationError, /This is deprecated/) end end it "does not print a warning if deprecation mode is silent" do Lumberjack::Utils.with_deprecation_mode("silent") do retval = Lumberjack::Utils.deprecated("test_method_3", "This is deprecated") { :foo } expect($stderr.string).to be_empty expect(retval).to eq :foo end end it "prints only the current line if $VERBOSE is false", deprecation_mode: :normal do $VERBOSE = false Lumberjack::Utils.deprecated("test_method_3", "This is deprecated") { :foo } expect($stderr.string.chomp.split(Lumberjack::LINE_SEPARATOR).length).to eq 1 end it "prints the full stack trace if $VERBOSE is true", deprecation_mode: :normal do $VERBOSE = true Lumberjack::Utils.deprecated("test_method_3", "This is deprecated") { :foo } expect($stderr.string.chomp.split(Lumberjack::LINE_SEPARATOR).length).to be > 1 end end describe ".current_line" do it "returns the line of code that calls the method" do expect(Lumberjack::Utils.current_line).to start_with("#{__FILE__}:#{__LINE__}") end it "can strip a root path from the line" do expect(Lumberjack::Utils.current_line(__dir__)).to start_with("#{File.basename(__FILE__)}:#{__LINE__}") end end end bdurand-lumberjack-ac97435/spec/lumberjack_spec.rb000066400000000000000000000115431515437321200222260ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Lumberjack do describe "VERSION" do it "is defined" do expect(Lumberjack::VERSION).not_to be_nil end end describe "#context" do it "should create a context with attributes for a block" do Lumberjack.context do Lumberjack.tag(foo: "bar") expect(Lumberjack.context_attributes).to eq({"foo" => "bar"}) end end it "should determine if it is inside a context block" do expect(Lumberjack.in_context?).to eq false Lumberjack.context do expect(Lumberjack.in_context?).to eq true end expect(Lumberjack.in_context?).to eq false end it "should return the result of the context block" do result = Lumberjack.context { :foo } expect(result).to eq :foo end end describe "#ensure_context" do it "should create a context if one does not exist" do expect(Lumberjack.in_context?).to eq false value = Lumberjack.ensure_context do expect(Lumberjack.in_context?).to eq true :foo end expect(Lumberjack.in_context?).to eq false expect(value).to eq :foo end it "does not create a new context if one already exists" do Lumberjack.context do value = Lumberjack.ensure_context do Lumberjack.tag(baz: "bap") :foo end expect(Lumberjack.context_attributes).to eq({"baz" => "bap"}) expect(value).to eq :foo end end end describe "#use_context" do it "should return the result of the use_context block" do result = Lumberjack.use_context(nil) { :foo } expect(result).to eq :foo end it "should create a context based on passed in context" do context = Lumberjack::Context.new context.assign_attributes(foo: "bar") Lumberjack.use_context(context) do expect(Lumberjack.context_attributes).to eq("foo" => "bar") end end end describe "#isolation_level" do around do |example| original_level = Lumberjack.isolation_level begin example.run ensure Lumberjack.isolation_level = original_level end end it "defaults to :fiber" do expect(Lumberjack.isolation_level).to eq :fiber end it "isolates the global context by fiber when set to :fiber" do Lumberjack.isolation_level = :fiber Lumberjack.context do fiber = Fiber.new do expect(Lumberjack.in_context?).to be false end fiber.resume expect(Lumberjack.in_context?).to be true end end it "isolates the global context by thread when set to :thread" do Lumberjack.isolation_level = :thread Lumberjack.context do thread = Thread.new do expect(Lumberjack.in_context?).to be false end fiber = Fiber.new do expect(Lumberjack.in_context?).to be true end fiber.resume thread.join expect(Lumberjack.in_context?).to be true end end it "is inherited by loggers" do expect(Lumberjack::Logger.new(:test).isolation_level).to eq :fiber Lumberjack.isolation_level = :thread expect(Lumberjack::Logger.new(:test).isolation_level).to eq :thread end end describe "#tag" do it "does nothing when called outside of a context block" do Lumberjack.tag(foo: "bar") expect(Lumberjack.context_attributes).to be_nil end it "sets attributes on the current context when called inside a context block" do Lumberjack.context do Lumberjack.tag(foo: "bar") expect(Lumberjack.context_attributes).to eq({"foo" => "bar"}) end end it "sets attributes in a new context block" do expect(Lumberjack.context_attributes).to be_nil Lumberjack.tag(foo: "bar") do expect(Lumberjack.context_attributes).to eq({"foo" => "bar"}) end expect(Lumberjack.context_attributes).to be_nil end it "returns the result of the block" do result = Lumberjack.tag(foo: "bar") { :foobar } expect(result).to eq :foobar end it "inherits attributes from the parent context" do Lumberjack.tag(bip: "bap") do Lumberjack.tag(foo: "bar") expect(Lumberjack.context_attributes).to eq({"foo" => "bar", "bip" => "bap"}) Lumberjack.tag(baz: "boo") do expect(Lumberjack.context_attributes).to eq({"foo" => "bar", "bip" => "bap", "baz" => "boo"}) end expect(Lumberjack.context_attributes).to eq({"foo" => "bar", "bip" => "bap"}) end expect(Lumberjack.context_attributes).to be_nil end end it "can build a formatter" do entry_formatter = Lumberjack.build_formatter do |formatter| formatter.format_class(Integer) { |i| i * 2 } end expect(entry_formatter).to be_a(Lumberjack::EntryFormatter) expect(entry_formatter.format(12, nil).first).to eq(24) end end bdurand-lumberjack-ac97435/spec/spec_helper.rb000066400000000000000000000033471515437321200213710ustar00rootroot00000000000000# frozen_string_literal: true require "logger" require "stringio" require "fileutils" require "timecop" require "tempfile" begin require "simplecov" SimpleCov.start do add_filter ["/spec/"] end rescue LoadError end require_relative "../lib/lumberjack" Lumberjack.raise_logger_errors = true Lumberjack.deprecation_mode = :raise RSpec.configure do |config| config.warnings = true config.disable_monkey_patching! config.default_formatter = "doc" if config.files_to_run.one? config.order = :random Kernel.srand config.seed config.around(:each, :deprecation_mode) do |example| Lumberjack::Utils.with_deprecation_mode(example.metadata[:deprecation_mode]) do example.run end end end def tmp_dir File.join(Dir.tmpdir, "lumberjack_test") end def create_tmp_dir FileUtils.rm_r(tmp_dir) if File.exist?(tmp_dir) FileUtils.mkdir_p(tmp_dir) end def delete_tmp_dir FileUtils.rm_r(tmp_dir) end def delete_tmp_files Dir.glob(File.join(tmp_dir, "*")) do |file| File.delete(file) end end # Minimal implementation of a Lumberjack::ContextLogger for testing to ensure that methods from # Lumberjack::Logger are not polluting any of the logic. class TestContextLogger include Lumberjack::ContextLogger attr_reader :entries def initialize(context = nil) @context = context @entries = [] end def add_entry(severity, message, progname = nil, attributes = nil) @entries << { severity: severity, message: message, progname: progname, attributes: attributes } end private def default_context @context end end class TestToLogFormat attr_reader :value def initialize(value) @value = value end def to_log_format "LOG FORMAT: #{@value}" end end