Repository: rocketjob/rails_semantic_logger Branch: master Commit: 7b05bf1b3460 Files: 117 Total size: 154.1 KB Directory structure: gitextract_hgorv55i/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .rubocop.yml ├── Appraisals ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── TESTING.md ├── gemfiles/ │ ├── rails_6.1.gemfile │ ├── rails_7.0.gemfile │ ├── rails_7.0b.gemfile │ ├── rails_7.1.1.gemfile │ ├── rails_7.1.gemfile │ ├── rails_7.1b.gemfile │ ├── rails_7.2.gemfile │ ├── rails_8.0.gemfile │ └── rails_8.1.gemfile ├── lib/ │ ├── rails_semantic_logger/ │ │ ├── action_controller/ │ │ │ └── log_subscriber.rb │ │ ├── action_mailer/ │ │ │ └── log_subscriber.rb │ │ ├── action_view/ │ │ │ └── log_subscriber.rb │ │ ├── active_job/ │ │ │ └── log_subscriber.rb │ │ ├── active_record/ │ │ │ └── log_subscriber.rb │ │ ├── delayed_job/ │ │ │ └── plugin.rb │ │ ├── engine.rb │ │ ├── extensions/ │ │ │ ├── action_cable/ │ │ │ │ └── tagged_logger_proxy.rb │ │ │ ├── action_controller/ │ │ │ │ └── live.rb │ │ │ ├── action_dispatch/ │ │ │ │ └── debug_exceptions.rb │ │ │ ├── action_view/ │ │ │ │ └── streaming_template_renderer.rb │ │ │ ├── active_job/ │ │ │ │ └── logging.rb │ │ │ ├── active_model_serializers/ │ │ │ │ └── logging.rb │ │ │ ├── active_support/ │ │ │ │ ├── log_subscriber.rb │ │ │ │ ├── logger.rb │ │ │ │ └── tagged_logging.rb │ │ │ ├── mongoid/ │ │ │ │ └── config.rb │ │ │ ├── rack/ │ │ │ │ └── server.rb │ │ │ ├── rackup/ │ │ │ │ └── server.rb │ │ │ ├── rails/ │ │ │ │ └── server.rb │ │ │ └── sidekiq/ │ │ │ └── sidekiq.rb │ │ ├── options.rb │ │ ├── rack/ │ │ │ └── logger.rb │ │ ├── sidekiq/ │ │ │ ├── defaults.rb │ │ │ ├── job_logger.rb │ │ │ └── loggable.rb │ │ └── version.rb │ └── rails_semantic_logger.rb ├── rails_semantic_logger.gemspec └── test/ ├── action_controller_test.rb ├── action_mailer_test.rb ├── active_job_test.rb ├── active_record_test.rb ├── controllers/ │ ├── articles_controller_test.rb │ ├── dashboard_controller_test.rb │ └── welcome_controller_test.rb ├── dummy/ │ ├── README.rdoc │ ├── Rakefile │ ├── app/ │ │ ├── assets/ │ │ │ ├── javascripts/ │ │ │ │ ├── application.js │ │ │ │ └── articles.js │ │ │ └── stylesheets/ │ │ │ ├── application.css │ │ │ ├── articles.css │ │ │ └── welcome.css │ │ ├── controllers/ │ │ │ ├── application_controller.rb │ │ │ ├── application_metal_controller.rb │ │ │ ├── articles_controller.rb │ │ │ ├── dashboard_controller.rb │ │ │ └── welcome_controller.rb │ │ ├── helpers/ │ │ │ ├── application_helper.rb │ │ │ ├── articles_helper.rb │ │ │ └── welcome_helper.rb │ │ ├── jobs/ │ │ │ ├── bad_job.rb │ │ │ └── simple_job.rb │ │ ├── mailers/ │ │ │ └── .gitkeep │ │ ├── models/ │ │ │ ├── .gitkeep │ │ │ └── sample.rb │ │ └── views/ │ │ ├── articles/ │ │ │ └── new.html.erb │ │ ├── layouts/ │ │ │ └── application.html.erb │ │ └── welcome/ │ │ └── index.html.erb │ ├── bin/ │ │ ├── bundle │ │ ├── puma │ │ ├── rails │ │ ├── rake │ │ └── setup │ ├── config/ │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments/ │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers/ │ │ │ ├── backtrace_silencers.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── payload_collector.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ ├── sidekiq.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales/ │ │ │ └── en.yml │ │ ├── routes.rb │ │ └── secrets.yml │ ├── config.ru │ ├── db/ │ │ ├── migrate/ │ │ │ └── 20170525020551_create_samples.rb │ │ └── schema.rb │ ├── lib/ │ │ └── assets/ │ │ └── .gitkeep │ ├── log/ │ │ └── .gitkeep │ ├── public/ │ │ ├── 404.html │ │ ├── 422.html │ │ └── 500.html │ ├── script/ │ │ └── rails │ └── test/ │ └── fixtures/ │ └── samples.yml ├── payload_collector.rb ├── rails_test.rb ├── sidekiq_test.rb └── test_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: reidmorrison ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Environment Provide at least: * Ruby Version. * Rails Version. * Semantic Logger Version. * Rails Semantic Logger Version. * Other Application/framework names and versions (e.g. Puma, etc.). * Rails configuration. Only need the settings related to Rails Semantic Logger and Semantic Logger. * Full Stack Trace, if an exception is being raised. Note: * Issues reported here should be related to monkey patches applied to Rails to make it use Semantic Logger. * For other logging and appender related issues, please report the issue at [Semantic Logger](https://github.com/reidmorrison/semantic_logger/issues. ### Expected Behavior * Describe your expectation of how Semantic Logger should behave, perhaps by showing how the builtin Rails logger behaves. * Provide a standalone Ruby script or a link to an example repository that helps reproduce the issue. ### Actual Behavior * Describe or show the actual behavior. * Provide text or screen capture showing the behavior. ### Pull Request * Consider submitting a Pull Request with a fix for the issue. * This is particularly helpful when running newer Rails versions, since we are not running it yet. * Or, even a Pull request that only includes a test that reproduces the problem. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Issue # (if available) ### Description of changes By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. ================================================ FILE: .github/workflows/ci.yml ================================================ name: build on: - push - pull_request jobs: test: name: "Test: Rails ${{ matrix.rails }} on Ruby ${{ matrix.ruby }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - rails: "6.1" ruby: "3.2" - rails: "7.0" ruby: "3.2" - rails: "7.0b" ruby: "3.2" - rails: "7.1" ruby: "3.2" - rails: "7.1b" ruby: "3.2" - rails: "7.1.1" ruby: "3.2" - rails: "7.2" ruby: "3.3" - rails: "8.0" ruby: "3.4" - rails: "8.1" ruby: "3.4" env: BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile DISPLAY: ":99.0" steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} # runs 'bundle install' and caches installed gems automatically bundler-cache: true - name: Gemfile run: echo $BUNDLE_GEMFILE - name: Ruby Version run: ruby --version - name: Run Tests run: bundle exec rake test ================================================ FILE: .gitignore ================================================ .bundle/ *.log pkg/ test/dummy/tmp/ test/dummy/db/test.sqlite3-* test/dummy/.sass-cache *.gem /.idea tags *.DS_Store Gemfile.lock /gemfiles/*.lock *.sqlite3 .rakeTasks TODO.md .tool-versions test/dummy/db/test.sqlite3 mise.toml ================================================ FILE: .rubocop.yml ================================================ AllCops: Exclude: - ".git/**/*" - "docs/**/*" - "gemfiles/*" NewCops: enable TargetRubyVersion: 2.5 # # RuboCop built-in settings. # For documentation on all settings see: https://docs.rubocop.org/en/stable # # Trailing periods. Layout/DotPosition: EnforcedStyle: trailing # Turn on auto-correction of equals alignment. Layout/EndAlignment: AutoCorrect: true # Prevent accidental windows line endings Layout/EndOfLine: EnforcedStyle: lf # Use a table layout for hashes Layout/HashAlignment: EnforcedHashRocketStyle: table EnforcedColonStyle: table # Soften limits Layout/LineLength: Max: 128 Exclude: - "**/test/**/*" # Match existing layout Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space # TODO: Soften Limits for phase 1 only Metrics/AbcSize: Max: 40 # Support long block lengths for tests Metrics/BlockLength: Exclude: - "test/**/*" - "**/*/cli.rb" AllowedMethods: - "aasm" - "included" # Soften limits Metrics/ClassLength: Max: 250 Exclude: - "test/**/*" # TODO: Soften Limits for phase 1 only Metrics/CyclomaticComplexity: Max: 15 # Soften limits Metrics/MethodLength: Max: 50 # Soften limits Metrics/ModuleLength: Max: 250 Metrics/ParameterLists: CountKeywordArgs: false # TODO: Soften Limits for phase 1 only Metrics/PerceivedComplexity: Max: 21 # Initialization Vector abbreviation Naming/MethodParameterName: AllowedNames: [ "iv", "_", "io", "ap", "id", "_id" ] # Does not allow Symbols to load Security/YAMLLoad: AutoCorrect: false # Needed for testing DateTime Style/DateTime: Exclude: [ "test/**/*" ] # TODO: Soften Limits for phase 1 only Style/Documentation: Enabled: false # One line methods Style/EmptyMethod: EnforcedStyle: expanded # Ruby 3 compatibility feature Style/FrozenStringLiteralComment: Enabled: false Style/NumericPredicate: AutoCorrect: true # Incorrectly changes job.fail to job.raise Style/SignalException: Enabled: false # Since English may not be loaded, cannot force its use. Style/SpecialGlobalVars: Enabled: false # Make it easier for developers to move between Elixir and Ruby. Style/StringLiterals: EnforcedStyle: double_quotes ================================================ FILE: Appraisals ================================================ appraise "rails_6.1" do gem "rails", "~> 6.1.0" gem "sidekiq", "~> 5.2" gem "sqlite3", "~> 1.4" end appraise "rails_7.0" do gem "rails", "~> 7.0.0" gem "sidekiq", "~> 6.2.0" gem "sqlite3", "~> 1.4" end appraise "rails_7.0b" do gem "rails", "~> 7.0.0" gem "sidekiq", "~> 6.5" gem "sqlite3", "~> 1.4" end appraise "rails_7.1.1" do gem "rails", "7.1.1" gem "sidekiq", "~> 7.0.9" gem "sqlite3", "~> 1.4" end appraise "rails_7.1" do gem "rails", "~> 7.1.0" gem "sidekiq", "~> 7.1.6" gem "sqlite3", "~> 1.4" end appraise "rails_7.1b" do gem "rails", "~> 7.1.0" gem "sidekiq", "~> 7.3.0" gem "sqlite3", "~> 1.4" end appraise "rails_7.2" do gem "rails", "~> 7.2.0" gem "sidekiq", "~> 7.2.4" end appraise "rails_8.0" do gem "rails", "~> 8.0.0" gem "sidekiq", "~> 7.2.4" end appraise "rails_8.1" do gem "rails", "~> 8.1.1" gem "sidekiq", "~> 7.2.4" end ================================================ FILE: Gemfile ================================================ source "https://rubygems.org" gemspec gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 8.1.0" gem "sidekiq", "~> 7.2.4" gem "sqlite3" gem "rubocop" ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2012, 2013, 2014, 2015 Reid Morrison Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Rails Semantic Logger [![Gem Version](https://img.shields.io/gem/v/rails_semantic_logger.svg)](https://rubygems.org/gems/rails_semantic_logger) [![Build Status](https://github.com/reidmorrison/rails_semantic_logger/workflows/build/badge.svg)](https://github.com/reidmorrison/rails_semantic_logger/actions?query=workflow%3Abuild) [![Downloads](https://img.shields.io/gem/dt/rails_semantic_logger.svg)](https://rubygems.org/gems/rails_semantic_logger) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Production%20Ready-blue.svg) Rails Semantic Logger replaces the Rails default logger with [Semantic Logger](https://logger.rocketjob.io/) When any large Rails application is deployed to production one of the first steps is to move to centralized logging, so that logs can be viewed and searched from a central location. Centralized logging quickly falls apart when trying to consume the current human readable log files: - Log entries often span multiple lines, resulting in unrelated log lines in the centralized logging system. For example, stack traces. - Complex Regular Expressions are needed to parse the text lines and make them machine readable. For example to build queries, or alerts that are looking for specific elements in the message. - Writing searches, alerts, or dashboards based on text logs is incredibly brittle, since a small change to the text logged can often break the parsing of those logs. - Every log entry often has a completely different format, making it difficult to make consistent searches against the data. For these and many other reasons switching to structured logging, or logs in JSON format, in testing and production makes centralized logging incredibly powerful. For example, adding these lines to `config/application.rb` and removing any other log overrides from other environments, will switch automatically to structured logging when running inside Kubernetes: ~~~ruby # Setup structured logging config.semantic_logger.application = "my_application" config.semantic_logger.environment = ENV["STACK_NAME"] || Rails.env config.log_level = ENV["LOG_LEVEL"] || :info # Switch to JSON Logging output to stdout when running on Kubernetes if ENV["LOG_TO_CONSOLE"] || ENV["KUBERNETES_SERVICE_HOST"] config.rails_semantic_logger.add_file_appender = false config.semantic_logger.add_appender(io: $stdout, formatter: :json) end ~~~ Then configure the centralized logging system to tell it that the data is in JSON format, so that it will parse it for you into a hierarchy. For example, the following will instruct [Observe](https://www.observeinc.com/) to parse the JSON data and create machine readable data from it: ~~~ruby interface "log", "log":log make_col event:parse_json(log) make_col time:parse_isotime(event.timestamp), application:string(event.application), environment:string(event.environment), duration:duration_ms(event.duration_ms), level:string(event.level), name:string(event.name), message:string(event.message), named_tags:event.named_tags, payload:event.payload, metric:string(event.metric), metric_amount:float64(event.metric_amount), tags:array(event.tags), exception:event.exception, host:string(event.host), pid:int64(event.pid), thread:string(event.thread), file:string(event.file), line:int64(event.line), dimensions:event.dimensions, backtrace:array(event.backtrace), level_index:int64(event.level_index) set_valid_from(time) drop_col timestamp, log, event, stream rename_col timestamp:time ~~~ Now queries can be built to drill down into each of these fields, including `payload` which is a nested object. For example to find all failed Sidekiq job calls where the causing exception class name is `NoMethodError`: ~~~ruby filter environment = "uat2" filter level = "error" filter metric = "sidekiq.job.perform" filter (string(exception.cause.name) = "NoMethodError") ~~~ Example: create a dashboard showing the duration of all successful Sidekiq jobs: ~~~ruby filter environment = "production" filter level = "info" filter metric = "sidekiq.job.perform" timechart duration:avg(duration), group_by(name) ~~~ Example: create a dashboard showing the queue latency of all Sidekiq jobs. The queue latency is the time between when the job was enqueued and when it was started: ~~~ruby filter environment = "production" filter level = "info" filter metric = "sidekiq.queue.latency" timechart latency:avg(metric_amount/1000), group_by(string(named_tags.queue)) ~~~ * http://github.com/reidmorrison/rails_semantic_logger ## Documentation For complete documentation see: https://logger.rocketjob.io/rails ## Upgrading to Semantic Logger V4.16 - Sidekiq Metrics Support Rails Semantic Logger now supports Sidekiq metrics. Below are the metrics that are now available when the JSON logging format is used: - `sidekiq.job.perform` - The duration of each Sidekiq job. - `duration` contains the time in milliseconds that the job took to run. - `sidekiq.queue.latency` - The time between when a Sidekiq job was enqueued and when it was started. - `metric_amount` contains the time in milliseconds that the job was waiting in the queue. ## Upgrading to Semantic Logger v4.15 & V4.16 - Sidekiq Support Rails Semantic Logger introduces direct support for Sidekiq v4, v5, v6, and v7. Please remove any previous custom patches or configurations to make Sidekiq work with Semantic Logger. To see the complete list of patches being made, and to contribute your own changes, see: [Sidekiq Patches](https://github.com/reidmorrison/rails_semantic_logger/blob/master/lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb) ## Upgrading to Semantic Logger v4.4 With some forking frameworks it is necessary to call `reopen` after the fork. With v4.4 the workaround for Ruby 2.5 crashes is no longer needed. I.e. Please remove the following line if being called anywhere: ~~~ruby SemanticLogger::Processor.instance.instance_variable_set(:@queue, Queue.new) ~~~ ## New Versions of Rails, etc. The primary purpose of the Rails Semantic Logger gem is to patch other gems, primarily Rails, to make them support structured logging though Semantic Logger. When new versions of Rails and other gems are published they often make changes to the internals, so the existing patches stop working. Rails Semantic Logger survives only when someone in the community upgrades to a newer Rails or other supported libraries, runs into problems, and then contributes the fix back to the community by means of a pull request. Additionally, when new popular gems come out, we rely only the community to supply the necessary patches in Rails Semantic Logger to make those gems support structured logging. ## Supported Platforms For the complete list of supported Ruby and Rails versions, see the [Testing file](https://github.com/reidmorrison/rails_semantic_logger/blob/master/.github/workflows/ci.yml). ## Author [Reid Morrison](https://github.com/reidmorrison) [Contributors](https://github.com/reidmorrison/rails_semantic_logger/graphs/contributors) ## Versioning This project uses [Semantic Versioning](http://semver.org/). ================================================ FILE: Rakefile ================================================ # Setup bundler to avoid having to run bundle exec all the time. require "rubygems" require "bundler/setup" require "rake/testtask" require_relative "lib/rails_semantic_logger/version" task :gem do system "gem build rails_semantic_logger.gemspec" end task publish: :gem do system "git tag -a v#{RailsSemanticLogger::VERSION} -m 'Tagging #{RailsSemanticLogger::VERSION}'" system "git push --tags" system "gem push rails_semantic_logger-#{RailsSemanticLogger::VERSION}.gem" system "rm rails_semantic_logger-#{RailsSemanticLogger::VERSION}.gem" end Rake::TestTask.new(:test) do |t| t.pattern = "test/**/*_test.rb" t.verbose = true t.warning = false end # By default run tests against all appraisals if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"] require "appraisal" task default: :appraisal else task default: :test end ================================================ FILE: TESTING.md ================================================ ## Installation Install all needed gems to run the tests: appraisal install The gems are installed into the global gem list. The Gemfiles in the `gemfiles` folder are also re-generated. ## Run Tests For all supported Rails/ActiveRecord versions: rake Or for specific rails version: appraisal rails_4.2 rake Or for one particular test file: appraisal rails_5.0 ruby test/controllers/articles_controller_test.rb Or down to one test case: appraisal rails_5.0 ruby test/controllers/articles_controller_test.rb -n "/shows new article/" ================================================ FILE: gemfiles/rails_6.1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 6.1.0" gem "sidekiq", "~> 5.2" gem "sqlite3", "~> 1.4" gem "rubocop" gemspec path: "../" ================================================ FILE: gemfiles/rails_7.0.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 7.0.0" gem "sidekiq", "~> 6.2.0" gem "sqlite3", "~> 1.4" gem "rubocop" gemspec path: "../" ================================================ FILE: gemfiles/rails_7.0b.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 7.0.0" gem "sidekiq", "~> 6.5" gem "sqlite3", "~> 1.4" gem "rubocop" gemspec path: "../" ================================================ FILE: gemfiles/rails_7.1.1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "7.1.1" gem "sidekiq", "~> 7.0.9" gem "sqlite3", "~> 1.4" gem "rubocop" gemspec path: "../" ================================================ FILE: gemfiles/rails_7.1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 7.1.0" gem "sidekiq", "~> 7.1.6" gem "sqlite3", "~> 1.4" gem "rubocop" gemspec path: "../" ================================================ FILE: gemfiles/rails_7.1b.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 7.1.0" gem "sidekiq", "~> 7.3.0" gem "sqlite3", "~> 1.4" gem "rubocop" gemspec path: "../" ================================================ FILE: gemfiles/rails_7.2.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 7.2.0" gem "sidekiq", "~> 7.2.4" gem "sqlite3" gem "rubocop" gemspec path: "../" ================================================ FILE: gemfiles/rails_8.0.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 8.0.0" gem "sidekiq", "~> 7.2.4" gem "sqlite3" gem "rubocop" gemspec path: "../" ================================================ FILE: gemfiles/rails_8.1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "puma" gem "active_model_serializers" gem "amazing_print" gem "minitest" gem "minitest-rails" gem "rake" gem "sprockets", "< 4.0" gem "rails", "~> 8.1.1" gem "sidekiq", "~> 7.2.4" gem "sqlite3" gem "rubocop" gemspec path: "../" ================================================ FILE: lib/rails_semantic_logger/action_controller/log_subscriber.rb ================================================ module RailsSemanticLogger module ActionController class LogSubscriber < ActiveSupport::LogSubscriber INTERNAL_PARAMS = %w[controller action format _method only_path].freeze class << self attr_accessor :action_message_format end # Log as debug to hide Processing messages in production def start_processing(event) controller_logger(event).debug { action_message("Processing", event.payload) } end def process_action(event) controller_logger(event).info do payload = event.payload.dup # Unused, but needed for Devise 401 status code monkey patch to still work. ::ActionController::Base.log_process_action(payload) params = payload[:params] if params.is_a?(Hash) || params.is_a?(::ActionController::Parameters) # According to PR https://github.com/reidmorrison/rails_semantic_logger/pull/37/files # params is not always a Hash. payload[:params] = params.to_unsafe_h unless params.is_a?(Hash) payload[:params] = params.except(*INTERNAL_PARAMS) if payload[:params].empty? payload.delete(:params) elsif params["file"] # When logging to JSON the entire tempfile is logged, so convert it to a string. payload[:params]["file"] = params["file"].inspect end end format = payload[:format] payload[:format] = format.to_s.upcase if format.is_a?(Symbol) payload[:path] = extract_path(payload[:path]) if payload.key?(:path) exception = payload.delete(:exception) if payload[:status].nil? && exception.present? exception_class_name = exception.first payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end # Rounds off the runtimes. For example, :view_runtime, :mongo_runtime, etc. payload.keys.each do |key| payload[key] = payload[key].to_f.round(2) if key.to_s =~ /(.*)_runtime/ end # Rails 6+ includes allocation count payload[:allocations] = event.allocations if event.respond_to?(:allocations) payload[:status_message] = ::Rack::Utils::HTTP_STATUS_CODES[payload[:status]] if payload[:status].present? # Causes excessive log output with Rails 5 RC1 payload.delete(:headers) # Causes recursion in Rails 6.1.rc1 payload.delete(:request) payload.delete(:response) { message: action_message("Completed", event.payload), duration: event.duration, payload: payload } end end def halted_callback(event) controller_logger(event).info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" } end def send_file(event) controller_logger(event).info(message: "Sent file", payload: {path: event.payload[:path]}, duration: event.duration) end def redirect_to(event) controller_logger(event).info(message: "Redirected to", payload: {location: event.payload[:location]}) end def send_data(event) controller_logger(event).info(message: "Sent data", payload: {file_name: event.payload[:filename]}, duration: event.duration) end def unpermitted_parameters(event) controller_logger(event).debug do unpermitted_keys = event.payload[:keys] "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(', ')}" end end %w[write_fragment read_fragment exist_fragment? expire_fragment expire_page write_page].each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(event) # enable_fragment_cache_logging as of Rails 5 return if ::ActionController::Base.respond_to?(:enable_fragment_cache_logging) && !::ActionController::Base.enable_fragment_cache_logging controller_logger(event).info do key_or_path = event.payload[:key] || event.payload[:path] {message: "#{method.to_s.humanize} \#{key_or_path}", duration: event.duration} end end METHOD end private # Returns the logger for the supplied event. # Returns ActionController::Base.logger if no controller is present def controller_logger(event) controller = event.payload[:controller] return ::ActionController::Base.logger unless controller controller.constantize.logger || ::ActionController::Base.logger rescue NameError ::ActionController::Base.logger end def extract_path(path) index = path.index("?") index ? path[0, index] : path end def action_message(message, payload) if self.class.action_message_format self.class.action_message_format.call(message, payload) else "#{message} ##{payload[:action]}" end end end end end ================================================ FILE: lib/rails_semantic_logger/action_mailer/log_subscriber.rb ================================================ require "active_support/log_subscriber" require "action_mailer" module RailsSemanticLogger module ActionMailer class LogSubscriber < ::ActiveSupport::LogSubscriber def deliver(event) ex = event.payload[:exception_object] message_id = event.payload[:message_id] duration = event.duration.round(1) if ex log_with_formatter event: event, log_duration: true, level: :error do |_fmt| { message: "Error delivering mail #{message_id} (#{duration}ms)", exception: ex } end else message = if event.payload[:perform_deliveries] "Delivered mail #{message_id} (#{duration}ms)" else "Skipped delivery of mail #{message_id} as `perform_deliveries` is false" end log_with_formatter event: event, log_duration: true do |_fmt| {message: message} end end end # An email was generated. def process(event) mailer = event.payload[:mailer] action = event.payload[:action] duration = event.duration.round(1) log_with_formatter event: event do |_fmt| {message: "#{mailer}##{action}: processed outbound mail in #{duration}ms"} end end private class EventFormatter def initialize(event:, log_duration: false) @event = event @log_duration = log_duration end def mailer event.payload[:mailer] end def payload {}.tap do |h| h[:event_name] = event.name h[:mailer] = mailer h[:action] = action h[:message_id] = event.payload[:message_id] h[:perform_deliveries] = event.payload[:perform_deliveries] h[:subject] = event.payload[:subject] h[:to] = event.payload[:to] h[:from] = event.payload[:from] h[:bcc] = event.payload[:bcc] h[:cc] = event.payload[:cc] h[:date] = date h[:duration] = event.duration.round(2) if log_duration? h[:args] = formatted_args end end def date if event.payload[:date].respond_to?(:to_time) event.payload[:date].to_time.utc elsif event.payload[:date].is_a?(String) Time.parse(date).utc end end private attr_reader :event def mailer event.payload[:mailer] end def action event.payload[:action] end def formatted_args if defined?(mailer.constantize.log_arguments?) && !mailer.constantize.log_arguments? "" elsif event.payload[:args].present? JSON.pretty_generate(event.payload[:args].map { |arg| format(arg) }) end end def format(arg) case arg when Hash arg.transform_values { |value| format(value) } when Array arg.map { |value| format(value) } when GlobalID::Identification begin arg.to_global_id rescue StandardError arg end else arg end end def log_duration? @log_duration end end def log_with_formatter(level: :info, **kw_args) fmt = EventFormatter.new(**kw_args) msg = yield fmt logger.public_send(level, **msg, payload: fmt.payload) end def logger ::ActionMailer::Base.logger end end end end ================================================ FILE: lib/rails_semantic_logger/action_view/log_subscriber.rb ================================================ require "active_support/log_subscriber" module RailsSemanticLogger module ActionView # Output Semantic logs from Action View. class LogSubscriber < ActiveSupport::LogSubscriber VIEWS_PATTERN = %r{^app/views/}.freeze class << self attr_reader :logger attr_accessor :rendered_log_level end def initialize @rails_root = nil super end def render_template(event) return unless should_log? payload = { template: from_rails_root(event.payload[:identifier]) } payload[:within] = from_rails_root(event.payload[:layout]) if event.payload[:layout] payload[:allocations] = event.allocations if event.respond_to?(:allocations) logger.measure( self.class.rendered_log_level, "Rendered", payload: payload, duration: event.duration ) end def render_partial(event) return unless should_log? payload = { partial: from_rails_root(event.payload[:identifier]) } payload[:within] = from_rails_root(event.payload[:layout]) if event.payload[:layout] payload[:cache] = event.payload[:cache_hit] unless event.payload[:cache_hit].nil? payload[:allocations] = event.allocations if event.respond_to?(:allocations) logger.measure( self.class.rendered_log_level, "Rendered", payload: payload, duration: event.duration ) end def render_collection(event) return unless should_log? identifier = event.payload[:identifier] || "templates" payload = { template: from_rails_root(identifier), count: event.payload[:count] } payload[:cache_hits] = event.payload[:cache_hits] if event.payload[:cache_hits] payload[:allocations] = event.allocations if event.respond_to?(:allocations) logger.measure( self.class.rendered_log_level, "Rendered", payload: payload, duration: event.duration ) end def start(name, id, payload) if ["render_template.action_view", "render_layout.action_view"].include?(name) && should_log? qualifier = " layout" if name == "render_layout.action_view" payload = {template: from_rails_root(payload[:identifier])} payload[:within] = from_rails_root(payload[:layout]) if payload[:layout] logger.send(self.class.rendered_log_level, message: "Rendering#{qualifier}", payload: payload) end super end if (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR > 7 class Start def start(name, _id, payload) return unless %w[render_template.action_view render_layout.action_view].include?(name) qualifier = " layout" if name == "render_layout.action_view" payload = {template: from_rails_root(payload[:identifier])} payload[:within] = from_rails_root(payload[:layout]) if payload[:layout] logger.debug(message: "Rendering#{qualifier}", payload: payload) end def finish(name, id, payload) end private def from_rails_root(string) string = string.sub(rails_root, "") string.sub!(VIEWS_PATTERN, "") string end def rails_root @root ||= "#{Rails.root}/" end def logger @logger ||= SemanticLogger["ActionView"] end end def self.attach_to(*) ActiveSupport::Notifications.unsubscribe("render_template.action_view") ActiveSupport::Notifications.unsubscribe("render_layout.action_view") ActiveSupport::Notifications.subscribe("render_template.action_view", RailsSemanticLogger::ActionView::LogSubscriber::Start.new) ActiveSupport::Notifications.subscribe("render_layout.action_view", RailsSemanticLogger::ActionView::LogSubscriber::Start.new) super end end private @logger = SemanticLogger["ActionView"] @rendered_log_level = :debug EMPTY = "".freeze def should_log? logger.send("#{self.class.rendered_log_level}?") end def from_rails_root(string) string = string.sub(rails_root, EMPTY) string.sub!(VIEWS_PATTERN, EMPTY) string end def rails_root @rails_root ||= "#{Rails.root}/" end def logger self.class.logger end end end end ================================================ FILE: lib/rails_semantic_logger/active_job/log_subscriber.rb ================================================ require "active_job" module RailsSemanticLogger module ActiveJob class LogSubscriber < ::ActiveSupport::LogSubscriber def enqueue(event) ex = event.payload[:exception_object] if ex log_with_formatter level: :error, event: event do |fmt| { message: "Failed enqueuing #{fmt.job_info} (#{ex.class} (#{ex.message})", exception: ex } end elsif event.payload[:aborted] log_with_formatter level: :info, event: event do |fmt| {message: "Failed enqueuing #{fmt.job_info}, a before_enqueue callback halted the enqueuing execution."} end else log_with_formatter event: event do |fmt| {message: "Enqueued #{fmt.job_info}"} end end end def enqueue_at(event) ex = event.payload[:exception_object] if ex log_with_formatter level: :error, event: event do |fmt| { message: "Failed enqueuing #{fmt.job_info} (#{ex.class} (#{ex.message})", exception: ex } end elsif event.payload[:aborted] log_with_formatter level: :info, event: event do |fmt| {message: "Failed enqueuing #{fmt.job_info}, a before_enqueue callback halted the enqueuing execution."} end else log_with_formatter event: event do |fmt| {message: "Enqueued #{fmt.job_info} at #{fmt.scheduled_at}"} end end end def perform_start(event) log_with_formatter event: event do |fmt| {message: "Performing #{fmt.job_info}"} end end def perform(event) ex = event.payload[:exception_object] if ex log_with_formatter event: event, log_duration: true, level: :error do |fmt| { message: "Error performing #{fmt.job_info} in #{event.duration.round(2)}ms", exception: ex } end else log_with_formatter event: event, log_duration: true do |fmt| {message: "Performed #{fmt.job_info} in #{event.duration.round(2)}ms"} end end end private class EventFormatter def initialize(event:, log_duration: false) @event = event @log_duration = log_duration end def job_info "#{job.class.name} (Job ID: #{job.job_id}) to #{queue_name}" end def payload {}.tap do |h| h[:event_name] = event.name h[:adapter] = adapter_name h[:queue] = job.queue_name h[:job_class] = job.class.name h[:job_id] = job.job_id h[:provider_job_id] = job.try(:provider_job_id) # Not available in Rails 4.2 h[:duration] = event.duration.round(2) if log_duration? h[:arguments] = formatted_args end end def queue_name adapter_name + "(#{job.queue_name})" end def scheduled_at Time.at(event.payload[:job].scheduled_at).utc end private attr_reader :event def job event.payload[:job] end def adapter_name event.payload[:adapter].class.name.demodulize.remove("Adapter") end def formatted_args if defined?(job.class.log_arguments?) && !job.class.log_arguments? "" else JSON.pretty_generate(job.arguments.map { |arg| format(arg) }) end end def format(arg) case arg when String arg.encode("UTF-8", invalid: :replace, undef: :replace) when Hash arg.transform_values { |value| format(value) } when Array arg.map { |value| format(value) } when GlobalID::Identification begin arg.to_global_id rescue StandardError arg end else arg end end def log_duration? @log_duration end end def log_with_formatter(level: :info, **kw_args) fmt = EventFormatter.new(**kw_args) msg = yield fmt logger.public_send(level, **msg, payload: fmt.payload) end def logger ::ActiveJob::Base.logger end end end end ================================================ FILE: lib/rails_semantic_logger/active_record/log_subscriber.rb ================================================ module RailsSemanticLogger module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN].freeze class << self attr_reader :logger end # Rails 7.1 stopped using runtime in log subscribers if Rails.version.to_f < 7.1 def self.runtime=(value) ::ActiveRecord::RuntimeRegistry.sql_runtime = value end def self.runtime ::ActiveRecord::RuntimeRegistry.sql_runtime ||= 0 end def self.reset_runtime rt = runtime self.runtime = 0 rt end end def sql(event) self.class.runtime += event.duration if self.class.respond_to?(:runtime) return unless logger.debug? payload = event.payload name = payload[:name] return if IGNORE_PAYLOAD_NAMES.include?(name) log_payload = {sql: payload[:sql]} log_payload[:binds] = bind_values(payload) unless (payload[:binds] || []).empty? log_payload[:allocations] = event.allocations if event.respond_to?(:allocations) log_payload[:cached] = event.payload[:cached] log_payload[:async] = true if event.payload[:async] log = { message: name, payload: log_payload, duration: event.duration } # Log the location of the query itself. if logger.send(:level_index) >= SemanticLogger.backtrace_level_index log[:backtrace] = SemanticLogger::Utils.strip_backtrace(caller) end logger.debug(log) end private @logger = SemanticLogger["ActiveRecord"] # When multiple values are received for a single bound field, it is converted into an array def add_bind_value(binds, key, value) key = key.downcase.to_sym unless key.nil? if rails_filter_params_include?(key) value = "[FILTERED]" elsif binds.key?(key) value = (Array(binds[key]) << value) end binds[key] = value end def rails_filter_params_include?(key) filter_parameters = Rails.configuration.filter_parameters return filter_parameters.first.match? key if filter_parameters.first.is_a? Regexp filter_parameters.include? key end def logger self.class.logger end # # Rails 3,4,5 hell trying to get the bind values # def bind_values_v3(payload) binds = {} payload[:binds].each do |col, v| if col add_bind_value(binds, col.name, v) else binds[nil] = v end end binds end def bind_values_v4(payload) binds = {} payload[:binds].each do |col, v| attr_name, value = render_bind(col, v) add_bind_value(binds, attr_name, value) end binds end def bind_values_v5_0_0(payload) binds = {} payload[:binds].each do |attr| attr_name, value = render_bind(attr) add_bind_value(binds, attr_name, value) end binds end def bind_values_v5_0_3(payload) binds = {} casted_params = type_casted_binds(payload[:binds], payload[:type_casted_binds]) payload[:binds].zip(casted_params).map do |attr, value| attr_name, value = render_bind(attr, value) add_bind_value(binds, attr_name, value) end binds end def bind_values_v5_1_5(payload) binds = {} casted_params = type_casted_binds(payload[:type_casted_binds]) payload[:binds].zip(casted_params).map do |attr, value| attr_name, value = render_bind(attr, value) add_bind_value(binds, attr_name, value) end binds end def bind_values_v6_1(payload) binds = {} casted_params = type_casted_binds(payload[:type_casted_binds]) payload[:binds].each_with_index do |attr, i| attr_name, value = render_bind(attr, casted_params[i]) add_bind_value(binds, attr_name, value) end binds end def render_bind_v4_2(column, value) if column if column.binary? # This specifically deals with the PG adapter that casts bytea columns into a Hash. value = value[:value] if value.is_a?(Hash) value = value ? "<#{value.bytesize} bytes of binary data>" : "" end [column.name, value] else [nil, value] end end def render_bind_v5_0_0(attribute) value = if attribute.type.binary? && attribute.value if attribute.value.is_a?(Hash) "<#{attribute.value_for_database.to_s.bytesize} bytes of binary data>" else "<#{attribute.value.bytesize} bytes of binary data>" end else attribute.value_for_database end [attribute.name, value] end def render_bind_v5_0_3(attr, value) if attr.is_a?(Array) attr = attr.first elsif attr.type.binary? && attr.value value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" end [attr&.name, value] end def render_bind_v6_1(attr, value) case attr when ActiveModel::Attribute value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" if attr.type.binary? && attr.value when Array attr = attr.first else attr = nil end [attr&.name || :nil, value] end def type_casted_binds_v5_0_3(binds, casted_binds) casted_binds || ::ActiveRecord::Base.connection.type_casted_binds(binds) end def type_casted_binds_v5_1_5(casted_binds) casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds end if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR.zero? && Rails::VERSION::TINY <= 2 # 5.0.0 - 5.0.2 alias bind_values bind_values_v5_0_0 alias render_bind render_bind_v5_0_0 elsif Rails::VERSION::MAJOR == 5 && ((Rails::VERSION::MINOR.zero? && Rails::VERSION::TINY <= 6) || (Rails::VERSION::MINOR == 1 && Rails::VERSION::TINY <= 4)) # 5.0.3 - 5.0.6 && 5.1.0 - 5.1.4 alias bind_values bind_values_v5_0_3 alias render_bind render_bind_v5_0_3 alias type_casted_binds type_casted_binds_v5_0_3 elsif (Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR > 0) || Rails::VERSION::MAJOR >= 7 # ~> 6.1.0 && >= 7.x.x alias bind_values bind_values_v6_1 alias render_bind render_bind_v6_1 alias type_casted_binds type_casted_binds_v5_1_5 elsif Rails::VERSION::MAJOR >= 5 # ~> 5.1.5 && ~> 5.0.7 && 6.x.x alias bind_values bind_values_v5_1_5 alias render_bind render_bind_v5_0_3 alias type_casted_binds type_casted_binds_v5_1_5 elsif Rails.version.to_i >= 4 # 4.x alias bind_values bind_values_v4 alias render_bind render_bind_v4_2 else # 3.x alias bind_values bind_values_v3 end end end end ================================================ FILE: lib/rails_semantic_logger/delayed_job/plugin.rb ================================================ module RailsSemanticLogger module DelayedJob class Plugin < Delayed::Plugin callbacks do |lifecycle| lifecycle.before(:execute) do |_job| ::SemanticLogger.reopen end end end end end ================================================ FILE: lib/rails_semantic_logger/engine.rb ================================================ require "rails" require "rails_semantic_logger/options" module RailsSemanticLogger class Engine < ::Rails::Engine # Make the SemanticLogger config available in the Rails application config # # Example: Add the MongoDB logging appender in the Rails environment # initializer in file config/environments/development.rb # # Rails::Application.configure do # # Add the MongoDB logger appender only once Rails is initialized # config.after_initialize do # appender = SemanticLogger::Appender::Mongo.new( # uri: 'mongodb://127.0.0.1:27017/test' # ) # config.semantic_logger.add_appender(appender: appender) # end # end config.semantic_logger = ::SemanticLogger config.rails_semantic_logger = RailsSemanticLogger::Options.new # Initialize SemanticLogger. In a Rails environment it will automatically # insert itself above the configured rails logger to add support for its # additional features # Replace Rails logger initializer Rails::Application::Bootstrap.initializers.delete_if { |i| i.name == :initialize_logger } initializer :initialize_logger, group: :all do config = Rails.application.config # Set the default log level based on the Rails config SemanticLogger.default_level = config.log_level if defined?(Rails::Rack::Logger) && config.rails_semantic_logger.semantic config.middleware.swap(Rails::Rack::Logger, RailsSemanticLogger::Rack::Logger, config.log_tags) end # Existing loggers are ignored because servers like trinidad supply their # own file loggers which would result in duplicate logging to the same log file Rails.logger = config.logger = begin if config.rails_semantic_logger.add_file_appender path = config.paths["log"].first FileUtils.mkdir_p(File.dirname(path)) unless File.exist?(File.dirname(path)) # Add the log file to the list of appenders # Use the colorized formatter if Rails colorized logs are enabled ap_options = config.rails_semantic_logger.ap_options formatter = config.rails_semantic_logger.format formatter = {color: {ap: ap_options}} if (formatter == :default) && (config.colorize_logging != false) # Set internal logger to log to file only, in case another appender experiences errors during writes appender = SemanticLogger::Appender::File.new(path, formatter: formatter) appender.name = "SemanticLogger" SemanticLogger::Processor.logger = appender # Check for previous file or stdout loggers SemanticLogger.appenders.each do |app| next unless app.is_a?(SemanticLogger::Appender::File) || app.is_a?(SemanticLogger::Appender::IO) app.formatter = formatter end SemanticLogger.add_appender(file_name: path, formatter: formatter, filter: config.rails_semantic_logger.filter) end SemanticLogger[Rails] rescue StandardError => e # If not able to log to file, log to standard error with warning level only SemanticLogger.default_level = :warn SemanticLogger::Processor.logger = SemanticLogger::Appender::IO.new($stderr) SemanticLogger.add_appender(io: $stderr) logger = SemanticLogger[Rails] logger.warn( "Rails Error: Unable to access log file. Please ensure that #{path} exists and is chmod 0666. " \ "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed.", e ) logger end # Replace Rails loggers %i[active_record action_controller action_mailer action_view].each do |name| ActiveSupport.on_load(name) { include SemanticLogger::Loggable } end ActiveSupport.on_load(:action_cable) { self.logger = SemanticLogger["ActionCable"] } end # Before any initializers run, but after the gems have been loaded config.before_initialize do if config.respond_to?(:assets) && defined?(Rails::Rack::Logger) && config.rails_semantic_logger.semantic config.rails_semantic_logger.quiet_assets = true if config.assets.quiet # Otherwise Sprockets can't find the Rails::Rack::Logger middleware config.assets.quiet = false end # Replace the Mongo Loggers Mongoid.logger = SemanticLogger[Mongoid] if defined?(Mongoid) Moped.logger = SemanticLogger[Moped] if defined?(Moped) Mongo::Logger.logger = SemanticLogger[Mongo] if defined?(Mongo::Logger) # Replace the Resque Logger Resque.logger = SemanticLogger[Resque] if defined?(Resque) && Resque.respond_to?(:logger=) # Replace the Sidekiq logger if config.rails_semantic_logger.replace_sidekiq_logger && defined?(::Sidekiq) ::Sidekiq.configure_client do |config| config.logger = ::SemanticLogger[::Sidekiq] end ::Sidekiq.configure_server do |config| config.logger = ::SemanticLogger[::Sidekiq] if config.respond_to?(:options) config.options[:job_logger] = RailsSemanticLogger::Sidekiq::JobLogger else config[:job_logger] = RailsSemanticLogger::Sidekiq::JobLogger end # Add back the default console logger unless already added SemanticLogger.add_appender(io: $stdout, formatter: :color) unless SemanticLogger.appenders.console_output? # Replace default error handler when present existing = RailsSemanticLogger::Sidekiq::Defaults.delete_default_error_handler(config.error_handlers) config.error_handlers << RailsSemanticLogger::Sidekiq::Defaults::ERROR_HANDLER if existing end if defined?(::Sidekiq::Job) && (::Sidekiq::VERSION.to_i != 5) ::Sidekiq::Job.singleton_class.prepend(RailsSemanticLogger::Sidekiq::Loggable) else ::Sidekiq::Worker.singleton_class.prepend(RailsSemanticLogger::Sidekiq::Loggable) end end # Replace the Sidetiq logger Sidetiq.logger = SemanticLogger[Sidetiq] if defined?(Sidetiq) && Sidetiq.respond_to?(:logger=) # Replace the DelayedJob logger if defined?(Delayed::Worker) Delayed::Worker.logger = SemanticLogger[Delayed::Worker] Delayed::Worker.plugins << RailsSemanticLogger::DelayedJob::Plugin end # Replace the Bugsnag logger Bugsnag.configure(false) { |config| config.logger = SemanticLogger[Bugsnag] } if defined?(Bugsnag) # Set the IOStreams PGP logger IOStreams::Pgp.logger = SemanticLogger["IOStreams::Pgp"] if defined?(IOStreams) end # After any initializers run, but after the gems have been loaded config.after_initialize do config = Rails.application.config # Replace the Bugsnag logger Bugsnag.configure(false) { |bugsnag_config| bugsnag_config.logger = SemanticLogger[Bugsnag] } if defined?(Bugsnag) # Rails Patches require("rails_semantic_logger/extensions/action_cable/tagged_logger_proxy") if defined?(::ActionCable) require("rails_semantic_logger/extensions/action_controller/live") if defined?(::ActionController::Live) if defined?(::ActionDispatch::DebugExceptions) require("rails_semantic_logger/extensions/action_dispatch/debug_exceptions") end if defined?(::ActionView::StreamingTemplateRenderer::Body) require("rails_semantic_logger/extensions/action_view/streaming_template_renderer") end require("rails_semantic_logger/extensions/active_job/logging") if defined?(::ActiveJob) require("rails_semantic_logger/extensions/active_model_serializers/logging") if defined?(::ActiveModelSerializers) if config.rails_semantic_logger.semantic # Active Job if defined?(::ActiveJob::Logging::LogSubscriber) RailsSemanticLogger.swap_subscriber( ::ActiveJob::Logging::LogSubscriber, RailsSemanticLogger::ActiveJob::LogSubscriber, :active_job ) end if defined?(::ActiveJob::LogSubscriber) RailsSemanticLogger.swap_subscriber( ::ActiveJob::LogSubscriber, RailsSemanticLogger::ActiveJob::LogSubscriber, :active_job ) end # Active Record if defined?(::ActiveRecord) require "active_record/log_subscriber" RailsSemanticLogger.swap_subscriber( ::ActiveRecord::LogSubscriber, RailsSemanticLogger::ActiveRecord::LogSubscriber, :active_record ) end # Rack RailsSemanticLogger::Rack::Logger.started_request_log_level = :info if config.rails_semantic_logger.started # Silence asset logging by applying a filter to the Rails logger itself, not any of the appenders. if config.rails_semantic_logger.quiet_assets && config.respond_to?(:assets) && config.assets.prefix assets_root = config.relative_url_root.to_s + config.assets.prefix assets_regex = %r(\A/{0,2}#{assets_root}) RailsSemanticLogger::Rack::Logger.logger.filter = ->(log) { log.payload[:path] !~ assets_regex if log.payload } end # Action View if defined?(::ActionView) require "action_view/log_subscriber" RailsSemanticLogger::ActionView::LogSubscriber.rendered_log_level = :info if config.rails_semantic_logger.rendered RailsSemanticLogger.swap_subscriber( ::ActionView::LogSubscriber, RailsSemanticLogger::ActionView::LogSubscriber, :action_view ) end # Action Controller if defined?(::ActionController) require "action_controller/log_subscriber" RailsSemanticLogger::ActionController::LogSubscriber.action_message_format = config.rails_semantic_logger.action_message_format RailsSemanticLogger.swap_subscriber( ::ActionController::LogSubscriber, RailsSemanticLogger::ActionController::LogSubscriber, :action_controller ) end # Action Mailer if defined?(::ActionMailer) require "action_mailer/log_subscriber" RailsSemanticLogger.swap_subscriber( ::ActionMailer::LogSubscriber, RailsSemanticLogger::ActionMailer::LogSubscriber, :action_mailer ) end if config.rails_semantic_logger.replace_sidekiq_logger && defined?(::Sidekiq) require("rails_semantic_logger/extensions/sidekiq/sidekiq") end end # # Forking Frameworks # # Passenger provides the :starting_worker_process event for executing # code after it has forked, so we use that and reconnect immediately. if defined?(PhusionPassenger) PhusionPassenger.on_event(:starting_worker_process) do |forked| SemanticLogger.reopen if forked end end # Re-open appenders after Resque has forked a worker Resque.after_fork { |_job| ::SemanticLogger.reopen } if defined?(Resque.after_fork) # Re-open appenders after Spring has forked a process Spring.after_fork { |_job| ::SemanticLogger.reopen } if defined?(Spring.after_fork) # Re-open appenders after SolidQueue worker/dispatcher/scheduler has finished booting SolidQueue.on_start { ::SemanticLogger.reopen } if defined?(SolidQueue.on_start) SolidQueue.on_worker_start { ::SemanticLogger.reopen } if defined?(SolidQueue.on_worker_start) SolidQueue.on_dispatcher_start { ::SemanticLogger.reopen } if defined?(SolidQueue.on_dispatcher_start) SolidQueue.on_scheduler_start { ::SemanticLogger.reopen } if defined?(SolidQueue.on_scheduler_start) console do |_app| # Don't use a background thread for logging SemanticLogger.sync! # Add a stderr logger when running inside a Rails console unless one has already been added. if config.rails_semantic_logger.console_logger && !SemanticLogger.appenders.console_output? SemanticLogger.add_appender(io: STDERR, formatter: :color) end # Include method names on log entries in the console SemanticLogger.backtrace_level = SemanticLogger.default_level end end end end ================================================ FILE: lib/rails_semantic_logger/extensions/action_cable/tagged_logger_proxy.rb ================================================ require "action_cable/connection/tagged_logger_proxy" module ActionCable module Connection class TaggedLoggerProxy def tag(logger, &block) current_tags = tags - logger.tags logger.tagged(*current_tags, &block) end end end end ================================================ FILE: lib/rails_semantic_logger/extensions/action_controller/live.rb ================================================ # Log actual exceptions, not a string representation require "action_controller" module ActionController module Live undef_method :log_error def log_error(exception) logger.fatal(exception) end end end ================================================ FILE: lib/rails_semantic_logger/extensions/action_dispatch/debug_exceptions.rb ================================================ # Log actual exceptions, not a string representation require "action_dispatch" module ActionDispatch class DebugExceptions private undef_method :log_error if (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR > 7 def log_error(request, wrapper) Rails.application.deprecators.silence do return if !log_rescued_responses?(request) && wrapper.rescue_response? level = request.get_header("action_dispatch.debug_exception_log_level") ActionController::Base.logger.log(level, wrapper.exception) end end else def log_error(_request, wrapper) ActiveSupport::Deprecation.silence do level = wrapper.respond_to?(:rescue_response?) && wrapper.rescue_response? ? :debug : :fatal ActionController::Base.logger.log(level, wrapper.exception) end end end end end ================================================ FILE: lib/rails_semantic_logger/extensions/action_view/streaming_template_renderer.rb ================================================ # Log actual exceptions, not a string representation require "action_view/renderer/streaming_template_renderer" module ActionView class StreamingTemplateRenderer class Body private undef_method :log_error def log_error(exception) ActionView::Base.logger.fatal(exception) end end end end ================================================ FILE: lib/rails_semantic_logger/extensions/active_job/logging.rb ================================================ # Patch ActiveJob logger require "active_job/logging" module ActiveJob module Logging include SemanticLogger::Loggable private undef_method :tag_logger def tag_logger(*tags, &block) if logger.respond_to?(:tagged) logger.tagged(*tags, &block) else yield end end end end ================================================ FILE: lib/rails_semantic_logger/extensions/active_model_serializers/logging.rb ================================================ # Patch ActiveModelSerializers logger require "active_model_serializers/logging" module ActiveModelSerializers module Logging include SemanticLogger::Loggable private def tag_logger(*tags, &block) logger.tagged(*tags, &block) end end class SerializableResource include SemanticLogger::Loggable end end ================================================ FILE: lib/rails_semantic_logger/extensions/active_support/log_subscriber.rb ================================================ if ActiveSupport::VERSION::STRING == "7.1.1" require "active_support/log_subscriber" module ActiveSupport class LogSubscriber # @override Rails 7.1 def silenced?(event) native_log_level = @event_levels.fetch(event, ::Logger::Severity::FATAL) logger.nil? || SemanticLogger::Levels.index(logger.level) > SemanticLogger::Levels.index(native_log_level) end end end end ================================================ FILE: lib/rails_semantic_logger/extensions/active_support/logger.rb ================================================ require "active_support/logger" module ActiveSupport # More hacks to try and stop Rails from being it's own worst enemy. class Logger class << self undef :logger_outputs_to? # Prevent broadcasting since SemanticLogger already supports multiple loggers if method_defined?(:broadcast) undef :broadcast def broadcast(_logger) Module.new end end end # Prevent Console from trying to merge loggers def self.logger_outputs_to?(*_args) true end def self.new(*_args, **_kwargs) SemanticLogger[self] end end end ================================================ FILE: lib/rails_semantic_logger/extensions/active_support/tagged_logging.rb ================================================ module ActiveSupport module TaggedLogging # Semantic Logger already does tagged logging def self.new(logger) logger end end end ================================================ FILE: lib/rails_semantic_logger/extensions/mongoid/config.rb ================================================ require "mongoid/config" module Mongoid module Config private # Remove log overrides def set_log_levels end end end ================================================ FILE: lib/rails_semantic_logger/extensions/rack/server.rb ================================================ module RailsSemanticLogger module Rack module Server def daemonize_app super SemanticLogger.reopen end end end end Rack::Server.prepend(RailsSemanticLogger::Rack::Server) ================================================ FILE: lib/rails_semantic_logger/extensions/rackup/server.rb ================================================ module RailsSemanticLogger module Rackup module Server def daemonize_app super SemanticLogger.reopen end end end end Rackup::Server.prepend(RailsSemanticLogger::Rackup::Server) ================================================ FILE: lib/rails_semantic_logger/extensions/rails/server.rb ================================================ # Patch the Rails::Server log_to_stdout so that it logs via SemanticLogger require "rails" module Rails class Server private undef_method :log_to_stdout if method_defined?(:log_to_stdout) def log_to_stdout wrapped_app # touch the app so the logger is set up SemanticLogger.add_appender(io: $stdout, formatter: :color) unless SemanticLogger.appenders.console_output? end end end ================================================ FILE: lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb ================================================ # Sidekiq patches if Sidekiq::VERSION.to_i == 4 require "sidekiq/logging" require "sidekiq/middleware/server/logging" require "sidekiq/processor" elsif Sidekiq::VERSION.to_i == 5 require "sidekiq/logging" end module Sidekiq # Sidekiq v4 & v5 if defined?(::Sidekiq::Logging) # Replace Sidekiq logging context module Logging def self.with_context(msg, &block) SemanticLogger.tagged(msg, &block) end def self.job_hash_context(job_hash) h = {jid: job_hash["jid"]} h[:bid] = job_hash["bid"] if job_hash["bid"] h[:queue] = job_hash["queue"] if job_hash["queue"] h end end end # Sidekiq v4 if defined?(::Sidekiq::Middleware::Server::Logging) # Convert string to machine readable format class Processor def log_context(job_hash) h = {jid: job_hash["jid"]} h[:bid] = job_hash["bid"] if job_hash["bid"] h[:queue] = job_hash["queue"] if job_hash["queue"] h end end # Let Semantic Logger handle duration logging module Middleware module Server class Logging # rubocop:disable Style/ExplicitBlockArgument def call(worker, item, queue) SemanticLogger.tagged(queue: queue) do if perform_messages_enabled? worker.logger.info( "Start #perform", metric: "sidekiq.queue.latency", metric_amount: job_latency_ms(item) ) worker.logger.measure_info( "Completed #perform", on_exception_level: :error, log_exception: :full, metric: "sidekiq.job.perform" ) { yield } else yield end end end def perform_messages_enabled? RailsSemanticLogger::Sidekiq::JobLogger.perform_messages != false end def job_latency_ms(job) return unless job && job["enqueued_at"] enqueued_at = job["enqueued_at"] if enqueued_at.is_a?(Float) # Sidekiq <= 7: seconds since epoch (Time.now.to_f - enqueued_at) * 1000 else # Sidekiq 8+: milliseconds since epoch now = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) now - enqueued_at end end end end end end end ================================================ FILE: lib/rails_semantic_logger/options.rb ================================================ module RailsSemanticLogger # Options for controlling Rails Semantic Logger behavior # # * Convert Action Controller and Active Record text messages to semantic data # # Rails -- Started -- { :ip => "127.0.0.1", :method => "GET", :path => "/dashboards/inquiry_recent_activity" } # UserController -- Completed #index -- { :action => "index", :db_runtime => 54.64, :format => "HTML", :method => "GET", :mongo_runtime => 0.0, :path => "/users", :status => 200, :status_message => "OK", :view_runtime => 709.88 } # # config.rails_semantic_logger.semantic = true # # * Change Rack started message to debug so that it does not appear in production # # config.rails_semantic_logger.started = false # # * Change Processing message to debug so that it does not appear in production # # config.rails_semantic_logger.processing = false # # * Change Action View render log messages to debug so that they do not appear in production # # ActionView::Base -- Rendered data/search/_user.html.haml (46.7ms) # # config.rails_semantic_logger.rendered = false # # * Override the Amazing Print options for logging Hash data as text: # # Any valid Amazing Print option for rendering data. # The defaults can changed be creating a `~/.aprc` file. # See: https://github.com/amazing-print/amazing_print # # Note: The option :multiline is set to false if not supplied. # Note: Has no effect if Amazing Print is not installed. # # config.rails_semantic_logger.ap_options = {multiline: false} # # * Whether to automatically add an environment specific log file appender. # For Example: 'log/development.log' # # Note: # When Semantic Logger fails to log to an appender it logs the error to an # internal logger, which by default writes to STDERR. # Example, change the default internal logger to log to stdout: # SemanticLogger::Processor.logger = SemanticLogger::Appender::IO.new($stdout, level: :warn) # # config.rails_semantic_logger.add_file_appender = true # # * Silence asset logging # # config.rails_semantic_logger.quiet_assets = false # # * Disable automatic logging to stderr when running a Rails console. # # config.rails_semantic_logger.console_logger = false # # * Override the output format for the primary Rails log file. # # Valid options: # * :default # Plain text output with no color. # * :color # Plain text output with color. # * :json # JSON output format. # * class # # * Proc # A block that will be called to format the output. # It is supplied with the `log` entry and should return the formatted data. # # Note: # * `:default` is automatically changed to `:color` if `config.colorize_logging` is `true`. # # JSON Example, in `application.rb`: # config.rails_semantic_logger.format = :json # # Custom Example, create `app/lib/my_formatter.rb`: # # # My Custom colorized formatter # class MyFormatter < SemanticLogger::Formatters::Color # # Return the complete log level name in uppercase # def level # "#{color}log.level.upcase#{color_map.clear}" # end # end # # # In application.rb: # config.rails_semantic_logger.format = MyFormatter.new # # # config.rails_semantic_logger.format = :default # # * Add a filter to the file logger [Regexp|Proc] # RegExp: Only include log messages where the class name matches the supplied # regular expression. All other messages will be ignored. # Proc: Only include log messages where the supplied Proc returns true. # The Proc must return true or false. # # config.rails_semantic_logger.filter = nil # # * named_tags: *DEPRECATED* # Instead, supply a Hash to config.log_tags # config.rails_semantic_logger.named_tags = nil # # * Change the message format of Action Controller action. # A block that will be called to format the message. # It is supplied with the `message` and `payload` and should return the formatted data. # # config.rails_semantic_logger.action_message_format = -> (message, payload) do # "#{message} - #{payload[:controller]}##{payload[:action]}" # end # # * Do not replace the Sidekiq logger with a Semantic Logger logger. # # config.rails_semantic_logger.replace_sidekiq_logger = false class Options attr_accessor :semantic, :started, :processing, :rendered, :ap_options, :add_file_appender, :quiet_assets, :format, :named_tags, :filter, :console_logger, :action_message_format, :replace_sidekiq_logger # Setup default values def initialize @semantic = true @started = false @processing = false @rendered = false @ap_options = {multiline: false} @add_file_appender = true @quiet_assets = false @format = :default @named_tags = nil @filter = nil @console_logger = true @action_message_format = nil @replace_sidekiq_logger = true end end end ================================================ FILE: lib/rails_semantic_logger/rack/logger.rb ================================================ require "active_support/core_ext/time/conversions" require "active_support/core_ext/object/blank" require "active_support/log_subscriber" require "action_dispatch/http/request" require "rack/body_proxy" module RailsSemanticLogger module Rack class Logger < ActiveSupport::LogSubscriber class << self attr_reader :logger attr_accessor :started_request_log_level end def initialize(app, taggers = nil) @app = app @taggers = taggers || [] end def call(env) request = ActionDispatch::Request.new(env) # Check for named tags (Hash) if @taggers && !@taggers.empty? tags = @taggers.is_a?(Hash) ? compute_named_tags(request) : compute_tags(request) logger.tagged(tags) { call_app(request, env) } else call_app(request, env) end end private @logger = SemanticLogger["Rack"] @started_request_log_level = :debug def call_app(request, env) instrumenter = ActiveSupport::Notifications.instrumenter if (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR > 7 handle = instrumenter.build_handle "request.action_dispatch", request: request instrumenter_finish = lambda { handle.finish } handle.start else instrumenter_state = instrumenter.start "request.action_dispatch", request: request instrumenter_finish = lambda { instrumenter.finish_with_state(instrumenter_state, "request.action_dispatch", request: request) } end logger.send(self.class.started_request_log_level) { started_request_message(request) } status, headers, body = @app.call(env) body = ::Rack::BodyProxy.new(body, &instrumenter_finish) [status, headers, body] rescue Exception instrumenter_finish.call raise end def started_request_message(request) { message: "Started", payload: { method: request.request_method, path: request.filtered_path, ip: request.remote_ip } } end def compute_tags(request) @taggers.collect do |tag| case tag when Proc tag.call(request) when Symbol request.send(tag) else tag end end end # Leave out any named tags with a nil value def compute_named_tags(request) tagged = {} @taggers.each_pair do |tag, value| resolved = case value when Proc value.call(request) when Symbol request.send(value) else value end tagged[tag] = resolved unless resolved.nil? end tagged end def logger self.class.logger end end end end ================================================ FILE: lib/rails_semantic_logger/sidekiq/defaults.rb ================================================ module RailsSemanticLogger module Sidekiq module Defaults # Prevent exception logging during standard error handling since the Job Logger below already logs the exception. ERROR_HANDLER = if ::Sidekiq::VERSION.to_f < 7.1 || (::Sidekiq::VERSION.to_f == 7.1 && ::Sidekiq::VERSION.split(".").last.to_i < 6) lambda do |_ex, ctx| unless ctx.empty? job_hash = ctx[:job] || {} klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"] logger = klass ? SemanticLogger[klass] : ::Sidekiq.logger ctx[:context] ? logger.warn(ctx[:context], ctx) : logger.warn(ctx) end end else lambda do |_ex, ctx, _default_configuration| unless ctx.empty? job_hash = ctx[:job] || {} klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"] logger = klass ? SemanticLogger[klass] : ::Sidekiq.logger ctx[:context] ? logger.warn(ctx[:context], ctx) : logger.warn(ctx) end end end # Returns the default logger after removing from the supplied list. # Returns [nil] when the default logger was not present. def self.delete_default_error_handler(error_handlers) return error_handlers.delete(::Sidekiq::Config::ERROR_HANDLER) if defined?(::Sidekiq::Config::ERROR_HANDLER) return error_handlers.delete(::Sidekiq::DEFAULT_ERROR_HANDLER) if defined?(::Sidekiq::DEFAULT_ERROR_HANDLER) return unless defined?(::Sidekiq::ExceptionHandler) existing = error_handlers.find { |handler| handler.is_a?(::Sidekiq::ExceptionHandler::Logger) } error_handlers.delete(existing) if existing end end end end ================================================ FILE: lib/rails_semantic_logger/sidekiq/job_logger.rb ================================================ module RailsSemanticLogger module Sidekiq class JobLogger class << self attr_writer :perform_messages def perform_messages instance_variable_defined?(:@perform_messages) ? @perform_messages : true end end # Sidekiq 6.5 does not take any arguments, whereas v7 is given a logger def initialize(*_args) end def call(item, queue, &block) klass = item["wrapped"] || item["class"] logger = klass ? SemanticLogger[klass] : Sidekiq.logger SemanticLogger.tagged(queue: queue) do if perform_messages_enabled? # Latency is the time between when the job was enqueued and when it started executing. logger.info( "Start #perform", metric: "sidekiq.queue.latency", metric_amount: job_latency_ms(item) ) end # Measure the duration of running the job if perform_messages_enabled? logger.measure_info( "Completed #perform", on_exception_level: :error, log_exception: :full, metric: "sidekiq.job.perform", &block ) else yield if block_given? end end end def prepare(job_hash, &block) level = job_hash["log_level"] if level SemanticLogger.silence(level) do SemanticLogger.tagged(job_hash_context(job_hash), &block) end else SemanticLogger.tagged(job_hash_context(job_hash), &block) end end private def perform_messages_enabled? self.class.perform_messages != false end def job_hash_context(job_hash) h = {jid: job_hash["jid"]} h[:bid] = job_hash["bid"] if job_hash["bid"] h[:tags] = job_hash["tags"] if job_hash["tags"] h[:queue] = job_hash["queue"] if job_hash["queue"] h end def job_latency_ms(job) return unless job && job["enqueued_at"] (Time.now.to_f - job["enqueued_at"].to_f) * 1000 end end end end ================================================ FILE: lib/rails_semantic_logger/sidekiq/loggable.rb ================================================ module RailsSemanticLogger module Sidekiq module Loggable def included(base) super base.include(SemanticLogger::Loggable) end end end end ================================================ FILE: lib/rails_semantic_logger/version.rb ================================================ module RailsSemanticLogger VERSION = "4.20.0".freeze end ================================================ FILE: lib/rails_semantic_logger.rb ================================================ require "semantic_logger" require "rails_semantic_logger/extensions/rails/server" if defined?(Rails::Server) require "rails_semantic_logger/engine" module RailsSemanticLogger module ActionController autoload :LogSubscriber, "rails_semantic_logger/action_controller/log_subscriber" end module ActionMailer autoload :LogSubscriber, "rails_semantic_logger/action_mailer/log_subscriber" end module ActionView autoload :LogSubscriber, "rails_semantic_logger/action_view/log_subscriber" end module ActiveJob autoload :LogSubscriber, "rails_semantic_logger/active_job/log_subscriber" end module ActiveRecord autoload :LogSubscriber, "rails_semantic_logger/active_record/log_subscriber" end module Rack autoload :Logger, "rails_semantic_logger/rack/logger" end module DelayedJob autoload :Plugin, "rails_semantic_logger/delayed_job/plugin" end module Sidekiq autoload :Defaults, "rails_semantic_logger/sidekiq/defaults" autoload :JobLogger, "rails_semantic_logger/sidekiq/job_logger" autoload :Loggable, "rails_semantic_logger/sidekiq/loggable" end autoload :Options, "rails_semantic_logger/options" # Swap an existing subscriber with a new one def self.swap_subscriber(old_class, new_class, notifier) subscribers = ActiveSupport::LogSubscriber.subscribers.select { |s| s.is_a?(old_class) } subscribers.each { |subscriber| unattach(subscriber) } new_class.attach_to(notifier) end def self.unattach(subscriber) subscriber_patterns(subscriber).each do |pattern| listeners_for(ActiveSupport::Notifications.notifier, pattern).each do |sub| next unless sub.instance_variable_get(:@delegate) == subscriber ActiveSupport::Notifications.unsubscribe(sub) end end ActiveSupport::LogSubscriber.subscribers.delete(subscriber) end def self.subscriber_patterns(subscriber) if subscriber.patterns.respond_to?(:keys) subscriber.patterns.keys else subscriber.patterns end end def self.listeners_for(notifier, pattern) if notifier.respond_to?(:all_listeners_for) # Rails >= 7.1 notifier.all_listeners_for(pattern) else notifier.listeners_for(pattern) end end private_class_method :listeners_for, :subscriber_patterns, :unattach end require("rails_semantic_logger/extensions/mongoid/config") if defined?(Mongoid) require("rails_semantic_logger/extensions/active_support/logger") if defined?(ActiveSupport::Logger) require("rails_semantic_logger/extensions/active_support/log_subscriber") if defined?(ActiveSupport::LogSubscriber) begin require "rackup" rescue LoadError # No need to do anything, will fall back to Rack end if defined?(Rackup::Server) require("rails_semantic_logger/extensions/rackup/server") elsif defined?(Rack::Server) require("rails_semantic_logger/extensions/rack/server") end ================================================ FILE: rails_semantic_logger.gemspec ================================================ $LOAD_PATH.push File.expand_path("lib", __dir__) # Maintain your gem's version: require "rails_semantic_logger/version" # Describe your gem and declare its dependencies: Gem::Specification.new do |s| s.name = "rails_semantic_logger" s.version = RailsSemanticLogger::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Reid Morrison"] s.homepage = "https://logger.rocketjob.io" s.summary = "Feature rich logging framework that replaces the Rails logger." s.files = Dir["lib/**/*", "LICENSE.txt", "Rakefile", "README.md"] s.license = "Apache-2.0" s.required_ruby_version = ">= 2.5" s.add_dependency "rack" s.add_dependency "railties", ">= 5.1" s.add_dependency "semantic_logger", "~> 4.16" s.metadata = { "bug_tracker_uri" => "https://github.com/reidmorrison/rails_semantic_logger/issues", "documentation_uri" => "https://logger.rocketjob.io", "source_code_uri" => "https://github.com/reidmorrison/rails_semantic_logger/tree/v#{RailsSemanticLogger::VERSION}", "rubygems_mfa_required" => "true" } end ================================================ FILE: test/action_controller_test.rb ================================================ require_relative "test_helper" class ActionControllerTest < Minitest::Test describe "RailsSemanticLogger::ActionController::LogSubscriber" do let(:subscriber) { RailsSemanticLogger::ActionController::LogSubscriber.new } describe "#process_action" do it "does not fail if params is not a Hash nor an instance of ActionController::Parameters" do event = ActiveSupport::Notifications::Event.new( "start_processing.action_controller", 5.seconds.ago, Time.zone.now, SecureRandom.uuid, { payload: "{}" } ) messages = semantic_logger_events do subscriber.process_action(event) end assert_equal 1, messages.count, messages end end end end ================================================ FILE: test/action_mailer_test.rb ================================================ require_relative "test_helper" class ActionMailerTest < Minitest::Test class MyMailer < ActionMailer::Base def some_email(opts) mail(to: opts[:to], from: opts[:from], subject: opts[:subject], body: "Hello") end end describe "ActionMailer" do describe "#deliver" do it "sets the ActionMailer logger" do assert_kind_of SemanticLogger::Logger, MyMailer.logger end it "sends the email" do MyMailer.some_email(to: "test@test.com", from: "test@test.com", subject: "test").deliver_now end it "writes log messages" do messages = semantic_logger_events do MyMailer.some_email(to: "test@test.com", from: "test@test.com", subject: "test").deliver_now end assert_equal 2, messages.count, messages assert_semantic_logger_event( messages[0], level: :info, name: "ActionMailer::Base", message_includes: "ActionMailerTest::MyMailer#some_email: processed outbound mail", payload_includes: { event_name: "process.action_mailer", mailer: "ActionMailerTest::MyMailer", action: :some_email } ) assert_semantic_logger_event( messages[1], level: :info, name: "ActionMailer::Base", message_includes: Rails::VERSION::MAJOR >= 6 ? "Delivered mail" : "Skipped delivery", payload_includes: { event_name: "deliver.action_mailer", mailer: "ActionMailerTest::MyMailer", perform_deliveries: Rails::VERSION::MAJOR >= 6 ? true : nil, subject: "test", to: ["test@test.com"], from: ["test@test.com"] } ) end end describe "Logging::LogSubscriber" do before do skip "Older rails does not support ActiveSupport::Notification" unless defined?(ActiveSupport::Notifications) end let(:subscriber) { RailsSemanticLogger::ActionMailer::LogSubscriber.new } let(:event) do ActiveSupport::Notifications::Event.new(event_name, 5.seconds.ago, Time.zone.now, SecureRandom.uuid, payload) end let(:payload) do { mailer: "MyMailer", action: :some_email } end let(:event_name) { "deliver.action_mailer" } let(:mailer) do MyMailer.some_email(to: "test@test.com", from: "test@test.com", subject: "test") end %i[deliver process].each do |method| describe "##{method}" do specify do assert ActionMailer::Base.logger.info subscriber.public_send(method, event) end end end describe "ActiveJob::Logging::LogSubscriber::EventFormatter" do let(:formatter) do RailsSemanticLogger::ActionMailer::LogSubscriber::EventFormatter.new(event: event, log_duration: true) end let(:event_name) { "deliver.action_mailer" } describe "#payload" do specify do assert_equal(formatter.payload[:event_name], "deliver.action_mailer") assert_equal(formatter.payload[:mailer], "MyMailer") assert_equal(formatter.payload[:action], :some_email) assert_kind_of(Float, formatter.payload[:duration]) end end end end end end ================================================ FILE: test/active_job_test.rb ================================================ require_relative "test_helper" class ActiveJobTest < Minitest::Test if defined?(ActiveJob) class MyJob < ActiveJob::Base queue_as :my_jobs def perform(record) "Received: #{record}" end end class SensitiveJob < ActiveJob::Base queue_as :my_jobs if Rails.version.to_f >= 6.1 self.log_arguments = false else def self.log_arguments? false end end def perform(record) "Received: #{record}" end end class TestModel include GlobalID::Identification def id 15 end end end describe "ActiveJob" do before do skip "Older rails does not support ActiveJob" unless defined?(ActiveJob) end describe ".perform_now" do it "sets the ActiveJob logger" do assert_kind_of SemanticLogger::Logger, MyJob.logger end it "runs the job" do MyJob.perform_now("hello") end end describe "Logging::LogSubscriber" do before do skip "Older rails does not support ActiveSupport::Notification" unless defined?(ActiveSupport::Notifications) end let(:subscriber) { RailsSemanticLogger::ActiveJob::LogSubscriber.new } let(:event) do ActiveSupport::Notifications::Event.new(event_name, 5.seconds.ago, Time.zone.now, SecureRandom.uuid, payload) end let(:event_name) { "enqueue.active_job" } let(:payload) do { adapter: ActiveJob::QueueAdapters::InlineAdapter.new, job: job } end let(:job) do MyJob.new(TestModel.new, 1, "string", foo: "bar") end %i[enqueue enqueue_at perform_start perform].each do |method| describe "##{method}" do specify do job.stub(:scheduled_at, Time.zone.now.to_i) do assert ActiveJob::Base.logger.info subscriber.public_send(method, event) end end end end describe "#perform with exception object" do let(:event_name) { "perform.active_job" } let(:payload) do { adapter: ActiveJob::QueueAdapters::InlineAdapter.new, job: job, exception_object: ArgumentError.new("error") } end it "logs messages" do messages = semantic_logger_events do subscriber.perform(event) end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :error, name: "Rails", message_includes: "Error performing ActiveJobTest::MyJob", payload_includes: { job_class: "ActiveJobTest::MyJob", queue: "my_jobs", event_name: "perform.active_job" } ) assert_includes messages[0].payload, :job_id exception = messages[0].exception assert exception.is_a?(ArgumentError) assert_equal "error", exception.message end end describe "#enqueue with exception object" do let(:event_name) { "enqueue.active_job" } let(:payload) do { adapter: ActiveJob::QueueAdapters::InlineAdapter.new, job: job, exception_object: ArgumentError.new("error") } end it "logs message" do messages = semantic_logger_events do subscriber.enqueue(event) end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :error, name: "Rails", message_includes: "Failed enqueuing ActiveJobTest::MyJob", payload_includes: { job_class: "ActiveJobTest::MyJob", queue: "my_jobs", event_name: "enqueue.active_job" } ) assert_includes messages[0].payload, :job_id exception = messages[0].exception assert exception.is_a?(ArgumentError) assert_equal "error", exception.message end end describe "#enqueue with throwing :abort" do let(:event_name) { "enqueue.active_job" } let(:payload) do { adapter: ActiveJob::QueueAdapters::InlineAdapter.new, job: job, aborted: true } end it "logs message" do messages = semantic_logger_events do subscriber.enqueue(event) end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :info, name: "Rails", message_includes: "Failed enqueuing ActiveJobTest::MyJob", payload_includes: { job_class: "ActiveJobTest::MyJob", queue: "my_jobs", event_name: "enqueue.active_job" } ) assert_match(/Failed enqueuing .*, a before_enqueue callback halted the enqueuing execution/, messages[0].message) assert_includes messages[0].payload, :job_id end end describe "#enqueue_at with exception object" do let(:event_name) { "enqueue.active_job" } let(:payload) do { adapter: ActiveJob::QueueAdapters::InlineAdapter.new, job: job, exception_object: ArgumentError.new("error") } end it "logs message" do messages = semantic_logger_events do subscriber.enqueue_at(event) end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :error, name: "Rails", message_includes: "Failed enqueuing ActiveJobTest::MyJob", payload_includes: { job_class: "ActiveJobTest::MyJob", queue: "my_jobs", event_name: "enqueue.active_job" } ) assert_includes messages[0].payload, :job_id exception = messages[0].exception assert exception.is_a?(ArgumentError) assert_equal "error", exception.message end end describe "#enqueue_at with throwing :abort" do let(:event_name) { "enqueue.active_job" } let(:payload) do { adapter: ActiveJob::QueueAdapters::InlineAdapter.new, job: job, aborted: true } end it "logs message" do messages = semantic_logger_events do subscriber.enqueue_at(event) end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :info, name: "Rails", message_includes: "Failed enqueuing ActiveJobTest::MyJob", payload_includes: { job_class: "ActiveJobTest::MyJob", queue: "my_jobs", event_name: "enqueue.active_job" } ) assert_match(/Failed enqueuing .*, a before_enqueue callback halted the enqueuing execution/, messages[0].message) assert_includes messages[0].payload, :job_id end end describe "ActiveJob::Logging::LogSubscriber::EventFormatter" do let(:formatter) do RailsSemanticLogger::ActiveJob::LogSubscriber::EventFormatter.new(event: event, log_duration: true) end let(:event_name) { "perform.active_job" } describe "#payload" do specify do assert_equal(formatter.payload[:event_name], "perform.active_job") assert_equal(formatter.payload[:adapter], "Inline") assert_equal(formatter.payload[:queue], "my_jobs") assert_kind_of(String, formatter.payload[:job_id]) assert_kind_of(Float, formatter.payload[:duration]) end describe "Show arguments in log" do let(:job) do MyJob.new(TestModel.new, 1, "string", foo: "bar") end specify do assert_equal(formatter.payload[:job_class], "ActiveJobTest::MyJob") arguments = <<~ARGS.chomp [ "gid://dummy/ActiveJobTest::TestModel/15", 1, "string", { "foo": "bar" } ] ARGS assert_equal(formatter.payload[:arguments], arguments) end end describe "Hide arguments from log" do let(:job) do SensitiveJob.new(TestModel.new, 1, "string", foo: "bar") end specify do assert_equal(formatter.payload[:job_class], "ActiveJobTest::SensitiveJob") assert_equal(formatter.payload[:arguments], "") end end end describe "#job_info" do specify do assert_match(/^ActiveJobTest::MyJob \(Job ID: [a-z\-0-9]+\) to Inline\(my_jobs\)$/, formatter.job_info) end end describe "#queue_name" do specify do assert_equal(formatter.queue_name, "Inline(my_jobs)") end end end end end end ================================================ FILE: test/active_record_test.rb ================================================ require_relative "test_helper" class ActiveRecordTest < Minitest::Test describe "ActiveRecord" do # Rails 5 has an extra space let(:extra_space) { Rails::VERSION::MAJOR >= 6 ? "" : " " } describe "logs" do it "sql" do expected_sql = "SELECT #{extra_space}\"samples\".* FROM \"samples\" ORDER BY \"samples\".\"id\" ASC LIMIT ?" messages = semantic_logger_events do Sample.first end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :debug, name: "ActiveRecord", message: "Sample Load", payload_includes: { sql: expected_sql, binds: {limit: 1} } ) assert_instance_of Integer, messages[0].payload[:allocations] if Rails.version.to_i >= 6 end it "sql with query cache" do expected_sql = "SELECT #{extra_space}\"samples\".* FROM \"samples\" WHERE \"samples\".\"name\" = ? ORDER BY \"samples\".\"id\" ASC LIMIT ?" messages = semantic_logger_events do Sample.cache { 2.times { Sample.where(name: "foo").first } } end assert_equal 2, messages.count, messages assert_semantic_logger_event( messages[0], level: :debug, name: "ActiveRecord", message: "Sample Load", payload_includes: { sql: expected_sql, binds: {name: "foo", limit: 1} } ) assert_instance_of Integer, messages[0].payload[:allocations] if Rails.version.to_i >= 6 assert_semantic_logger_event( messages[1], level: :debug, name: "ActiveRecord", message: "Sample Load", payload_includes: { sql: expected_sql, binds: {name: "foo", limit: 1}, cached: true } ) assert_instance_of Integer, messages[0].payload[:allocations] if Rails.version.to_i >= 6 end it "single bind value" do expected_sql = if Rails.version.to_f >= 5.2 "SELECT #{extra_space}\"samples\".* FROM \"samples\" WHERE \"samples\".\"name\" = ? ORDER BY \"samples\".\"id\" ASC LIMIT ?" else "SELECT \"samples\".* FROM \"samples\" WHERE \"samples\".\"name\" = ? ORDER BY \"samples\".\"id\" ASC LIMIT ?" end messages = semantic_logger_events do Sample.where(name: "Jack").first end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :debug, name: "ActiveRecord", message: "Sample Load", payload_includes: { sql: expected_sql, binds: {name: "Jack", limit: 1} } ) assert_instance_of Integer, messages[0].payload[:allocations] if Rails.version.to_i >= 6 end it "filtered bind value" do filter_params_setting true, %i[name] do expected_sql = if Rails.version.to_f >= 5.2 "SELECT #{extra_space}\"samples\".* FROM \"samples\" WHERE \"samples\".\"name\" = ? ORDER BY \"samples\".\"id\" ASC LIMIT ?" else "SELECT \"samples\".* FROM \"samples\" WHERE \"samples\".\"name\" = ? ORDER BY \"samples\".\"id\" ASC LIMIT ?" end messages = semantic_logger_events do Sample.where(name: "Jack").first end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :debug, name: "ActiveRecord", message: "Sample Load", payload_includes: { sql: expected_sql, binds: {name: "[FILTERED]", limit: 1} } ) assert_instance_of Integer, messages[0].payload[:allocations] if Rails.version.to_i >= 6 end end it "filtered bind value when filter_parameters set as regex" do filter_params_regex_setting true, %i[name] do expected_sql = if Rails.version.to_f >= 5.2 "SELECT #{extra_space}\"samples\".* FROM \"samples\" WHERE \"samples\".\"name\" = ? ORDER BY \"samples\".\"id\" ASC LIMIT ?" else "SELECT \"samples\".* FROM \"samples\" WHERE \"samples\".\"name\" = ? ORDER BY \"samples\".\"id\" ASC LIMIT ?" end messages = semantic_logger_events do Sample.where(name: "Jack").first end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :debug, name: "ActiveRecord", message: "Sample Load", payload_includes: { sql: expected_sql, binds: {name: "[FILTERED]", limit: 1} } ) assert_instance_of Integer, messages[0].payload[:allocations] if Rails.version.to_i >= 6 end end it "multiple bind values" do skip "Not applicable to older rails" if Rails.version.to_f <= 5.1 expected_sql = "SELECT #{extra_space}\"samples\".* FROM \"samples\" WHERE \"samples\".\"age\" BETWEEN ? AND ? ORDER BY \"samples\".\"id\" ASC LIMIT ?" messages = semantic_logger_events do Sample.where(age: 2..21).first end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :debug, name: "ActiveRecord", message: "Sample Load", payload_includes: { sql: expected_sql, binds: {age: [2, 21], limit: 1} } ) assert_instance_of Integer, messages[0].payload[:allocations] if Rails.version.to_i >= 6 end it "works with an IN clause" do skip "Not applicable to older rails" if Rails.version.to_f <= 5.1 expected_sql = "SELECT #{extra_space}\"samples\".* FROM \"samples\" WHERE \"samples\".\"age\" IN (?, ?) ORDER BY \"samples\".\"id\" ASC LIMIT ?" messages = semantic_logger_events do Sample.where(age: [2, 3]).first end assert_equal 1, messages.count, messages assert_semantic_logger_event( messages[0], level: :debug, name: "ActiveRecord", message: "Sample Load", payload_includes: { sql: expected_sql, binds: {age: [2, 3], limit: 1} } ) end end describe "async queries" do before do skip "Not applicable to older rails" if Rails.version.to_f < 7.1 ActiveRecord::Base.asynchronous_queries_tracker.start_session end after do ActiveRecord::Base.asynchronous_queries_tracker.finalize_session end it "marks async queries with async: true" do expected_sql = 'SELECT COUNT(*) FROM "samples"' messages = semantic_logger_events do Sample.count Sample.async_count.value end assert_equal 2, messages.count, messages messages.each do |message| assert_semantic_logger_event( message, level: :debug, name: "ActiveRecord", message: "Sample Count", payload_includes: {sql: expected_sql} ) end # On Rails prior to 8.0.2, these assertions will mostly pass, but not always. # https://github.com/rails/rails/pull/54344 skip "Older Rails has flakey async instrumentation" if Rails::VERSION::MAJOR < 8 refute messages[0].payload.key?(:async) assert_equal true, messages[1].payload[:async] end end # we could feasibly pull this back to rails 7.1. This update is related to rails 8.1 # https://github.com/reidmorrison/rails_semantic_logger/pull/276#issuecomment-3533151110 describe "runtime=" do it "allow reads and writes to the runtime" do unless RailsSemanticLogger::ActiveRecord::LogSubscriber.respond_to?(:runtime) skip "runtime support ended with Rails v7.1" end RailsSemanticLogger::ActiveRecord::LogSubscriber.runtime = 5.0 assert_equal RailsSemanticLogger::ActiveRecord::LogSubscriber.runtime, 5.0 end it "write to the runtime" do unless RailsSemanticLogger::ActiveRecord::LogSubscriber.respond_to?(:runtime) skip "runtime support ended with Rails v7.1" end initial_value = RailsSemanticLogger::ActiveRecord::LogSubscriber.runtime RailsSemanticLogger::ActiveRecord::LogSubscriber.runtime = initial_value + 5000.0 assert_equal RailsSemanticLogger::ActiveRecord::LogSubscriber.runtime, initial_value + 5000.0 end end end end ================================================ FILE: test/controllers/articles_controller_test.rb ================================================ require_relative "../test_helper" class ArticlesControllerTest < ActionDispatch::IntegrationTest describe ArticlesController do let(:params) { {article: {text: "Text1", title: "Title1"}} } describe "#new" do it "shows new article" do get article_url(:new) assert_response :success end end describe "#create" do it "has no errors" do post articles_url(params: params) assert_response :success end it "successfully logs message" do messages = semantic_logger_events do post articles_url(params: params) end assert_equal 5, messages.count, messages assert_semantic_logger_event( messages[0], message: "Started", name: "Rack", level: :debug, payload: { method: "POST", path: "/articles?article%5Btext%5D=Text1&article%5Btitle%5D=Title1", ip: "127.0.0.1" } ) assert_semantic_logger_event( messages[1], message: "Processing #create", name: "ArticlesController", level: :debug ) assert_semantic_logger_event( messages[2], message: "Rendering", name: "ActionView", level: :debug, payload: { template: "text template" } ) assert_semantic_logger_event( messages[3], message: "Rendered", name: "ActionView", level: :debug ) assert_semantic_logger_event( messages[4], message: "Completed #create", name: "ArticlesController", level: :info, payload_includes: { controller: "ArticlesController", action: "create", params: { "article" => { "text" => "Text1", "title" => "Title1" } }, format: "HTML", method: "POST", path: "/articles", status: 200, status_message: "OK" } ) end it "customize action message" do old_action_message_format = RailsSemanticLogger::ActionController::LogSubscriber.action_message_format RailsSemanticLogger::ActionController::LogSubscriber.action_message_format = -> (message, payload) do "#{message} #{payload[:controller]}##{payload[:action]}" end messages = semantic_logger_events do post articles_url(params: params) end assert_equal 5, messages.count, messages assert_semantic_logger_event( messages[0], message: "Started" ) assert_semantic_logger_event( messages[1], message: "Processing ArticlesController#create" ) assert_semantic_logger_event( messages[2], message: "Rendering" ) assert_semantic_logger_event( messages[3], message: "Rendered" ) assert_semantic_logger_event( messages[4], message: "Completed ArticlesController#create", ) ensure RailsSemanticLogger::ActionController::LogSubscriber.action_message_format = old_action_message_format end end describe "#show" do it "raises and logs exception" do # we're testing ActionDispatch::DebugExceptions in fact messages = semantic_logger_events do old_show = Rails.application.env_config["action_dispatch.show_exceptions"] begin Rails.application.env_config["action_dispatch.show_exceptions"] = :all get article_url(:show) rescue ActiveRecord::RecordNotFound => e # expected ensure Rails.application.env_config["action_dispatch.show_exceptions"] = old_show end end assert_equal 4, messages.count, messages assert_kind_of ActiveRecord::RecordNotFound, messages[3].exception end it "raises and does not log exception when action_dispatch.log_rescued_responses is false" do skip "Not applicable to older rails" if Rails.version.to_f < 7.1 # we're testing ActionDispatch::DebugExceptions here too messages = semantic_logger_events do old_show = Rails.application.env_config["action_dispatch.show_exceptions"] old_log_rescued_responses = Rails.application.env_config["action_dispatch.log_rescued_responses"] begin Rails.application.env_config["action_dispatch.show_exceptions"] = :all Rails.application.env_config["action_dispatch.log_rescued_responses"] = false get article_url(:show) rescue ActiveRecord::RecordNotFound => e # expected ensure Rails.application.env_config["action_dispatch.show_exceptions"] = old_show Rails.application.env_config["action_dispatch.log_rescued_responses"] = old_log_rescued_responses end end assert_equal 3, messages.count, messages end end end end ================================================ FILE: test/controllers/dashboard_controller_test.rb ================================================ require_relative "../test_helper" class DashboardControllerTest < ActionDispatch::IntegrationTest describe DashboardController do describe "#show" do it "has no errors" do get dashboard_url assert_response :success end it "logs message" do messages = semantic_logger_events do get dashboard_url end assert_equal 3, messages.count, messages assert_semantic_logger_event( messages[0], level: :debug, name: "Rack", message: "Started", payload: { method: "GET", path: "/dashboard", ip: "127.0.0.1" } ) assert_semantic_logger_event( messages[1], level: :debug, name: "Rails", message: "Processing #show", payload: nil ) assert_semantic_logger_event( messages[2], level: :info, name: "Rails", message: "Completed #show", payload_includes: { controller: "DashboardController", action: "show", format: "HTML", method: "GET", path: "/dashboard", status: 200, status_message: "OK" } ) end it "does not break rails notifications" do PayloadCollector.wrap do get dashboard_url end payload = PayloadCollector.last assert_equal payload[:params], {"controller" => "dashboard", "action" => "show"} end end end end ================================================ FILE: test/controllers/welcome_controller_test.rb ================================================ require_relative "../test_helper" class WelcomeControllerTest < ActionDispatch::IntegrationTest describe WelcomeController do describe "#index" do it "succeeds" do get "/welcome/index" assert_response :success end end end end ================================================ FILE: test/dummy/README.rdoc ================================================ == Welcome to Rails Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Control pattern. This pattern splits the view (also called the presentation) into "dumb" templates that are primarily responsible for inserting pre-built data in between HTML tags. The model contains the "smart" domain objects (such as Account, Product, Person, Post) that holds all the business logic and knows how to persist themselves to a database. The controller handles the incoming requests (such as Save New Account, Update Product, Show Post) by manipulating the model and directing data to the view. In Rails, the model is handled by what's called an object-relational mapping layer entitled Active Record. This layer allows you to present the data from database rows as objects and embellish these data objects with business logic methods. You can read more about Active Record in link:files/vendor/rails/activerecord/README.html. The controller and view are handled by the Action Pack, which handles both layers by its two parts: Action View and Action Controller. These two layers are bundled in a single package due to their heavy interdependence. This is unlike the relationship between the Active Record and Action Pack that is much more separate. Each of these packages can be used independently outside of Rails. You can read more about Action Pack in link:files/vendor/rails/actionpack/README.html. == Getting Started 1. At the command prompt, create a new Rails application: rails new myapp (where myapp is the application name) 2. Change directory to myapp and start the web server: cd myapp; rails server (run with --help for options) 3. Go to http://localhost:3000/ and you'll see: "Welcome aboard: You're riding Ruby on Rails!" 4. Follow the guidelines to start developing your application. You can find the following resources handy: * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ == Debugging Rails Sometimes your application goes wrong. Fortunately there are a lot of tools that will help you debug it and get it back on the rails. First area to check is the application log files. Have "tail -f" commands running on the server.log and development.log. Rails will automatically display debugging and runtime information to these files. Debugging info will also be shown in the browser on requests from 127.0.0.1. You can also log your own messages directly into the log file from your code using the Ruby logger class from inside your controllers. Example: class WeblogController < ActionController::Base def destroy @weblog = Weblog.find(params[:id]) @weblog.destroy logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") end end The result will be a message in your log file along the lines of: Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! More information on how to use the logger is at http://www.ruby-doc.org/core/ Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are several books available online as well: * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) These two books will bring you up to speed on the Ruby language and also on programming in general. == Debugger Debugger support is available through the debugger command when you start your Mongrel or WEBrick server with --debugger. This means that you can break out of execution at any point in the code, investigate and change the model, and then, resume execution! You need to install ruby-debug to run the server in debugging mode. With gems, use sudo gem install ruby-debug. Example: class WeblogController < ActionController::Base def index @posts = Post.all debugger end end So the controller will accept the action, run the first line, then present you with a IRB prompt in the server window. Here you can do things like: >> @posts.inspect => "[#nil, "body"=>nil, "id"=>"1"}>, #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" >> @posts.first.title = "hello from a debugger" => "hello from a debugger" ...and even better, you can examine how your runtime objects actually work: >> f = @posts.first => #nil, "body"=>nil, "id"=>"1"}> >> f. Display all 152 possibilities? (y or n) Finally, when you're ready to resume execution, you can enter "cont". == Console The console is a Ruby shell, which allows you to interact with your application's domain model. Here you'll have all parts of the application configured, just like it is when the application is running. You can inspect domain models, change values, and save to the database. Starting the script without arguments will launch it in the development environment. To start the console, run rails console from the application directory. Options: * Passing the -s, --sandbox argument will rollback any modifications made to the database. * Passing an environment name as an argument will load the corresponding environment. Example: rails console production. To reload your controllers and models after launching the console run reload! More information about irb can be found at: link:http://www.rubycentral.org/pickaxe/irb.html == dbconsole You can go to the command line of your database directly through rails dbconsole. You would be connected to the database with the credentials defined in database.yml. Starting the script without arguments will connect you to the development database. Passing an argument will connect you to a different database, like rails dbconsole production. Currently works for MySQL, PostgreSQL and SQLite 3. == Description of Contents The default directory structure of a generated Ruby on Rails application: |-- app | |-- assets | |-- images | |-- javascripts | `-- stylesheets | |-- controllers | |-- helpers | |-- mailers | |-- models | `-- views | `-- layouts |-- config | |-- environments | |-- initializers | `-- locales |-- db |-- doc |-- lib | `-- tasks |-- log |-- public |-- script |-- test | |-- fixtures | |-- functional | |-- integration | |-- performance | `-- unit |-- tmp | |-- cache | |-- pids | |-- sessions | `-- sockets `-- vendor |-- assets `-- stylesheets `-- plugins app Holds all the code that's specific to this particular application. app/assets Contains subdirectories for images, stylesheets, and JavaScript files. app/controllers Holds controllers that should be named like weblogs_controller.rb for automated URL mapping. All controllers should descend from ApplicationController which itself descends from ActionController::Base. app/models Holds models that should be named like post.rb. Models descend from ActiveRecord::Base by default. app/views Holds the template files for the view that should be named like weblogs/index.html.erb for the WeblogsController#index action. All views use eRuby syntax by default. app/views/layouts Holds the template files for layouts to be used with views. This models the common header/footer method of wrapping views. In your views, define a layout using the layout :default and create a file named default.html.erb. Inside default.html.erb, call <% yield %> to render the view using this layout. app/helpers Holds view helpers that should be named like weblogs_helper.rb. These are generated for you automatically when using generators for controllers. Helpers can be used to wrap functionality for your views into methods. config Configuration files for the Rails environment, the routing map, the database, and other dependencies. db Contains the database schema in schema.rb. db/migrate contains all the sequence of Migrations for your schema. doc This directory is where your application documentation will be stored when generated using rake doc:app lib Application specific libraries. Basically, any kind of custom code that doesn't belong under controllers, models, or helpers. This directory is in the load path. public The directory available for the web server. Also contains the dispatchers and the default HTML files. This should be set as the DOCUMENT_ROOT of your web server. script Helper scripts for automation and generation. test Unit and functional tests along with fixtures. When using the rails generate command, template test files will be generated for you and placed in this directory. vendor External libraries that the application depends on. Also includes the plugins subdirectory. If the app has frozen rails, those gems also go here, under vendor/rails/. This directory is in the load path. ================================================ FILE: test/dummy/Rakefile ================================================ #!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path("config/application", __dir__) Dummy::Application.load_tasks ================================================ FILE: test/dummy/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // //# require jquery //# require jquery_ujs //# require_tree . ================================================ FILE: test/dummy/app/assets/javascripts/articles.js ================================================ // Place all the behaviors and hooks related to the matching controller here. // All this logic will automatically be available in application.js. ================================================ FILE: test/dummy/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the top of the * compiled file, but it's generally better to create a new file per style scope. * *= require_self *= require_tree . */ ================================================ FILE: test/dummy/app/assets/stylesheets/articles.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: test/dummy/app/assets/stylesheets/welcome.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: test/dummy/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base protect_from_forgery with: :exception end ================================================ FILE: test/dummy/app/controllers/application_metal_controller.rb ================================================ class ApplicationMetalController < ActionController::Metal MODULES = [ ActionController::Instrumentation, AbstractController::Rendering, ActionController::Rendering, ActionController::Renderers::All # Helpers::Controller ].freeze MODULES.each do |mod| include mod end end ================================================ FILE: test/dummy/app/controllers/articles_controller.rb ================================================ class ArticlesController < ApplicationController def new end def create render plain: params[:article].inspect end def show raise ActiveRecord::RecordNotFound end end ================================================ FILE: test/dummy/app/controllers/dashboard_controller.rb ================================================ class DashboardController < ApplicationMetalController def show end end ================================================ FILE: test/dummy/app/controllers/welcome_controller.rb ================================================ class WelcomeController < ApplicationController def index end end ================================================ FILE: test/dummy/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/dummy/app/helpers/articles_helper.rb ================================================ module ArticlesHelper end ================================================ FILE: test/dummy/app/helpers/welcome_helper.rb ================================================ module WelcomeHelper end ================================================ FILE: test/dummy/app/jobs/bad_job.rb ================================================ class BadJob include Sidekiq::Worker sidekiq_options retry: false def perform raise ArgumentError, "This is a bad job" end end ================================================ FILE: test/dummy/app/jobs/simple_job.rb ================================================ class SimpleJob include Sidekiq::Worker def perform "SimpleJob is working" end end ================================================ FILE: test/dummy/app/mailers/.gitkeep ================================================ ================================================ FILE: test/dummy/app/models/.gitkeep ================================================ ================================================ FILE: test/dummy/app/models/sample.rb ================================================ class Sample < ActiveRecord::Base end ================================================ FILE: test/dummy/app/views/articles/new.html.erb ================================================

New Article

<%= form_for :article, url: articles_path do |f| %>

<%= f.label :title %>
<%= f.text_field :title %>

<%= f.label :text %>
<%= f.text_area :text %>

<%= f.submit %>

<% end %> ================================================ FILE: test/dummy/app/views/layouts/application.html.erb ================================================ Dummy <%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> <%= yield %> ================================================ FILE: test/dummy/app/views/welcome/index.html.erb ================================================

Welcome#index

Find me in app/views/welcome/index.html.erb

================================================ FILE: test/dummy/bin/bundle ================================================ #!/usr/bin/env ruby ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) load Gem.bin_path("bundler", "bundle") ================================================ FILE: test/dummy/bin/puma ================================================ #!/usr/bin/env ruby # # This file was generated by Bundler. # # The application 'puma' is installed as part of a gem, and # this file is here to facilitate running it. # require "pathname" ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath) require "rubygems" require "bundler/setup" load Gem.bin_path("puma", "puma") ================================================ FILE: test/dummy/bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" ================================================ FILE: test/dummy/bin/rake ================================================ #!/usr/bin/env ruby require_relative "../config/boot" require "rake" Rake.application.run ================================================ FILE: test/dummy/bin/setup ================================================ #!/usr/bin/env ruby require "pathname" # path to your application root. APP_ROOT = Pathname.new File.expand_path("..", __dir__) Dir.chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file: puts "== Installing dependencies ==" system "gem install bundler --conservative" system "bundle check || bundle install" # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") # system "cp config/database.yml.sample config/database.yml" # end puts "\n== Removing old logs and tempfiles ==" system "rm -f log/*" system "rm -rf tmp/cache" puts "\n== Restarting application server ==" system "touch tmp/restart.txt" end ================================================ FILE: test/dummy/config/application.rb ================================================ require File.expand_path("boot", __dir__) require "rails/all" Bundler.require module Dummy class Application < Rails::Application config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" if config.respond_to?(:load_defaults) # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] config.active_record.sqlite3.represent_boolean_as_integer = true if config.active_record.sqlite3 config.active_record.async_query_executor = :global_thread_pool if (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR > 7 # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de # Do not swallow errors in after_commit/after_rollback callbacks. # config.active_record.raise_in_transactional_callbacks = true config.semantic_logger.default_level = :trace # Warning: Set to :error or higher in production to avoid performance issues. config.semantic_logger.backtrace_level = :trace # Test out Amazing Print config.rails_semantic_logger.ap_options = {multiline: false, ruby19_syntax: true} end end ================================================ FILE: test/dummy/config/boot.rb ================================================ require "rubygems" gemfile = File.expand_path("../../../Gemfile", __dir__) if File.exist?(gemfile) ENV["BUNDLE_GEMFILE"] = gemfile require "bundler" Bundler.setup end $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) ================================================ FILE: test/dummy/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: adapter: sqlite3 database: db/test.sqlite3 pool: 5 timeout: 5000 production: adapter: sqlite3 database: db/production.sqlite3 pool: 5 timeout: 5000 ================================================ FILE: test/dummy/config/environment.rb ================================================ # Load the rails application require File.expand_path("application", __dir__) # Initialize the rails application Dummy::Application.initialize! ================================================ FILE: test/dummy/config/environments/development.rb ================================================ Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. # config.active_record.migration_error = :page_load # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true # Asset digests allow you to set far-future HTTP expiration dates on all assets, # yet still be able to expire them through the digest params. config.assets.digest = true # Adds additional error checking when serving assets at runtime. # Checks for improperly declared sprockets dependencies. # Raises helpful error messages. config.assets.raise_runtime_errors = true # Raises error for missing translations # config.action_view.raise_on_missing_translations = true config.rails_semantic_logger.quiet_assets = true config.rails_semantic_logger.ap_options = {multiline: true} end ================================================ FILE: test/dummy/config/environments/production.rb ================================================ Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb # Full error reports are disabled and caching is turned on config.consider_all_requests_local = false config.perform_caching = true config.cache_classes = true config.eager_load = true # Disable Rails's static asset server (Apache or nginx could already do this) config.serve_static_files = true # SSL is handled by the load balancer # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = false # See everything in the log (default is :info) config.log_level = :info # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) config.i18n.fallbacks = true # Send deprecation notices to registered listeners config.active_support.deprecation = :notify # Disable colorized logging # config.colorize_logging = false end ================================================ FILE: test/dummy/config/environments/test.rb ================================================ Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Configure static asset server for tests with Cache-Control for performance # config.serve_static_assets = true # Log error messages when you accidentally call methods on nil config.whiny_nils = true config.eager_load = false # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates config.action_dispatch.show_exceptions = Rails::VERSION::MAJOR >= 7 ? :none : false # Disable request forgery protection in test environment config.action_controller.allow_forgery_protection = false # Print deprecation notices to the stderr config.active_support.deprecation = :stderr config.active_support.test_order = :random end ================================================ FILE: test/dummy/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/dummy/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format # (all these examples are active by default): # ActiveSupport::Inflector.inflections do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: test/dummy/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone ================================================ FILE: test/dummy/config/initializers/payload_collector.rb ================================================ ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args| event = ActiveSupport::Notifications::Event.new(*args) PayloadCollector.append(event.payload) end ================================================ FILE: test/dummy/config/initializers/secret_token.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. Dummy::Application.config.secret_token = "afd0f2e89c2a660f839bfb8e982358b6748c7fcd7c61c98b1edfc164539fae5beafece2906858801a4c7f5bbcd2f2e606db2a766630e1b0b1468f50ad6025630" ================================================ FILE: test/dummy/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. Rails.application.config.session_store :cookie_store, key: "_dummy_session" ================================================ FILE: test/dummy/config/initializers/sidekiq.rb ================================================ # In tests we force Sidekiq into thinking it is running as a server, # so it creates a stdout logger. Remove it here: Rails.application.config.after_initialize do SemanticLogger.appenders.delete_if { |appender| appender.is_a?(SemanticLogger::Appender::IO) } if Rails.env.test? end ================================================ FILE: test/dummy/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] if respond_to?(:wrap_parameters) end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: test/dummy/config/locales/en.yml ================================================ # Sample localization file for English. Add more files in this directory for other locales. # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: hello: "Hello world" ================================================ FILE: test/dummy/config/routes.rb ================================================ Dummy::Application.routes.draw do get "welcome/index" resources :articles resource :dashboard, controller: :dashboard root "welcome#index" end ================================================ FILE: test/dummy/config/secrets.yml ================================================ # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. development: secret_key_base: 72c4f52428cb36cd0afb2cf6e36a8a67fa39f8fd2396f533b85e42441feafdcdcac4f7629f3e1c5b846b1c452ad6e3ecfe368e913b71cd147d4536b35406b2fd test: secret_key_base: 29dc974bc32244b62fd69259cd4f9ff7223a4cfe9fe40ee948dd1e64a99cd6bbef4a5d75f1b106ef50fa750613542e52e21d7fd67695356cd529264f614c1c3d # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> ================================================ FILE: test/dummy/config.ru ================================================ # This file is used by Rack-based servers to start the application. require File.expand_path("config/environment", __dir__) run Dummy::Application ================================================ FILE: test/dummy/db/migrate/20170525020551_create_samples.rb ================================================ class CreateSamples < ActiveRecord::Migration def change create_table :samples do |t| t.string :name t.integer :age t.date :joined t.timestamps null: false end end end ================================================ FILE: test/dummy/db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # Note that this schema.rb definition is the authoritative source for your # database schema. If you need to create the application database on another # system, you should be using db:schema:load, not running all the migrations # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20_170_525_020_551) do create_table "samples", force: :cascade do |t| t.string "name" t.integer "age" t.date "joined" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end ================================================ FILE: test/dummy/lib/assets/.gitkeep ================================================ ================================================ FILE: test/dummy/log/.gitkeep ================================================ ================================================ FILE: test/dummy/public/404.html ================================================ The page you were looking for doesn't exist (404)

The page you were looking for doesn't exist.

You may have mistyped the address or the page may have moved.

================================================ FILE: test/dummy/public/422.html ================================================ The change you wanted was rejected (422)

The change you wanted was rejected.

Maybe you tried to change something you didn't have access to.

================================================ FILE: test/dummy/public/500.html ================================================ We're sorry, but something went wrong (500)

We're sorry, but something went wrong.

================================================ FILE: test/dummy/script/rails ================================================ #!/usr/bin/env ruby # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. APP_PATH = File.expand_path("../config/application", __dir__) require File.expand_path("../config/boot", __dir__) require "rails/commands" ================================================ FILE: test/dummy/test/fixtures/samples.yml ================================================ # Read about fixtures at # http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html one: name: MyString age: 1 joined: 2017-05-24 two: name: MyString age: 1 joined: 2017-05-24 ================================================ FILE: test/payload_collector.rb ================================================ class PayloadCollector class << self def wrap @store = true yield ensure @store = false end def append(payload) data.append(payload) if @store end def last data.last end def flush @data = [] end private def data @data ||= [] end end end ================================================ FILE: test/rails_test.rb ================================================ require_relative "test_helper" class RailsTest < Minitest::Test describe "Rails" do describe ".logger" do it "replaces the Rails logger" do assert_kind_of SemanticLogger::Logger, Rails.logger end it "uses the colorized formatter" do assert_kind_of SemanticLogger::Formatters::Color, SemanticLogger.appenders.first.formatter end it "is compatible with Rails logger" do assert_nil Rails.logger.formatter Rails.logger.formatter = "blah" assert_equal "blah", Rails.logger.formatter end end end end ================================================ FILE: test/sidekiq_test.rb ================================================ require_relative "test_helper" class SidekiqTest < Minitest::Test # Cannot use inline testing since it bypasses the Sidekiq logging calls. describe Sidekiq::Worker do let(:job) { SimpleJob } let(:args) { [] } describe "#logger" do it "has its own logger with the same name as the job" do assert_kind_of SemanticLogger::Logger, SimpleJob.logger assert_kind_of SemanticLogger::Logger, job.logger assert_equal job.logger.name, job.name refute_same Sidekiq.logger, job.logger end end describe "#perform" do let(:config) { Sidekiq.default_configuration } let(:msg) { Sidekiq.dump_json({"class" => job.to_s, "args" => args, "enqueued_at" => 1.minute.ago}) } let(:uow) { Sidekiq::BasicFetch::UnitOfWork.new("queue:default", msg) } if Sidekiq::VERSION.to_i == 6 && Sidekiq::VERSION.to_f < 6.5 let(:processor) do mgr = Minitest::Mock.new opts = Sidekiq.options opts[:fetch] = Sidekiq::BasicFetch.new(opts) Sidekiq::Processor.new(mgr, opts) end elsif Sidekiq::VERSION.to_i == 6 let(:processor) do config = Sidekiq config[:fetch] = Sidekiq::BasicFetch.new(config) Sidekiq::Processor.new(config) { |*args| } end elsif Sidekiq::VERSION.to_i < 7 let(:processor) do opts = Sidekiq.options mgr = Minitest::Mock.new mgr.expect(:options, opts) mgr.expect(:options, opts) mgr.expect(:options, opts) Sidekiq::Processor.new(mgr) end else let(:processor) { Sidekiq::Processor.new(config.default_capsule) { |*args| } } end it "a simple job" do # SimpleJob.perform_async messages = semantic_logger_events do processor.send(:process, uow) end assert_equal 2, messages.count, -> { messages.collect(&:to_h).ai } assert_semantic_logger_event( messages[0], level: :info, name: "SimpleJob", message: "Start #perform", metric: "sidekiq.queue.latency", named_tags: {jid: nil, queue: "default"} ) assert messages[0].metric_amount.is_a?(Float) assert_semantic_logger_event( messages[1], level: :info, name: "SimpleJob", message: "Completed #perform", metric: "sidekiq.job.perform", named_tags: {jid: nil, queue: "default"} ) assert messages[1].duration.is_a?(Float) end describe "with Bad Job" do let(:job) { BadJob } it "a job that raises an exception" do # BadJob.perform_async messages = semantic_logger_events do assert_raises ArgumentError do processor.send(:process, uow) end end assert_equal 3, messages.count, -> { messages.collect(&:to_h).ai } assert_semantic_logger_event( messages[0], level: :info, name: "BadJob", message: "Start #perform", metric: "sidekiq.queue.latency", named_tags: {jid: nil, queue: "default"}, exception: :nil ) assert messages[0].metric_amount.is_a?(Float) assert_semantic_logger_event( messages[1], level: :error, name: "BadJob", message: "Completed #perform", metric: "sidekiq.job.perform", named_tags: {jid: nil, queue: "default"}, exception: ArgumentError ) assert messages[1].duration.is_a?(Float) assert_semantic_logger_event( messages[2], level: :warn, name: "BadJob", message: "Job raised exception", payload_includes: {context: "Job raised exception"}, exception: :nil ) assert_equal messages[2].payload[:job]["class"], "BadJob" assert_equal messages[2].payload[:job]["args"], [] end end end end end ================================================ FILE: test/test_helper.rb ================================================ ENV["RAILS_ENV"] ||= "test" ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] = "1" # Load first so Sidekiq thinks it is running as a server instance require "sidekiq/cli" if defined?(Sidekiq::DEFAULT_ERROR_HANDLER) # Set by Sidekiq CLI at startup Sidekiq.options[:error_handlers] << Sidekiq::DEFAULT_ERROR_HANDLER end require_relative "dummy/config/environment" require "rails/test_help" require "minitest/autorun" require "minitest/rails" require "amazing_print" require_relative "payload_collector" # Include the complete backtrace? Minitest.backtrace_filter = Minitest::BacktraceFilter.new if ENV["BACKTRACE"].present? Rails.backtrace_cleaner.remove_silencers! # Add Semantic Logger helpers for Minitest Minitest::Test.include SemanticLogger::Test::Minitest ActionMailer::Base.delivery_method = :test def filter_params_setting(value, user_defined_params, &block) original_value = Rails.configuration.filter_parameters Rails.configuration.filter_parameters = user_defined_params block.call ensure Rails.configuration.filter_parameters = original_value end def filter_params_regex_setting(value, user_defined_params, &block) original_value = Rails.configuration.filter_parameters Rails.configuration.filter_parameters += user_defined_params filter_params_regex = Rails.configuration.filter_parameters.map do |key| "(?i:#{key})" end.join("|") Rails.configuration.filter_parameters = [/(?-mix:#{filter_params_regex})/] block.call ensure Rails.configuration.filter_parameters = original_value end