├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── docker-compose.yml ├── gemfiles ├── .bundle │ └── config ├── rails_32.gemfile ├── rails_4.gemfile ├── rails_40.gemfile ├── rails_41.gemfile ├── rails_42.gemfile ├── rails_5.gemfile ├── rails_50.gemfile └── rails_51.gemfile ├── lib ├── rails-tracer.rb └── rails │ ├── active_record │ └── tracer.rb │ ├── active_support │ └── cache │ │ ├── core_ext.rb │ │ ├── dalli_tracer.rb │ │ ├── manual_tracer.rb │ │ ├── subscriber.rb │ │ └── tracer.rb │ ├── rack │ └── tracer.rb │ ├── span_helpers.rb │ └── tracer.rb ├── rails-tracer.gemspec └── spec ├── rails ├── active_record │ └── tracer_spec.rb ├── active_suppport │ └── cache │ │ ├── dalli_tracer_spec.rb │ │ └── tracer_spec.rb ├── rack │ └── tracer_spec.rb ├── span_helpers_spec.rb └── tracer_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── test_app ├── .gitignore ├── .rspec ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ └── stylesheets │ │ └── application.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── application_controller.rb │ └── concerns │ │ └── .keep ├── helpers │ └── application_helper.rb ├── jobs │ └── application_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── article.rb │ └── concerns │ │ └── .keep └── views │ └── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb ├── bin ├── bundle ├── rails ├── rake ├── setup └── update ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb └── secrets.yml ├── db ├── migrate │ └── 20170815215418_create_articles.rb ├── schema.rb └── seeds.rb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── tmp └── .keep └── vendor └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # Appraisal 12 | *.gemfile.lock 13 | 14 | # rspec failure tracking 15 | .rspec_status 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.4 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: ruby 3 | rvm: 4 | - 2.2 5 | - 2.3 6 | - 2.4 7 | gemfile: 8 | - gemfiles/rails_32.gemfile 9 | - gemfiles/rails_40.gemfile 10 | - gemfiles/rails_41.gemfile 11 | - gemfiles/rails_42.gemfile 12 | - gemfiles/rails_50.gemfile 13 | - gemfiles/rails_51.gemfile 14 | matrix: 15 | exclude: 16 | - rvm: 2.4 17 | gemfile: gemfiles/rails_32.gemfile 18 | - rvm: 2.4 19 | gemfile: gemfiles/rails_40.gemfile 20 | - rvm: 2.4 21 | gemfile: gemfiles/rails_41.gemfile 22 | before_install: 23 | - gem install bundler -v 1.15.3 24 | services: 25 | - memcached 26 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-32" do 2 | gem "rails", "~> 3.2.0" 3 | gem 'test-unit', '~> 3.0' 4 | end 5 | 6 | appraise "rails-40" do 7 | gem "rails", "~> 4.0.0" 8 | end 9 | 10 | appraise "rails-41" do 11 | gem "rails", "~> 4.1.0" 12 | end 13 | 14 | appraise "rails-42" do 15 | gem "rails", "~> 4.2.0" 16 | end 17 | 18 | appraise "rails-50" do 19 | gem "rails", "~> 5.0.0" 20 | end 21 | 22 | appraise "rails-51" do 23 | gem "rails", "~> 5.1.0" 24 | end 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.5.0 2 | 3 | * Expose global Rails::Tracer which instruments all sub-modules [#26](https://github.com/iaintshine/ruby-rails-tracer/pull/26) 4 | * Allow to disable sub-tracers [#25](https://github.com/iaintshine/ruby-rails-tracer/pull/25) 5 | * Support Evented and Timed subscribers in cache instrumentation [#24](https://github.com/iaintshine/ruby-rails-tracer/pull/24) 6 | * Test and add support for multiple Rails versions - 3.2+ [#23](https://github.com/iaintshine/ruby-rails-tracer/pull/23) 7 | * Development instructions update [#27](https://github.com/iaintshine/ruby-rails-tracer/pull/27) 8 | 9 | ## v0.4.3 10 | 11 | * Handle instrumentation exceptions and mark spans as failed [#21](https://github.com/iaintshine/ruby-rails-tracer/pull/21) 12 | 13 | ## v0.4.2 14 | 15 | * Test the gem with multiple Ruby versions 2.2+ [#20](https://github.com/iaintshine/ruby-rails-tracer/pull/20) 16 | * Set explicit Ruby version requirement in gemspec. 17 | 18 | ## v0.4.1 19 | 20 | * Use tracing matchers in tests [#19](https://github.com/iaintshine/ruby-rails-tracer/pull/19) 21 | 22 | ## v0.4.0 23 | 24 | * Start maintaining CHANGELOG.md [#18](https://github.com/iaintshine/ruby-rails-tracer/pull/18) 25 | * Auto enable tracing-logger when Dalli is auto-instrumented [#17](https://github.com/iaintshine/ruby-rails-tracer/pull/17) 26 | * Introduce Dalli and ActiveSupport::Cache::DalliStore auto-instrumentation [#9](https://github.com/iaintshine/ruby-rails-tracer/pull/9) 27 | * Introduce docker-compose with all required external dependencies. 28 | 29 | ## v0.3.0 30 | 31 | * Introduce ActiveSupport::Cache auto-instrumentation [#4](https://github.com/iaintshine/ruby-rails-tracer/pull/4) 32 | * Add ActiveRecord::Tracer tests for active span propagation 33 | 34 | ## v0.2.0 35 | 36 | * Introduce ActiveRecord auto-instrumentation [#3](https://github.com/iaintshine/ruby-rails-tracer/pull/3) 37 | * Add Rails test application to be used in specs 38 | 39 | ## v0.1.1 40 | 41 | * Replace RecordingTracer with Test::Tracer [#6](https://github.com/iaintshine/ruby-rails-tracer/pull/6) 42 | * README typo fix [#2](https://github.com/iaintshine/ruby-rails-tracer/pull/2) 43 | 44 | ## v0.1.0 45 | 46 | * Initial release 47 | * Introduced a rack middleware, to generate more informative operation names based on information supplied by ActionDispatch. [#1](https://github.com/iaintshine/ruby-rails-tracer/pull/1) 48 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in rails-tracer.gemspec 6 | gemspec 7 | 8 | gem 'pry' 9 | gem 'pry-stack_explorer' 10 | gem 'pry-byebug' 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenTracing Rails Instrumentation 2 | 3 | This gem is an attempt to introduce OpenTracing instrumentation into Rails. It's in an early stage. 4 | 5 | The following instrumentation is supported: 6 | 7 | * ActionDispatch - The library introduces a rack middleware, which is intended to be used together with `rack-tracer`, to generate more informative operation names based on information supplied by ActionDispatch. 8 | * ActiveRecord - The library hooks up into Rails, and instruments all ActiveRecord query. 9 | * ActionSupport::Cache - The library hooks up into Rails, and instruments cache events. 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'rails-tracer' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install rails-tracer 26 | 27 | ## Rails::Tracer 28 | 29 | The library hooks up into Rails using `ActiveSupport::Notifications`, and instruments all previously mentioned modules. 30 | To enable instrumentation, you can either use sub-tracers directly (see sections below) or global `Rails::Tracer` which 31 | will enabled all of them (except for Rack/ActionDispatch instrumentation). 32 | 33 | ### Configuration Options 34 | 35 | * `tracer: OpenTracing::Tracer` an OT compatible tracer. Default `OpenTracing.global_tracer` 36 | * `active_span: boolean` an active span provider. Default: `nil`. 37 | * `active_record: boolean` whether to enable `ActiveRecord` instrumentation. Default: `true`. 38 | * `active_support_cache: boolean` whether to enable `ActionDispatch::Cache` instrumentation. Default: `true`. 39 | * `dalli: boolean` if set to `true` you will hook up into `Dalli` low-level details. Default: `false`. 40 | * `rack: boolean` whether to enable extended `Rack` instrumentation. Default: `false`. 41 | * `middlewares: ActionDispatch::MiddlewareStack` a middlewares stack. Default: `Rails.configuration.middleware`. 42 | 43 | ### Usage 44 | 45 | ```ruby 46 | require 'rails/tracer' 47 | 48 | Rails::Tracer.instrument 49 | ``` 50 | 51 | ## ActionDispatch 52 | 53 | When you use `rack-tracer`, the generated operation name corresponds to the request's http method e.g. GET, POST etc. 54 | It's not perfect. You need to dig into the trace to understand with what url it's related. 55 | 56 | The `rails-tracer` introduces another rack middleware, which is intended to be used together with `rack-tracer`, to generate more informative operation names in the form `ControllerName#action`. 57 | 58 | ### Usage 59 | 60 | ```ruby 61 | require 'rack/tracer' 62 | require 'rails/tracer' 63 | 64 | Rails.configuration.middleware.use(Rack::Tracer) 65 | Rails.configuration.middleware.insert_after(Rack::Tracer, Rails::Rack::Tracer) 66 | ``` 67 | 68 | or simpler 69 | 70 | ```ruby 71 | Rails::Rack::Tracer.instrument 72 | ``` 73 | 74 | optionally you can pass `tracer` argument to `instrument` method. 75 | 76 | ## ActiveRecord 77 | 78 | The library hooks up into Rails using `ActiveSupport::Notifications`, and instruments all `ActiveRecord` query. 79 | 80 | ### Usage 81 | 82 | Auto-instrumentation example. 83 | 84 | ```ruby 85 | require 'rails/tracer' 86 | 87 | ActiveRecord::Tracer.instrument(tracer: OpenTracing.global_tracer, 88 | active_span: -> { OpenTracing.global_tracer.active_span }) 89 | ``` 90 | 91 | There are times when you might want to skip ActiveRecord's magic, and use connection directly. Still the library 92 | can help you with span creation. Instead of auto-instrumenting you can manually call `ActiveRecord::Tracer.start_span` as shown below. 93 | 94 | ```ruby 95 | def q(name, sql) 96 | span = ActiveRecord::Tracer.start_span(name, 97 | tracer: OpenTracing.global_tracer, 98 | active_span: -> { OpenTracing.global_tracer.active_span }, 99 | sql: sql) 100 | ActiveRecord::Base. 101 | connection. 102 | raw_connection. 103 | query(sql). 104 | each(as: :hash) 105 | ensure 106 | span&.finish 107 | end 108 | 109 | q("FirstUser", "SELECT * FROM users LIMIT 1") 110 | ``` 111 | 112 | ## ActiveSupport::Cache 113 | 114 | The library hooks up into Rails using `ActiveSupport::Notifications`, and instruments all `ActiveSupport::Cache` events. 115 | 116 | ### Usage 117 | 118 | Auto-instrumentation example. 119 | 120 | ```ruby 121 | require 'rails/tracer' 122 | 123 | ActiveSupport::Cache::Tracer.instrument(tracer: OpenTracing.global_tracer, 124 | active_span: -> { OpenTracing.global_tracer.active_span }) 125 | ``` 126 | 127 | If you use [Dalli](https://github.com/petergoldstein/dalli/) and `ActiveSupport::Cache::DalliStore` as your application's cache store, you can get low-level details about Memcached calls by setting `dalli` option to `true`. If you want to get even more details, simply require [tracing-logger](https://github.com/iaintshine/ruby-tracing-logger) and Dalli error logs will be attached to the current active span. The library will wrap current `Dalli.logger` into a `Tracing::CompositeLogger` and append additional `Tracing::Logger` with severity level set to `Logger::ERROR`. 128 | 129 | ```ruby 130 | ActiveSupport::Cache::Tracer.instrument(tracer: OpenTracing.global_tracer, 131 | active_span: -> { OpenTracing.global_tracer.active_span }, 132 | dalli: true) 133 | ``` 134 | 135 | If you want to skip the auto-instrumentation, still the library can help you with span creation and setting up proper tags. Instead of auto-instrumenting, as shown above, you can manually call `ActiveSupport::Cache::Tracer.start_span` as shown below. 136 | 137 | ```ruby 138 | def read(key) 139 | span = ActiveSupport::Cache::Tracer.start_span("InMemoryCache#read", 140 | tracer: OpenTracing.global_tracer, 141 | active_span: -> { OpenTracing.global_tracer.active_span }, 142 | key: key) 143 | result = in_memory_cache[key] 144 | span.set_tag('cache.hit', !!result) 145 | result 146 | ensure 147 | span&.finish 148 | end 149 | 150 | read("user-1") 151 | ``` 152 | 153 | ## Development 154 | 155 | After checking out the repo, install dependencies. 156 | 157 | ``` 158 | bundle install 159 | appraisal install 160 | ``` 161 | 162 | The tests depends on having memcached running locally within docker container. It means you need to install docker, and docker-compose first. 163 | Once you're done to run the containers: 164 | 165 | ``` 166 | docker-compose up -d 167 | ``` 168 | 169 | Then, to run tests for all appraisals: 170 | 171 | ``` 172 | appraisal bundle exec rspec spec 173 | ``` 174 | 175 | You can also run `bin/console` for an interactive prompt that will allow you to experiment. 176 | 177 | To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 178 | 179 | ## Contributing 180 | 181 | Bug reports and pull requests are welcome on GitHub at https://github.com/iaintshine/ruby-rails-tracer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 182 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task :default => :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "rails/tracer" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | memcached: 2 | image: memcached 3 | ports: 4 | - 11211:11211 5 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | -------------------------------------------------------------------------------- /gemfiles/rails_32.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry" 6 | gem "pry-stack_explorer" 7 | gem "pry-byebug" 8 | gem "rails", "~> 3.2.0" 9 | gem "test-unit", "~> 3.0" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry" 6 | gem "pry-stack_explorer" 7 | gem "pry-byebug" 8 | gem "rails", "~> 5.1.3" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_40.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry" 6 | gem "pry-stack_explorer" 7 | gem "pry-byebug" 8 | gem "rails", "~> 4.0.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_41.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry" 6 | gem "pry-stack_explorer" 7 | gem "pry-byebug" 8 | gem "rails", "~> 4.1.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_42.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry" 6 | gem "pry-stack_explorer" 7 | gem "pry-byebug" 8 | gem "rails", "~> 4.2.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry" 6 | gem "pry-stack_explorer" 7 | gem "pry-byebug" 8 | gem "rails", "~> 5.1.3" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_50.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry" 6 | gem "pry-stack_explorer" 7 | gem "pry-byebug" 8 | gem "rails", "~> 5.0.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_51.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry" 6 | gem "pry-stack_explorer" 7 | gem "pry-byebug" 8 | gem "rails", "~> 5.1.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /lib/rails-tracer.rb: -------------------------------------------------------------------------------- 1 | require 'rails/tracer' 2 | -------------------------------------------------------------------------------- /lib/rails/active_record/tracer.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Tracer 3 | DEFAULT_OPERATION_NAME = "sql.query".freeze 4 | 5 | class << self 6 | def instrument(tracer: OpenTracing.global_tracer, active_span: nil) 7 | clear_subscribers 8 | @subscriber = ::ActiveSupport::Notifications.subscribe('sql.active_record') do |*args| 9 | ActiveRecord::Tracer.sql(tracer: tracer, active_span: active_span, args: args) 10 | end 11 | 12 | self 13 | end 14 | 15 | def disable 16 | if @subscriber 17 | ActiveSupport::Notifications.unsubscribe(@subscriber) 18 | @subscriber = nil 19 | end 20 | 21 | self 22 | end 23 | alias :clear_subscribers :disable 24 | 25 | def sql(tracer: OpenTracing.global_tracer, active_span: nil, args:) 26 | _, start, finish, _, payload = *args 27 | 28 | span = start_span(payload.fetch(:name), 29 | tracer: tracer, 30 | active_span: active_span, 31 | start_time: start, 32 | sql: payload.fetch(:sql), 33 | cached: payload.fetch(:cached, false), 34 | connection_id: payload.fetch(:connection_id)) 35 | 36 | if payload[:exception] 37 | Rails::Tracer::SpanHelpers.set_error(span, payload[:exception_object] || payload[:exception]) 38 | end 39 | 40 | span.finish(end_time: finish) 41 | end 42 | 43 | 44 | def start_span(operation_name, tracer: OpenTracing.global_tracer, active_span: nil, start_time: Time.now, **fields) 45 | connection_config = ::ActiveRecord::Base.connection_config 46 | 47 | span = tracer.start_span(operation_name || DEFAULT_OPERATION_NAME, 48 | child_of: active_span.respond_to?(:call) ? active_span.call : active_span, 49 | start_time: start_time, 50 | tags: { 51 | 'component' => 'ActiveRecord', 52 | 'span.kind' => 'client', 53 | 'db.user' => connection_config.fetch(:username, 'unknown'), 54 | 'db.instance' => connection_config.fetch(:database), 55 | 'db.vendor' => connection_config.fetch(:adapter), 56 | 'db.connection_id' => fields.fetch(:connection_id, 'unknown'), 57 | 'db.cached' => fields.fetch(:cached, false), 58 | 'db.statement' => fields.fetch(:sql), 59 | 'db.type' => 'sql' 60 | }) 61 | span 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/rails/active_support/cache/core_ext.rb: -------------------------------------------------------------------------------- 1 | module ActiveSupport 2 | module Cache 3 | class Store 4 | # See the PR https://github.com/rails/rails/pull/15943/files 5 | # In order to make the instrumentation to work we need to override the original implementation 6 | def self.instrument 7 | true 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rails/active_support/cache/dalli_tracer.rb: -------------------------------------------------------------------------------- 1 | require "method/tracer" 2 | 3 | module Dalli 4 | class Tracer 5 | class << self 6 | def instrument(tracer: OpenTracing.global_tracer, active_span: nil) 7 | ::Dalli::Server.class_eval do 8 | @tracer = tracer 9 | @active_span = active_span 10 | 11 | class << self 12 | attr_reader :tracer, :active_span 13 | end 14 | 15 | def tracer 16 | self.class.tracer 17 | end 18 | 19 | def active_span 20 | self.class.active_span 21 | end 22 | 23 | alias_method :request_without_instrumentation, :request 24 | 25 | def request(op, *args) 26 | tags = { 27 | 'component' => 'Dalli::Server', 28 | 'span.kind' => 'client', 29 | 'db.statement' => op.to_s, 30 | 'db.type' => 'memcached', 31 | 'peer.hostname' => hostname, 32 | 'peer.port' => port, 33 | 'peer.weight' => weight 34 | } 35 | Method::Tracer.trace("Dalli::Server#request", tracer: tracer, child_of: active_span, tags: tags) do 36 | request_without_instrumentation(op, *args) 37 | end 38 | end 39 | end 40 | 41 | ::Dalli::Client.class_eval do 42 | @tracer = tracer 43 | @active_span = active_span 44 | 45 | class << self 46 | attr_reader :tracer, :active_span 47 | end 48 | 49 | def tracer 50 | self.class.tracer 51 | end 52 | 53 | def active_span 54 | self.class.active_span 55 | end 56 | 57 | alias_method :perform_without_instrumentation, :perform 58 | 59 | def perform(*args) 60 | Method::Tracer.trace("Dalli::Client#perform", tracer: tracer, child_of: active_span) do 61 | perform_without_instrumentation(*args) 62 | end 63 | end 64 | end 65 | end 66 | 67 | def remove_instrumentation 68 | ::Dalli::Server.class_eval do 69 | alias_method :request, :request_without_instrumentation 70 | remove_method :request_without_instrumentation 71 | end 72 | 73 | ::Dalli::Client.class_eval do 74 | alias_method :perform, :perform_without_instrumentation 75 | remove_method :perform_without_instrumentation 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/rails/active_support/cache/manual_tracer.rb: -------------------------------------------------------------------------------- 1 | module ActiveSupport 2 | module Cache 3 | module Tracer 4 | class << self 5 | def start_span(operation_name, tracer: OpenTracing.global_tracer, active_span: nil, start_time: Time.now, event:, **fields) 6 | span = tracer.start_span(operation_name, 7 | child_of: active_span.respond_to?(:call) ? active_span.call : active_span, 8 | start_time: start_time, 9 | tags: { 10 | 'component' => 'ActiveSupport::Cache', 11 | 'span.kind' => 'client', 12 | 'cache.key' => fields.fetch(:key, 'unknown') 13 | }) 14 | 15 | if event == 'read' 16 | span.set_tag('cache.hit', fields.fetch(:hit, false)) 17 | end 18 | 19 | span 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/rails/active_support/cache/subscriber.rb: -------------------------------------------------------------------------------- 1 | module ActiveSupport 2 | module Cache 3 | module Tracer 4 | class Subscriber 5 | attr_reader :tracer, :active_span, :event, :operation_name 6 | 7 | def initialize(tracer: OpenTracing.global_tracer, active_span: nil, event:) 8 | @tracer = tracer 9 | @active_span = active_span 10 | @event = event 11 | @operation_name = "cache.#{event}" 12 | end 13 | 14 | # For compatibility with Rails 3.2 15 | def call(*args) 16 | _, start, finish, _, payload = *args 17 | 18 | span = Tracer.start_span(operation_name, 19 | event: event, 20 | tracer: tracer, 21 | active_span: active_span, 22 | start_time: start, 23 | **payload) 24 | 25 | if payload[:exception] 26 | Rails::Tracer::SpanHelpers.set_error(span, payload[:exception_object] || payload[:exception]) 27 | end 28 | 29 | span.finish(end_time: finish) 30 | end 31 | 32 | def start(name, _, payload) 33 | span = tracer.start_span(operation_name, 34 | child_of: active_span.respond_to?(:call) ? active_span.call : active_span, 35 | tags: { 36 | 'component' => 'ActiveSupport::Cache', 37 | 'span.kind' => 'client' 38 | }) 39 | 40 | payload[:__OT_SPAN__] = span 41 | end 42 | 43 | def finish(name, _, payload) 44 | span = payload[:__OT_SPAN__] 45 | return unless span 46 | 47 | span.set_tag('cache.key', payload.fetch(:key, 'unknown')) 48 | 49 | if event == 'read' 50 | span.set_tag('cache.hit', payload.fetch(:hit, false)) 51 | end 52 | 53 | if payload[:exception] 54 | Rails::Tracer::SpanHelpers.set_error(span, payload[:exception_object] || payload[:exception]) 55 | end 56 | 57 | span.finish 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/rails/active_support/cache/tracer.rb: -------------------------------------------------------------------------------- 1 | require 'rails/active_support/cache/core_ext' 2 | require 'rails/active_support/cache/manual_tracer' 3 | require 'rails/active_support/cache/subscriber' 4 | 5 | module ActiveSupport 6 | module Cache 7 | module Tracer 8 | class << self 9 | def instrument(tracer: OpenTracing.global_tracer, active_span: nil, dalli: false) 10 | clear_subscribers 11 | events = %w(read write generate delete clear) 12 | @subscribers = events.map do |event| 13 | subscriber = ActiveSupport::Cache::Tracer::Subscriber.new(tracer: tracer, 14 | active_span: active_span, 15 | event: event) 16 | ActiveSupport::Notifications.subscribe("cache_#{event}.active_support", subscriber) 17 | end 18 | 19 | instrument_dalli(tracer: tracer, active_span: active_span) if dalli 20 | 21 | self 22 | end 23 | 24 | def disable 25 | if @subscribers 26 | @subscribers.each { |subscriber| ActiveSupport::Notifications.unsubscribe(subscriber) } 27 | @subscribers.clear 28 | end 29 | 30 | self 31 | end 32 | 33 | alias :clear_subscribers :disable 34 | 35 | def instrument_dalli(tracer:, active_span:) 36 | return unless defined?(ActiveSupport::Cache::DalliStore) 37 | require 'rails/active_support/cache/dalli_tracer' 38 | 39 | Dalli::Tracer.instrument(tracer: tracer, active_span: active_span) 40 | instrument_dalli_logger(active_span: active_span) 41 | end 42 | 43 | def instrument_dalli_logger(active_span:, level: Logger::ERROR) 44 | return unless defined?(Tracing::Logger) 45 | return unless active_span 46 | return if [Tracing::Logger, Tracing::CompositeLogger].any? { |t| Dalli.logger.is_a?(t) } 47 | 48 | tracing_logger = Tracing::Logger.new(active_span: active_span, level: level) 49 | loggers = [tracing_logger, Dalli.logger].compact 50 | Dalli.logger = Tracing::CompositeLogger.new(*loggers) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/rails/rack/tracer.rb: -------------------------------------------------------------------------------- 1 | module Rails 2 | module Rack 3 | class Tracer 4 | class << self 5 | def instrument(tracer: OpenTracing.global_tracer, middlewares: Rails.configuration.middleware) 6 | return unless defined?(::Rack::Tracer) 7 | @owns_all_middlewares = false 8 | unless middlewares.include?(::Rack::Tracer) 9 | middlewares.use(::Rack::Tracer, tracer: tracer) 10 | @owns_all_middlewares = true 11 | end 12 | middlewares.insert_after(::Rack::Tracer, Rails::Rack::Tracer) 13 | end 14 | 15 | def disable(middlewares: Rails.configuration.middleware) 16 | middlewares.delete(Rails::Rack::Tracer) 17 | if @owns_all_middlewares 18 | middlewares.delete(::Rack::Tracer) 19 | @owns_all_middlewares = false 20 | end 21 | rescue 22 | end 23 | end 24 | 25 | def initialize(app) 26 | @app = app 27 | end 28 | 29 | def call(env) 30 | @app.call(env) 31 | ensure 32 | enhance_rack_span(env) 33 | end 34 | 35 | private 36 | 37 | def enhance_rack_span(env) 38 | span = extract_span(env) 39 | if span && route_found?(env) 40 | span.operation_name = operation_name(env) 41 | end 42 | end 43 | 44 | def extract_span(env) 45 | env['rack.span'] 46 | end 47 | 48 | def route_found?(env) 49 | env["action_dispatch.request.path_parameters"] 50 | end 51 | 52 | def operation_name(env) 53 | path_parameters = env["action_dispatch.request.path_parameters"] 54 | action_controller = env["action_controller.instance"] 55 | controller = action_controller ? action_controller.class.to_s : path_parameters[:controller] 56 | action = path_parameters[:action] 57 | "#{controller}##{action}" 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/rails/span_helpers.rb: -------------------------------------------------------------------------------- 1 | module Rails 2 | module Tracer 3 | module SpanHelpers 4 | class << self 5 | def set_error(span, exception) 6 | span.set_tag('error', true) 7 | 8 | case exception 9 | when Array 10 | exception_class, exception_message = exception 11 | span.log(event: 'error', :'error.kind' => exception_class, message: exception_message) 12 | when Exception 13 | span.log(event: 'error', :'error.object' => exception) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rails/tracer.rb: -------------------------------------------------------------------------------- 1 | require "rails/span_helpers" 2 | require "rails/rack/tracer" 3 | require "rails/active_record/tracer" 4 | require "rails/active_support/cache/tracer" 5 | 6 | module Rails 7 | module Tracer 8 | class << self 9 | def instrument(tracer: OpenTracing.global_tracer, active_span: nil, 10 | rack: false, middlewares: Rails.configuration.middleware, 11 | active_record: true, 12 | active_support_cache: true, dalli: false) 13 | Rails::Rack::Tracer.instrument(tracer: tracer, middlewares: middlewares) if rack 14 | ActiveRecord::Tracer.instrument(tracer: tracer, active_span: active_span) if active_record 15 | ActiveSupport::Cache::Tracer.instrument(tracer: tracer, active_span: active_span, dalli: dalli) if active_support_cache 16 | end 17 | 18 | def disable 19 | ActiveRecord::Tracer.disable 20 | ActiveSupport::Cache::Tracer.disable 21 | Rails::Rack::Tracer.disable 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /rails-tracer.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "rails-tracer" 7 | spec.version = "0.5.0" 8 | spec.authors = ["iaintshine"] 9 | spec.email = ["bodziomista@gmail.com"] 10 | 11 | spec.summary = %q{Rack OpenTracing middleware enhanced for Rails} 12 | spec.description = %q{} 13 | spec.homepage = "https://github.com/iaintshine/ruby-rails-tracer" 14 | spec.license = "Apache-2.0" 15 | 16 | spec.required_ruby_version = ">= 2.2.0" 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 19 | f.match(%r{^(test|spec|features)/}) 20 | end 21 | spec.bindir = "exe" 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_dependency 'opentracing', '~> 0.3.1' 26 | spec.add_dependency "rack-tracer", "~> 0.3.0" 27 | spec.add_dependency "method-tracer", "~> 1.1" 28 | 29 | spec.add_development_dependency "sqlite3" 30 | spec.add_development_dependency "puma", "~> 3.7" 31 | spec.add_development_dependency "rspec-rails", "~> 3.6" 32 | spec.add_development_dependency "database_cleaner", "~> 1.6" 33 | spec.add_development_dependency "dalli", "~> 2.7.6" 34 | spec.add_development_dependency "tracing-logger", "~> 1.0" 35 | spec.add_development_dependency "test-tracer", "~> 1.0", ">= 1.2.1" 36 | spec.add_development_dependency "tracing-matchers", "~> 1.0", ">= 1.3.0" 37 | spec.add_development_dependency "spanmanager", "~> 0.3.0" 38 | spec.add_development_dependency "bundler", "~> 1.15" 39 | spec.add_development_dependency "rake", "~> 10.0" 40 | spec.add_development_dependency "rspec", "~> 3.0" 41 | spec.add_development_dependency "simplecov" 42 | spec.add_development_dependency "simplecov-console" 43 | spec.add_development_dependency "appraisal" 44 | end 45 | -------------------------------------------------------------------------------- /spec/rails/active_record/tracer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe ActiveRecord::Tracer do 4 | let(:tracer) { Test::Tracer.new } 5 | 6 | describe "active span propagation" do 7 | let(:root_span) { tracer.start_span("root") } 8 | 9 | before do 10 | ActiveRecord::Tracer.instrument(tracer: tracer, active_span: -> { root_span }) 11 | Article.first 12 | end 13 | 14 | after do 15 | ActiveRecord::Tracer.disable 16 | end 17 | 18 | it "creates the new span with active span trace_id" do 19 | expect(tracer).to have_traces(1) 20 | end 21 | 22 | it "creates the new span with active span as a parent" do 23 | cache_span = tracer.finished_spans.last 24 | expect(cache_span).to be_child_of(root_span) 25 | end 26 | end 27 | 28 | describe "auto-instrumentation" do 29 | before do 30 | ActiveRecord::Tracer.instrument(tracer: tracer) 31 | Article.count 32 | end 33 | 34 | after do 35 | ActiveRecord::Tracer.disable 36 | end 37 | 38 | it "creates a new span" do 39 | expect(tracer).to have_spans 40 | end 41 | 42 | it "sets operation_name to event's name" do 43 | expect(tracer).to have_span("sql.query") 44 | end 45 | 46 | it "sets standard OT tags" do 47 | [ 48 | ['component', 'ActiveRecord'], 49 | ['span.kind', 'client'] 50 | ].each do |key, value| 51 | expect(tracer).to have_span.with_tag(key, value) 52 | end 53 | end 54 | 55 | it "sets database specific OT tags" do 56 | sql = /SELECT COUNT\(\*\) FROM \"articles\"/ 57 | [ 58 | ['db.type', 'sql'], 59 | ['db.vendor', 'sqlite3'], 60 | ['db.statement', sql], 61 | ].each do |key, value| 62 | expect(tracer).to have_span.with_tag(key, value) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/rails/active_suppport/cache/dalli_tracer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "dalli" 3 | require "active_support/cache/dalli_store" 4 | require "rails/active_support/cache/dalli_tracer" 5 | 6 | RSpec.describe Dalli::Tracer do 7 | let(:tracer) { Test::Tracer.new } 8 | let(:root_span) { tracer.start_span("root") } 9 | 10 | let(:hostname) { "localhost" } 11 | let(:port) { 11211 } 12 | let(:servers) { ["#{hostname}:#{port}"] } 13 | let(:cache) { ActiveSupport::Cache::DalliStore.new(servers) } 14 | let(:test_key) { "test-key" } 15 | 16 | describe "active span propagation" do 17 | let(:root_span) { tracer.start_span("root") } 18 | 19 | before do 20 | Dalli::Tracer.instrument(tracer: tracer, active_span: -> { root_span }) 21 | cache.read(test_key) 22 | root_span.finish 23 | end 24 | 25 | after do 26 | Dalli::Tracer.remove_instrumentation 27 | end 28 | 29 | it "creates spans for each part of the chain" do 30 | expect(tracer).to have_spans(3) 31 | end 32 | 33 | it "all spans contains the same trace_id" do 34 | expect(tracer).to have_traces(1) 35 | end 36 | 37 | it "propagates parent child relationship properly" do 38 | server_span = tracer.finished_spans[0] 39 | client_span = tracer.finished_spans[1] 40 | expect(server_span).to be_child_of(root_span) 41 | expect(client_span).to be_child_of(root_span) 42 | end 43 | end 44 | 45 | describe "auto-instrumentation" do 46 | before do 47 | Dalli::Tracer.instrument(tracer: tracer) 48 | cache.read(test_key) 49 | end 50 | 51 | after do 52 | Dalli::Tracer.remove_instrumentation 53 | end 54 | 55 | it "creates a new span" do 56 | expect(tracer).to have_spans.finished 57 | end 58 | 59 | it "creates 2 spans, one for a server, and second for client" do 60 | expect(tracer).to have_spans(2).finished 61 | end 62 | 63 | describe "server span" do 64 | it "sets operation_name to ClassName#method" do 65 | expect(tracer).to have_span("Dalli::Server#request") 66 | end 67 | 68 | it "sets standard OT tags" do 69 | [ 70 | ['component', 'Dalli::Server'], 71 | ['span.kind', 'client'] 72 | ].each do |key, value| 73 | expect(tracer).to have_span.with_tag(key, value) 74 | end 75 | end 76 | 77 | it "sets cache specific OT tags" do 78 | [ 79 | ['db.statement', 'get'], 80 | ['db.type', 'memcached'], 81 | ['peer.hostname', hostname], 82 | ['peer.port', port], 83 | ['peer.weight', 1], 84 | ].each do |key, value| 85 | expect(tracer).to have_span.with_tag(key, value) 86 | end 87 | end 88 | end 89 | 90 | describe "client span" do 91 | it "sets operation_name to ClassName#method" do 92 | expect(tracer).to have_span("Dalli::Client#perform") 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/rails/active_suppport/cache/tracer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "spanmanager" 3 | 4 | RSpec.describe ActiveSupport::Cache::Tracer do 5 | let(:tracer) { Test::Tracer.new } 6 | let(:test_key) { "test-key" } 7 | 8 | describe "active span propagation" do 9 | let(:root_span) { tracer.start_span("root") } 10 | 11 | before do 12 | ActiveSupport::Cache::Tracer.instrument(tracer: tracer, active_span: -> { root_span }) 13 | Rails.cache.read(test_key) 14 | end 15 | 16 | after do 17 | ActiveSupport::Cache::Tracer.disable 18 | Rails.cache.clear 19 | end 20 | 21 | it "creates the new span with active span trace_id" do 22 | expect(tracer).to have_traces(1) 23 | end 24 | 25 | it "creates the new span with active span as a parent" do 26 | cache_span = tracer.finished_spans.last 27 | expect(cache_span).to be_child_of(root_span) 28 | end 29 | end 30 | 31 | describe "nested context" do 32 | let(:test_tracer) { Test::Tracer.new } 33 | let(:tracer) { SpanManager::Tracer.new(test_tracer) } 34 | 35 | before do 36 | ActiveSupport::Cache::Tracer.instrument(tracer: tracer, active_span: -> { tracer.active_span }) 37 | 38 | root = tracer.start_span("root") 39 | Rails.cache.fetch(test_key) do 40 | tracer.start_span("nested").finish 41 | "test-value" 42 | end 43 | root.finish 44 | end 45 | 46 | after do 47 | ActiveSupport::Cache::Tracer.disable 48 | Rails.cache.clear 49 | end 50 | 51 | it "creates a single trace" do 52 | expect(test_tracer).to have_traces(1) 53 | end 54 | 55 | it "creates 5 spans" do 56 | expect(test_tracer).to have_spans(5) 57 | end 58 | 59 | if Gem::Version.new(Rails.version) >= Gem::Version.new("4.0.0") 60 | it "creates nested spans tree" do 61 | expect(test_tracer).to have_span("root") 62 | expect(test_tracer).to have_span("cache.read").with_parent("root") 63 | expect(test_tracer).to have_span("cache.generate").with_parent("root") 64 | expect(test_tracer).to have_span("nested").with_parent("cache.generate") 65 | expect(test_tracer).to have_span("cache.write").with_parent("root") 66 | end 67 | else 68 | it "creates flat spans tree" do 69 | expect(test_tracer).to have_span("root") 70 | expect(test_tracer).to have_span("cache.read").with_parent("root") 71 | expect(test_tracer).to have_span("cache.generate").with_parent("root") 72 | expect(test_tracer).to have_span("nested").with_parent("root") 73 | expect(test_tracer).to have_span("cache.write").with_parent("root") 74 | end 75 | end 76 | end 77 | 78 | describe "auto-instrumentation" do 79 | before do 80 | ActiveSupport::Cache::Tracer.instrument(tracer: tracer) 81 | Rails.cache.read(test_key) 82 | end 83 | 84 | after do 85 | ActiveSupport::Cache::Tracer.disable 86 | Rails.cache.clear 87 | end 88 | 89 | it "creates a new span" do 90 | expect(tracer).to have_spans 91 | end 92 | 93 | it "sets operation_name to event's name" do 94 | expect(tracer).to have_span("cache.read") 95 | end 96 | 97 | it "sets standard OT tags" do 98 | [ 99 | ['component', 'ActiveSupport::Cache'], 100 | ['span.kind', 'client'] 101 | ].each do |key, value| 102 | expect(tracer).to have_span.with_tag(key, value) 103 | end 104 | end 105 | 106 | it "sets cache specific OT tags" do 107 | [ 108 | ['cache.key', test_key], 109 | ].each do |key, value| 110 | expect(tracer).to have_span.with_tag(key, value) 111 | end 112 | end 113 | 114 | context "cache entry not found during read" do 115 | it "sets cache.hit tag to false" do 116 | Rails.cache.read(test_key) 117 | expect(tracer).to have_span.with_tag('cache.hit', false) 118 | end 119 | end 120 | 121 | context "cache entry found during read" do 122 | it "sets cache.hit tag to true" do 123 | Rails.cache.write(test_key, "a value") 124 | Rails.cache.read(test_key) 125 | expect(tracer).to have_span.with_tag('cache.hit', false) 126 | end 127 | end 128 | 129 | context "exception thrown during cache operation" do 130 | it "sets error on span" do 131 | exception = Timeout::Error.new("couldn't reach cache server") 132 | expect { Rails.cache.fetch(test_key) { raise exception } }.to raise_error(exception) 133 | if Gem::Version.new(Rails.version) >= Gem::Version.new("5.0.0") 134 | # exception_object was introduced in Rails version 5+ 135 | expect(tracer).to have_span 136 | .with_tag('error', true) 137 | .with_log(event: 'error', :'error.object' => exception) 138 | else 139 | expect(tracer).to have_span 140 | .with_tag('error', true) 141 | .with_log(event: 'error', :'error.kind' => "Timeout::Error", message: "couldn't reach cache server") 142 | end 143 | end 144 | end 145 | end 146 | 147 | describe "dalli store auto-instrumentation option" do 148 | def instrument(dalli:) 149 | ActiveSupport::Cache::Tracer.instrument(tracer: tracer, active_span: -> { }, dalli: dalli).disable 150 | end 151 | 152 | context "Dalli wasn't required" do 153 | context "dalli: false" do 154 | let(:enabled) { false } 155 | 156 | it "doesn't enable dalli auto-instrumentation" do 157 | expect(Dalli::Tracer).not_to receive(:instrument) 158 | instrument(dalli: enabled) 159 | end 160 | end 161 | 162 | context "dalli: true" do 163 | let(:enabled) { true } 164 | 165 | before do 166 | HiddenDalliStore = ActiveSupport::Cache::DalliStore 167 | ActiveSupport::Cache.send(:remove_const, "DalliStore") 168 | end 169 | 170 | after do 171 | ActiveSupport::Cache::DalliStore = HiddenDalliStore 172 | end 173 | 174 | it "doesn't enable dalli auto-instrumentation" do 175 | expect(Dalli::Tracer).not_to receive(:instrument) 176 | instrument(dalli: enabled) 177 | end 178 | end 179 | end 180 | 181 | context "Dalli was required" do 182 | context "dalli: false" do 183 | let(:enabled) { false } 184 | 185 | it "doesn't enable dalli auto-instrumentation" do 186 | expect(Dalli::Tracer).not_to receive(:instrument) 187 | instrument(dalli: enabled) 188 | end 189 | end 190 | 191 | context "dalli: true" do 192 | let(:enabled) { true } 193 | 194 | it "enables dalli auto-instrumentation" do 195 | expect(Dalli::Tracer).to receive(:instrument) 196 | instrument(dalli: enabled) 197 | end 198 | 199 | describe "Dalli tracing logger" do 200 | context "logger already instrumented" do 201 | it "keeps current logger intact" do 202 | logger = Tracing::Logger.new(active_span: -> { }) 203 | Dalli.logger = logger 204 | instrument(dalli: enabled) 205 | expect(Dalli.logger).to eq(logger) 206 | 207 | logger = Tracing::CompositeLogger.new(logger) 208 | Dalli.logger = logger 209 | instrument(dalli: enabled) 210 | expect(Dalli.logger).to eq(logger) 211 | end 212 | end 213 | 214 | context "logger wasn't instrumented" do 215 | let(:stdout_logger) { Logger.new(STDOUT) } 216 | 217 | before do 218 | Dalli.logger = stdout_logger 219 | instrument(dalli: enabled) 220 | end 221 | 222 | it "creates composite logger" do 223 | expect(Dalli.logger).to be_instance_of(Tracing::CompositeLogger) 224 | end 225 | 226 | it "wraps existing logger" do 227 | expect(Dalli.logger.destinations).to include(stdout_logger) 228 | end 229 | 230 | it "wraps tracing logger with ERROR severity level" do 231 | logger = Dalli.logger.destinations.find { |d| d.is_a?(Tracing::Logger) } 232 | expect(logger).not_to be_nil 233 | expect(logger.level).to eq(Logger::ERROR) 234 | end 235 | end 236 | end 237 | end 238 | end 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /spec/rails/rack/tracer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "rack/tracer" 3 | require "rack/mock" 4 | 5 | RSpec.describe Rails::Rack::Tracer do 6 | let(:tracer) { Test::Tracer.new } 7 | let(:env) { Rack::MockRequest.env_for('/api/user', method: 'GET') } 8 | let(:response) { [200, {'Content-Type' => 'application/json'}, ['{"users": []}']] } 9 | 10 | describe "Class Methods" do 11 | subject { described_class } 12 | 13 | it { should respond_to :instrument } 14 | it { should respond_to :disable } 15 | end 16 | 17 | describe :instrument do 18 | let(:stack) { ActionDispatch::MiddlewareStack.new } 19 | 20 | context "Rack::Tracer already present" do 21 | before do 22 | stack.use(::Rack::Tracer) 23 | Rails::Rack::Tracer.instrument(middlewares: stack) 24 | end 25 | 26 | it "does not insert additional Rack::Tracer" do 27 | rack_tracers = stack.middlewares.select { |m| m == ::Rack::Tracer } 28 | expect(rack_tracers.size).to eq(1) 29 | end 30 | 31 | it "inserts the tracer after Rack::Tracer" do 32 | rack_tracer = stack.middlewares.find_index(::Rack::Tracer) 33 | rails_tracer = stack.middlewares.find_index(Rails::Rack::Tracer) 34 | 35 | expect(rack_tracer).not_to be_nil 36 | expect(rails_tracer).not_to be_nil 37 | 38 | expect(rails_tracer).to be > rack_tracer 39 | end 40 | end 41 | 42 | context "Rack::Tracer wasn't present" do 43 | before do 44 | Rails::Rack::Tracer.instrument(middlewares: stack) 45 | end 46 | 47 | it "inserts additional Rack::Tracer" do 48 | rack_tracers = stack.middlewares.select { |m| m == ::Rack::Tracer } 49 | expect(rack_tracers.size).to eq(1) 50 | end 51 | 52 | it "inserts the tracer after Rack::Tracer" do 53 | rack_tracer = stack.middlewares.find_index(::Rack::Tracer) 54 | rails_tracer = stack.middlewares.find_index(Rails::Rack::Tracer) 55 | 56 | expect(rack_tracer).not_to be_nil 57 | expect(rails_tracer).not_to be_nil 58 | 59 | expect(rails_tracer).to be > rack_tracer 60 | end 61 | end 62 | end 63 | 64 | describe :disable do 65 | let(:stack) { ActionDispatch::MiddlewareStack.new } 66 | 67 | context "Rack::Tracer already present" do 68 | before do 69 | stack.use(::Rack::Tracer) 70 | Rails::Rack::Tracer.instrument(middlewares: stack) 71 | Rails::Rack::Tracer.disable(middlewares: stack) 72 | end 73 | 74 | it "leaves Rack::Tracer intact" do 75 | expect(stack.middlewares).to include(::Rack::Tracer) 76 | end 77 | 78 | it "removes rails tracer" do 79 | expect(stack.middlewares).not_to include(Rails::Rack::Tracer) 80 | end 81 | end 82 | 83 | context "Rack::Tracer wasn't present" do 84 | before do 85 | Rails::Rack::Tracer.instrument(middlewares: stack) 86 | Rails::Rack::Tracer.disable(middlewares: stack) 87 | end 88 | 89 | it "removes Rack::Tracer" do 90 | expect(stack.middlewares).not_to include(::Rack::Tracer) 91 | end 92 | 93 | it "removes rails tracer" do 94 | expect(stack.middlewares).not_to include(Rails::Rack::Tracer) 95 | end 96 | end 97 | end 98 | 99 | def respond_with(&app) 100 | enhance_middleware = Rails::Rack::Tracer.new(app) 101 | middleware = Rack::Tracer.new(enhance_middleware, tracer: tracer) 102 | middleware.call(env) 103 | end 104 | 105 | context 'when path was not found' do 106 | it 'leaves the operation_name as it was' do 107 | respond_with { response } 108 | 109 | expect(tracer).to have_spans(1) 110 | expect(tracer).to have_span('GET').finished 111 | end 112 | end 113 | 114 | context 'when path was found' do 115 | it 'enhances the operation_name to Controller#action' do 116 | respond_with do |env| 117 | env["action_dispatch.request.path_parameters"] = {controller: "/api/users", action: "index"} 118 | env["action_controller.instance"] = Api::UsersController.new 119 | response 120 | end 121 | 122 | expect(tracer).to have_spans(1) 123 | expect(tracer).to have_span('Api::UsersController#index').finished 124 | end 125 | end 126 | 127 | module Api 128 | class UsersController 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/rails/span_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Rails::Tracer::SpanHelpers do 4 | describe "Class Methods" do 5 | it { should respond_to :set_error } 6 | end 7 | 8 | describe :set_error do 9 | let(:tracer) { Test::Tracer.new } 10 | let(:span) { tracer.start_span("failed span") } 11 | 12 | context "exception object passed" do 13 | let(:exception) { Exception.new("test exception") } 14 | 15 | before do 16 | Rails::Tracer::SpanHelpers.set_error(span, exception) 17 | end 18 | 19 | it 'marks the span as failed' do 20 | expect(span).to have_tag('error', true) 21 | end 22 | 23 | it 'logs the error' do 24 | expect(span).to have_log(event: 'error', :'error.object' => exception) 25 | end 26 | end 27 | 28 | context "array passed" do 29 | let(:exception) { ["Exception", "test exception"] } 30 | 31 | before do 32 | Rails::Tracer::SpanHelpers.set_error(span, exception) 33 | end 34 | 35 | it 'marks the span as failed' do 36 | expect(span).to have_tag('error', true) 37 | end 38 | 39 | it 'logs the error' do 40 | expect(span).to have_log(event: 'error', :'error.kind' => "Exception", message: "test exception") 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/rails/tracer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Rails::Tracer do 4 | describe "Class Methods" do 5 | it { should respond_to :instrument } 6 | it { should respond_to :disable } 7 | end 8 | 9 | describe :instrument do 10 | let(:tracer) { Test::Tracer.new } 11 | 12 | before do 13 | OpenTracing.global_tracer = tracer 14 | end 15 | 16 | context "default arguments" do 17 | it "doesn't instrument Rack" do 18 | expect(Rails::Rack::Tracer).not_to receive(:instrument) 19 | 20 | Rails::Tracer.instrument 21 | end 22 | 23 | it "instruments ActiveRecord" do 24 | expect(ActiveRecord::Tracer).to receive(:instrument) 25 | .with(tracer: tracer, 26 | active_span: nil) 27 | 28 | Rails::Tracer.instrument 29 | end 30 | 31 | it "instruments ActiveSupport::Cache" do 32 | expect(ActiveSupport::Cache::Tracer).to receive(:instrument) 33 | .with(tracer: tracer, 34 | active_span: nil, 35 | dalli: false) 36 | 37 | Rails::Tracer.instrument 38 | end 39 | end 40 | 41 | context "sub-tracers explicitly disabled" do 42 | it "doesn't instrument Rack" do 43 | expect(Rails::Rack::Tracer).not_to receive(:instrument) 44 | 45 | Rails::Tracer.instrument(rack: false) 46 | end 47 | 48 | it "doesn't instrument ActiveRecord" do 49 | expect(ActiveRecord::Tracer).not_to receive(:instrument) 50 | 51 | Rails::Tracer.instrument(active_record: false) 52 | end 53 | 54 | it "doesn't instrument ActiveSupport::Cache" do 55 | expect(ActiveSupport::Cache::Tracer).not_to receive(:instrument) 56 | 57 | Rails::Tracer.instrument(active_support_cache: false) 58 | end 59 | end 60 | 61 | context "sub-tracers explicitly enabled" do 62 | it "instruments Rack" do 63 | expect(Rails::Rack::Tracer).to receive(:instrument) 64 | .with(tracer: tracer, 65 | middlewares: Rails.configuration.middleware) 66 | 67 | Rails::Tracer.instrument(rack: true) 68 | end 69 | end 70 | 71 | describe "passing arguments to sub-tracers" do 72 | it "pass middlewares argument to rack tracer" do 73 | stack = double 74 | expect(Rails::Rack::Tracer).to receive(:instrument) 75 | .with(tracer: tracer, 76 | middlewares: stack) 77 | 78 | Rails::Tracer.instrument(rack: true, middlewares: stack) 79 | end 80 | 81 | it "pass dalli argument to cache tracer" do 82 | [true, false].each do |enabled| 83 | expect(ActiveSupport::Cache::Tracer).to receive(:instrument) 84 | .with(tracer: tracer, 85 | active_span: nil, 86 | dalli: enabled) 87 | 88 | Rails::Tracer.instrument(dalli: enabled) 89 | end 90 | end 91 | end 92 | end 93 | 94 | describe :disable do 95 | it "disables all submodules" do 96 | [ 97 | Rails::Rack::Tracer, 98 | ActiveRecord::Tracer, 99 | ActiveSupport::Cache::Tracer 100 | ].each do |tracer_class| 101 | expect(tracer_class).to receive(:disable) 102 | end 103 | 104 | Rails::Tracer.disable 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require "test_app/config/environment" 3 | # Prevent database truncation if the environment is production 4 | abort("The Rails environment is running in production mode!") if Rails.env.production? 5 | require 'rspec/rails' 6 | require 'database_cleaner' 7 | 8 | # Checks for pending migration and applies them before tests are run. 9 | # ActiveRecord::Migration.maintain_test_schema! 10 | system("cd spec/test_app; bin/rake db:create db:migrate db:test:prepare") 11 | 12 | RSpec.configure do |config| 13 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 14 | 15 | config.use_transactional_fixtures = true 16 | 17 | config.infer_spec_type_from_file_location! 18 | config.filter_rails_from_backtrace! 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "simplecov" 3 | require "simplecov-console" 4 | 5 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 6 | SimpleCov::Formatter::HTMLFormatter, 7 | SimpleCov::Formatter::Console, 8 | ]) 9 | SimpleCov.start 10 | 11 | RSpec.configure do |config| 12 | # Enable flags like --only-failures and --next-failure 13 | config.example_status_persistence_file_path = ".rspec_status" 14 | 15 | # Disable RSpec exposing methods globally on `Module` and `main` 16 | config.disable_monkey_patching! 17 | 18 | config.expect_with :rspec do |c| 19 | c.syntax = :expect 20 | end 21 | end 22 | 23 | 24 | require "rails_helper" 25 | require "test/tracer" 26 | require "tracing/matchers" 27 | require "tracing/logger" 28 | require "rails-tracer" 29 | -------------------------------------------------------------------------------- /spec/test_app/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | 3 | # Ignore the default SQLite database. 4 | /db/*.sqlite3 5 | /db/*.sqlite3-journal 6 | 7 | # Ignore all logfiles and tempfiles. 8 | /log/* 9 | /tmp/* 10 | !/log/.keep 11 | !/tmp/.keep 12 | -------------------------------------------------------------------------------- /spec/test_app/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /spec/test_app/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iaintshine/ruby-rails-tracer/7d7173e3b03078d9525b65e9febf38adfe59e8ac/spec/test_app/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/test_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/test_app/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | if Gem::Version.new(Rails.version) >= Gem::Version.new("5.0.0") 2 | module ApplicationCable 3 | class Channel < ActionCable::Channel::Base 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/test_app/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | if Gem::Version.new(Rails.version) >= Gem::Version.new("5.0.0") 2 | module ApplicationCable 3 | class Connection < ActionCable::Connection::Base 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/test_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | end 4 | -------------------------------------------------------------------------------- /spec/test_app/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iaintshine/ruby-rails-tracer/7d7173e3b03078d9525b65e9febf38adfe59e8ac/spec/test_app/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/test_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/test_app/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | if Gem::Version.new(Rails.version) >= Gem::Version.new("4.2.0") 2 | class ApplicationJob < ActiveJob::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/test_app/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /spec/test_app/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/test_app/app/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < ApplicationRecord 2 | end 3 | -------------------------------------------------------------------------------- /spec/test_app/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iaintshine/ruby-rails-tracer/7d7173e3b03078d9525b65e9febf38adfe59e8ac/spec/test_app/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/test_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |