{"id":18837069,"url":"https://github.com/bkuhlmann/cogger","last_synced_at":"2025-04-14T06:22:14.616Z","repository":{"id":45138500,"uuid":"477331071","full_name":"bkuhlmann/cogger","owner":"bkuhlmann","description":"A customizable and feature rich logger.","archived":false,"fork":false,"pushed_at":"2025-03-28T15:04:55.000Z","size":545,"stargazers_count":8,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-13T09:47:44.054Z","etag":null,"topics":["color","logger","logging"],"latest_commit_sha":null,"homepage":"https://alchemists.io/projects/cogger","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bkuhlmann.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.adoc","code_of_conduct":null,"threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["bkuhlmann"]}},"created_at":"2022-04-03T12:12:27.000Z","updated_at":"2025-03-28T15:04:59.000Z","dependencies_parsed_at":"2023-10-24T03:28:48.732Z","dependency_job_id":"ba23a59a-6e4e-4c26-a3e7-3dbb6cabcabb","html_url":"https://github.com/bkuhlmann/cogger","commit_stats":{"total_commits":68,"total_committers":1,"mean_commits":68.0,"dds":0.0,"last_synced_commit":"3c5799669979c279d39dfde28b88d623bdb0e082"},"previous_names":[],"tags_count":42,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Fcogger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Fcogger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Fcogger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Fcogger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bkuhlmann","download_url":"https://codeload.github.com/bkuhlmann/cogger/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248695308,"owners_count":21146952,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["color","logger","logging"],"created_at":"2024-11-08T02:33:42.772Z","updated_at":"2025-04-14T06:22:14.573Z","avatar_url":"https://github.com/bkuhlmann.png","language":"Ruby","funding_links":["https://github.com/sponsors/bkuhlmann"],"categories":[],"sub_categories":[],"readme":":toc: macro\n:toclevels: 5\n:figure-caption!:\n\n:format_link: link:https://ruby-doc.org/3.2.2/format_specifications_rdoc.html[String Format Specification]\n:logger_link: link:https://rubyapi.org/o/s?q=Logger[Logger]\n:pattern_matching_link: link:https://alchemists.io/articles/ruby_pattern_matching[pattern matching]\n:rack_link: link:https://github.com/rack/rack[Rack]\n:rfc_3339_link: link:https://datatracker.ietf.org/doc/html/rfc3339[RFC 3339]\n:tone_link: link:https://alchemists.io/projects/tone[Tone]\n\n= Cogger\n\nCogger is a portmanteau for custom logger (i.e. `[c]ustom + l[ogger] = cogger`) which enhances Ruby's native {logger_link} functionality with additional features such as dynamic emojis, colorized text, structured JSON, multiple streams, and much more. 🚀\n\ntoc::[]\n\n== Features\n\n* Enhances Ruby's default {logger_link} with additional functionality and firepower.\n* Provides customizable templates that use keys, emojis, and/or elements as an enhanced form of the {format_link}.\n* Provides colored output via the {tone_link} gem.\n* Provides customizable formatters.\n* Provides multiple streams so you can log the same information to several outputs at once.\n* Provides global and individual tagging.\n* Provides filtering of sensitive information.\n* Provides {rack_link} middleware for HTTP request logging.\n\n== Screenshots\n\nimage::https://alchemists.io/images/projects/cogger/screenshots/demo.png[Rack,width=703,height=1151]\n\n== Requirements\n\n. link:https://www.ruby-lang.org[Ruby].\n\n== Setup\n\nTo install _with_ security, run:\n\n[source,bash]\n----\n# 💡 Skip this line if you already have the public certificate installed.\ngem cert --add \u003c(curl --compressed --location https://alchemists.io/gems.pem)\ngem install cogger --trust-policy HighSecurity\n----\n\nTo install _without_ security, run:\n\n[source,bash]\n----\ngem install cogger\n----\n\nYou can also add the gem directly to your project:\n\n[source,bash]\n----\nbundle add cogger\n----\n\nOnce the gem is installed, you only need to require it:\n\n[source,ruby]\n----\nrequire \"cogger\"\n----\n\n== Usage\n\nAll behavior is provided by creating an instance of `Cogger`. Example:\n\n[source,ruby]\n----\nlogger = Cogger.new\nlogger.info \"Demo\" # 🟢 [console] Demo\n----\n\nIf you set your logging level to `debug`, you can walk through each level:\n\n[source,ruby]\n----\nlogger = Cogger.new level: :debug\n\n# Without blocks.\nlogger.debug \"Demo\"                  # 🔎 [console] Demo\nlogger.info \"Demo\"                   # 🟢 [console] Demo\nlogger.warn \"Demo\"                   # ⚠️ [console] Demo\nlogger.error \"Demo\"                  # 🛑 [console] Demo\nlogger.fatal \"Demo\"                  # 🔥 [console] Demo\nlogger.unknown \"Demo\"                # ⚫️ [console] Demo\nlogger.any \"Demo\"                    # ⚫️ [console] Demo\nlogger.add Logger::INFO, \"Demo\"      # 🟢 [console] Demo\n\n# With blocks.\nlogger.debug { \"Demo\" }              # 🔎 [console] Demo\nlogger.info { \"Demo\" }               # 🟢 [console] Demo\nlogger.warn { \"Demo\" }               # ⚠️ [console] Demo\nlogger.error { \"Demo\" }              # 🛑 [console] Demo\nlogger.fatal { \"Demo\" }              # 🔥 [console] Demo\nlogger.unknown { \"Demo\" }            # ⚫️ [console] Demo\nlogger.any { \"Demo\" }                # ⚫️ [console] Demo\nlogger.add(Logger::INFO) { \"Demo\" }  # 🟢 [console] Demo\n----\n\nThe `[console]`, in the above output, is the program ID which is the ID of this gem's IRB console.\n\n=== Initialization\n\nWhen creating a new logger, you can configure behavior via the following attributes:\n\n* `id`: The program/process ID which shows up in the logs as your `id`. Default: `$PROGRAM_NAME`. For example, if run within a `demo.rb` script, the `id` would be `\"demo\"`,\n* `io`: The input/output stream. This can be `STDOUT/$stdout`, a file/path, or `nil`. Default: `$stdout`.\n* `level`: The log level you want to log at. Can be `:debug`, `:info`, `:warn`, `:error`, `:fatal`, or `:unknown`. Default: `:info`.\n* `formatter`: The formatter to use for formatting your log output. Default: `Cogger::Formatter::Color`. See the _Formatters_ section for more info.\n* `tags`: The global tags used for all log entries. _Must_ be an array of objects you wish to use for tagging purposes. Default: `[]`.\n* `datetime_format`: The global date/time format used for all `Time`, `Date`, and/or `DateTime` values in your log entries. Default: `%Y-%m-%dT%H:%M:%S.%L%:z`.\n* `mode`: The binary mode which determines if your logs should be written in binary mode or not. Can be `true` or `false` and is identical to the `binmode` functionality found in the {logger_link} class. Default: `false`.\n* `age`: The rotation age of your log. This only applies when logging to a file. This is equivalent to the `shift_age` as found with the {logger_link} class. Default: `0`.\n* `size`: The rotation size of your log. This only applies when logging to a file. This is equivalent to the `shift_size` as found with the {logger_link} class. Default: `1,048,576` (i.e. 1 MB).\n* `suffix`: The rotation suffix. This only applies when logging to a file. This is equivalent to the `shift_period_suffix` as found with the {logger_link} class and is used when creating new rotation files. Default: `%Y-%m-%d`.\n\nGiven the above description, here's how'd you create a new logger instance with all attributes:\n\n[source,ruby]\n----\n# Default\nlogger = Cogger.new\n\n# Custom\nlogger = Cogger.new id: :demo,\n                    io: \"demo.log\",\n                    level: :debug,\n                    formatter: :json,\n                    tags: %w[DEMO DB],\n                    datetime_format: \"%Y-%m-%d\",\n                    mode: false,\n                    age: 5,\n                    size: 1_000,\n                    suffix: \"%Y\"\n----\n\n=== Levels\n\nSupported levels can be obtained via `Cogger::LEVELS`. Example:\n\n[source,ruby]\n----\nCogger::LEVELS\n# [\"debug\", \"info\", \"warn\", \"error\", \"fatal\", \"unknown\"]\n----\n\n=== Date/Time\n\nThe default date/time format used for _all_ log values can be viewed via the following:\n\n[source,ruby]\n----\nCogger::DATETIME_FORMAT\n# \"%Y-%m-%dT%H:%M:%S.%L%:z\n----\n\nThe above adheres to {rfc_3339_link} and can be customized -- as mentioned earlier -- when creating a new logger instance. Example:\n\n[source,ruby]\n----\nCogger.new datetime_format: \"%Y-%m-%d\"\n----\n\n=== Environment\n\nYou can use your environment to define the desired default log level. The default log level is: `\"info\"`. Although, you can set the log level to any of the following:\n\n[source,bash]\n----\nexport LOG_LEVEL=debug\nexport LOG_LEVEL=info\nexport LOG_LEVEL=warn\nexport LOG_LEVEL=error\nexport LOG_LEVEL=fatal\nexport LOG_LEVEL=unknown\n----\n\nWhile downcase is preferred for each log level value, you can use upcased values as well. If the `LOG_LEVEL` environment variable is not set, `Cogger` will fall back to `\"info\"` unless overwritten during initialization. Example: `Cogger.new level: :debug`. Otherwise, an invalid log level will result in an `ArgumentError`.\n\n=== Mutations\n\nEach instance can be mutated using the following messages:\n\n[source,ruby]\n----\nlogger = Cogger.new io: StringIO.new\n\nlogger.close                                       # nil\nlogger.reopen                                      # Logger\nlogger.debug!                                      # 0\nlogger.info!                                       # 1\nlogger.warn!                                       # 2\nlogger.error!                                      # 3\nlogger.fatal!                                      # 4\nlogger.formatter = Cogger::Formatters::Simple.new  # Cogger::Formatters::Simple\nlogger.level = Logger::WARN                        # 2\n----\n\nPlease see the {logger_link} documentation for more information.\n\n=== Emojis\n\nEmojis can be used to decorate and add visual emphasis to your logs. Here are the defaults:\n\n[source,ruby]\n----\nCogger.emojis\n\n# {\n#   :debug =\u003e \"🔎\",\n#    :info =\u003e \"🟢\",\n#    :warn =\u003e \"⚠️\",\n#   :error =\u003e \"🛑\",\n#   :fatal =\u003e \"🔥\",\n#     :any =\u003e \"⚫️\"\n# }\n----\n\nThe `:emoji` formatter is the default formatter which provides dynamic rendering of emojis based on log level. Example:\n\n[source,ruby]\n----\nlogger = Cogger.new\nlogger.info \"Demo\"\n\n# 🟢 [console] Demo\n----\n\nTo add multiple custom emojis, you can chain messages together when registering them:\n\n[source,ruby]\n----\nCogger.add_emoji(:tada, \"🎉\")\n      .add_emoji :favorite, \"❇️\"\n----\n\nIf you always want to use the _same_ emoji, you could use the emoji formatter with a specific template:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: Cogger::Formatters::Emoji.new(\"%\u003cemoji:tada\u003es %\u003cmessage:dynamic\u003es\")\n\nlogger.info \"Demo\"\nlogger.warn \"Demo\"\n\n# 🎉 Demo\n# 🎉 Demo\n----\n\nAs you can see, using a specific emoji will _always_ display regardless of the current log level.\n\n💡 Emojis are used by the color and emoji formatters so check out the _Templates_ and _Formatters_ sections below to learn more.\n\n=== Aliases\n\nAliases are specific to the {tone_link} gem which allows you _alias_ specific colors/styles via a new name. Here's how you can use them:\n\n[source,ruby]\n----\nCogger.add_alias :haze, :bold, :white, :on_purple\nCogger.aliases\n----\n\nThe above would add a `:haze` alias which consists of bold white text on a purple background. Once added, you'd then be able to view a list of all default and custom aliases. You can also override an existing alias if you'd like something else.\n\nAliases are a powerful way to customize colors via concise syntax in your templates. Building upon the aliases, added above, you'd be able to use them in your templates as follows:\n\n[source,ruby]\n----\n# Element\n\"\u003chaze\u003e%\u003cmessage\u003e\u003c/haze\u003e\"\n\n# Key\n\"%\u003cmessage:haze\u003e\"\n----\n\n💡 Aliases are used by the color and emoji formatters so check out the {tone_link} documentation and/or _Templates_ and _Formatters_ sections below to learn more.\n\n=== Templates\n\nTemplates are used by all formatters and adhere to an _enhanced_ version of the {format_link} as used by `Kernel#format`. Here’s what is provided by default:\n\n[source,ruby]\n----\nCogger.templates\n\n# {\n#   :color =\u003e \"\u003cdynamic\u003e[%\u003cid\u003es]\u003c/dynamic\u003e %\u003cmessage:dynamic\u003es\",\n#   :detail =\u003e \"[%\u003cid\u003es] [%\u003clevel\u003es] [%\u003cat\u003es] %\u003cmessage\u003es\",\n#   :emoji =\u003e \"%\u003cemoji:dynamic\u003es \u003cdynamic\u003e[%\u003cid\u003es]\u003c/dynamic\u003e %\u003cmessage:dynamic\u003es\",\n#   :json =\u003e nil,\n#   :property =\u003e nil,\n#   :simple =\u003e \"[%\u003cid\u003es] %\u003cmessage\u003es\",\n#   :rack =\u003e \"[%\u003cid\u003es] [%\u003clevel\u003es] [%\u003cat\u003es] %\u003cverb\u003es %\u003cstatus\u003es %\u003cduration\u003es %\u003cip\u003es %\u003cpath\u003es %\u003clength\u003e# s %\u003cparams\u003es\"\n# }\n----\n\nAll {format_link} specifiers, flags, width, and precision are supported except for the following restrictions:\n\n* Use of _reference by name_ is required which means `%\u003cdemo\u003es` is allowed but `%{demo}` is not. This is because _reference by name_ is required for regular expressions and/or {pattern_matching_link}.\n* Use of the `n$` flag is prohibited because it's not compatible with the above.\n\nIn addition to the above, the {format_link} is further enhanced with the use of keys, emojis, and/or elements. Each is explained in detail below.\n\n==== Keys\n\nTemplate keys works exactly as you'd expect when formatting a string using the {format_link} where each key in the template will be replaced with the corresponding attribute that matches the key. Example:\n\n[source,ruby]\n----\n# Template\n\"%\u003clevel\u003es %\u003cat\u003es %\u003cid\u003es %\u003cmessage\u003es\"\n\n# Output\n# INFO 2024-08-25 10:44:58 -0600 console demo\n----\n\nEach key can be _enhanced_ further by delimiting the key with a colon and supplying a directive. Directives can be any of the following:\n\n* *Dynamic*: Color is automatically calculated based on current log level.\n* *Specific*: Color is specific/static while ignoring current log level.\n\nHere's a few examples to illustrate:\n\n[source,ruby]\n----\n# Dynamic\n\"%\u003clevel:dynamic\u003es %\u003cat:dynamic\u003es %\u003cid:dynamic\u003es %\u003cmessage:dynamic\u003es\"\n\n# Specific\n\"%\u003clevel:purple\u003es %\u003cat:yellow\u003es %\u003cid:cyan\u003es %\u003cmessage:green\u003es\"\n----\n\nIn the dynamic example, the color of each key is determined by current log level (i.e. info, warn, error, etc) which is looked up via the `Cogger.aliases` hash:\n\n[source,ruby]\n----\nCogger.aliases\n# {\n#   debug: %i[white],\n#   info: %i[green],\n#   warn: %i[yellow],\n#   error: %i[red],\n#   fatal: %i[bold white on_red],\n#   any: %i[dim bright_white]\n# }\n----\n\nIn the specific example, the `level` is purple; `at` is yellow; `id` is cyan; and `message` is green. This is means you can mix-n-match dynamic and specific directives as desired:\n\n[source,ruby]\n----\n\"%\u003clevel:dynamic\u003es %\u003cat:yellow\u003es %\u003cid:dynamic\u003es %\u003cmessage:green\u003es\"\n----\n\nAssuming the current log level is _info_, then `level` is green; `at` is yellow; `id` is green; and `message` is green.\n\n==== Emojis\n\nTemplate emojis work similar to _keys_ but the `emoji` key is _special_ in that you can't use `emoji` as a key in your log messages. In other words the `emoji` key can only be used in templates. That said, emojis can be dynamic or specific. Example:\n\n[source,ruby]\n----\n# Dynamic\n\"%\u003cemoji:dynamic\u003es %\u003cmessage:dynamic\u003es\"\n\n# Specific\n\"%\u003cemoji:any\u003es %\u003cmessage:dynamic\u003es\"\n----\n\nIn the dynamic example, the emoji is determined by current log level (i.e. info, warn, error, etc) which is looked up via the `Cogger.emojis` hash:\n\n[source,ruby]\n----\nCogger.emojis\n# {\n#   :debug =\u003e \"🔎\",\n#    :info =\u003e \"🟢\",\n#    :warn =\u003e \"⚠️\",\n#   :error =\u003e \"🛑\",\n#   :fatal =\u003e \"🔥\",\n#     :any =\u003e \"⚫️\"\n# }\n----\n\nIn the specific example, the emoji will be rendered exactly as defined.\n\n==== Elements\n\nTemplate elements are slightly different than _keys_ and _emojis_ in that they behave more like HTML elements. This means you can use open and close tags to use dynamic or specific colors. Example:\n\n[source,ruby]\n----\n# Dynamic\n\"\u003cdynamic\u003e%\u003clevel\u003es %\u003cat\u003es %\u003cid\u003es %\u003cmessage\u003es\u003c/dynamic\u003e\"\n\n# Specific\n\"\u003cpurple\u003e%\u003clevel\u003es %\u003cat\u003es %\u003cid\u003es %\u003cmessage\u003es\u003c/purple\u003e\"\n----\n\nIn the dynamic example, all characters within the template string will use the same color as determined by the current log level. In the specific example, all characters will be purple.\n\nUsing template elements, in this manner, keeps your templates simple when needing to apply the same color to multiple characters at once.\n\n==== Combinations\n\nNow that you know how template keys; emojis; and elements works, this means you can mix and match them in interesting combinations. Example:\n\n[source,ruby]\n----\n\"[%\u003cid:purple\u003es] \u003cdynamic\u003e[%\u003clevel\u003es] [%\u003cat\u003es]\u003c/dynamic\u003e %\u003cmessage:cyan\u003es\"\n----\n\nThe above will render as follows:\n\n* The opening and closing brackets will be white (default color).\n* The `id` will be purple.\n* The `level` and `at` will be dynamic in color based on current log level (this includes the bracket characters).\n* The `message` will be cyan.\n\n==== Guidelines\n\nEach log entry provides you with default keys you can use for the log event metadata in your templates. This stems from the fact that {logger_link} entries always have the following keys:\n\n* `id`: The program/process ID you created your logger with (i.e. `Cogger.new id: :demo`).\n* `level`: The level at which you messaged your logger (i.e. `Cogger#info`).\n* `at`: The date/time as which your log event was created.\n\nAdditional keys as provided by your message hash and/or tags can be customized as desired but the above is _always_ available to you.\n\nTemplate keys, emojis, and elements do have a few restrictions:\n\n* Use the special `emoji` key to provide dynamic or specific emoji logging.\n* Use the special `tags` key to provide tagged logging. More information on tags can be found later in this document.\n* Avoid supplying the same keys as the default keys. Example: `logger.info id: :bad, at: Time.now, level: :bogus`. This is because these keys will be ignored. In other words, you can't _override_ the default keys.\n* Avoid wrapping keys and/or emojis in elements because nesting isn't supported and can lead to strange output. Example: `\u003cgreen\u003e%\u003cemoji:error\u003es %\u003cid:dynamic\u003es\u003c/green\u003e`.\n* Avoid wrapping elements within elements because nesting isn't supported and can lead to strange output. Example: `\u003cdynamic\u003e\u003ccyan\u003e%\u003cmessage\u003es\u003c/cyan\u003e\u003c/dynamic\u003e`.\n* Avoid situations where a message hash doesn't match the keys in the template because an empty message will be logged instead. This applies to all formatters except the JSON formatter which will log any key/value that doesn't have a `nil` value.\n\n=== Formatters\n\nMultiple formatters are provided for you which can be further customized as needed. Here's what is provided by default:\n\n[source,ruby]\n----\nCogger.formatters\n\n# {\n#      :color =\u003e [\n#     Cogger::Formatters::Color \u003c Cogger::Formatters::Abstract,\n#     \"\u003cdynamic\u003e[%\u003cid\u003es]\u003c/dynamic\u003e %\u003cmessage:dynamic\u003es\"\n#   ],\n#     :detail =\u003e [\n#     Cogger::Formatters::Simple \u003c Cogger::Formatters::Abstract,\n#     \"[%\u003cid\u003es] [%\u003clevel\u003es] [%\u003cat\u003es] %\u003cmessage\u003es\"\n#   ],\n#      :emoji =\u003e [\n#     Cogger::Formatters::Emoji \u003c Cogger::Formatters::Color,\n#     \"%\u003cemoji:dynamic\u003es \u003cdynamic\u003e[%\u003cid\u003es]\u003c/dynamic\u003e %\u003cmessage:dynamic\u003es\"\n#   ],\n#       :json =\u003e [\n#     Cogger::Formatters::JSON \u003c Cogger::Formatters::Abstract,\n#     nil\n#   ],\n#   :property =\u003e [\n#     Cogger::Formatters::Property \u003c Cogger::Formatters::Abstract,\n#     nil\n#   ],\n#     :simple =\u003e [\n#     Cogger::Formatters::Simple \u003c Cogger::Formatters::Abstract,\n#     \"[%\u003cid\u003es] %\u003cmessage\u003es\"\n#   ],\n#       :rack =\u003e [\n#     Cogger::Formatters::Simple \u003c Cogger::Formatters::Abstract,\n#     \"[%\u003cid\u003es] [%\u003clevel\u003es] [%\u003cat\u003es] %\u003cverb\u003es %\u003cstatus\u003es %\u003cduration\u003es %\u003cip\u003es %\u003cpath\u003es %\u003clength\u003es # %\u003cparams\u003es\"\n#   ]\n# }\n----\n\nYou can add a formatter by providing a key, class, and _optional_ template. If a template isn't supplied, then the formatter's default template will be used instead (more on this shortly). Example:\n\n[source,ruby]\n----\n# Registration\n\nCogger.add_formatter :basic, Cogger::Formatters::Simple, \"%\u003clevel\u003es %\u003cmessage\u003es\"\n\n# Usage\n\nCogger.get_formatter :basic\n# [Cogger::Formatters::Simple, \"%\u003clevel\u003es %\u003cmessage\u003es\"]\n\nCogger.get_formatter :bogus\n# Unregistered formatter: bogus. (KeyError)\n----\n\nSymbols or strings can be used interchangeably when adding/getting formatters. As mentioned above, a template doesn't have to be supplied if you want to use the formatter's default template which can be inspected via `Cogger.templates` as mentioned earlier.\n\n💡 When you find yourself customizing any of the default formatters, you can reduce typing by adding your custom configuration to the registry and then referring to it via it's associated key when initializing a new logger.\n\n==== Simple\n\nThe simple formatter is a bare bones formatter that uses no color information and only supports basic {format_link} as mentioned in the _Templates_ section earlier. Example:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :simple\n----\n\nThis formatter can be used via the following template variations:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :detail\nlogger = Cogger.new formatter: :rack\n----\n\nℹ️ Any leading or trailing whitespace is automatically removed after the template has been formatted in order to account for template attributes that might be `nil` or empty strings so you don't have visual indentation in your output.\n\n==== Color\n\nThe color formatter allows you to have color coded logs and can be used as follows:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :color\n----\n\nPlease refer back to the _Templates_ section on how to customize this formatter with more sophisticated templates. In addition to template customization, you can customize your color aliases as well. Default colors are provided by {tone_link} which are _aliased_ by log level:\n\n[source,ruby]\n----\nCogger.aliases\n\n{\n  debug: [:white],\n  info: [:green],\n  warn: [:yellow],\n  error: [:red],\n  fatal: %i[bold white on_red],\n  any: [dim bright_white]\n}\n----\n\nThis allows a color -- or combination of color styles (i.e. foreground + background) -- to be dynamically applied based on log level. You can add additional aliases via:\n\n[source,ruby]\n----\nCogger.add_alias :mystery, :white, :on_purple\n----\n\nOnce an alias is added, it can be immediately applied via the template of your formatter. Example:\n\n[source,ruby]\n----\n# Applies the `mystery` alias universally to your template.\nlogger = Cogger.new formatter: Cogger::Formatters::Color.new(\"\u003cmystery\u003e%\u003cmessage\u003es\u003c/mystery\u003e\")\n----\n\nℹ️ Much like the simple formatter, any leading or trailing whitespace is automatically removed after the template has been formatted.\n\n==== Emoji\n\nThe emoji formatter is enabled by default and is the equivalent of initializing with either of the following:\n\n[source,ruby]\n----\nlogger = Cogger.new\nlogger = Cogger.new formatter: :emoji\nlogger = Cogger.new formatter: Cogger::Formatters::Emoji.new\n----\n\nAll of the above examples are identical so you can see how different formatters can be used and customized further. The default emojis are registered as follows:\n\n[source,ruby]\n----\nCogger.emojis\n\n# {\n#   :debug =\u003e \"🔎\",\n#    :info =\u003e \"🟢\",\n#    :warn =\u003e \"⚠️\",\n#   :error =\u003e \"🛑\",\n#   :fatal =\u003e \"🔥\",\n#     :any =\u003e \"⚫️\"\n# }\n----\n\nThis allows an emoji to be dynamically applied based on log level. You can add or modify aliases as follows:\n\n[source,ruby]\n----\nCogger.add_emoji :warn, \"🟡\"\n----\n\nOnce an alias is added/updated, it can be immediately applied via the template of your formatter. Example:\n\n[source,ruby]\n----\nlogger = Cogger.new\nlogger.warn \"Demo\"\n# 🟡 [console] Demo\n----\n\nℹ️ Much like the simple and color formatters, any leading or trailing whitespace is automatically removed after the template has been formatted.\n\n==== Detail\n\nThis formatter is the _Simple_ formatter with a different template and can be configured as follows:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :detail\n----\n\n==== Property\n\nThis formatter is similar in behavior to the _simple_ formatter except the template allows you to _order_ the layout of your keys. All other template information is ignored. Example:\n\n*Default Order*\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :property\n\nlogger.info verb: \"GET\", path: \"/\"\n# id=console level=INFO at=2024-08-28T14:47:09.447-06:00 verb=GET path=/\n----\n\n*Custom Order*\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: Cogger::Formatters::Property.new(\"%\u003clevel\u003es %\u003cverb\u003es\")\n\nlogger.info verb: \"GET\", path: \"/\"\n# level=INFO verb=GET id=console at=2024-08-28T14:49:13.861-06:00 path=/\n----\n\nYour template can be a full or partial match of keys. If no keys match what is defined in the template, then the original order of the keys will be used instead.\n\nYou can always supply a message as your first argument -- or specify it by using the `:message` key -- but is removed if not supplied which is why the above doesn't print a message in the output. To illustrate, the following are equivalent:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :property\n\nlogger.info \"Demo\"\nid=console level=INFO at=2024-08-28T14:50:01.990-06:00 message=Demo\n\nlogger.info message: \"demo\"\n# id=console level=INFO at=2024-08-28T14:50:25.344-06:00 message=demo\n----\n\nWhen tags are provided, the `:tags` key will appear in the output depending on whether you are using _single tags_. If hash tags are used, they'll show up as additional attributes in the output. Here's an example where a mix of single and hash keys are used:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :property\n\nlogger.info \"Demo\", tags: [\"WEB\", \"PRIMARY\", {service: :api, demo: true}]\n# id=console\n# level=INFO\n# at=2024-08-28T14:51:06.600-06:00\n# message=Demo\n# tags=\"[WEB, PRIMARY]\"\n# service=api\n# demo=true\n----\n\nNotice, with the above, that the single tags of `WEB` and `PRIMARY` show up in the `tags` stringified array while the `:service` and `:demo` keys show up at the top level of the hash. Since the `:tags`, `:service`, `:demo` keys are normal keys, like any key in your output, this means you can use a custom template to arrange the order of these keys if you don't like the default.\n\nEmojis, spaces, tabs, new lines, and control characters will all be escaped and wrapped in quotes if detected for any value. Here's where the message has the special characters but this formatting would be applied to any value.\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :property\n\nlogger.info \"☀️ An example.\\t\\n \\x1F\"\n# id=console level=INFO at=2024-08-28T15:03:24.107-06:00 message=\"\\u2600\\uFE0F An example.\\t\\n \\x1F\"\n----\n\n==== JSON\n\nThis formatter is similar in behavior to the _property_ formatter because you can _order_ the layout of your keys. All other template information is ignored, only the order of your template keys matters. Example:\n\n*Default Order*\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :json\n\nlogger.info verb: \"GET\", path: \"/\"\n# {\"id\":\"console\",\"level\":\"INFO\",\"at\":\"2023-12-10T18:42:32.844+00:00\",\"verb\":\"GET\",\"path\":\"/\"}\n----\n\n*Custom Order*\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: Cogger::Formatters::JSON.new(\"%\u003clevel\u003es %\u003cverb\u003es\")\n\nlogger.info verb: \"GET\", path: \"/\"\n# {\"level\":\"INFO\",\"verb\":\"GET\",\"id\":\"console\",\"at\":\"2023-12-10T18:43:03.805+00:00\",\"path\":\"/\"}\n----\n\nYour template can be a full or partial match of keys. If no keys match what is defined in the template, then the original order of the keys will be used instead.\n\nYou can always supply a message as your first argument -- or specify it by using the `:message` key -- but is removed if not supplied which is why the above doesn't print a message in the output. To illustrate, the following are equivalent:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :json\n\nlogger.info \"Demo\"\n# {\"id\":\"console\",\"level\":\"INFO\",\"at\":\"2023-12-10T18:43:42.029+00:00\",\"message\":\"Demo\"}\n\nlogger.info message: \"Demo\"\n# {\"id\":\"console\",\"level\":\"INFO\",\"at\":\"2023-12-10T18:44:14.568+00:00\",\"message\":\"Demo\"}\n----\n\nWhen tags are provided, the `:tags` key will appear in the output depending on whether you are using _single tags_. If hash tags are used, they'll show up as additional attributes in the output. Here's an example where a mix of single and hash keys are used:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :json\n\nlogger.info \"Demo\", tags: [\"WEB\", \"PRIMARY\", {service: :api, demo: true}]\n# {\n#   \"id\":\"console\",\n#   \"level\":\"INFO\",\n#   \"at\":\"2023-12-10T18:44:32.723+00:00\",\n#   \"message\":\"Demo\",\n#   \"tags\":[\"WEB\",\n#   \"PRIMARY\"],\n#   \"service\":\"api\",\n#   \"demo\":true\n# }\n----\n\nNotice, with the above, that the single tags of `WEB` and `PRIMARY` show up in the `tags` array while the `:service` and `:demo` keys show up at the top level of the hash. Since the `:tags`, `:service`, `:demo` keys are normal keys, like any key in your JSON output, this means you can use a custom template to arrange the order of these keys if you don't like the default.\n\n==== Rack\n\nThis formatter is the _Simple_ formatter with a different template and can be configured as follows:\n\n[source,ruby]\n----\nlogger = Cogger.new formatter: :rack\n----\n\n==== Native\n\nShould you wish to use the native formatter as provided by original/native {logger_link}, it will work but not in the manner you might expect. Example:\n\n[source,ruby]\n----\nrequire \"logger\"\n\nlogger = Cogger.new formatter: Logger::Formatter.new\nlogger.info \"Demo\"\n\n# I, [2024-08-28T15:57:31.930722 #69391]  INFO -- console: #\u003cdata Cogger::Entry id=\"console\", level=:INFO, at=2024-08-28 15:57:31.930696 -0600, message=\"Demo\", tags=[], datetime_format=\"%Y-%m-%dT%H:%M:%S.%L%:z\", payload={}\u003e\n----\n\nWhile the above doesn't cause an error, you only get a dump of the `Cogger::Entry` which is not what you want. To replicate native {logger_link} functionality, you can use the `Simple` formatter as follows:\n\n[source,ruby]\n----\nformatter = Cogger::Formatters::Simple.new(\n  \"%\u003clevel\u003es, [%\u003cat\u003es]  %\u003clevel\u003es -- %\u003cid\u003es: %\u003cmessage\u003es\"\n)\nlogger = Cogger.new(formatter:)\nlogger.info \"Demo\"\n\n# INFO, [2023-10-15 15:07:13 -0600]  INFO -- console: Demo\n----\n\nThe above is the rough equivalent of what {logger_link} provides for you by default.\n\n==== Custom\n\nShould none of the built-in formatters be to your liking, you can implement, use, and/or register a custom formatter as well. A minimum implementation would be to inherit from the `Abstract` superclass as follows:\n\n[source,ruby]\n----\nclass MyFormatter \u003c Cogger::Formatters::Abstract\n  TEMPLATE = \"%\u003cmessage\u003es\"\n\n  def initialize template = TEMPLATE\n    super()\n    @template = template\n  end\n\n  def call(*input)\n    *, entry = input\n    attributes = sanitize entry, :tagged\n\n    \"#{format(template, attributes).tap(\u0026:strip!)}\\n\"\n  end\n\n  private\n\n  attr_reader :template\nend\n----\n\nThere is no restriction on the dependencies you might want to inject into your custom formatter but -- at a minimum -- you'll want to provide a default template so it can be sanitized by the superclass. The only other requirement is that you must implement `#call` which takes a log entry which is an array of positional arguments (i.e. `level`, `at`, `id`, `entry`) and answers back a formatted string. If you need more examples you can look at any of the formatters provided within this gem.\n\n=== Tags\n\nTags allow you to tag your messages at both a global and local (i.e. per message) level. _Please note that tags are mostly universal in behavior but can differ based on formatter used._ For example, here's a single global tag:\n\n[source,ruby]\n----\nlogger = Cogger.new tags: %w[WEB]\nlogger.info \"Demo\"\n\n# 🟢 [console] [WEB] Demo\n----\n\nYou can use multiple tags as well:\n\n[source,ruby]\n----\nlogger = Cogger.new tags: %w[WEB EXAMPLE]\nlogger.info \"Demo\"\n\n# 🟢 [console] [WEB] [EXAMPLE] Demo\n----\n\nYou are not limited to string-based tags. Any object will work:\n\n[source,ruby]\n----\nlogger = Cogger.new tags: [\"ONE\", :two, 3, {four: \"FOUR\"}, proc { \"FIVE\" }]\nlogger.info \"Demo\"\n\n# 🟢 [console] [ONE] [two] [3] [FIVE] [four=FOUR] Demo\n----\n\nWith the above, we have string, symbol, integer, hash, and proc tags. With hashes, you'll always get a the key/value pair formatted as: `key=value`. Procs/lambdas allow you to lazy evaluate your tag at time of logging which provides a powerful way to acquire the current process ID, thread ID, and so forth.\n\nIn addition to global tags, you can use local tags per log message. Example:\n\n[source,ruby]\n----\nlogger = Cogger.new\nlogger.info \"Demo\", tags: [\"ONE\", :two, 3, {four: \"FOUR\"}, proc { \"FIVE\" }]\n\n# 🟢 [console] [ONE] [two] [3] [FIVE] [four=FOUR] Demo\n----\n\nYou can also combine global and local tags:\n\n[source,ruby]\n----\nlogger = Cogger.new tags: [\"ONE\", :two]\nlogger.info \"Demo\", tags: [3, proc { \"FOUR\" }]\n\n# 🟢 [console] [ONE] [two] [3] [FOUR] Demo\n----\n\nAs you can see, tags are highly versatile. That said, the following guidelines are worth consideration when using them:\n\n* Prefer uppercase tag names to make them visually stand out.\n* Prefer short names, ideally 1-4 characters since long tags defeat the purpose of brevity.\n* Prefer consistent tag names by using tags that are not synonymous or ambiguous.\n* Prefer using tags by feature rather than things like environments. Examples: API, DB, MAILER.\n* Prefer the JSON formatter for structured metadata instead of tags. Logging JSON formatted messages with tags will work but sticking with a traditional hash, instead of tags, will probably serve you better.\n\n=== Filters\n\nFilters allow you to mask sensitive information you don't want showing up in your logs. The default is an empty set:\n\n[source,ruby]\n----\nCogger.filters  # #\u003cSet: {}\u003e\n----\n\nTo add filters, use:\n\n[source,ruby]\n----\nCogger.add_filter(:login)\n      .add_filter \"email\"\n\nCogger.filters  # #\u003cSet: {:login, :email}\u003e\n----\n\nSymbols and strings can be used interchangeably but are stored as symbols since symbols are used when filtering log entries. Once your filters are in place, you can immediately see their effects:\n\n[source,ruby]\n----\nCogger.add_filter :password\nlogger = Cogger.new formatter: :json\nlogger.info login: \"jayne\", password: \"secret\"\n\n# {\n#   \"id\": \"console\",\n#   \"level\": \"INFO\",\n#   \"at\": \"2024-08-28T16:09:26.132-06:00\",\n#   \"login\": \"jayne\",\n#   \"password\": \"[FILTERED]\"\n# }\n----\n\n=== Streams\n\nYou can add multiple log streams (outputs) by using:\n\n[source,ruby]\n----\nlogger = Cogger.new\n               .add_stream(io: \"tmp/demo.log\")\n               .add_stream(io: nil)\n\nlogger.info \"Demo.\"\n----\n\nThe above would log the `\"Demo.\"` message to `$stdout` -- the default stream -- to the `tmp/demo.log` file, and to `/dev/null`. All attributes used to construct your default logger apply to all additional streams unless customized further. This means any custom template/formatter can be applied to your streams. Example:\n\n[source,ruby]\n----\nlogger = Cogger.new.add_stream(io: \"tmp/demo.log\", formatter: :json)\nlogger.info \"Demo.\"\n----\n\nIn this situation, you'd get colorized output to `$stdout` and JSON output to the `tmp/demo.log` file.\n\nThere is a lot you can do with streams. For example, if you wanted to experiment with the same message formatted by multiple formatters, you could add a stream per format. Example:\n\n[source,ruby]\n----\nlogger = Cogger.new\n               .add_stream(formatter: :color)\n               .add_stream(formatter: :detail)\n               .add_stream(formatter: :json)\n               .add_stream(formatter: :simple)\n\nlogger.info \"Demo\"\n\n# 🟢 [console] Demo\n# [console] Demo\n# [console] [INFO] [2024-08-28T16:10:27.833-06:00] Demo\n# {\"id\":\"console\",\"level\":\"INFO\",\"at\":\"2024-08-28T16:10:27.833-06:00\",\"message\":\"Demo\"}\n# [console] Demo\n----\n\n=== Abort\n\nAborting a program is mostly syntax sugar for Command Line Interfaces (CLIs) which aids in situations where you need to log an error message _and_ exit the program at the same time with an exit code of `1` (similar to how `Kernel#abort` behaves). This allows your CLI to log an error and ensure the exit status is correct when displaying status, piping commands together, etc. All of the arguments, when messaging `#error` directly, are the same. Here's how it works:\n\n[source,ruby]\n----\nlogger = Cogger.new\n\nlogger.abort \"Danger!\"\n# 🛑 [console] Danger!\n# Exits with status code: 1.\n\nlogger.abort { \"Danger!\" }\n# 🛑 [console] Danger!\n# Exits with status code: 1.\n\nlogger.abort message: \"Danger!\"\n# 🛑 [console] Danger!\n# Exits with status code: 1.\n----\n\nYou can use `#abort` without a message which will not log anything and immediately exit:\n\n[source,ruby]\n----\nlogger.abort\n# Logs no message and exits with status code: 1.\n----\n\nThis is _not recommended_ since using `Kernel#exit` directly is more performant.\n\n=== Rack\n\n{rack_link} is _implicitly_ supported which means your middleware _must be_ Rack-based and _must require_ the Rack gem since `Cogger::Rack::Logger` doesn't _explicitly_ require Rack by default. If these requirements are met then, to add HTTP request logging, you only need to use it. Example:\n\n[source,ruby]\n----\nuse Rails::Rack::Logger\n----\n\nLike any other {rack_link} middleware, `Rails::Rack::Logger` is initialized with your current application along with any custom options. Example:\n\n[source,ruby]\n----\nmiddleware = Cogger::Rack::Logger.new application\nmiddleware.call environment\n----\n\nThe following defaults are supported:\n\n[source,ruby]\n----\nCogger::Rack::Logger::DEFAULTS\n\n# {\n#   logger: Cogger.new(formatter: :json),\n#   timer: Cogger::Time::Span.new,\n#   :key_map =\u003e {\n#       :verb =\u003e \"REQUEST_METHOD\",\n#         :ip =\u003e \"REMOTE_ADDR\",\n#       :path =\u003e \"PATH_INFO\",\n#     :params =\u003e \"QUERY_STRING\",\n#     :length =\u003e \"CONTENT_LENGTH\"\n#   }\n# }\n----\n\nThe defaults can be customized. Example:\n\n[source,]\n----\nCogger::Rack::Logger.new application, {logger: Cogger.new}\n----\n\nIn the above example, we see `Cogger.new` overrides the default `Cogger.new(formatter: :json)`. In practice, you'll want to customize the logger and key map. Here's how each default is configured to be used:\n\n* `logger`: Defaults to JSON formatted logging but you'll want to pass in the same logger as globally configured for your application in order to reduce duplication and save on memory.\n* `timer`: The timer calculates the total duration of the request and defaults to nanosecond precision but you can swap this out with your own timer if desired. When providing your own timer, the only requirement is that the timer respond to the `#call` message with a block.\n* `key_map`: The key map is used to map the HTTP Headers to keys (i.e. tags) used in the log output. You can use the existing key map, provide your own, or use a hybrid.\n\nOnce this middleware is configured and used within your application, you'll start seeing the following kinds of log entries (depending on your specific settings and tags used):\n\n[source,json]\n----\n{\n  \"id\":\"demo\",\n  \"level\":\"INFO\",\n  \"at\":\"2023-12-10T22:37:06.341+00:00\",\n  \"verb\":\"GET\",\n  \"ip\":\"127.0.0.1\",\n  \"path\":\"/dashboard\",\n  \"status\":200,\n  \"duration\":83,\n  \"unit\":\"ms\"\n}\n----\n\n*Rails*\n\nTo build upon the above -- and if using the Rails framework -- you could configure your application as follows:\n\n[source,ruby]\n----\n# demo/config/application.rb\nmodule Demo\n  class Application \u003c Rails::Application\n    config.logger = Cogger.new id: :demo, formatter: :json,\n    config.middleware.swap Rails::Rack::Logger, Cogger::Rack::Logger, {logger: config.logger}\n  end\nend\n----\n\nThe above defines `Cogger` as the default logger for the entire application, ensures `Cogger::Rack::Logger` is configured to use it and swaps itself with the default `Rails::Rack::Logger` so you don't have two pieces of middleware logging the same HTTP requests.\n\nAlternatively, you could use a more advanced configuration with even more detailed logging:\n\n[source,ruby]\n----\n# demo/config/application.rb\nmodule Demo\n  class Application \u003c Rails::Application\n    config.version = ENV.fetch \"PROJECT_VERSION\"\n\n    config.logger = Cogger.new id: :demo,\n                               formatter: :json,\n                               tags: [\n                                 proc { {pid: Process.pid, thread: Thread.current.object_id} },\n                                 {team: \"acme\", version: config.version}\n                               ]\n\n    unless Rails.env.test?\n      config.middleware.swap Rails::Rack::Logger, Cogger::Rack::Logger, {logger: config.logger}\n    end\n  end\nend\n----\n\nThe above does the following:\n\n* Fetches the project version from the environment and then logs the version as a tag.\n* PID and thread information are dynamically calculated at runtime, via the proc, as tags too.\n* Team information is also captured as a tag.\n* The middleware is only configured for use in any environment other than the test environment.\n\nYou could also add the following to your Development and Test environments so you capture all logs in a log file:\n\n[source,ruby]\n----\n# Add this to your development and/or test environment configuration.\nconfig.logger = Cogger.new io: Rails.root.join(\"log/#{Rails.env}.log\")\n----\n\n=== Defaults\n\nShould you ever need quick access to the defaults, you can use:\n\n[source,ruby]\n----\nCogger.defaults\n----\n\nThis is primarily meant for display/inspection purposes, though.\n\n=== Inspection\n\nEach instance can be inspected via the `#inspect` message:\n\n[source,ruby]\n----\nlogger = Cogger.new\nlogger.inspect\n\n# \"#\u003cCogger::Hub @id=console,\n#                @io=IO,\n#                @level=1,\n#                @formatter=Cogger::Formatters::Emoji,\n#                @datetime_format=\\\"%Y-%m-%dT%H:%M:%S.%L%:z\\\",\n#                @tags=[],\n#                @mode=false,\n#                @age=0,\n#                @size=1048576,\n#                @suffix=\\\"%Y-%m-%d\\\",\n#                @entry=Cogger::Entry,\n#                @logger=Logger\u003e\"\n----\n\nYou can also look at individual attributes:\n\n[source,ruby]\n----\nlogger = Cogger.new\n\nlogger.id      # \"console\"\nlogger.io      # #\u003cIO:\u003cSTDOUT\u003e\u003e\nlogger.tags    # []\nlogger.mode    # false\nlogger.age     # 0\nlogger.size    # 1048576\nlogger.suffix  # \"%Y-%m-%d\"\n\nlogger.level      # 1\nlogger.formatter  # Cogger::Formatters::Emoji\nlogger.debug?     # false\nlogger.info?      # true\nlogger.warn?      # true\nlogger.error?     # true\nlogger.fatal?     # true\n----\n\n=== Testing\n\nWhen testing, you might find it convenient to rewind and read from the stream you are writing too (i.e. `IO`, `StringIO`, `File`). For instance, here is an example where I inject the default logger into my `Demo` class and then, for testing purposes, create a new logger to be injected which only logs to `StringIO` so I can buffer and read for test verification:\n\n[source,ruby]\n----\nclass Demo\n  def initialize logger: Cogger.new\n    @logger = logger\n  end\n\n  def call(text) = logger.info { text }\n\n  private\n\n  attr_reader :logger\nend\n\nRSpec.describe Demo do\n  subject(:demo) { described_class.new logger: }\n\n  let(:logger) { Cogger.new io: StringIO.new }\n\n  describe \"#call\" do\n    it \"logs message\" do\n      demo.call \"Test.\"\n      expect(logger.reread).to include(\"Test.\")\n    end\n  end\nend\n----\n\nThe ability to `#reread` is only available for the default (first) stream and doesn't work with any additional streams that you add to your logger. That said, this does make it easy to test the `Demo` implementation while also keeping your test suite output clean at the same time. 🎉\n\n== Development\n\nTo contribute, run:\n\n[source,bash]\n----\ngit clone https://github.com/bkuhlmann/cogger\ncd cogger\nbin/setup\n----\n\nYou can also use the IRB console for direct access to all objects:\n\n[source,bash]\n----\nbin/console\n----\n\nLastly, there is a `bin/demo` script which displays multiple log formats for quick visual reference. This is the same script used to generate the screenshots shown at the top of this document.\n\n== Tests\n\nTo test, run:\n\n[source,bash]\n----\nbin/rake\n----\n\n== link:https://alchemists.io/policies/license[License]\n\n== link:https://alchemists.io/policies/security[Security]\n\n== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]\n\n== link:https://alchemists.io/policies/contributions[Contributions]\n\n== link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin]\n\n== link:https://alchemists.io/projects/cogger/versions[Versions]\n\n== link:https://alchemists.io/community[Community]\n\n== Credits\n\n* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].\n* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbkuhlmann%2Fcogger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbkuhlmann%2Fcogger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbkuhlmann%2Fcogger/lists"}