treblle 1.0.3 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33af7f59af8142ab9221eeae5e96157831ebd85f103d282bbfe94527f3430a50
4
- data.tar.gz: 46a242aba5acb59cc02eb83b7ddfecbdcafdaccf6b096a1d3130d1f5176b0898
3
+ metadata.gz: f42bc2d08d4eb451f07f8924e5ba73719b4c2e92c5b10de40ee7b28565750167
4
+ data.tar.gz: 16576e8b2c202f02c6d484ace4aa8fce5e1125b7feddd5f203953fc2238d5850
5
5
  SHA512:
6
- metadata.gz: c4c40f9be2cdf09ef85ba0c2a9b8fc5ff8da22fd96356c8154602d1d48003ccb47a126f35cd27abf44b865d0299d6c6b8a25f7507d836fa38422156d7034f70a
7
- data.tar.gz: 2546afe677f060122e034453992bf565d6008dd62fff234bf48aef93ac6c6251e34e4879baf1464a8e9669aeeba5f04eba77ceeeb4aeb4e28c1f26e04526795d
6
+ metadata.gz: 4807e68c43c329e0c770a30194724de8f49a231d171301ec6463d2894ccfeb4bc659ec72cc7ce077ac74e2a334c37def9a0fcbc70a6e91266d9b61e119236ae8
7
+ data.tar.gz: 6044a7c79673530ca87a84a5367e5dc102c2011e5ada7fd66e5aa7f4f6811e3c12606ee86e0dc15fc56df184655d9843993ebacbeb51a354eba1e87f8d277269
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (C) 2024 Treblle
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
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell 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
13
+ all 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
18
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ <div align="center">
2
+ <img src="https://treblle-github.s3.us-east-1.amazonaws.com/github-header.jpg"/>
3
+ </div>
4
+ <div align="center">
5
+
6
+ # Treblle
7
+
8
+ <a href="https://docs.treblle.com/en/integrations" target="_blank">Integrations</a>
9
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
10
+ <a href="http://treblle.com/" target="_blank">Website</a>
11
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
12
+ <a href="https://docs.treblle.com" target="_blank">Docs</a>
13
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
14
+ <a href="https://blog.treblle.com" target="_blank">Blog</a>
15
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
16
+ <a href="https://twitter.com/treblleapi" target="_blank">Twitter</a>
17
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
18
+ <a href="https://treblle.com/chat" target="_blank">Discord</a>
19
+ <br />
20
+
21
+ <hr />
22
+ </div>
23
+
24
+ Treblle is a lightweight SDK that helps Engineering and Product teams build, ship & maintain REST based APIs faster.
25
+
26
+ ## Features
27
+
28
+ <div align="center">
29
+ <br />
30
+ <img src="https://treblle-github.s3.amazonaws.com/features.png"/>
31
+ <br />
32
+ <br />
33
+ </div>
34
+
35
+ - [API Monitoring & Observability](https://www.treblle.com/features/api-monitoring-observability)
36
+ - [Auto-generated API Docs](https://www.treblle.com/features/auto-generated-api-docs)
37
+ - [API analytics](https://www.treblle.com/features/api-analytics)
38
+ - [Treblle API Score](https://www.treblle.com/features/api-quality-score)
39
+ - [API Lifecycle Collaboration](https://www.treblle.com/features/api-lifecycle)
40
+ - [Native Treblle Apps](https://www.treblle.com/features/native-apps)
41
+
42
+
43
+ ## How Treblle Works
44
+ Once you’ve integrated a Treblle SDK in your codebase, this SDK will send requests and response data to your Treblle Dashboard.
45
+
46
+ In your Treblle Dashboard you get to see real-time requests to your API, auto-generated API docs, API analytics like how fast the response was for an endpoint, the load size of the response, etc.
47
+
48
+ Treblle also uses the requests sent to your Dashboard to calculate your API score which is a quality score that’s calculated based on the performance, quality, and security best practices for your API.
49
+
50
+ > Visit [https://docs.treblle.com](http://docs.treblle.com) for the complete documentation.
51
+
52
+ ## Security
53
+
54
+ ### Masking fields
55
+ Masking fields ensure certain sensitive data are removed before being sent to Treblle.
56
+
57
+ To make sure masking is done before any data leaves your server [we built it into all our SDKs](https://docs.treblle.com/en/security/masked-fields#fields-masked-by-default).
58
+
59
+ This means data masking is super fast and happens on a programming level before the API request is sent to Treblle. You can [customize](https://docs.treblle.com/en/security/masked-fields#custom-masked-fields) exactly which fields are masked when you’re integrating the SDK.
60
+
61
+ > Visit the [Masked fields](https://docs.treblle.com/en/security/masked-fields) section of the [docs](https://docs.sailscasts.com) for the complete documentation.
62
+
63
+
64
+ ## Get Started
65
+
66
+ 1. Sign in to [Treblle](https://app.treblle.com).
67
+ 2. [Create a Treblle project](https://docs.treblle.com/en/dashboard/projects#creating-a-project).
68
+ 3. [Setup the SDK](#install-the-SDK) for your platform.
69
+
70
+ #### SDK Requirements
71
+
72
+ ```ruby
73
+ gem "rails", ">= 6.1"
74
+ ```
75
+
76
+ ### Install the SDK
77
+
78
+ Add the gem to your Gemfile:
79
+
80
+ ```ruby
81
+ # Gemfile
82
+ gem "treblle", "~> 2.0"
83
+ ```
84
+ Configure Treblle:
85
+
86
+ ```ruby
87
+ # config/initializers/treblle.rb
88
+
89
+ Treblle.configure do |config|
90
+ config.api_key = ENV.fetch('TREBLLE_API_KEY')
91
+ config.project_id = ENV.fetch('TREBLLE_PROJECT_ID')
92
+ config.enabled_environments = %w[staging development]
93
+ config.sensitive_attrs = %w[top_secret_custom_attribute password_digest]
94
+ config.restricted_endpoints = %w[/api/users /api/v2/*]
95
+ config.whitelisted_endpoints = %w[/api/]
96
+ end
97
+ ```
98
+
99
+ - `config.api_key` (required)
100
+ - `config.project_id` (required)
101
+ - `config.enabled_environments` (required) - enables monitoring on specified environments
102
+ - `config.sensitive_attrs` (optional) - mask additional attributes
103
+ - `config.restricted_endpoints` (optional) - routes that you don't want to monitor
104
+ - `config.whitelisted_endpoints` (optional) - routes that you want to monitor, by default `/api/`
105
+
106
+ > See the [docs](https://docs.treblle.com/en/integrations/ruby) for this SDK to learn more.
107
+
108
+ ## Available SDKs
109
+
110
+ Treblle provides [open-source SDKs](https://docs.treblle.com/en/integrations) that let you seamlessly integrate Treblle with your REST-based APIs.
111
+
112
+ - [`treblle-laravel`](https://github.com/Treblle/treblle-laravel): SDK for Laravel
113
+ - [`treblle-php`](https://github.com/Treblle/treblle-php): SDK for PHP
114
+ - [`treblle-symfony`](https://github.com/Treblle/treblle-symfony): SDK for Symfony
115
+ - [`treblle-lumen`](https://github.com/Treblle/treblle-lumen): SDK for Lumen
116
+ - [`treblle-sails`](https://github.com/Treblle/treblle-sails): SDK for Sails
117
+ - [`treblle-adonisjs`](https://github.com/Treblle/treblle-adonisjs): SDK for AdonisJS
118
+ - [`treblle-fastify`](https://github.com/Treblle/treblle-fastify): SDK for Fastify
119
+ - [`treblle-directus`](https://github.com/Treblle/treblle-directus): SDK for Directus
120
+ - [`treblle-strapi`](https://github.com/Treblle/treblle-strapi): SDK for Strapi
121
+ - [`treblle-express`](https://github.com/Treblle/treblle-express): SDK for Express
122
+ - [`treblle-koa`](https://github.com/Treblle/treblle-koa): SDK for Koa
123
+ - [`treblle-go`](https://github.com/Treblle/treblle-go): SDK for Go
124
+ - [`treblle-ruby`](https://github.com/Treblle/treblle-ruby): SDK for Ruby on Rails
125
+ - [`treblle-python`](https://github.com/Treblle/treblle-python): SDK for Python/Django
126
+
127
+ > See the [docs](https://docs.treblle.com/en/integrations) for more on SDKs and Integrations.
128
+
129
+ ## Other Packages
130
+
131
+ Besides the SDKs, we also provide helpers and configuration used for SDK
132
+ development. If you're thinking about contributing to or creating a SDK, have a look at the resources
133
+ below:
134
+
135
+ - [`treblle-utils`](https://github.com/Treblle/treblle-utils): A set of helpers and
136
+ utility functions useful for the JavaScript SDKs.
137
+ - [`php-utils`](https://github.com/Treblle/php-utils): A set of helpers and
138
+ utility functions useful for the PHP SDKs.
139
+
140
+ ## Community 💙
141
+
142
+ First and foremost: **Star and watch this repository** to stay up-to-date.
143
+
144
+ Also, follow our [Blog](https://blog.treblle.com), and on [Twitter](https://twitter.com/treblleapi).
145
+
146
+ You can chat with the team and other members on [Discord](https://treblle.com/chat) and follow our tutorials and other video material at [YouTube](https://youtube.com/@treblle).
147
+
148
+ [![Treblle Discord](https://img.shields.io/badge/Treblle%20Discord-Join%20our%20Discord-F3F5FC?labelColor=7289DA&style=for-the-badge&logo=discord&logoColor=F3F5FC&link=https://treblle.com/chat)](https://treblle.com/chat)
149
+
150
+ [![Treblle YouTube](https://img.shields.io/badge/Treblle%20YouTube-Subscribe%20on%20YouTube-F3F5FC?labelColor=c4302b&style=for-the-badge&logo=YouTube&logoColor=F3F5FC&link=https://youtube.com/@treblle)](https://youtube.com/@treblle)
151
+
152
+ [![Treblle on Twitter](https://img.shields.io/badge/Treblle%20on%20Twitter-Follow%20Us-F3F5FC?labelColor=1DA1F2&style=for-the-badge&logo=Twitter&logoColor=F3F5FC&link=https://twitter.com/treblleapi)](https://twitter.com/treblleapi)
153
+
154
+ ### How to contribute
155
+
156
+ Here are some ways of contributing to making Treblle better:
157
+
158
+ - **[Try out Treblle](https://docs.treblle.com/en/introduction#getting-started)**, and let us know ways to make Treblle better for you. Let us know here on [Discord](https://treblle.com/chat).
159
+ - Join our [Discord](https://treblle.com/chat) and connect with other members to share and learn from.
160
+ - Send a pull request to any of our [open source repositories](https://github.com/Treblle) on Github. Check the contribution guide on the repo you want to contribute to for more details about how to contribute. We're looking forward to your contribution!
161
+
162
+ ### Contributors
163
+ <!-- Replace link with the link of the SDK contributors-->
164
+ <a href="https://github.com/Treblle/treblle-ruby/graphs/contributors">
165
+ <p align="center">
166
+ <img src="https://contrib.rocks/image?repo=Treblle/treblle-ruby" alt="A table of avatars from the project's contributors" />
167
+ </p>
168
+ </a>
data/SECURITY.md ADDED
@@ -0,0 +1,12 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ The current release series and the next most recent one (by major-minor version) will receive patches and new versions in case of a security issue.
6
+
7
+ ### Unsupported Release Series
8
+
9
+ When a release series is no longer supported, it’s your own responsibility to deal with bugs and security issues. If you are not comfortable maintaining your own versions, you should upgrade to a supported version.
10
+
11
+ ## Reporting a security issue
12
+ Contact the Treblle support at treblle.com.
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/configuration_errors'
4
+
5
+ module Treblle
6
+ class Configuration
7
+ DEFAULT_SENSITIVE_ATTRS = %w[
8
+ card_number
9
+ cardNumber
10
+ cc
11
+ ccv
12
+ credit_score
13
+ creditScore
14
+ password
15
+ password_confirmation
16
+ passwordConfirmation
17
+ pwd
18
+ secretn
19
+ ssn
20
+ ].freeze
21
+
22
+ attr_accessor :restricted_endpoints, :whitelisted_endpoints,
23
+ :api_key, :project_id, :app_version
24
+ attr_reader :sensitive_attrs, :enabled_environments
25
+
26
+ def initialize
27
+ @restricted_endpoints = []
28
+ @whitelisted_endpoints = '/api/'
29
+ @app_version = nil
30
+ @sensitive_attrs = DEFAULT_SENSITIVE_ATTRS
31
+ end
32
+
33
+ def monitoring_enabled?(request_url)
34
+ whitelisted_endpoint?(request_url) && !restricted_endpoint?(request_url)
35
+ end
36
+
37
+ def sensitive_attrs=(attributes)
38
+ @sensitive_attrs = attributes ? DEFAULT_SENSITIVE_ATTRS + attributes : DEFAULT_SENSITIVE_ATTRS
39
+ end
40
+
41
+ def enabled_environments=(value)
42
+ @enabled_environments = Array(value)
43
+ end
44
+
45
+ def enabled_environment?
46
+ return false if enabled_environments.empty?
47
+
48
+ enabled_environments.include?(environment)
49
+ end
50
+
51
+ def validate_credentials!
52
+ raise Errors::MissingApiKeyError if api_key.to_s.empty?
53
+ raise Errors::MissingProjectIdError if project_id.to_s.empty?
54
+ end
55
+
56
+ private
57
+
58
+ def environment
59
+ @environment || ::Rails.env
60
+ end
61
+
62
+ def whitelisted_endpoint?(request_url)
63
+ Array(whitelisted_endpoints).any? do |endpoint|
64
+ request_url.start_with?(endpoint)
65
+ end
66
+ end
67
+
68
+ def restricted_endpoint?(request_url)
69
+ restricted_endpoints.any? do |endpoint|
70
+ pattern = Regexp.escape(endpoint).gsub('\*', '.*')
71
+ Regexp.new("^#{pattern}$") =~ request_url
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'securerandom'
6
+ require 'treblle/logging'
7
+
8
+ module Treblle
9
+ class Dispatcher
10
+ include Logging
11
+
12
+ TREBLLE_URIS = %w[
13
+ https://rocknrolla.treblle.com
14
+ https://punisher.treblle.com
15
+ https://sicario.treblle.com
16
+ ].freeze
17
+
18
+ def initialize(payload:, configuration: Treblle.configuration)
19
+ @payload = payload
20
+ @uri = get_uri
21
+ @configuration = configuration
22
+ end
23
+
24
+ def call
25
+ send_payload_to_treblle
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :payload, :uri, :configuration
31
+
32
+ def get_uri
33
+ URI(TREBLLE_URIS.sample)
34
+ end
35
+
36
+ def send_payload_to_treblle
37
+ Thread.new do
38
+ response = make_http_request
39
+
40
+ if response.code.to_i >= 400
41
+ log_error(response.body)
42
+ else
43
+ log_success(response.body)
44
+ end
45
+ rescue StandardError => e
46
+ log_error(e.message)
47
+ end
48
+ end
49
+
50
+ def make_http_request
51
+ http = Net::HTTP.new(uri.hostname, uri.port)
52
+ http.use_ssl = true
53
+ http.read_timeout = 2
54
+ http.open_timeout = 2
55
+
56
+ http.start do |http_instance|
57
+ http_instance.request(build_request)
58
+ end
59
+ end
60
+
61
+ def build_request
62
+ req = Net::HTTP::Post.new(uri)
63
+ req['Content-Type'] = 'application/json'
64
+ req['x-api-key'] = configuration.api_key
65
+ req['x-treblle-trace-id'] = SecureRandom.uuid
66
+ req.body = payload
67
+ req
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,21 @@
1
+ module Treblle
2
+ module Errors
3
+ class ConfigurationError < StandardError
4
+ def initialize(msg = "Configuration error")
5
+ super(msg)
6
+ end
7
+ end
8
+
9
+ class MissingApiKeyError < ConfigurationError
10
+ def initialize(msg = "API key is missing")
11
+ super(msg)
12
+ end
13
+ end
14
+
15
+ class MissingProjectIdError < ConfigurationError
16
+ def initialize(msg = "Project ID is missing")
17
+ super(msg)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'treblle/version'
4
+ require 'treblle/utils/hash_sanitizer'
5
+
6
+ module Treblle
7
+ class GeneratePayload
8
+ SDK_LANG = 'ruby'
9
+ TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
10
+
11
+ def initialize(request:, response:, started_at:, configuration: Treblle.configuration)
12
+ @request = request
13
+ @response = response
14
+ @started_at = started_at
15
+ @configuration = configuration
16
+ end
17
+
18
+ def call
19
+ payload.to_json
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :request, :response, :started_at, :configuration
25
+
26
+ def sanitize(body)
27
+ Utils::HashSanitizer.sanitize(body, configuration.sensitive_attrs)
28
+ end
29
+
30
+ def timestamp
31
+ started_at.strftime(TIME_FORMAT)
32
+ end
33
+
34
+ def load_time
35
+ Time.now - started_at
36
+ end
37
+
38
+ def payload
39
+ {
40
+ api_key: configuration.api_key,
41
+ project_id: configuration.project_id,
42
+ version: Treblle::API_VERSION,
43
+ sdk: SDK_LANG,
44
+ data: {
45
+ server: {
46
+ ip: request.server.remote_addr,
47
+ timezone: request.server.timezone,
48
+ software: request.server.software,
49
+ signature: '',
50
+ protocol: request.server.protocol,
51
+ os: {
52
+ name: request.server.os_name,
53
+ release: '',
54
+ architecture: request.server.os_architecture
55
+ }
56
+ },
57
+ language: {
58
+ name: SDK_LANG,
59
+ version: RUBY_VERSION
60
+ },
61
+ request: {
62
+ timestamp: timestamp,
63
+ ip: request.client.ip,
64
+ url: request.client.url,
65
+ user_agent: request.client.user_agent,
66
+ method: request.method,
67
+ headers: request.headers,
68
+ body: sanitize(request.body)
69
+ },
70
+ response: {
71
+ headers: response.headers,
72
+ code: response.status,
73
+ size: response.size,
74
+ load_time: load_time,
75
+ body: response.body,
76
+ errors: errors
77
+ }
78
+ }
79
+ }
80
+ end
81
+
82
+ def errors
83
+ return [] if response.exception.nil?
84
+
85
+ [{
86
+ source: 'onError',
87
+ type: response.exception.type,
88
+ message: response.exception.message,
89
+ file: response.exception.file_path,
90
+ line: response.exception.line_number
91
+ }]
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Treblle
6
+ module Logging
7
+ def logger
8
+ @logger ||= Logger.new($stdout)
9
+ end
10
+
11
+ def log_error(message)
12
+ logger.error("Treblle monitoring failed: #{message}")
13
+ end
14
+
15
+ def log_success(message)
16
+ logger.info("Successfully sent to Treblle: #{message}")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'treblle/dispatcher'
4
+ require 'treblle/request_builder'
5
+ require 'treblle/response_builder'
6
+ require 'treblle/generate_payload'
7
+ require 'treblle/logging'
8
+ require 'treblle'
9
+
10
+ module Treblle
11
+ class Middleware
12
+ include Logging
13
+
14
+ def initialize(app, configuration: Treblle.configuration)
15
+ @app = app
16
+ @configuration = configuration
17
+ end
18
+
19
+ def call(env)
20
+ if should_monitor?(env)
21
+ call_with_treblle_monitoring(env)
22
+ else
23
+ @app.call(env)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :configuration
30
+
31
+ def call_with_treblle_monitoring(env)
32
+ started_at = Time.now
33
+
34
+ response = @app.call(env)
35
+ handle_monitoring(env, response, started_at)
36
+
37
+ response
38
+ end
39
+
40
+ def handle_monitoring(env, rack_response, started_at)
41
+ request = RequestBuilder.new(env).build
42
+ response = ResponseBuilder.new(rack_response).build
43
+ payload = GeneratePayload.new(request: request, response: response, started_at: started_at).call
44
+
45
+ Dispatcher.new(payload: payload).call
46
+ rescue StandardError => e
47
+ log_error(e.message)
48
+ end
49
+
50
+ def should_monitor?(env)
51
+ configuration.enabled_environment? && configuration.monitoring_enabled?(env['PATH_INFO'])
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treblle
4
+ module Models
5
+ class Request
6
+ class Client
7
+ def initialize(request)
8
+ @ip = request.ip
9
+ @url = request.original_url
10
+ @user_agent = request.user_agent
11
+ end
12
+
13
+ attr_reader :ip, :url, :user_agent
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Treblle
6
+ module Models
7
+ class Request
8
+ class Server
9
+ def initialize(request)
10
+ @request = request
11
+ @software = get_server_software
12
+ @protocol = get_server_protocol
13
+ @os_architecture = get_platform
14
+ @os_name = get_host_os
15
+ @timezone = get_timezone
16
+ @remote_addr = get_remote_addr
17
+ end
18
+
19
+ attr_reader :software, :protocol, :os_name, :os_architecture, :remote_addr, :timezone
20
+
21
+ private
22
+
23
+ attr_reader :request
24
+
25
+ def get_host_os
26
+ RbConfig::CONFIG.fetch('host_os', 'unknown')
27
+ end
28
+
29
+ def get_platform
30
+ Gem::Platform.local.os
31
+ end
32
+
33
+ def get_remote_addr
34
+ request.env.fetch('REMOTE_ADDR', '')
35
+ end
36
+
37
+ def get_server_software
38
+ request.env.fetch('SERVER_SOFTWARE', '')
39
+ end
40
+
41
+ def get_server_protocol
42
+ request.env.fetch('SERVER_PROTOCOL', '')
43
+ end
44
+
45
+ def get_timezone
46
+ Time.now.zone
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treblle
4
+ module Models
5
+ class Request
6
+ attr_accessor :server, :client, :method, :headers, :body
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treblle
4
+ module Models
5
+ class Response
6
+ class Exception
7
+ def initialize(response)
8
+ @response = response
9
+ @trace = fetch_trace
10
+ @type = fetch_type
11
+ @message = fetch_message
12
+ @file_path, @line_number = get_exception_path_and_line
13
+ end
14
+
15
+ attr_reader :file_path, :line_number, :message, :type, :trace
16
+
17
+ private
18
+
19
+ attr_reader :response
20
+
21
+ def fetch_trace
22
+ response.body.dig("traces", "Application Trace")&.first&.[]("trace")
23
+ end
24
+
25
+ def fetch_type
26
+ response.body["error"] || response.body["errors"] || 'Unhandled error'
27
+ end
28
+
29
+ def fetch_message
30
+ response.body["exception"] || response.body["error"] || response.body["errors"]
31
+ end
32
+
33
+ def get_exception_path_and_line
34
+ return [nil, nil] if trace.nil?
35
+
36
+ match_data = trace.match(/^(.*):(\d+):in `.*'$/)
37
+ return [nil, nil] if match_data.nil?
38
+
39
+ file_path = match_data[1]
40
+ line_number = match_data[2]
41
+
42
+ [file_path, line_number]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treblle
4
+ module Models
5
+ class Response
6
+ attr_accessor :status, :headers, :body, :size, :exception
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ require 'treblle/middleware'
2
+ require 'rails'
3
+
4
+ module Treblle
5
+ module Rails
6
+ class Railtie < ::Rails::Railtie
7
+ initializer 'treblle.install_middleware' do |app|
8
+ app.config.middleware.insert_after ActionDispatch::ShowExceptions, Treblle::Middleware
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'treblle/models/request'
4
+ require 'treblle/models/request/server'
5
+ require 'treblle/models/request/client'
6
+ require 'action_dispatch'
7
+
8
+ module Treblle
9
+ class RequestBuilder
10
+ MAX_BODY_LENGTH = 2048
11
+
12
+ HEADER_PREFIXES = %w[
13
+ HTTP AUTHORIZATION QUERY CONTENT REMOTE
14
+ REQUEST SERVER ACCEPT USER HOST X PATH
15
+ VARY REFERRER
16
+ ].freeze
17
+
18
+ def initialize(rack_env)
19
+ @rack_env = rack_env
20
+ end
21
+
22
+ def build
23
+ Models::Request.new.tap do |request|
24
+ apply_to_request(request)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :rack_env
31
+
32
+ def apply_to_request(request)
33
+ request_metadata = ActionDispatch::Request.new(rack_env)
34
+
35
+ request.server = Models::Request::Server.new(request_metadata)
36
+ request.client = Models::Request::Client.new(request_metadata)
37
+ request.method = request_metadata.method
38
+ request.headers = get_headers(request_metadata)
39
+ request.body = get_body(request_metadata)
40
+
41
+ request
42
+ end
43
+
44
+ def get_headers(request_metadata)
45
+ request_metadata.headers.select { |header, _value| header_to_include?(header) }.to_h
46
+ .transform_keys { |header| normalize_header(header) }
47
+ end
48
+
49
+ def normalize_header(header)
50
+ header.delete_prefix('HTTP_').tr('_', '-')
51
+ end
52
+
53
+ def header_to_include?(header)
54
+ return false if header.start_with?('rack', 'puma')
55
+
56
+ HEADER_PREFIXES.any? do |prefix|
57
+ header.to_s.start_with?(prefix)
58
+ end
59
+ end
60
+
61
+ def get_body(request_metadata)
62
+ case request_metadata.media_type
63
+ when 'application/x-www-form-urlencoded', 'multipart/form-data'
64
+ request_metadata.POST.dup
65
+ else
66
+ body = request_metadata.body.read
67
+ request_metadata.body.rewind
68
+ body.byteslice(0, MAX_BODY_LENGTH).force_encoding('utf-8').scrub
69
+
70
+ request_metadata.media_type == 'application/json' ? JSON.parse(body) : body
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'treblle/models/response'
4
+ require 'treblle/models/response/exception'
5
+ require 'active_support'
6
+ require 'json'
7
+
8
+ module Treblle
9
+ class ResponseBuilder
10
+ def initialize(rack_response)
11
+ @rack_response = rack_response
12
+ end
13
+
14
+ def build
15
+ Models::Response.new.tap do |response|
16
+ apply_to_response(response)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :rack_response, :handle_errors
23
+
24
+ def apply_to_response(response)
25
+ status, headers, response_data = rack_response || [500, [], nil]
26
+
27
+ response.status = status
28
+ response.headers = headers
29
+ response.body = parse_body(response_data) || parse_error_body(response_data)
30
+ response.size = calculate_size(response.body, response.headers)
31
+ response.exception = Models::Response::Exception.new(response) if status >= 400
32
+
33
+ response
34
+ end
35
+
36
+ def calculate_size(body, headers)
37
+ return 0 if body.nil?
38
+
39
+ headers.fetch('content-length', nil) || ActiveSupport::JSON.encode(body).size
40
+ end
41
+
42
+ def parse_body(response_data)
43
+ return nil unless response_data.respond_to?(:body)
44
+
45
+ JSON.parse(response_data.body)
46
+ rescue JSON::ParserError
47
+ response_data.body
48
+ end
49
+
50
+ def parse_error_body(response_data)
51
+ return nil unless response_data.is_a?(Array) && !response_data.empty?
52
+
53
+ JSON.parse(response_data.first)
54
+ rescue JSON::ParserError
55
+ response_data.body
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treblle
4
+ module Utils
5
+ class HashSanitizer
6
+ class << self
7
+ def sanitize(body, sensitive_attrs)
8
+ return {} if body.nil? || body.empty?
9
+ return hash unless body.is_a?(Hash) || body.is_a?(Array)
10
+
11
+ if body.is_a?(Hash)
12
+ sanitize_hash(body, sensitive_attrs)
13
+ elsif body.is_a?(Array)
14
+ sanitize_array(body, sensitive_attrs)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def sanitize_hash(hash, sensitive_attrs)
21
+ hash.each_with_object({}) do |(key, value), result|
22
+ result[key] = if value.is_a?(Hash) || value.is_a?(Array)
23
+ sanitize(value, sensitive_attrs)
24
+ else
25
+ sanitize_value(key, value, sensitive_attrs)
26
+ end
27
+ end
28
+ end
29
+
30
+ def sanitize_array(array, sensitive_attrs)
31
+ array.map { |item| sanitize(item, sensitive_attrs) }
32
+ end
33
+
34
+ def sanitize_value(key, value, sensitive_attrs)
35
+ sensitive_attrs.include?(key.to_s) ? '*' * value.to_s.length : value
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treblle
4
+ VERSION = '2.0.2'
5
+ API_VERSION = '0.6'
6
+ end
data/lib/treblle.rb CHANGED
@@ -1,93 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'uri'
4
- require 'net/http'
5
- require 'treblle/data_builder'
3
+ require 'treblle/middleware'
4
+ require 'treblle/configuration'
5
+ require 'treblle/rails/railtie'
6
6
 
7
7
  # Treblle middleware for request interception and gathering.
8
- class Treblle
9
- TREBLLE_URI = 'https://rocknrolla.treblle.com'
10
- TREBLLE_VERSION = '0.6'
11
-
12
- def initialize(app)
13
- @app = app
14
- end
15
-
16
- def call(env)
17
- request = ActionDispatch::Request.new(env)
18
- started_at = Time.now
19
-
20
- begin
21
- status, headers, response = @app.call(env)
22
- to_parse = response.clone
23
- to_parse = to_parse.try(:body) if to_parse.respond_to?(:body)
24
-
25
- begin
26
- json_response = JSON.parse(to_parse)
27
- rescue JSON::ParserError, TypeError
28
- # continue with original values when unable to parse reponse to json
29
- return [status, headers, response]
30
- end
31
-
32
- params = {
33
- ended_at: Time.now,
34
- env: env,
35
- headers: headers,
36
- json_response: json_response,
37
- request: request,
38
- started_at: started_at,
39
- status: status
40
- }
41
- capture(params)
42
- rescue Exception => e
43
- status = status_code_for_exception(e)
44
- params = {
45
- ended_at: Time.now,
46
- env: env,
47
- exception: e,
48
- headers: headers,
49
- request: request,
50
- started_at: started_at,
51
- status: status
52
- }
53
- # send error payload to treblle, but raise exception as well
54
- capture(params)
55
- raise e
8
+ module Treblle
9
+ class << self
10
+ def configuration
11
+ @configuration ||= Configuration.new
56
12
  end
57
13
 
58
- [status, headers, response]
59
- end
14
+ def configure
15
+ yield(configuration)
60
16
 
61
- # Creates data to send in a format that Treblle expects and does the sending in a new thread
62
- # for perfomance reasons.
63
- # @param [Object] params
64
- def capture(params)
65
- data = DataBuilder.new(params).call
66
- return if data&.bytesize.to_i > 2.megabytes # ignore the capture for unnaturally large requests
67
-
68
- Thread.new do
69
- send_to_treblle(data)
70
- end
71
- rescue Exception => e
72
- Rails.logger.error(e.message)
73
- end
74
-
75
- def send_to_treblle(data)
76
- uri = URI(TREBLLE_URI)
77
- req = Net::HTTP::Post.new(uri)
78
- req['Content-Type'] = 'application/json'
79
- req['x-api-key'] = ENV.fetch('TREBLLE_API_KEY') { '' }
80
- req.body = data
81
- res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
82
- http.request(req)
17
+ configuration.validate_credentials! if configuration.enabled_environment?
83
18
  end
84
- puts res.try(:body)
85
- end
86
-
87
- def status_code_for_exception(exception)
88
- exception_wrapper = ActionDispatch::ExceptionWrapper.new(nil, exception)
89
- exception_wrapper.status_code
90
- rescue
91
- 500
92
19
  end
93
20
  end
metadata CHANGED
@@ -1,23 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: treblle
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
- - Hrvoje Grdic
7
+ - Borna Kapusta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-01 00:00:00.000000000 Z
12
- dependencies: []
13
- description: TreblleSDK for ruby
14
- email: hrvoje.grdic@bitlab.hr
11
+ date: 2024-03-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.1.0
27
+ description: |
28
+ Treblle is a lightweight SDK that helps Engineering and Product teams
29
+ build, ship & maintain REST based APIs faster.
30
+ email: borna.kapusta@cactus-code.com
15
31
  executables: []
16
32
  extensions: []
17
33
  extra_rdoc_files: []
18
34
  files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - SECURITY.md
19
38
  - lib/treblle.rb
20
- - lib/treblle/data_builder.rb
39
+ - lib/treblle/configuration.rb
40
+ - lib/treblle/dispatcher.rb
41
+ - lib/treblle/errors/configuration_errors.rb
42
+ - lib/treblle/generate_payload.rb
43
+ - lib/treblle/logging.rb
44
+ - lib/treblle/middleware.rb
45
+ - lib/treblle/models/request.rb
46
+ - lib/treblle/models/request/client.rb
47
+ - lib/treblle/models/request/server.rb
48
+ - lib/treblle/models/response.rb
49
+ - lib/treblle/models/response/exception.rb
50
+ - lib/treblle/rails/railtie.rb
51
+ - lib/treblle/request_builder.rb
52
+ - lib/treblle/response_builder.rb
53
+ - lib/treblle/utils/hash_sanitizer.rb
54
+ - lib/treblle/version.rb
21
55
  homepage: https://rubygems.org/gems/treblle
22
56
  licenses:
23
57
  - MIT
@@ -37,8 +71,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
37
71
  - !ruby/object:Gem::Version
38
72
  version: '0'
39
73
  requirements: []
40
- rubygems_version: 3.2.22
74
+ rubygems_version: 3.5.6
41
75
  signing_key:
42
76
  specification_version: 4
43
- summary: TreblleSDK
77
+ summary: Middleware for monitoring your API endpoints
44
78
  test_files: []
@@ -1,142 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'socket'
4
-
5
- # Gathers and builds body payload to be sent to Treblle in json format.
6
- # Hides sensitive data based on default values and additional ones provided via env variable.
7
- class DataBuilder
8
- DEFAULT_SENSITIVE_FIELDS = Set[
9
- 'card_number',
10
- 'cardNumber',
11
- 'cc',
12
- 'ccv',
13
- 'credit_score',
14
- 'creditScore',
15
- 'password',
16
- 'password_confirmation',
17
- 'passwordConfirmation',
18
- 'pwd',
19
- 'secret',
20
- 'ssn'
21
- ].freeze
22
-
23
- attr_accessor :env, :status, :started_at, :ended_at, :request, :headers, :json_response, :exception
24
-
25
- def initialize(params)
26
- @ended_at = params[:ended_at]
27
- @env = params[:env]
28
- @exception = params[:exception]
29
- @headers = params[:headers]
30
- @json_response = params[:json_response]
31
- @request = params[:request]
32
- @started_at = params[:started_at]
33
- @status = params[:status]
34
- end
35
-
36
- def call
37
- time_spent = ended_at - started_at
38
- user_agent = env['HTTP_USER_AGENT']
39
- ip = env['action_dispatch.remote_ip'].calculate_ip
40
- request_method = env['REQUEST_METHOD']
41
- project_id = ENV.fetch('TREBLLE_PROJECT_ID') { '' }
42
- request_body = request_method.downcase == 'get' ? request.query_parameters : safe_to_json(request.raw_post)
43
-
44
- data = {
45
- api_key: ENV.fetch('TREBLLE_API_KEY') { '' },
46
- project_id: project_id,
47
- version: Treblle::TREBLLE_VERSION,
48
- sdk: 'ruby',
49
- data: {
50
- server: {
51
- ip: server_ip,
52
- timezone: Time.zone.name,
53
- software: request_headers.try(:[], 'SERVER_SOFTWARE'),
54
- signature: '',
55
- protocol: request_headers.try(:[], 'SERVER_PROTOCOL'),
56
- os: {}
57
- },
58
- language: {
59
- name: 'ruby',
60
- version: RUBY_VERSION
61
- },
62
- request: {
63
- timestamp: started_at.to_formatted_s(:db),
64
- ip: ip,
65
- url: request.original_url,
66
- user_agent: user_agent,
67
- method: request_method,
68
- headers: request_headers,
69
- body: without_sensitive_attrs(request_body)
70
- },
71
- response: {
72
- headers: headers || {},
73
- code: status,
74
- size: json_response&.to_json&.bytesize || 0,
75
- load_time: time_spent,
76
- body: without_sensitive_attrs(json_response),
77
- errors: build_error_object(exception)
78
- }
79
- }
80
- }
81
-
82
- begin
83
- data.to_json
84
- rescue Exception
85
- JSON.dump(data)
86
- end
87
- end
88
-
89
- private
90
-
91
- def without_sensitive_attrs(obj)
92
- return {} unless obj.present?
93
-
94
- obj.each do |k, v|
95
- value = v || k
96
- if value.is_a?(Hash) || value.is_a?(Array)
97
- without_sensitive_attrs(value)
98
- elsif sensitive_attrs.include?(k.to_s)
99
- obj[k] = '*' * v.to_s.length
100
- end
101
- end
102
- obj
103
- rescue StandardError => e
104
- Rails.logger.error e.message
105
- end
106
-
107
- def sensitive_attrs
108
- @sensitive_attrs ||= user_sensitive_fields.merge(DEFAULT_SENSITIVE_FIELDS)
109
- end
110
-
111
- def user_sensitive_fields
112
- fields = ENV.fetch('TREBLLE_SENSITIVE_FIELDS') { '' }.gsub(/\s+/, '')
113
- fields.split(',').to_set
114
- end
115
-
116
- def build_error_object(exception)
117
- return [] unless exception.present?
118
-
119
- [
120
- {
121
- source: 'onError',
122
- type: exception.class.to_s || 'Unhandled error',
123
- message: exception.message,
124
- file: exception.backtrace.try(:first) || ''
125
- }
126
- ]
127
- end
128
-
129
- def server_ip
130
- Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }.ip_address
131
- end
132
-
133
- def request_headers
134
- @request_headers ||= request.headers.env.reject { |key| key.to_s.include?('.') }
135
- end
136
-
137
- def safe_to_json(obj)
138
- JSON.parse(obj)
139
- rescue StandardError => e
140
- {}
141
- end
142
- end