├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── active_endpoint.gemspec ├── app ├── assets │ ├── config │ │ └── active_endpoint_manifest.js │ ├── javascripts │ │ └── active_endpoint │ │ │ └── application.js │ └── stylesheets │ │ └── active_endpoint │ │ └── application.css ├── controllers │ └── active_endpoint │ │ ├── application_controller.rb │ │ ├── dashboard_controller.rb │ │ ├── probes_controller.rb │ │ └── unregistred_probes_controller.rb ├── helpers │ └── active_endpoint │ │ └── application_helper.rb ├── models │ └── active_endpoint │ │ ├── application_record.rb │ │ ├── probe.rb │ │ └── unregistred_probe.rb └── views │ ├── active_endpoint │ ├── application │ │ ├── _flashes.html.erb │ │ └── _header.html.erb │ ├── dashboard │ │ ├── _blacklist.html.erb │ │ ├── _constraints.html.erb │ │ ├── _settings.html.erb │ │ ├── _tags.html.erb │ │ └── index.html.erb │ ├── probes │ │ ├── index.html.erb │ │ ├── show.html.erb │ │ └── show_response.html.erb │ └── unregistred_probes │ │ └── index.html.erb │ └── layouts │ └── active_endpoint │ └── application.html.erb ├── bin ├── console ├── rails └── setup ├── config └── routes.rb ├── lib ├── active_endpoint.rb ├── active_endpoint │ ├── concerns │ │ ├── configurable.rb │ │ ├── constraintable.rb │ │ ├── optionable.rb │ │ ├── rails_routable.rb │ │ └── settingable.rb │ ├── engine.rb │ ├── extentions │ │ ├── active_record.rb │ │ └── active_support.rb │ ├── logger.rb │ ├── proxy.rb │ ├── rails │ │ ├── middleware.rb │ │ └── railtie.rb │ ├── request.rb │ ├── response.rb │ ├── routes │ │ ├── blacklist.rb │ │ ├── cache │ │ │ ├── proxy.rb │ │ │ ├── proxy │ │ │ │ └── redis_store_proxy.rb │ │ │ └── store.rb │ │ ├── constraint_rule.rb │ │ ├── constraints.rb │ │ ├── matcher.rb │ │ └── momento.rb │ ├── storage.rb │ ├── tags.rb │ └── version.rb └── generators │ ├── active_endpoint │ └── install_generator.rb │ └── templates │ ├── active_endpoint.rb │ └── migration.erb └── spec ├── active_endpoint ├── concerns │ ├── configurabe_spec.rb │ ├── constrainable_spec.rb │ ├── optionable_spec.rb │ ├── rails_routable_spec.rb │ └── settingable_spec.rb ├── extentions │ └── active_record_spec.rb ├── logger_spec.rb ├── proxy_spec.rb ├── request_spec.rb ├── response_spec.rb ├── routes │ ├── blacklist_spec.rb │ ├── cache │ │ ├── proxy │ │ │ └── redis_store_proxy_spec.rb │ │ ├── proxy_spec.rb │ │ └── store_spec.rb │ ├── constraint_rule_spec.rb │ ├── constraints_spec.rb │ ├── matcher_spec.rb │ └── momento_spec.rb ├── storage_spec.rb └── tags_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | 52 | # other 53 | /.rspec_status 54 | /.byebug_history 55 | /dump.rdb 56 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Encoding: 2 | Enabled: false 3 | 4 | LineLength: 5 | Max: 120 6 | 7 | MethodLength: 8 | Max: 25 9 | 10 | AbcSize: 11 | Max: 30 12 | 13 | Documentation: 14 | Enabled: false 15 | 16 | IndentationConsistency: 17 | Enabled: false 18 | 19 | ModuleFunction: 20 | Enabled: false 21 | 22 | RegexpLiteral: 23 | MaxSlashes: 0 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.1.9 5 | - 2.2.5 6 | - 2.3.1 7 | - 2.3.4 8 | - 2.4.1 9 | 10 | before_install: gem install bundler 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at marat@khusnetdinov.ru. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'sass-rails' 6 | 7 | group :development do 8 | gem 'bundler' 9 | gem 'reek' 10 | gem 'rspec' 11 | gem 'rubocop' 12 | gem 'pry' 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | active_endpoint (0.2.0) 5 | rack 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionpack (4.1.16) 11 | actionview (= 4.1.16) 12 | activesupport (= 4.1.16) 13 | rack (~> 1.5.2) 14 | rack-test (~> 0.6.2) 15 | actionview (4.1.16) 16 | activesupport (= 4.1.16) 17 | builder (~> 3.1) 18 | erubis (~> 2.7.0) 19 | activesupport (4.1.16) 20 | i18n (~> 0.6, >= 0.6.9) 21 | json (~> 1.7, >= 1.7.7) 22 | minitest (~> 5.1) 23 | thread_safe (~> 0.1) 24 | tzinfo (~> 1.1) 25 | ast (2.3.0) 26 | axiom-types (0.1.1) 27 | descendants_tracker (~> 0.0.4) 28 | ice_nine (~> 0.11.0) 29 | thread_safe (~> 0.3, >= 0.3.1) 30 | builder (3.2.3) 31 | codeclimate-engine-rb (0.4.0) 32 | virtus (~> 1.0) 33 | coderay (1.1.1) 34 | coercible (1.0.0) 35 | descendants_tracker (~> 0.0.1) 36 | concurrent-ruby (1.0.5) 37 | descendants_tracker (0.0.4) 38 | thread_safe (~> 0.3, >= 0.3.1) 39 | diff-lcs (1.3) 40 | equalizer (0.0.11) 41 | erubis (2.7.0) 42 | ffi (1.9.18) 43 | i18n (0.8.6) 44 | ice_nine (0.11.2) 45 | json (1.8.6) 46 | method_source (0.8.2) 47 | minitest (5.10.3) 48 | parallel (1.12.0) 49 | parser (2.4.0.0) 50 | ast (~> 2.2) 51 | powerpack (0.1.1) 52 | pry (0.10.4) 53 | coderay (~> 1.1.0) 54 | method_source (~> 0.8.1) 55 | slop (~> 3.4) 56 | rack (1.5.5) 57 | rack-test (0.6.3) 58 | rack (>= 1.0) 59 | railties (4.1.16) 60 | actionpack (= 4.1.16) 61 | activesupport (= 4.1.16) 62 | rake (>= 0.8.7) 63 | thor (>= 0.18.1, < 2.0) 64 | rainbow (2.2.2) 65 | rake 66 | rake (10.5.0) 67 | rb-fsevent (0.10.2) 68 | rb-inotify (0.9.10) 69 | ffi (>= 0.5.0, < 2) 70 | redis (3.3.3) 71 | redis-activesupport (5.0.3) 72 | activesupport (>= 3, < 6) 73 | redis-store (~> 1.3.0) 74 | redis-store (1.3.0) 75 | redis (>= 2.2) 76 | reek (4.7.2) 77 | codeclimate-engine-rb (~> 0.4.0) 78 | parser (>= 2.4.0.0, < 2.5) 79 | rainbow (~> 2.0) 80 | rspec (3.6.0) 81 | rspec-core (~> 3.6.0) 82 | rspec-expectations (~> 3.6.0) 83 | rspec-mocks (~> 3.6.0) 84 | rspec-core (3.6.0) 85 | rspec-support (~> 3.6.0) 86 | rspec-expectations (3.6.0) 87 | diff-lcs (>= 1.2.0, < 2.0) 88 | rspec-support (~> 3.6.0) 89 | rspec-mocks (3.6.0) 90 | diff-lcs (>= 1.2.0, < 2.0) 91 | rspec-support (~> 3.6.0) 92 | rspec-support (3.6.0) 93 | rubocop (0.49.1) 94 | parallel (~> 1.10) 95 | parser (>= 2.3.3.1, < 3.0) 96 | powerpack (~> 0.1) 97 | rainbow (>= 1.99.1, < 3.0) 98 | ruby-progressbar (~> 1.7) 99 | unicode-display_width (~> 1.0, >= 1.0.1) 100 | ruby-progressbar (1.8.1) 101 | sass (3.5.1) 102 | sass-listen (~> 4.0.0) 103 | sass-listen (4.0.0) 104 | rb-fsevent (~> 0.9, >= 0.9.4) 105 | rb-inotify (~> 0.9, >= 0.9.7) 106 | sass-rails (5.0.6) 107 | railties (>= 4.0.0, < 6) 108 | sass (~> 3.1) 109 | sprockets (>= 2.8, < 4.0) 110 | sprockets-rails (>= 2.0, < 4.0) 111 | tilt (>= 1.1, < 3) 112 | slop (3.6.0) 113 | sprockets (3.7.1) 114 | concurrent-ruby (~> 1.0) 115 | rack (> 1, < 3) 116 | sprockets-rails (3.2.0) 117 | actionpack (>= 4.0) 118 | activesupport (>= 4.0) 119 | sprockets (>= 3.0.0) 120 | thor (0.19.4) 121 | thread_safe (0.3.6) 122 | tilt (2.0.8) 123 | tzinfo (1.2.3) 124 | thread_safe (~> 0.1) 125 | unicode-display_width (1.3.0) 126 | virtus (1.0.5) 127 | axiom-types (~> 0.1) 128 | coercible (~> 1.0) 129 | descendants_tracker (~> 0.0, >= 0.0.3) 130 | equalizer (~> 0.0, >= 0.0.9) 131 | 132 | PLATFORMS 133 | ruby 134 | 135 | DEPENDENCIES 136 | actionpack (>= 3.0.0) 137 | active_endpoint! 138 | activesupport (>= 3.0.0) 139 | bundler 140 | pry 141 | rake 142 | redis-activesupport 143 | reek 144 | rspec 145 | rubocop 146 | sass-rails 147 | 148 | BUNDLED WITH 149 | 1.14.3 150 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marat Khusnetdinov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveEndpoint [![Build Status](https://travis-ci.org/khusnetdinov/active_endpoint.svg?branch=master)](https://travis-ci.org/khusnetdinov/active_endpoint) [![Gem Version](https://badge.fury.io/rb/active_endpoint.svg)](https://badge.fury.io/rb/active_endpoint) [![Open Source Helpers](https://www.codetriage.com/khusnetdinov/active_endpoint/badges/users.svg)](https://www.codetriage.com/khusnetdinov/active_endpoint) 2 | 3 | ## Your request tracking tool for rails applications 4 | 5 | ## Attention! Gem in under active test and is preparing for first release ! 6 | 7 | ![img](http://res.cloudinary.com/dtoqqxqjv/image/upload/c_scale,w_346/v1501331806/github/active_probe.jpg) 8 | 9 | 10 | ## Usage 11 | 12 | ActiveEndpoint is middleware for Rails applications that collects and analyses requests and responses per request for endpoint. It works with minimal impact on application's response time. 13 | 14 | This gem uses `ActiveSupport::Notifications` and `Cache Storage` to reduce possible impact on application request / response processing time. 15 | 16 | ## Features 17 | 18 | - Metrics are stored in database for further tracking and analysis. 19 | - History rotation with configurable limits of records amount and age. 20 | - Routes filter (blacklist). 21 | - Probes tagging by processing time. 22 | 23 | ## Metrics 24 | 25 | These endpoint metrics are stored in DB: 26 | 27 | - `:uuid` - uniq probe identifier 28 | - `:endpoint` - requested endpoint 29 | - `:path` - requested full path 30 | - `:query_string` - request query string 31 | - `:request_method` - http request method 32 | - `:ip` - ip address asked request 33 | - `:url` - request full url 34 | - `:xhr` - is request ajax? 35 | - `:started_at` - probe start time 36 | - `:finished_at` - probe finish time 37 | - `:duration` - probe request duration 38 | - `:params` - parsed requested params 39 | - `:response` - Base64 encoded html response 40 | - `:body` - Base64 encoded request body 41 | 42 | Additional information is taken from Rack log: 43 | 44 | `:base_url, :content_charset, :content_length, :content_type, :fullpath, :http_version, :http_connection, :http_accept_encoding, :http_accept_language, :media_type, :media_type_params, :method, :path_info, :pattern, :port, :protocol, :server_name, :ssl`. 45 | 46 | Requests which are not recognized by rails router are stored as `unregistred`. 47 | 48 | ## Requirements 49 | 50 | - `redis` as cache storage 51 | 52 | Be sure that you have all requrements installed on you machine. 53 | 54 | ## Installation 55 | 56 | Add this line to your application's Gemfile: 57 | 58 | ```ruby 59 | gem 'active_endpoint' 60 | ``` 61 | 62 | And then execute: 63 | 64 | $ bundle 65 | 66 | Or install it yourself as: 67 | 68 | $ gem install active_endpoint 69 | 70 | Setup project for using gem: 71 | 72 | $ rails generate active_endpoint:install 73 | 74 | Migrate database for models: 75 | 76 | $ rake db:migrate # Rails <=4 77 | $ rails db:migrate # Rails >=5 78 | 79 | Now project has all files and settings that allow you to use gem. 80 | 81 | ## Configuration 82 | 83 | ### Endpoints filter (blacklist) 84 | 85 | By default ActiveEndpoint treats all routes as `whitelist` routes. To filter some endpoints you can use `blackilist` configuration, as shown below: 86 | 87 | ```ruby 88 | ActiveEndpoint.configure do |endpoint| 89 | endpoint.blacklist.configure do |blacklist| 90 | # Ignore endpoint "welcome#index" 91 | blacklist.add(endpoint: "welcome#index") 92 | 93 | # Ignore "web/users" controller actions 94 | blacklist.add(resources: ["web/users"]) 95 | 96 | # Ignore "web/users#show" action with scoped controller 97 | blacklist.add(scope: "web", resources: "users", actions: ["show"]) 98 | 99 | # Ignore "admin" scope controllers 100 | blacklist.add(scope: "admin") 101 | end 102 | end 103 | ``` 104 | 105 | #### Ignore one endpoint 106 | 107 | `blacklist.add(endpoint: "users#index")` - Ignore one endpoint. 108 | 109 | #### Ignore controller actions 110 | 111 | `blacklist.add(resources: "users")` - Ignore all actions for `UsersController`. 112 | 113 | `blacklist.add(resources: ["users", "managers"])` - Ignore all actions in `UsersController` and `ManagersController`. 114 | 115 | `blacklist.add(resources: "users", actions: ["show"])` - Ignore only `show` action in `UsersController`. 116 | 117 | #### Ignore namespace or scope 118 | 119 | `blacklist.add(scope: "admin")` - Ignore all controllers and actions for `admin` namespace or scope. 120 | 121 | ### Constraints 122 | 123 | You can specify the amount and period of request records to keep in database. Records which exceed these limits are automatically removed from database. 124 | See example below: 125 | 126 | ```ruby 127 | ActiveEndpoint.configure do |endpoint| 128 | # Defines default settings, 1 probe per 10 minutes for endpoint request 129 | constraint_limit = 1 130 | constraint_period = 10.minutes 131 | 132 | endpoint.constraints.configure do |constraints| 133 | # Constraint endpoint "welcome#index" with 1 minute period and default limit 134 | # and configure database constraints to keep 1000 probes per 1 week. 135 | constraints.add(endpoint: "welcome#index", 136 | rule: { 1.minute }, 137 | storage: { limit: 1000, period: 1.week }) 138 | # Constraints "web/users" controller actions with custom limit and period 139 | # with defailt storage constraints 140 | constraints.add(resources: ["web/users"], rule: { limit: 100, period: 5.minutes }) 141 | end 142 | end 143 | ``` 144 | NOTE: To define a constraint you should define at least one limit or period. 145 | 146 | ### Storage settings 147 | 148 | ActiveEndpoint creates two models in you rails application: `Probe` and it's child `UnregistredProbe`. 149 | To prevent problems with database probes are removed when user defines custom period. Also you can limit storage probes in database. 150 | It is recommended to define own storage default to prevent unwanted probes deletion. See example below: 151 | 152 | ```ruby 153 | ActiveEndpoint.configure do |endpoint| 154 | # Define default limit for maximum probes amount 155 | endpoint.storage_limit = 1000 156 | 157 | # Define default period to keep probes in database. 158 | endpoint.storage_period = 1.week 159 | 160 | # Define amount of periods (constraint periods) that endpoints are kept in database. 161 | endpoint.storage_keep_periods = 2 162 | end 163 | ``` 164 | 165 | ### Tagging probes 166 | You can group probes by tags automatically assigned according to request processing time (ms). See example below: 167 | 168 | ```ruby 169 | ActiveEndpoint.configure do |endpoint| 170 | endpoint.tags.configure do |tags| 171 | tags.add(:fast, { less_than: 250 }) 172 | tags.add(:normal, { greater_than_or_equal_to: 250, less_than: 500 }) 173 | tags.add(:slow, { greater_than_or_equal_to: 500, less_than: 750 }) 174 | tags.add(:acceptable, { greater_than_or_equal_to: 500, less_than: 1000 }) 175 | tags.add(:need_optimization, { greater_than_or_equal_to: 1000 }) 176 | end 177 | end 178 | ``` 179 | 180 | #### Mehods for conditions 181 | 182 | - greater_than = '>' 183 | - greater_than_or_equal_to = '>=', 184 | - equal_to = '=', 185 | - less_than = '<', 186 | - less_than_or_equal_to = '<=', 187 | 188 | #### Tagged model scopes 189 | 190 | Defined tags are also usefull for scopes queries: 191 | 192 | ```ruby 193 | ActiveEndpoint::Probe.tagged_as(:need_optimization) 194 | #=> Returns all probes having corresponding tag and thus matching the condition 195 | # { greater_than_or_equal_to: 1000 } 196 | ``` 197 | 198 | #### Instance methods 199 | 200 | Check tag on model: 201 | 202 | ```ruby 203 | ActiveEndpoint::Probe.last.tag 204 | #=> Returns probe's tag 205 | ``` 206 | 207 | ### Web UI 208 | 209 | ActiveEndpoint offer rails engine for managing probes. Mount it: 210 | 211 | ```ruby 212 | mount ActiveEndpoint::Engine => '/active_endpoint' 213 | ``` 214 | 215 | ![img](http://res.cloudinary.com/dtoqqxqjv/image/upload/v1502260319/github/%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA_%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0_2017-08-09_%D0%B2_9.06.08_mmwh5d.png) 216 | 217 | ## License 218 | 219 | This gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 220 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /active_endpoint.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'active_endpoint/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.version = ActiveEndpoint::VERSION 8 | 9 | spec.name = 'active_endpoint' 10 | spec.authors = ['Marat Khusnetdinov'] 11 | spec.email = ['marat@khusnetdinov.ru'] 12 | 13 | spec.summary = 'Your request tracking tool for rails applications' 14 | spec.description = 'ActiveEndpoint is middleware for Rails applications that collects and analyses requests and responses per request for endpoint. It works with minimal impact on application\'s response time.' 15 | spec.homepage = 'https://github.com/khusnetdinov/active_endpoint' 16 | spec.license = 'MIT' 17 | 18 | spec.homepage = 'https://github.com/khusnetdinov/active_endpoint' 19 | spec.summary = 'Summary of ActiveEndpoint.' 20 | spec.description = 'Description of ActiveEndpoint.' 21 | 22 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 23 | f.match(%r{^(test|spec|features)/}) 24 | end 25 | spec.bindir = 'exe' 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ['lib'] 28 | spec.test_files = Dir.glob('spec/**/*') 29 | 30 | spec.required_ruby_version = '>= 2.0.0' 31 | 32 | spec.add_dependency 'rack' 33 | 34 | spec.add_development_dependency 'rake' 35 | spec.add_development_dependency 'activesupport', '>= 3.0.0' 36 | spec.add_development_dependency 'actionpack', '>= 3.0.0' 37 | spec.add_development_dependency 'redis-activesupport' 38 | end 39 | -------------------------------------------------------------------------------- /app/assets/config/active_endpoint_manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../javascripts/active_endpoint .js 2 | //= link_directory ../stylesheets/active_endpoint .css 3 | -------------------------------------------------------------------------------- /app/assets/javascripts/active_endpoint/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/active_endpoint/application.css: -------------------------------------------------------------------------------- 1 | /* = require_tree . */ 2 | /* = require_self */ 3 | 4 | .list-group-item > .badge { 5 | background-color: auto !important; 6 | } 7 | 8 | .btn-sm { 9 | padding: 0 9px !important; 10 | } -------------------------------------------------------------------------------- /app/controllers/active_endpoint/application_controller.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery with: :exception 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/active_endpoint/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'active_endpoint/application_controller' 2 | 3 | module ActiveEndpoint 4 | class DashboardController < ApplicationController 5 | def index; end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/active_endpoint/probes_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'active_endpoint/application_controller' 2 | 3 | module ActiveEndpoint 4 | class ProbesController < ApplicationController 5 | def index 6 | @probes_group = ActiveEndpoint::Probe.group_by_endpoint 7 | end 8 | 9 | def show 10 | @probes = ActiveEndpoint::Probe.where(endpoint: params[:id]) 11 | end 12 | 13 | def show_response 14 | @probe = ActiveEndpoint::Probe.find(params[:probe_id]) 15 | end 16 | 17 | def destroy 18 | ActiveEndpoint::Probe.find(params[:id]).destroy 19 | redirect_to :back 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/controllers/active_endpoint/unregistred_probes_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'active_endpoint/application_controller' 2 | 3 | module ActiveEndpoint 4 | class UnregistredProbesController < ApplicationController 5 | def index 6 | @probes = ActiveEndpoint::UnregistredProbe.all.select(:id, :uuid, :started_at, :path, :query_string) 7 | end 8 | 9 | def destroy 10 | ActiveEndpoint::UnregistredProbe.find(params[:id]).destroy 11 | redirect_to action: :index 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/helpers/active_endpoint/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module ApplicationHelper 3 | SETTINGS = [ 4 | :cache_store_client, 5 | :cache_prefix, 6 | :constraint_limit, 7 | :constraint_period, 8 | :logger, 9 | :storage_limit, 10 | :storage_period, 11 | :storage_keep_periods 12 | ].freeze 13 | 14 | TAGS_COLORS = [ 15 | 'label-info', 16 | 'label-success', 17 | 'label-warning', 18 | 'label-danger', 19 | 'label-default' 20 | ].freeze 21 | 22 | def settings 23 | SETTINGS 24 | end 25 | 26 | def tags 27 | tags = [] 28 | ActiveEndpoint.tags.definition.to_a.each_with_index do |definition, index| 29 | tags << [ 30 | definition.first, 31 | tag_label_by(index), 32 | definition.last, 33 | "ActiveEndpoint::Probe.tagged_as(:#{definition.first})" 34 | ] 35 | end 36 | tags 37 | end 38 | 39 | def label_for(tag) 40 | TAGS_COLORS[ActiveEndpoint.tags.definition.keys.index(tag.to_sym)] 41 | end 42 | 43 | def endpoints_constraints 44 | ActiveEndpoint.constraints.endpoints.map do |endpoint, constraints| 45 | constraints_for_html(endpoint, constraints) 46 | end 47 | end 48 | 49 | def resources_constraints 50 | ActiveEndpoint.constraints.resources.map do |endpoint, constraints| 51 | constraints_for_html(endpoint, constraints) 52 | end 53 | end 54 | 55 | def actions_constraints 56 | ActiveEndpoint.constraints.actions.map do |endpoint, constraints| 57 | constraints_for_html(endpoint, constraints) 58 | end 59 | end 60 | 61 | def scopes_constraints 62 | ActiveEndpoint.constraints.scopes.map do |endpoint, constraints| 63 | constraints_for_html(endpoint, constraints) 64 | end 65 | end 66 | 67 | private 68 | 69 | def tag_label_by(index) 70 | if TAGS_COLORS.length > index 71 | TAGS_COLORS[index] 72 | else 73 | TAGS_COLORS.last 74 | end 75 | end 76 | 77 | def constraints_for_html(endpoint, constraints) 78 | rule = constraints[:rule] 79 | storage = constraints[:storage] 80 | 81 | rules = "#{rule[:limit]} per #{distance_of_time_in_words(rule[:period])}" 82 | storages = "#{storage[:limit]} per #{distance_of_time_in_words(storage[:period])}" 83 | 84 | [endpoint, rules, storages] 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /app/models/active_endpoint/application_record.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class ApplicationRecord < ActiveRecord::Base 3 | self.abstract_class = true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/active_endpoint/probe.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class Probe < ApplicationRecord 3 | validates :uuid, :started_at, :duration, :endpoint, :ip, :path, 4 | :request_method, :url, presence: true 5 | 6 | validates :finished_at, presence: true, unless: :skip_validation? 7 | 8 | scope :unregistred, ->() { where(endpoint: :unregistred) } 9 | scope :registred, ->() { where.not(endpoint: :unregistred) } 10 | scope :probe, ->(endpoint) { where(endpoint: endpoint) } 11 | scope :taken_before, ->(period) { where('created_at < ?', period) } 12 | 13 | def tag 14 | methods = ActiveEndpoint::Extentions::ActiveRecord::METHODS 15 | 16 | ActiveEndpoint.tags.definition.each do |tag_name, operators| 17 | last_operation_index = operators.length - 1 18 | 19 | exec_operator = '' 20 | operators.each_with_index do |(key, value), index| 21 | exec_operator << "#{duration * 1000} #{methods[key]} #{value}" 22 | exec_operator << (index == last_operation_index ? '' : ' && ') 23 | end 24 | 25 | return tag_name if eval(exec_operator) 26 | end 27 | end 28 | 29 | private 30 | 31 | def skip_validation? 32 | type == 'ActiveEndpoint::UnregistredProbe' 33 | end 34 | 35 | def self.group_by_endpoint 36 | sql = <<-sql 37 | select count(*) as amount, endpoint, request_method as method, avg(duration) as duration 38 | from active_endpoint_probes group by endpoint, request_method 39 | having endpoint != 'unregistred' 40 | sql 41 | 42 | results = ActiveRecord::Base.find_by_sql(sql) 43 | 44 | if results.present? 45 | return results.map(&:deep_symbolize_keys) 46 | else 47 | return nil 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/models/active_endpoint/unregistred_probe.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class UnregistredProbe < Probe; end 3 | end 4 | -------------------------------------------------------------------------------- /app/views/active_endpoint/application/_flashes.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |key, message| %> 2 | 5 | <% end %> -------------------------------------------------------------------------------- /app/views/active_endpoint/application/_header.html.erb: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /app/views/active_endpoint/dashboard/_blacklist.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Blacklist
3 | 57 |
-------------------------------------------------------------------------------- /app/views/active_endpoint/dashboard/_constraints.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Constraints
3 | 117 |
-------------------------------------------------------------------------------- /app/views/active_endpoint/dashboard/_settings.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Settings
3 | 13 |
-------------------------------------------------------------------------------- /app/views/active_endpoint/dashboard/_tags.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Tags
3 | 20 |
-------------------------------------------------------------------------------- /app/views/active_endpoint/dashboard/index.html.erb: -------------------------------------------------------------------------------- 1 |

Dashboard

2 | <%= render 'settings' %> 3 |
4 | <%= render 'tags' %> 5 |
6 | <%= render 'blacklist' %> 7 |
8 | <%= render 'constraints' %> -------------------------------------------------------------------------------- /app/views/active_endpoint/probes/index.html.erb: -------------------------------------------------------------------------------- 1 |

Probes

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% if @probes_group.any?%> 15 | <% @probes_group.each do |group| %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% end %> 24 | <% else %> 25 | 26 | <% end %> 27 | 28 |
endpointmethodamount in periodAverage duration
<%= group[:endpoint] %><%= group[:method] %><%= group[:amount] %><%= (group[:duration].round(3) * 1000).to_i %><%= link_to 'Show', active_endpoint.probe_path(id: group[:endpoint]), class: 'btn btn-info btn-sm' %>
No Probes
-------------------------------------------------------------------------------- /app/views/active_endpoint/probes/show.html.erb: -------------------------------------------------------------------------------- 1 |

Probes for <%= @endpoint %>

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <%- if @probes.any? %> 19 | <%- @probes.each do |probe| %> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 37 | 38 | <% end%> 39 | <% else %> 40 | 41 | <% end %> 42 | 43 |
uuidipxhr?query stringparamsdurationtagcreated at
<%= probe.uuid %><%= probe.ip %><%= probe.xhr %><%= probe.query_string %><%= probe.params %><%= (probe.duration * 1000).to_i %> 28 | 29 | <%= probe.tag %> 30 | 31 | <%= probe.created_at %> 34 | <%= link_to 'Show Response', active_endpoint.probe_show_response_path(probe), class: 'btn btn-info btn-sm' %> 35 | <%= link_to 'Remove', active_endpoint.probe_path(probe), method: :delete, class: 'btn btn-danger btn-sm' %> 36 |
No Probes
-------------------------------------------------------------------------------- /app/views/active_endpoint/probes/show_response.html.erb: -------------------------------------------------------------------------------- 1 |

Proberesponse for <%= @probe.endpoint %> with <%= @probe.uuid %>

2 | 3 |
4 | <%= Base64.decode64(@probe.response) %> 5 |
6 | 7 |
8 | 9 | <%= link_to 'Back', :back, class: '' %> 10 | -------------------------------------------------------------------------------- /app/views/active_endpoint/unregistred_probes/index.html.erb: -------------------------------------------------------------------------------- 1 |

Unregistred Probes

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% if @probes.any?%> 15 | <% @probes.each do |probe| %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% end%> 24 | <% else %> 25 | 26 | <% end %> 27 | 28 |
uuidpathquery stringstarted_at
<%= probe.uuid %><%= probe.path %><%= probe.query_string%><%= probe.started_at %><%= link_to 'Remove', active_endpoint.unregistred_probe_path(probe.id), method: :delete, class: 'btn btn-danger btn-sm' %>
No Probes
-------------------------------------------------------------------------------- /app/views/layouts/active_endpoint/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ActiveEndpoint 8 | 9 | 10 | 11 | 12 | 13 | <%= stylesheet_link_tag 'active_endpoint/application', media: 'all' %> 14 | <%= csrf_meta_tags %> 15 | 16 | 17 |
18 |
19 | <%= render 'flashes' %> 20 |
21 |
22 | <%= render 'header' %> 23 |
24 |
25 |
26 | <%= yield %> 27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | <%= javascript_include_tag 'active_endpoint/application' %> 38 | 39 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'active_endpoint' 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/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 6 | ENGINE_PATH = File.expand_path('../../lib/active_endpoint/engine', __FILE__) 7 | 8 | # Set up gems listed in the Gemfile. 9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 10 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 11 | 12 | require 'rails/all' 13 | require 'rails/engine/commands' 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActiveEndpoint::Engine.routes.draw do 2 | root 'dashboard#index' 3 | 4 | resources :probes, only: [:index, :show, :destroy] do 5 | get :show_response 6 | end 7 | resources :unregistred_probes, only: [:index, :destroy] 8 | end 9 | -------------------------------------------------------------------------------- /lib/active_endpoint.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | require 'rack' 3 | require 'rack/request' 4 | require 'active_support/time' 5 | require 'active_support/rails' 6 | require 'active_support/core_ext/module/delegation' 7 | 8 | require 'active_endpoint/extentions/active_support' 9 | require 'active_endpoint/concerns/settingable' 10 | require 'active_endpoint/concerns/configurable' 11 | require 'active_endpoint/concerns/constraintable' 12 | require 'active_endpoint/concerns/optionable' 13 | require 'active_endpoint/concerns/rails_routable' 14 | require 'active_endpoint/extentions/active_record' 15 | require 'active_endpoint/routes/momento' 16 | require 'active_endpoint/routes/blacklist' 17 | require 'active_endpoint/routes/constraints' 18 | require 'active_endpoint/routes/constraint_rule' 19 | require 'active_endpoint/routes/matcher' 20 | require 'active_endpoint/routes/cache/proxy/redis_store_proxy' 21 | require 'active_endpoint/routes/cache/proxy' 22 | require 'active_endpoint/routes/cache/store' 23 | require 'active_endpoint/proxy' 24 | require 'active_endpoint/logger' 25 | require 'active_endpoint/request' 26 | require 'active_endpoint/response' 27 | require 'active_endpoint/storage' 28 | require 'active_endpoint/tags' 29 | require 'active_endpoint/version' 30 | 31 | module ActiveEndpoint 32 | extend Settingable 33 | extend Configurable 34 | 35 | define_setting :blacklist, ActiveEndpoint::Routes::Blacklist.new 36 | 37 | define_setting :cache_store_client, :redis 38 | define_setting :cache_prefix, 'active_endpoint' 39 | 40 | define_setting :constraint_limit, 10 41 | define_setting :constraint_period, 10.minutes 42 | define_setting :constraints, ActiveEndpoint::Routes::Constraints.new 43 | 44 | define_setting :logger, ActiveEndpoint::Logger 45 | 46 | define_setting :storage_limit, 1000 47 | define_setting :storage_period, 1.day 48 | define_setting :storage_keep_periods, 2 49 | 50 | define_setting :tags, ActiveEndpoint::Tags.new 51 | end 52 | 53 | if defined?(::Rails) 54 | require 'rails/generators' 55 | require 'rails/generators/migration' 56 | 57 | require 'active_endpoint/rails/middleware' 58 | require 'active_endpoint/rails/railtie' 59 | require 'active_endpoint/engine' 60 | end 61 | -------------------------------------------------------------------------------- /lib/active_endpoint/concerns/configurable.rb: -------------------------------------------------------------------------------- 1 | module Configurable 2 | def configure 3 | yield self 4 | end 5 | end -------------------------------------------------------------------------------- /lib/active_endpoint/concerns/constraintable.rb: -------------------------------------------------------------------------------- 1 | module Constraintable 2 | def constraints(options) 3 | { 4 | rule: rule_constraints(options), 5 | storage: storage_constraints(options) 6 | } 7 | end 8 | 9 | def rule_constraints(options) 10 | rule_options = fetch_rule(options) 11 | 12 | defined_rule_constraints = { 13 | limit: fetch_limit(rule_options), 14 | period: fetch_period(rule_options) 15 | }.reject { |_key, value| value.nil? } 16 | 17 | default_rule_constraints.merge(defined_rule_constraints) 18 | end 19 | 20 | def storage_constraints(options) 21 | storage_options = fetch_storage(options) 22 | 23 | defined_storage_constraints = { 24 | limit: fetch_limit(storage_options), 25 | period: fetch_period(storage_options) 26 | }.reject { |_key, value| value.nil? } 27 | 28 | default_storage_constraints.merge(defined_storage_constraints) 29 | end 30 | 31 | def default_constraints 32 | { 33 | rule: default_rule_constraints, 34 | storage: default_storage_constraints 35 | } 36 | end 37 | 38 | def default_rule_constraints 39 | { 40 | limit: ActiveEndpoint.constraint_limit, 41 | period: ActiveEndpoint.constraint_period 42 | } 43 | end 44 | 45 | def default_storage_constraints 46 | { 47 | limit: ActiveEndpoint.storage_limit, 48 | period: ActiveEndpoint.storage_period 49 | } 50 | end 51 | end -------------------------------------------------------------------------------- /lib/active_endpoint/concerns/optionable.rb: -------------------------------------------------------------------------------- 1 | module Optionable 2 | def fetch_endpoint(options) 3 | return nil unless options 4 | options[:endpoint] 5 | end 6 | 7 | def fetch_actions(options) 8 | return nil unless options 9 | options[:actions] 10 | end 11 | 12 | def fetch_resources(options) 13 | return nil unless options 14 | options[:resources] 15 | end 16 | 17 | def fetch_scope(options) 18 | return nil unless options 19 | options[:scope] 20 | end 21 | 22 | def fetch_limit(options) 23 | return nil unless options 24 | options[:limit] 25 | end 26 | 27 | def fetch_period(options) 28 | return nil unless options 29 | options[:period] 30 | end 31 | 32 | def fetch_storage(options) 33 | return nil unless options 34 | options[:storage] 35 | end 36 | 37 | def fetch_rule(options) 38 | return nil unless options 39 | options[:rule] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/active_endpoint/concerns/rails_routable.rb: -------------------------------------------------------------------------------- 1 | module RailsRoutable 2 | ACTION_KEYS = [:controller, :action].freeze 3 | 4 | def rails_action?(request) 5 | rails_action(request).present? 6 | end 7 | 8 | def rails_request_params(request) 9 | action = rails_action(request) 10 | return unless action 11 | action.reject do |key, _value| 12 | ACTION_KEYS.include?(key) 13 | end 14 | end 15 | 16 | def rails_endpoint(request) 17 | action = rails_action(request) 18 | return unless action 19 | action.select do |key, _value| 20 | ACTION_KEYS.include?(key) 21 | end 22 | end 23 | 24 | def rails_action(request) 25 | rails_routes.recognize_path(request.path, method: request.method) 26 | rescue ActionController::RoutingError 27 | nil 28 | end 29 | 30 | def rails_route_pattern(request) 31 | rails_routes.router.recognize(request) do |route| 32 | return route.path.spec.to_s 33 | end 34 | rescue ActionController::RoutingError 35 | nil 36 | end 37 | 38 | private 39 | 40 | def rails_routes 41 | ::Rails.application.routes 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/active_endpoint/concerns/settingable.rb: -------------------------------------------------------------------------------- 1 | module Settingable 2 | def define_setting(name, default = nil) 3 | class_variable_set("@@#{name}", default) 4 | 5 | define_class_method "#{name}=" do |value| 6 | class_variable_set("@@#{name}", value) 7 | end 8 | 9 | define_class_method name do 10 | class_variable_get("@@#{name}") 11 | end 12 | end 13 | 14 | def get_settings(name) 15 | class_variable_get("@@#{name}") 16 | end 17 | 18 | private 19 | 20 | def define_class_method(name, &block) 21 | (class << self; self; end).instance_eval do 22 | define_method name, &block 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/active_endpoint/engine.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class Engine < ::Rails::Engine 3 | isolate_namespace ActiveEndpoint 4 | 5 | initializer 'active_endpoint.assets.precompile' do |app| 6 | app.config.assets.precompile += %w[] 7 | end 8 | 9 | config.generators do |g| 10 | g.orm :active_record 11 | g.template_engine :erb 12 | g.test_framework :rspec 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/active_endpoint/extentions/active_record.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Extentions 3 | module ActiveRecord 4 | METHODS = { 5 | greater_than: '>', 6 | greater_than_or_equal_to: '>=', 7 | equal_to: '=', 8 | less_than: '<', 9 | less_than_or_equal_to: '<=' 10 | }.freeze 11 | 12 | def tagged_as(tag, tags = ActiveEndpoint.tags.definition) 13 | return [] unless tags.keys.include?(tag) 14 | 15 | time_operators = tags[tag] 16 | last_operator_index = time_operators.keys.length - 1 17 | 18 | query = '' 19 | time_operators.each_with_index do |(key, value), index| 20 | operator = METHODS[key] 21 | duration = (value.to_f / 1000).to_s 22 | and_operator = last_operator_index == index ? '' : ' AND ' 23 | query << 'duration ' + operator + ' ' + duration + ' ' + and_operator 24 | end 25 | 26 | where(query) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/active_endpoint/extentions/active_support.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Extentions 3 | module ActiveSupport 4 | class ::Numeric 5 | def days 6 | ::ActiveSupport::Duration.new(24.hours * self, [[:days, self]]) 7 | end 8 | alias :day :days 9 | 10 | def weeks 11 | ::ActiveSupport::Duration.new(7.days * self, [[:days, self * 7]]) 12 | end 13 | alias :week :weeks 14 | 15 | def fortnights 16 | ::ActiveSupport::Duration.new(2.weeks * self, [[:days, self * 14]]) 17 | end 18 | alias :fortnight :fortnights 19 | end 20 | 21 | class ::Integer 22 | def months 23 | ::ActiveSupport::Duration.new(30.days * self, [[:months, self]]) 24 | end 25 | alias :month :months 26 | 27 | def years 28 | ::ActiveSupport::Duration.new(365.25.days * self, [[:years, self]]) 29 | end 30 | alias :year :years 31 | end 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /lib/active_endpoint/logger.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class Logger 3 | class << self 4 | def info(caller, info) 5 | logger.info(message(caller, info)) 6 | end 7 | 8 | def debug(caller, message) 9 | logger.debug(message(caller, message)) 10 | end 11 | 12 | def error(caller, error) 13 | logger.error(message(caller, error)) 14 | end 15 | 16 | private 17 | 18 | def logger 19 | ::Rails.logger 20 | end 21 | 22 | def message(caller, message) 23 | "ActiveEndpoint::Logger [#{caller}] - #{message}" 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/active_endpoint/proxy.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class Proxy 3 | def initialize(matcher = ActiveEndpoint::Routes::Matcher.new) 4 | @matcher = matcher 5 | @created_at = Time.now 6 | end 7 | 8 | def track(env, &block) 9 | request = ActiveEndpoint::Request.new(env) 10 | 11 | ActiveEndpoint.logger.debug('ActiveEndpoint::Blacklist', ActiveEndpoint.blacklist) 12 | ActiveEndpoint.logger.debug('ActiveEndpoint::Constraints', ActiveEndpoint.constraints) 13 | 14 | if @matcher.whitelisted?(request) 15 | track_begin(request) 16 | status, headers, response = yield block 17 | track_end(response) 18 | [status, headers, response] 19 | else 20 | register(request) if @matcher.unregistred?(request) 21 | 22 | yield block 23 | end 24 | rescue => error 25 | @logger.error(self.class, error) 26 | 27 | yield block 28 | end 29 | 30 | private 31 | 32 | def track_begin(request) 33 | @request = request.probe 34 | end 35 | 36 | def track_end(response, finished_at = Time.now) 37 | @response = ActiveEndpoint::Response.new(response).probe 38 | @finished_at = finished_at 39 | 40 | probe = { 41 | created_at: @created_at, 42 | finished_at: @finished_at, 43 | request: @request, 44 | response: @response 45 | } 46 | 47 | ActiveSupport::Notifications.instrument('active_endpoint.tracked_probe', probe: probe) if @matcher.allow_account?(@request) 48 | end 49 | 50 | def register(request) 51 | unregistred = { 52 | created_at: @created_at, 53 | finished_at: @finished_at, 54 | request: request.probe 55 | } 56 | 57 | ActiveSupport::Notifications.instrument('active_endpoint.unregistred_probe', probe: unregistred) if @matcher.allow_register?(request) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/active_endpoint/rails/middleware.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Rails 3 | class Middleware 4 | def initialize(app) 5 | @app = app 6 | end 7 | 8 | def call(env) 9 | dup._call(env, ActiveEndpoint::Proxy.new) 10 | end 11 | 12 | private 13 | 14 | def _call(env, proxy) 15 | proxy.track(env) { @app.call(env) } 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/active_endpoint/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Rails 3 | class Railtie < ::Rails::Railtie 4 | initializer 'active_endpoint.configure_rails_initialization' do |app| 5 | ActiveSupport.on_load(:active_record) do 6 | ActiveRecord::Base.send(:extend, ::ActiveEndpoint::Extentions::ActiveRecord) 7 | end 8 | 9 | app.middleware.insert(0, ActiveEndpoint::Rails::Middleware) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/active_endpoint/request.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class Request < Rack::Request 3 | include RailsRoutable 4 | 5 | def probe 6 | { 7 | base_url: base_url, 8 | body: body, 9 | content_charset: content_charset, 10 | content_length: content_length, 11 | content_type: content_type, 12 | endpoint: endpoint, 13 | fullpath: fullpath, 14 | http_version: http_version, 15 | http_connection: http_connection, 16 | http_accept_encoding: http_accept_encoding, 17 | http_accept_language: http_accept_language, 18 | ip: ip, 19 | media_type: media_type, 20 | media_type_params: media_type_params, 21 | method: method, 22 | params: params, 23 | path: path, 24 | path_info: path_info, 25 | pattern: pattern, 26 | port: port, 27 | protocol: protocol, 28 | query_string: query_string, 29 | request_method: request_method, 30 | server_name: server_name, 31 | ssl: ssl?, 32 | url: url, 33 | xhr: xhr? 34 | } 35 | end 36 | 37 | private 38 | 39 | def method 40 | request_method.downcase.to_sym 41 | end 42 | 43 | def endpoint 44 | action = rails_endpoint(self) 45 | return unless action 46 | "#{action[:controller]}##{action[:action]}" 47 | end 48 | 49 | def http_accept_encoding 50 | get_header('HTTP_ACCEPT_ENCODING') 51 | end 52 | 53 | def http_accept_language 54 | get_header('HTTP_ACCEPT_LANGUAGE') 55 | end 56 | 57 | def http_connection 58 | get_header('HTTP_CONNECTION') 59 | end 60 | 61 | def http_version 62 | get_header('HTTP_VERSION') 63 | end 64 | 65 | def params 66 | rails_request_params(self) 67 | end 68 | 69 | def pattern 70 | rails_route_pattern(self) 71 | end 72 | 73 | def protocol 74 | get_header('SERVER_PROTOCOL') 75 | end 76 | 77 | def server_name 78 | get_header('SERVER_NAME') 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/active_endpoint/response.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class Response 3 | def initialize(response) 4 | @response = response 5 | end 6 | 7 | def probe 8 | { 9 | body: body 10 | } 11 | end 12 | 13 | private 14 | 15 | def body 16 | @response.join if @response.respond_to? :join 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/active_endpoint/routes/blacklist.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Routes 3 | class Blacklist < Momento 4 | def initialize 5 | super(Array) 6 | end 7 | 8 | private 9 | 10 | def add_endpoint(options) 11 | @endpoints << fetch_endpoint(options) 12 | end 13 | 14 | def add_resources(options) 15 | resources = fetch_resources(options) 16 | actions = fetch_actions(options) 17 | scope = fetch_scope(options) 18 | 19 | if actions.present? && actions.any? 20 | temp_actions = [] 21 | if resources.is_a?(Array) 22 | resources.each do |controller_name| 23 | actions.each { |action| temp_actions << "#{controller_name}##{action}" } 24 | end 25 | else 26 | actions.each { |action| temp_actions << "#{resources}##{action}" } 27 | end 28 | @actions += apply(scope, temp_actions) 29 | else 30 | temp_resources = [] 31 | if resources.is_a?(Array) 32 | resources.each { |resource| temp_resources << resource } 33 | else 34 | temp_resources << resources 35 | end 36 | @resources += apply(scope, temp_resources) 37 | end 38 | end 39 | 40 | def add_scopes(options) 41 | scope = fetch_scope(options) 42 | @scopes << scope unless @scopes.include?(scope) 43 | end 44 | 45 | def present_endpoint?(request) 46 | @endpoints.include?(request[:endpoint]) 47 | end 48 | 49 | def present_resource?(request) 50 | check_present(@resources, request) 51 | end 52 | 53 | def present_action?(request) 54 | @actions.include?(request[:endpoint]) 55 | end 56 | 57 | def present_scope?(request) 58 | check_present(@scopes, request) 59 | end 60 | 61 | def apply(scope, collection) 62 | return collection unless scope.present? 63 | collection.map { |subject| "#{scope}/#{subject}" } 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/active_endpoint/routes/cache/proxy.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Routes 3 | module Cache 4 | module Proxy 5 | class AdapterError < ::StandardError; end 6 | 7 | CLIENTS = { 8 | redis: 'RedisStoreProxy' 9 | }.freeze 10 | 11 | def self.build(adapter) 12 | unless CLIENTS.keys.include?(adapter) 13 | message "You try to use unsupported cache store adapter! #{adapter}\n" 14 | raise ActiveEndpoint::Routes::Cache::Proxy::AdapterError.new(message) 15 | end 16 | 17 | "ActiveEndpoint::Routes::Cache::Proxy::#{CLIENTS[adapter]}".constantize.new 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/active_endpoint/routes/cache/proxy/redis_store_proxy.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Routes 3 | module Cache 4 | module Proxy 5 | class RedisStoreProxy 6 | def initialize(cache_prefix = ActiveEndpoint.cache_prefix, store = ::Redis::Store.new) 7 | @prefix = cache_prefix 8 | @store = store 9 | end 10 | 11 | def read(unprefixed_key) 12 | @store.get("#{@prefix}:#{unprefixed_key}") 13 | end 14 | 15 | def write(unprefixed_key, value, expires_in = nil) 16 | store_key = store_key(unprefixed_key) 17 | 18 | if expires_in.present? 19 | @store.setex(store_key, expires_in, value) 20 | else 21 | @store.set(store_key, value) 22 | end 23 | 24 | true 25 | end 26 | 27 | def expires_in(unprefixed_key) 28 | time = @store.ttl(store_key(unprefixed_key)).to_i 29 | time == -1 || time == -2 ? 0 : time 30 | end 31 | 32 | private 33 | 34 | def store_key(unprefixed_key) 35 | "#{@prefix}:#{unprefixed_key}" 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/active_endpoint/routes/cache/store.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Routes 3 | module Cache 4 | class Store 5 | delegate :read, to: :@store 6 | delegate :write, to: :@store 7 | delegate :expires_in, to: :@store 8 | 9 | def initialize(cache_store_client = ActiveEndpoint.cache_store_client) 10 | @store = ActiveEndpoint::Routes::Cache::Proxy.build(cache_store_client) 11 | end 12 | 13 | def allow?(rule) 14 | rule_cache_allow?(rule) && storage_cache_allow?(rule) 15 | end 16 | 17 | def unregistred?(probe) 18 | path = probe[:path] 19 | read(path).present? ? false : write(path, :unregistred) 20 | end 21 | 22 | private 23 | 24 | def rule_cache_allow?(options) 25 | cache_allow?(:rule, options) 26 | end 27 | 28 | def storage_cache_allow?(options) 29 | cache_allow?(:storage, options) do |key, period| 30 | expired = { 31 | key: key, 32 | period: period 33 | } 34 | 35 | ActiveSupport::Notifications.instrument('active_endpoint.clean_expired', expired: expired) 36 | end 37 | end 38 | 39 | def cache_allow?(prefix, options, &block) 40 | 41 | key = "#{prefix}:#{options[:key]}" 42 | constraints = options[prefix] 43 | 44 | cache = read(key) 45 | limit = cache.nil? ? cache : cache.to_i 46 | period = expires_in(key) 47 | 48 | ActiveEndpoint.logger.debug('ActiveEndpoint::Cache::Store Prefix', prefix) 49 | ActiveEndpoint.logger.debug('ActiveEndpoint::Cache::Store Limit', limit) 50 | ActiveEndpoint.logger.debug('ActiveEndpoint::Cache::Store Period', period) 51 | 52 | limited = limit.present? && limit.zero? 53 | expired = period.zero? 54 | 55 | if limit.nil? && expired && block_given? 56 | yield(options[:key], constraints[:period]) 57 | end 58 | 59 | if limit.present? && !expired 60 | return false if limited 61 | write(key, limit - 1, period) 62 | else 63 | write(key, constraints[:limit] - 1, constraints[:period]) 64 | end 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/active_endpoint/routes/constraint_rule.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Routes 3 | class ConstraintRule 4 | include Optionable 5 | include Constraintable 6 | 7 | def initialize(request, constraints = ActiveEndpoint.constraints) 8 | @request = request 9 | @constraints = constraints 10 | end 11 | 12 | def rule 13 | { 14 | key: "#{prefix}:#{@request[:endpoint]}" 15 | }.merge(fetch_constraints) 16 | end 17 | 18 | private 19 | 20 | def prefix 21 | return :endpoints if @constraints.send(:present_endpoint?, @request) 22 | return :resources if @constraints.send(:present_resource?, @request) 23 | return :actions if @constraints.send(:present_action?, @request) 24 | return :scopes if @constraints.send(:present_scope?, @request) 25 | :defaults 26 | end 27 | 28 | def fetch_constraints 29 | if prefix == :defaults 30 | default_constraints 31 | else 32 | constraints = @constraints.public_send(prefix)[@request[:endpoint]] 33 | constraints.present? ? constraints : default_constraints 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/active_endpoint/routes/constraints.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Routes 3 | class Constraints < Momento 4 | include Constraintable 5 | 6 | def initialize 7 | super(Hash) 8 | end 9 | 10 | def add(options) 11 | super(options) do |options| 12 | if fetch_rule(options).nil? 13 | message = "Constraints can't have empty limit and period!\n" 14 | raise ActiveEndpoint::Routes::Constraints::Error.new(message) 15 | end 16 | end 17 | end 18 | 19 | private 20 | 21 | def present_endpoint?(request) 22 | @endpoints.keys.include?(request[:endpoint]) 23 | end 24 | 25 | def present_resource?(request) 26 | check_present(@resources.keys, request) 27 | end 28 | 29 | def present_action?(request) 30 | @actions.keys.include?(request[:endpoint]) 31 | end 32 | 33 | def present_scope?(request) 34 | check_present(@scopes.keys, request) 35 | end 36 | 37 | def add_endpoint(options) 38 | @endpoints[fetch_endpoint(options)] = constraints(options) 39 | end 40 | 41 | def add_resources(options) 42 | resources = fetch_resources(options) 43 | actions = fetch_actions(options) 44 | scope = fetch_scope(options) 45 | 46 | if actions.present? && actions.any? 47 | temp_actions = {} 48 | if resources.is_a?(Array) 49 | resources.each do |controller_name| 50 | actions.each { |action| temp_actions["#{controller_name}##{action}"] = constraints(options) } 51 | end 52 | else 53 | actions.each { |action| temp_actions["#{resources}##{action}"] = constraints(options) } 54 | end 55 | @actions = @actions.merge(apply(scope, temp_actions)) 56 | else 57 | temp_resources = {} 58 | if resources.is_a?(Array) 59 | resources.each { |resource| temp_resources[resource] = constraints(options) } 60 | else 61 | temp_resources[resources] = constraints(options) 62 | end 63 | @resources = @resources.merge(apply(scope, temp_resources)) 64 | end 65 | end 66 | 67 | def add_scopes(options) 68 | @scopes[fetch_scope(options)] = constraints(options) 69 | end 70 | 71 | def apply(scope, collection) 72 | return collection unless scope.present? 73 | 74 | collection.inject({}) do |hash, (key, value)| 75 | hash["#{scope}/#{key}"] = value 76 | hash 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/active_endpoint/routes/matcher.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Routes 3 | class Matcher 4 | include RailsRoutable 5 | 6 | def initialize(blacklist = ActiveEndpoint.blacklist, cache_store = ActiveEndpoint::Routes::Cache::Store.new) 7 | @blacklist = blacklist 8 | @cache_store = cache_store 9 | end 10 | 11 | def whitelisted?(request) 12 | trackable?(request) && rails_action?(request) 13 | end 14 | 15 | def blacklisted?(probe) 16 | @blacklist.include?(probe) 17 | end 18 | 19 | def unregistred?(request) 20 | trackable?(request) && !rails_action(request) 21 | end 22 | 23 | def assets?(request) 24 | request.path.start_with?('/assets') 25 | end 26 | 27 | def allow_account?(request) 28 | @constraint_rule = ActiveEndpoint::Routes::ConstraintRule.new(request).rule 29 | @cache_store.allow?(@constraint_rule) 30 | end 31 | 32 | def allow_register?(request) 33 | @cache_store.unregistred?(request.probe) 34 | end 35 | 36 | private 37 | 38 | def favicon?(request) 39 | request.path == '/favicon.ico' 40 | end 41 | 42 | def engine?(request) 43 | request.path.include?('active_endpoint') 44 | end 45 | 46 | def trackable?(request) 47 | !(engine?(request) || assets?(request) || favicon?(request) || blacklisted?(request.probe)) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/active_endpoint/routes/momento.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Routes 3 | class Momento 4 | include Configurable 5 | include Optionable 6 | 7 | class Error < ::StandardError; end 8 | 9 | attr_reader :endpoints, :resources, :actions, :scopes 10 | 11 | def initialize(structure_class) 12 | @structure = structure_class 13 | @endpoints = structure_class.new 14 | @resources = structure_class.new 15 | @actions = structure_class.new 16 | @scopes = structure_class.new 17 | end 18 | 19 | def include?(request) 20 | [ 21 | present_endpoint?(request), 22 | present_resource?(request), 23 | present_action?(request), 24 | present_scope?(request) 25 | ].any? 26 | end 27 | 28 | def add(**options, &block) 29 | yield(options) if block_given? 30 | 31 | add_endpoint(options) if fetch_endpoint(options).present? 32 | add_resources(options) if fetch_resources(options).present? 33 | add_scopes(options) if fetch_scope(options).present? 34 | end 35 | 36 | private 37 | 38 | def add_endpoint(_options) 39 | raise NotImplementedError 40 | end 41 | 42 | def add_resource(_options) 43 | raise NotImplementedError 44 | end 45 | 46 | def add_scopes(_options) 47 | raise NotImplementedError 48 | end 49 | 50 | def present_endpoint?(_request) 51 | raise NotImplementedError 52 | end 53 | 54 | def present_resource?(_request) 55 | raise NotImplementedError 56 | end 57 | 58 | def present_action?(_request) 59 | raise NotImplementedError 60 | end 61 | 62 | def present_scope?(_request) 63 | raise NotImplementedError 64 | end 65 | 66 | def apply(_scope, _collection) 67 | raise NotImplementedError 68 | end 69 | 70 | def check_present(collection, request) 71 | endpoint = request[:endpoint] 72 | collection.map { |subject| endpoint.present? && endpoint.start_with?(subject) }.any? 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/active_endpoint/storage.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class Storage 3 | STORING_FIELDS = %i[ 4 | body 5 | response 6 | started_at 7 | finished_at 8 | duration 9 | endpoint 10 | ip 11 | params 12 | path 13 | request_method 14 | url 15 | xhr 16 | ].freeze 17 | 18 | LOGGING_FIELDS = %i[ 19 | base_url 20 | content_charset 21 | content_length 22 | content_type 23 | fullpath 24 | http_version 25 | http_connection 26 | http_accept_encoding 27 | http_accept_language 28 | media_type 29 | media_type_params 30 | method 31 | path_info 32 | pattern 33 | port 34 | protocol 35 | server_name 36 | ssl 37 | ].freeze 38 | 39 | class << self 40 | private 41 | 42 | def notifier 43 | ActiveSupport::Notifications 44 | end 45 | 46 | def store!(params) 47 | handle_creation(ActiveEndpoint::Probe.new(params)) 48 | end 49 | 50 | def register!(params) 51 | handle_creation(ActiveEndpoint::UnregistredProbe.new(params.merge(endpoint: :unregistred))) 52 | end 53 | 54 | def clean!(endpoint, period) 55 | ActiveEndpoint::Probe.registred.probe(endpoint).taken_before(period).destroy_all 56 | end 57 | 58 | def handle_creation(probe) 59 | probe.save 60 | rescue => error 61 | ActiveEndpoint.logger.error('ActiveEndpoint::Probe', error) 62 | end 63 | 64 | def probe_params(transaction_id, probe) 65 | request = probe.delete(:request) 66 | request_body = request.delete(:body) 67 | 68 | response = probe.delete(:response) 69 | response_body = response.present? ? response[:body] : nil 70 | 71 | params = { 72 | uuid: transaction_id, 73 | response: response_body ? Base64.encode64(response_body) : '', 74 | started_at: probe[:created_at], 75 | finished_at: probe[:finished_at], 76 | duration: probe[:finished_at] ? (probe[:finished_at] - probe[:created_at]).second.round(3) : 0, 77 | body: request_body.is_a?(Puma::NullIO) ? '' : request_body 78 | }.merge(request) 79 | 80 | [params.dup.except(*LOGGING_FIELDS), params.dup.except(*STORING_FIELDS)] 81 | end 82 | end 83 | 84 | notifier.subscribe('active_endpoint.tracked_probe') do |_name, _start, _ending, id, payload| 85 | store_params, logging_params = probe_params(id, payload[:probe]) 86 | 87 | ActiveEndpoint.logger.info('ActiveEndpoint::Storage', logging_params) 88 | 89 | store!(store_params) 90 | end 91 | 92 | notifier.subscribe('active_endpoint.unregistred_probe') do |_name, _start, _ending, id, payload| 93 | store_params, logging_params = probe_params(id, payload[:probe]) 94 | 95 | ActiveEndpoint.logger.debug('ActiveEndpoint::Storage', store_params) 96 | ActiveEndpoint.logger.debug('ActiveEndpoint::Storage', logging_params) 97 | 98 | register!(store_params) 99 | end 100 | 101 | notifier.subscribe('active_endpoint.clean_expired') do |_name, _start, _ending, id, payload| 102 | key = payload[:expired][:key].split(':').last 103 | period = DateTime.now - (payload[:expired][:period] * ActiveEndpoint.storage_keep_periods).seconds 104 | 105 | ActiveEndpoint.logger.info('ActiveEndpoint::Storage', { key: key, period: period, uuid: id }) 106 | 107 | clean!(key, period) 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/active_endpoint/tags.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | class Tags 3 | include Configurable 4 | 5 | attr_reader :definition 6 | 7 | def initialize 8 | @definition = {} 9 | end 10 | 11 | def add(tag, condition) 12 | @definition[tag] = condition 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/active_endpoint/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | VERSION = '0.2.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/active_endpoint/install_generator.rb: -------------------------------------------------------------------------------- 1 | module ActiveEndpoint 2 | module Generators 3 | class InstallGenerator < ::Rails::Generators::Base 4 | include ::Rails::Generators::Migration 5 | 6 | source_root File.expand_path('../templates', __dir__) 7 | 8 | class << self 9 | def next_migration_number(_dirname) 10 | Time.current.strftime('%Y%m%d%H%M%S') 11 | end 12 | end 13 | 14 | puts '=> Creates a ActiveEndpoint initializer, migration and migrate data base for your application.' 15 | 16 | def copy_initializer_file 17 | puts '=> Copy initializer file to config/initializers/active_endpoint.rb' 18 | copy_file 'active_endpoint.rb', 'config/initializers/active_endpoint.rb' 19 | end 20 | 21 | def copy_migration_file 22 | puts '=> Create migration file in db/migrate/***_create_active_endpoint_probes.rb' 23 | migration_template 'migration.erb', 'db/migrate/create_active_endpoint_probes.rb', 24 | migration_version: migration_version 25 | end 26 | 27 | private 28 | 29 | def migration_version 30 | last_rails = ::Rails.version.start_with?('5') 31 | "[#{::Rails::VERSION::MAJOR}.#{::Rails::VERSION::MINOR}]" if last_rails 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/templates/active_endpoint.rb: -------------------------------------------------------------------------------- 1 | ActiveEndpoint.configure do |endpoint| 2 | endpoint.blacklist.configure do |list| 3 | # list.add(endpoint: 'web/welcome#index') 4 | # list.add(resources: 'web/users', actions: ['show']) 5 | # list.add(scope: 'web', resources: 'users', actions: ['show']) 6 | # list.add(scope: 'web') 7 | end 8 | 9 | endpoint.constraint_limit = 100 10 | endpoint.constraint_period = 1.minute 11 | 12 | endpoint.storage_limit = 1000 13 | endpoint.storage_period = 1.day 14 | endpoint.storage_keep_periods = 2 15 | 16 | endpoint.constraints.configure do |constraints| 17 | # constraints.add(endpoint: 'web/welcome#index', 18 | # rule: { limit: 100, period: 1.minute }, 19 | # storage: { limit: 1000, period: 1.week } 20 | # }) 21 | # constraints.add(scope: 'web', resources: 'users', rule: { limit: 100 }) 22 | # constraints.add(scope: 'web', resources: ['users'], 23 | # actions: [:show, :edit, :update], 24 | # rule: { limit: 10, period: 1.minute }) 25 | # constraints.add(scope: 'web', rule: { limit: 100, period: 1.minute }) 26 | end 27 | 28 | endpoint.tags.configure do |tags| 29 | # tags.add(:fast, { less_than: 250 }) 30 | # tags.add(:normal, { greater_than_or_equal_to: 250, less_than: 500 }) 31 | # tags.add(:slow, { greater_than_or_equal_to: 250, less_than: 500 }) 32 | # tags.add(:acceptable, { greater_than_or_equal_to: 500, less_than: 100 }) 33 | # tags.add(:need_optimization, { greater_than_or_equal_to: 1000 }) 34 | end 35 | 36 | define_setting :logger, ActiveEndpoint::Logger 37 | end 38 | -------------------------------------------------------------------------------- /lib/generators/templates/migration.erb: -------------------------------------------------------------------------------- 1 | class CreateActiveEndpointProbes < ActiveRecord::Migration<%= migration_version %> 2 | def change 3 | create_table :active_endpoint_probes do |t| 4 | t.string :type, default: '' 5 | 6 | t.string :uuid, null: false 7 | t.string :endpoint, null: false, index: true 8 | t.string :path, null: false 9 | 10 | t.string :query_string, null: false 11 | t.string :request_method, null: false 12 | t.string :ip, null: false 13 | t.string :url, null: false 14 | 15 | t.boolean :xhr 16 | 17 | t.datetime :started_at, null: false 18 | t.datetime :finished_at 19 | 20 | t.float :duration, nill: false 21 | 22 | t.json :params, default: '{}' 23 | 24 | t.text :response, default: '' 25 | t.text :body, default: '' 26 | 27 | t.timestamps 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /spec/active_endpoint/concerns/configurabe_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Configurable do 4 | module Test 5 | extend Configurable 6 | end 7 | 8 | describe '#configure' do 9 | before do 10 | Test.configure do |test| 11 | test.setting = :not_default 12 | end 13 | end 14 | 15 | subject { Test.setting } 16 | 17 | it { is_expected.to eql(:not_default) } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/active_endpoint/concerns/constrainable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Constraintable do 4 | let(:constraint_limit) { 22 } 5 | let(:constraint_period) { 22.minutes } 6 | let(:storage_limit) { 2222 } 7 | let(:storage_period) { 2.week } 8 | let(:rule) { { limit: constraint_limit, period: constraint_period } } 9 | let(:storage) { { limit: storage_limit, period: storage_period } } 10 | let(:constraints) { { rule: rule, storage: storage } } 11 | 12 | before do 13 | module Test 14 | extend Optionable 15 | extend Constraintable 16 | end 17 | 18 | ActiveEndpoint.configure do |config| 19 | config.constraint_limit = constraint_limit 20 | config.constraint_period = constraint_period 21 | config.storage_limit = storage_limit 22 | config.storage_period = storage_period 23 | end 24 | end 25 | 26 | describe '#constraints' do 27 | context 'default' do 28 | context 'rule constraints' do 29 | it 'containts default rule' do 30 | expect(Test.default_rule_constraints).to eq(rule) 31 | end 32 | 33 | it 'containts default storage' do 34 | expect(Test.default_storage_constraints).to eq(storage) 35 | end 36 | end 37 | end 38 | 39 | context 'part of defaults' do 40 | context 'rule' do 41 | it 'constraints rule' do 42 | constraints = Test.rule_constraints(rule: { limit: 300 }) 43 | result = { limit: 300, period: constraint_period } 44 | expect(constraints).to eq(result) 45 | end 46 | end 47 | 48 | context 'storage' do 49 | it 'constrainrts storage' do 50 | constraints = Test.storage_constraints(storage: { period: 2.minutes }) 51 | result = { limit: storage_limit, period: 2.minutes } 52 | expect(constraints).to eq(result) 53 | end 54 | end 55 | end 56 | 57 | context 'storage constraints' do 58 | subject { Test.constraints(constraints) } 59 | it { is_expected.to eq(constraints) } 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/active_endpoint/concerns/optionable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Optionable do 4 | let(:empty_options) { {} } 5 | 6 | %i[ 7 | endpoint 8 | actions 9 | resources 10 | scope 11 | limit 12 | period 13 | storage 14 | rule 15 | ].each do |method| 16 | describe "#fetch_#{method}" do 17 | let(:object) { Object.new } 18 | let(:options) { { method => true } } 19 | 20 | before do 21 | object.extend(described_class) 22 | end 23 | 24 | context 'if exists option' do 25 | subject { object.send("fetch_#{method}".to_sym, options) } 26 | it { is_expected.to be_truthy } 27 | end 28 | 29 | context 'if option empty' do 30 | subject { object.send("fetch_#{method}".to_sym, empty_options) } 31 | it { is_expected.to be_falsey } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/active_endpoint/concerns/rails_routable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe RailsRoutable do 4 | let(:request) { {} } 5 | let(:action) { { controller: 'web/welcome', action: 'index' } } 6 | 7 | before do 8 | module Test 9 | extend RailsRoutable 10 | end 11 | 12 | allow(Test).to receive(:rails_action).and_return(action) 13 | end 14 | 15 | describe '#rails_action?' do 16 | it { is_expected.to be_truthy } 17 | end 18 | 19 | describe '#rails_request_params' do 20 | subject { Test.rails_request_params(request) } 21 | it { is_expected.to eql({}) } 22 | end 23 | 24 | describe '#rails_endpoint' do 25 | subject { Test.rails_endpoint(request) } 26 | it { is_expected.to eql(action) } 27 | end 28 | 29 | describe '#rails_action' do 30 | end 31 | 32 | describe '#rails_route_pattern' do 33 | end 34 | 35 | describe '#rails_routes' do 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/active_endpoint/concerns/settingable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Settingable do 4 | module Test 5 | extend Settingable 6 | 7 | define_setting :setting, :default 8 | end 9 | 10 | before do 11 | Test.configure do |test| 12 | test.setting = :not_default 13 | end 14 | end 15 | 16 | describe '#define_setting' do 17 | subject { Test.setting } 18 | it { is_expected.to eql(:not_default) } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/active_endpoint/extentions/active_record_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Extentions::ActiveRecord do 4 | describe '#tagged_as' do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/active_endpoint/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Logger do 4 | describe '#info' do 5 | end 6 | 7 | describe '#debug' do 8 | end 9 | 10 | describe '#error' do 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/active_endpoint/proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Proxy do 4 | describe '#track' do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/active_endpoint/request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Request do 4 | describe '#probe' do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/active_endpoint/response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Response do 4 | describe '#body' do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/active_endpoint/routes/blacklist_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Routes::Blacklist do 4 | before do 5 | ActiveEndpoint.configure do |config| 6 | config.blacklist.configure do |list| 7 | list.add(endpoint: 'test#index') 8 | list.add(resources: 'test_resource') 9 | list.add(resources: 'test_actions', actions: ['show']) 10 | list.add(scope: 'web') 11 | end 12 | end 13 | end 14 | 15 | describe '#add' do 16 | context 'endpoint' do 17 | subject { ActiveEndpoint.blacklist.endpoints } 18 | it { is_expected.to include('test#index') } 19 | it { is_expected.not_to include('test_resources#show') } 20 | end 21 | 22 | context 'resource' do 23 | subject { ActiveEndpoint.blacklist.resources } 24 | it { is_expected.to include('test_resource') } 25 | it { is_expected.not_to include('test#index') } 26 | end 27 | 28 | context 'action' do 29 | subject { ActiveEndpoint.blacklist.actions } 30 | it { is_expected.to include('test_actions#show') } 31 | it { is_expected.not_to include('test_actions#index') } 32 | end 33 | 34 | context 'scope' do 35 | subject { ActiveEndpoint.blacklist.scopes } 36 | it { is_expected.to include('web') } 37 | it { is_expected.not_to include('test') } 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/active_endpoint/routes/cache/proxy/redis_store_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Routes::Cache::Proxy::RedisStoreProxy do 4 | describe '#read' do 5 | end 6 | 7 | describe '#write' do 8 | end 9 | 10 | describe '#expires_in' do 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/active_endpoint/routes/cache/proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Routes::Cache::Proxy do 4 | describe '#build' do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/active_endpoint/routes/cache/store_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Routes::Cache::Store do 4 | describe '#allow?' do 5 | end 6 | 7 | describe '#unregistred?' do 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/active_endpoint/routes/constraint_rule_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Routes::ConstraintRule do 4 | let(:request) do 5 | { 6 | endpoint: 'test#index' 7 | } 8 | end 9 | 10 | let(:rule) do 11 | { 12 | key: 'defaults:test#index', 13 | rule: { limit: ActiveEndpoint.constraint_limit, period: ActiveEndpoint.constraint_period }, 14 | storage: { limit: ActiveEndpoint.storage_limit, period: ActiveEndpoint.storage_period } 15 | } 16 | end 17 | 18 | describe '#rule' do 19 | describe ':key' do 20 | subject { ActiveEndpoint::Routes::ConstraintRule.new(request).rule[:key] } 21 | it { is_expected.to eql(rule[:key]) } 22 | end 23 | 24 | describe ':rule' do 25 | subject { ActiveEndpoint::Routes::ConstraintRule.new(request).rule[:rule] } 26 | it { is_expected.to eq(rule[:rule]) } 27 | end 28 | 29 | describe ':storage' do 30 | subject { ActiveEndpoint::Routes::ConstraintRule.new(request).rule[:storage] } 31 | it { is_expected.to eq(rule[:storage]) } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/active_endpoint/routes/constraints_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Routes::Constraints do 4 | let(:rule) { { limit: 1, period: 2.seconds } } 5 | 6 | before do 7 | ActiveEndpoint.configure do |config| 8 | config.constraints.configure do |list| 9 | list.add(endpoint: 'test#index', rule: rule) 10 | list.add(resources: 'test_resource', rule: rule) 11 | list.add(resources: 'test_actions', actions: ['show'], rule: rule) 12 | list.add(scope: 'web', rule: rule) 13 | end 14 | end 15 | end 16 | 17 | describe '#add' do 18 | context 'endpoint' do 19 | subject { ActiveEndpoint.constraints.endpoints.keys } 20 | it { is_expected.to include('test#index') } 21 | it { is_expected.not_to include('test_resources#show') } 22 | end 23 | 24 | context 'resource' do 25 | subject { ActiveEndpoint.constraints.resources.keys } 26 | it { is_expected.to include('test_resource') } 27 | it { is_expected.not_to include('test#index') } 28 | end 29 | 30 | context 'action' do 31 | subject { ActiveEndpoint.constraints.actions.keys } 32 | it { is_expected.to include('test_actions#show') } 33 | it { is_expected.not_to include('test_actions#index') } 34 | end 35 | 36 | context 'scope' do 37 | subject { ActiveEndpoint.constraints.scopes.keys } 38 | it { is_expected.to include('web') } 39 | it { is_expected.not_to include('test') } 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/active_endpoint/routes/matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Routes::Matcher do 4 | let(:request) { double('Request', probe: { endpoint: 'welcome#index' }) } 5 | let(:cache_store) { double('Cache::Store') } 6 | let(:constraint_rule) { double('ConstraintRule', rule: {}) } 7 | let(:matcher) { described_class.new } 8 | 9 | before do 10 | allow(ActiveEndpoint::Routes::ConstraintRule).to receive(:new).and_return(constraint_rule) 11 | allow(ActiveEndpoint::Routes::Cache::Store).to receive(:new).and_return(cache_store) 12 | allow(cache_store).to receive(:allow?).and_return(true) 13 | allow(cache_store).to receive(:unregistred?).and_return(true) 14 | allow(request).to receive(:path).and_return('/') 15 | end 16 | 17 | describe '#whitelisted?' do 18 | subject { matcher.whitelisted?(request) } 19 | xit { is_expected.to be_falsey } 20 | end 21 | 22 | describe '#blacklisted' do 23 | let(:probe) { { endpoint: 'welcome#index' } } 24 | context 'ignored' do 25 | subject { matcher.blacklisted?(probe) } 26 | it { is_expected.to be_falsey } 27 | end 28 | 29 | context 'not ignored' do 30 | subject { matcher.blacklisted?(probe) } 31 | it { is_expected.to be_falsey } 32 | end 33 | end 34 | 35 | describe '#unregistred?' do 36 | subject { matcher.unregistred?(request) } 37 | xit { is_expected.to be_falsey } 38 | end 39 | 40 | describe '#assets?' do 41 | let(:probe) { double('Probe', path: '/assets') } 42 | subject { matcher.assets?(probe) } 43 | it { is_expected.to be_truthy } 44 | end 45 | 46 | describe '#allow_account?' do 47 | subject { matcher.allow_account?(request) } 48 | it { is_expected.to be_truthy } 49 | end 50 | 51 | describe '#allow_register?' do 52 | subject { matcher.allow_register?(request) } 53 | it { is_expected.to be_truthy } 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/active_endpoint/routes/momento_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Routes::Momento do 4 | describe '#initialize' do 5 | subject { described_class.new(Array).scopes } 6 | it { is_expected.to eql([]) } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/active_endpoint/storage_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Storage do 4 | describe 'creates probe' do 5 | end 6 | 7 | describe 'creates unregistred probe' do 8 | end 9 | 10 | describe 'removes expired probes' do 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/active_endpoint/tags_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ActiveEndpoint::Tags do 4 | describe '#add' do 5 | let(:tag_name) { :tag_name } 6 | let(:definition) { :definition } 7 | let(:tags) { ActiveEndpoint::Tags.new } 8 | 9 | before do 10 | tags.configure do |tag| 11 | tag.add(tag_name, definition) 12 | end 13 | end 14 | 15 | subject { tags.definition[tag_name] } 16 | 17 | it { is_expected.to eql(definition) } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'active_endpoint' 3 | 4 | RSpec.configure do |config| 5 | config.example_status_persistence_file_path = '.rspec_status' 6 | 7 | config.expect_with :rspec do |c| 8 | c.syntax = :expect 9 | end 10 | end 11 | --------------------------------------------------------------------------------