macaw_framework 1.4.2 → 1.5.0

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: b99afc6c030d78802a79875fad25f4739d0a5cc6789ad02d42faaa5699320e43
4
- data.tar.gz: e1e3f6b720367cab7767b09d7c7169db61ccd38cac5c0acfaf15c9e8f475b7e7
3
+ metadata.gz: 803f40a0a12304b555d3f998e323b4e475f10194518f566dc98e24c6b3984461
4
+ data.tar.gz: c0e90bf375a372105324e4444419ce05b6362e056bc2d2894820653cf9d7ef0a
5
5
  SHA512:
6
- metadata.gz: 1f27c7790bde337380fe1a103a4512c3eda23258e8394bbea25fec07c3f30b6dbf3daa43956d84e7c4608d6a5d0d485c81cac06c4c3bb7bec5510b6b7f6abace
7
- data.tar.gz: 93d87525ce2a24d16b46ce2e5377917facf98cb52dc2d63e4858d397a80fa5c53c7f7c1406e6b9b29488c711cb0e6f57ec43c688aa770483b2c279b0fa84dd58
6
+ metadata.gz: 0d43d8371216644879492cf22e3269aa743b4d7f48638e2e06d6fa72c5d2ee0975611111306742457c57b0d443937b6983ee95c00a8400c897aa6c9ff8a083b9
7
+ data.tar.gz: 7f453d9a093520e088dce07c93c9a818869511dc8b63a9737de6d958edd9ba88281d7f2104a46f0c39db945041de06e2d8adf016eb860d87df2fa406f38292f7
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.0
2
+ TargetRubyVersion: 3.2
3
3
  SuggestExtensions: false
4
4
  NewCops: disable
5
5
 
data/CHANGELOG.md CHANGED
@@ -161,5 +161,50 @@
161
161
  ## [1.4.1] - 2026-02-01
162
162
  - Fixing issue when re-spawning new threads
163
163
 
164
- ## [1.4.1] - 2026-02-24
164
+ ## [1.4.2] - 2026-02-24
165
165
  - Removing unused Rate Limiting Middleware
166
+
167
+ ## [1.4.3] - 2026-03-04
168
+ - Fix critical bug in `sanitize_parameter_value` where the first `gsub` result was discarded, leaving special characters unsanitized
169
+ - Fix header parsing regex to correctly accept real-world headers (Authorization, Cookie, User-Agent, Host with port, etc.)
170
+ - Fix deadlock in `maintain_worker_pool` caused by non-reentrant mutex re-acquisition when respawning dead workers
171
+ - Add missing `require 'digest'` in `LogDataFilter`, preventing `NameError` when sensitive fields are configured
172
+ - Fix thread-safety in `Cache#write` and `Cache#read`: both now always synchronize on the internal mutex
173
+ - Fix thread-safety of `@session` hash: `declare_client_session` now synchronizes on a dedicated mutex
174
+ - Fix `**kwargs` being silently dropped in `LoggingAspect` and `PrometheusAspect` when forwarding to `super`
175
+ - Fix `CacheAspect` holding mutex during entire endpoint execution on cache miss; endpoint now runs outside the lock
176
+ - Remove blocking `sleep(2)` from `MemoryInvalidationMiddleware` constructor; eviction thread no longer halts server startup
177
+ - Remove `sleep(1)` per cron job from `CronRunner#start_cron_job_thread`; startup is no longer O(N) seconds
178
+ - Remove dead code in `CronRunner`: duplicate `start_delay ||= 0` and always-true `unless start_delay.nil?` guard
179
+ - Make `application.json` path configurable via `MACAW_CONFIG` env var; rescue `Errno::ENOENT` and `JSON::ParserError` with graceful fallback
180
+ - Remove broken SSL2 and SSL3 from `SupportedSSLVersions` (POODLE/DROWN, unavailable on modern OpenSSL)
181
+ - `LoggingAspect` now logs request params, body, and response status on every endpoint call
182
+ - Use `Process.clock_gettime(Process::CLOCK_MONOTONIC)` in `PrometheusAspect` instead of `Time.now` for accurate duration measurement
183
+ - Fix Prometheus `/metrics` endpoint `Content-Type` to `text/plain; version=0.0.4; charset=utf-8`
184
+ - Fix RFC 7230 non-compliance: remove stray space before `\r\n` in HTTP status line
185
+ - Add eviction thread error rescue in `MemoryInvalidationMiddleware` to prevent silent thread death
186
+ - Set `frozen_string_literal: true` consistently across all library files
187
+ - Implement HTTP keep-alive: persistent TCP connections now reuse the same socket for multiple requests without a new handshake
188
+ - Add configurable per-connection read timeout (default 30 s, configurable via `keep_alive_timeout` in `application.json`) to prevent idle or faulty clients from holding worker threads indefinitely
189
+ - Server now responds with `Connection: keep-alive` or `Connection: close` headers according to the client's request
190
+ - Fix request parser to properly detect client EOF and raise `EOFError` instead of propagating `nil` through the parsing pipeline
191
+
192
+ ## [1.5.0] - 2026-03-04
193
+ - Use `IO#timeout=` (Ruby 3.2+) for socket timeout with `SO_RCVTIMEO` fallback for older Ruby and SSL sockets
194
+ - Remove Prometheus integration: `PrometheusAspect`, `PrometheusMiddleware`, and `prometheus-client` gem dependency removed
195
+ - Remove built-in session management: `declare_client_session`, `set_session`, `@session`, and all related configuration removed
196
+ - Remove `CronRunner` and `setup_job`: periodic job scheduling is no longer a responsibility of the framework; use a dedicated job library instead
197
+ - Remove `start_without_server!` method, which existed solely to support cron-only deployments
198
+ - Handle SIGTERM for graceful shutdown in containerised deployments; SIGTERM now triggers the same shutdown path as SIGINT
199
+ - Add `Content-Length: 0` to inline 404 and 500 error responses for RFC 7230 compliance
200
+ - Change default `bind` from `'localhost'` to `'0.0.0.0'` so the server is reachable in containers without explicit configuration
201
+ - Sanitize `\r` and `\n` from response header keys and values to prevent HTTP response splitting attacks
202
+ - Add configurable request body size limit (`max_body_size` in `application.json`, default 1 MB); requests exceeding the limit are rejected with 413 Content Too Large before the body is read
203
+ - Fix `maintain_worker_pool` iteration bug: `each_with_index` + `delete_at` skipped elements after a deletion; replaced with `reject!` + bulk respawn
204
+ - Fix cache TTL fallback: `nil.to_i` returned 0 when `cache_invalidation` was absent, silently creating a zero-TTL cache; replaced with safe navigation `&.to_i || 3_600`
205
+ - Fix `CacheAspect` crash when endpoint returns `nil`: guard added before `response[1]` access
206
+ - Replace `sanitize_parameter_value` character stripping with proper CGI URL-decoding (preserves emails, UUIDs, decimal values)
207
+ - Broaden HTTP version pattern in request parser from `HTTP/1.1` literal to regex — HTTP/1.0 requests now route correctly
208
+ - Add `rescue StandardError` guard in `Cache#invalidation_process` to prevent silent background eviction-thread death
209
+ - Fix `ThreadServer#shutdown` poison-pill count: uses actual `@workers.size` instead of `@num_threads`
210
+ - Add C extension scaffold (`ext/macaw_framework_ext/`) as a foundation for future native performance-sensitive routines
data/Gemfile CHANGED
@@ -6,7 +6,6 @@ gemspec
6
6
 
7
7
  gem 'logger', '~> 1.7'
8
8
  gem 'openssl'
9
- gem 'prometheus-client', '~> 4.1'
10
9
 
11
10
  group :test do
12
11
  gem 'minitest', '~> 5.0'
data/README.md CHANGED
@@ -2,8 +2,7 @@
2
2
  # MacawFramework
3
3
 
4
4
  MacawFramework is a lightweight, easy-to-use web framework for Ruby designed to simplify the development of small to
5
- medium-sized web applications. Weighting less than 26Kb with support for various HTTP methods, caching, and session management,
6
- MacawFramework provides developers with the essential tools to quickly build and deploy their applications.
5
+ medium-sized web applications. Weighing less than 26Kb with support for various HTTP methods and response caching,
7
6
 
8
7
  - [MacawFramework](#macawframework)
9
8
  * [Features](#features)
@@ -14,11 +13,8 @@ MacawFramework provides developers with the essential tools to quickly build and
14
13
  * [Usage](#usage)
15
14
  + [Basic routing: Define routes with support for GET, POST, PUT, PATCH, and DELETE HTTP methods](#basic-routing-define-routes-with-support-for-get-post-put-patch-and-delete-http-methods)
16
15
  + [Caching: Improve performance by caching responses and configuring cache invalidation](#caching-improve-performance-by-caching-responses-and-configuring-cache-invalidation)
17
- + [Session management: Handle user sessions securely with server-side in-memory storage](#session-management-handle-user-sessions-securely-with-server-side-in-memory-storage)
18
- + [Configuration: Customize various aspects of the framework through the application.json configuration file, such as SSL support and Prometheus integration](#configuration-customize-various-aspects-of-the-framework-through-the-applicationjson-configuration-file-such-as-ssl-support-and-prometheus-integration)
19
- + [Monitoring: Easily monitor your application performance and metrics with built-in Prometheus support](#monitoring-easily-monitor-your-application-performance-and-metrics-with-built-in-prometheus-support)
16
+ + [Configuration: Customize various aspects of the framework through the application.json configuration file](#configuration-customize-various-aspects-of-the-framework-through-the-applicationjson-configuration-file)
20
17
  + [Routing for "public" Folder: Serve Static Assets](#routing-for-public-folder-serve-static-assets)
21
- + [Periodic Jobs](#periodic-jobs)
22
18
  + [Tips](#tips)
23
19
  * [Contributing](#contributing)
24
20
  * [License](#license)
@@ -27,10 +23,8 @@ MacawFramework provides developers with the essential tools to quickly build and
27
23
  ## Features
28
24
 
29
25
  - Simple routing with support for GET, POST, PUT, PATCH, and DELETE HTTP methods
30
- - Caching middleware for improved performance
31
- - Session management with server-side in-memory storage
26
+ - Response caching middleware for improved performance
32
27
  - SSL support
33
- - Prometheus integration for monitoring and metrics
34
28
  - Less than 26Kb
35
29
  - Easy to learn
36
30
 
@@ -52,7 +46,7 @@ We evaluated MacawFramework (Version 1.2.0) to assess its ability to handle simu
52
46
 
53
47
  MacawFramework is built to be highly compatible, since it uses only native Ruby code:
54
48
 
55
- - **MRI**: MacawFramework is compatible with Matz's Ruby Interpreter (MRI), version 3.0.0 and onwards. If you are using this version or a more recent one, you should not encounter any compatibility issues.
49
+ - **MRI**: MacawFramework is compatible with Matz's Ruby Interpreter (MRI), version 3.2.0 and onwards. If you are using this version or a more recent one, you should not encounter any compatibility issues.
56
50
 
57
51
  - **TruffleRuby**: TruffleRuby is another Ruby interpreter that is fully compatible with MacawFramework. This provides developers with more flexibility in their choice of Ruby interpreter.
58
52
 
@@ -93,10 +87,9 @@ end
93
87
 
94
88
  m.post('/submit_data/:path_variable') do |context|
95
89
  context[:body] # Client body data
96
- context[:params] # Client params, like URL parameters or variables
90
+ context[:params] # Client params, like URL parameters or path variables
97
91
  context[:headers] # Client headers
98
92
  context[:params][:path_variable] # The defined path variable can be found in :params
99
- context[:client] # Client session
100
93
  end
101
94
 
102
95
  m.start!
@@ -125,70 +118,30 @@ MacawFramework::Cache.read(:name) # Maria
125
118
 
126
119
  Manual cache does not need any additional configuration.
127
120
 
128
- ### Session management: Handle user sessions with server-side in-memory storage
129
-
130
- Session will only be enabled if it's configurations exists in the `application.json` file.
131
- The session mechanism works by recovering the Session ID from a client sent header. The default
132
- header is `X-Session-ID`, but it can be changed in the `application.json` file.
133
-
134
- This header will be sent back to the user on every response if Session is enabled. Also, the
135
- session ID will be automatically generated and sent to a client if this client does not provide
136
- a session id in the HTTP request. In the case of the client sending an ID of an expired session
137
- the framework will return a new session with a new ID.
138
-
139
- ```ruby
140
- m = MacawFramework::Macaw.new
141
-
142
- m.get('/login') do |context|
143
- # Authenticate user
144
- context[:client][:user_id] = user_id
145
- end
146
-
147
- m.get('/dashboard') do |context|
148
- # Check if the user is logged in
149
- if context[:client][:user_id]
150
- # Show dashboard
151
- else
152
- # Redirect to login
153
- end
154
- end
155
- ```
156
-
157
- ### Configuration: Customize various aspects of the framework through the application.json configuration file, such as SSL support and Prometheus integration
121
+ ### Configuration: Customize various aspects of the framework through the application.json configuration file
158
122
 
159
123
  ```json
160
124
  {
161
125
  "macaw": {
162
126
  "port": 8080,
163
- "bind": "localhost",
127
+ "bind": "0.0.0.0",
164
128
  "threads": 200,
129
+ "keep_alive_timeout": 30,
130
+ "max_body_size": 1048576,
165
131
  "cache": {
166
132
  "cache_invalidation": 3600
167
133
  },
168
- "prometheus": {
169
- "endpoint": "/metrics"
170
- },
171
134
  "ssl": {
172
- "min": "SSL3",
135
+ "min": "TLS1.2",
173
136
  "max": "TLS1.3",
174
137
  "key_type": "EC",
175
138
  "cert_file_name": "path/to/cert/file/file.crt",
176
139
  "key_file_name": "path/to/cert/key/file.key"
177
- },
178
- "session": {
179
- "secure_header": "X-Session-ID",
180
- "invalidation_time": 3600
181
140
  }
182
141
  }
183
142
  }
184
143
  ```
185
144
 
186
- ### Monitoring: Easily monitor your application performance and metrics with built-in Prometheus support
187
-
188
- ```shell
189
- curl http://localhost:8080/metrics
190
- ```
191
-
192
145
  ### Routing for "public" Folder: Serve Static Assets
193
146
 
194
147
  MacawFramework allows you to serve static assets, such as CSS, JavaScript, images, etc., through the "public" folder.
@@ -207,29 +160,6 @@ be accessible at http://yourdomain.com/img/logo.png without any additional confi
207
160
 
208
161
  #### Caution: This is incompatible with most non-unix systems, such as Windows. If you are using a non-unix system, you will need to manually configure the "public" folder and use dir as nil to avoid problems.
209
162
 
210
- ### Periodic Jobs
211
-
212
- Macaw Framework supports the declaration of periodic jobs right in your application code. This feature allows developers to
213
- define tasks that run at set intervals, starting after an optional delay. Each job runs in a separate thread, meaning
214
- your periodic jobs can execute in parallel without blocking the rest of your application.
215
-
216
- Here's an example of how to declare a periodic job:
217
-
218
- ```ruby
219
- m = MacawFramework::Macaw.new
220
-
221
- m.setup_job(interval: 5, start_delay: 5, job_name: "cron job 1") do
222
- puts "i'm a periodic job that runs every 5 secs!"
223
- end
224
- ```
225
-
226
- Values for interval and start_delay are in seconds.
227
-
228
- **Caution: Defining a lot of jobs with low interval can severely degrade performance.**
229
-
230
- If you want to build an application with just cron jobs, that don't need to run a web server, you can start
231
- MacawFramework without running a web server with the `start_without_server!` method, instead of `start!`.
232
-
233
163
  ### Tips
234
164
 
235
165
  - The automatic logging and log aspect are now optional. To disable them, simply start Macaw with `custom_log` set to nil.
@@ -270,19 +200,13 @@ m.threads = 300
270
200
  - If the SSL configuration is provided in the `application.json` file with valid certificate and key files, the TCP server
271
201
  will be wrapped with HTTPS security using the provided certificate.
272
202
 
273
- - The supported values for `min` and `max` in the SSL configuration are: `SSL2`, `SSL3`, `TLS1.1`, `TLS1.2`, and `TLS1.3`,
203
+ - The supported values for `min` and `max` in the SSL configuration are: `TLS1.2` and `TLS1.3`,
274
204
  and the supported values for `key_type` are `RSA` and `EC`.
275
205
 
276
- - If Prometheus is enabled, a GET endpoint will be defined at path `/metrics` to collect Prometheus metrics. This path
277
- is configurable via the `application.json` file.
278
-
279
206
  - The verb methods must always return a string or nil (used as the response), a number corresponding to the HTTP status
280
207
  code to be returned to the client, and the response headers as a Hash or nil. If an endpoint doesn't return a value or
281
208
  returns nil for body, status code, and headers, a default 200 OK status will be sent as the response.
282
209
 
283
- - For cron jobs without a start_delay, a value of 0 will be used. For a job without a name, a unique name will be generated
284
- for it.
285
-
286
210
  - Ensure the "public" folder is placed in the same directory as the main.rb file: The "public" folder should contain any static assets,
287
211
  such as CSS, JavaScript, or images, that your web application requires. Placing it in the same directory as the main.rb file ensures
288
212
  that the server can correctly serve these assets.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ append_cflags(%w[-Wall -Wextra -Wno-unused-parameter])
6
+
7
+ create_makefile('macaw_framework_ext')
@@ -0,0 +1,8 @@
1
+ #include "macaw_framework_ext.h"
2
+
3
+ VALUE rb_mMacawFramework;
4
+
5
+ void
6
+ Init_macaw_framework_ext(void) {
7
+ rb_mMacawFramework = rb_define_module("MacawFramework");
8
+ }
@@ -0,0 +1,10 @@
1
+ #ifndef MACAW_FRAMEWORK_EXT_H
2
+ #define MACAW_FRAMEWORK_EXT_H
3
+
4
+ #include "ruby.h"
5
+
6
+ extern VALUE rb_mMacawFramework;
7
+
8
+ void Init_macaw_framework_ext(void);
9
+
10
+ #endif
@@ -4,17 +4,20 @@
4
4
  # Aspect that provide cache for the endpoints.
5
5
  module CacheAspect
6
6
  def call_endpoint(cache, *args, **kwargs)
7
- return super(*args, **kwargs) unless !cache[:cache].nil? && cache[:endpoints_to_cache]&.include?(args[0])
7
+ return super(*args, **kwargs) if cache[:cache].nil? || !cache[:endpoints_to_cache]&.include?(args[0])
8
8
 
9
9
  cache_filtered_name = cache_name_filter(args[1], cache[:cached_methods][args[0]])
10
10
 
11
- cache[:cache].mutex.synchronize do
12
- return cache[:cache].cache[cache_filtered_name][0] unless cache[:cache].cache[cache_filtered_name].nil?
11
+ cached_response = cache[:cache].mutex.synchronize { cache[:cache].cache[cache_filtered_name]&.dig(0) }
12
+ return cached_response unless cached_response.nil?
13
13
 
14
- response = super(*args, **kwargs)
15
- cache[:cache].cache[cache_filtered_name] = [response, Time.now] if should_cache_response?(response[1])
16
- response
14
+ response = super(*args, **kwargs)
15
+ if response && should_cache_response?(response[1])
16
+ cache[:cache].mutex.synchronize do
17
+ cache[:cache].cache[cache_filtered_name] = [response, Time.now]
18
+ end
17
19
  end
20
+ response
18
21
  end
19
22
 
20
23
  private
@@ -1,4 +1,4 @@
1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'logger'
4
4
  require_relative '../data_filters/log_data_filter'
@@ -11,13 +11,22 @@ module LoggingAspect
11
11
  def call_endpoint(logger, *args, **kwargs)
12
12
  return super(*args, **kwargs) if logger.nil?
13
13
 
14
+ endpoint_name = args[1]
15
+ client_data = args[2]
16
+
17
+ logger.info("Calling endpoint: #{endpoint_name} | " \
18
+ "params: #{LogDataFilter.sanitize_for_logging(client_data[:params].to_s)} | " \
19
+ "body: #{LogDataFilter.sanitize_for_logging(client_data[:body])}")
20
+
14
21
  begin
15
- response = super(*args)
22
+ response = super(*args, **kwargs)
16
23
  rescue StandardError => e
17
24
  logger.error("#{e.message}\n#{e.backtrace.join("\n")}")
18
25
  raise e
19
26
  end
20
27
 
28
+ logger.info("Response from endpoint: #{endpoint_name} | status: #{response&.dig(1)}")
29
+
21
30
  response
22
31
  end
23
32
  end
@@ -37,13 +37,8 @@ class MacawFramework::Cache
37
37
  # @example
38
38
  # MacawFramework::Cache.write("name", "Maria", expires_in: 7200)
39
39
  def write(tag, value, expires_in: 3600)
40
- if read(tag).nil?
41
- @mutex.synchronize do
42
- @cache.store(tag, { value: value, expires_in: Time.now + expires_in })
43
- end
44
- else
45
- @cache[tag][:value] = value
46
- @cache[tag][:expires_in] = Time.now + expires_in
40
+ @mutex.synchronize do
41
+ @cache.store(tag, { value: value, expires_in: Time.now + expires_in })
47
42
  end
48
43
  end
49
44
 
@@ -65,7 +60,9 @@ class MacawFramework::Cache
65
60
  #
66
61
  # @example
67
62
  # MacawFramework::Cache.read("name") # Maria
68
- def read(tag) = @cache.dig(tag, :value)
63
+ def read(tag)
64
+ @mutex.synchronize { @cache.dig(tag, :value) }
65
+ end
69
66
 
70
67
  private
71
68
 
@@ -86,6 +83,8 @@ class MacawFramework::Cache
86
83
  @mutex.synchronize do
87
84
  @cache.delete_if { |_, v| v[:expires_in] < Time.now }
88
85
  end
86
+ rescue StandardError => e
87
+ warn "Cache invalidation error: #{e.message}"
89
88
  end
90
89
  end
91
90
  end
@@ -3,10 +3,8 @@
3
3
  require_relative '../../middlewares/memory_invalidation_middleware'
4
4
  require_relative '../../data_filters/response_data_filter'
5
5
  require_relative '../../utils/supported_ssl_versions'
6
- require_relative '../../aspects/prometheus_aspect'
7
6
  require_relative '../../aspects/logging_aspect'
8
7
  require_relative '../../aspects/cache_aspect'
9
- require 'securerandom'
10
8
 
11
9
  ##
12
10
  # Base module for Server classes. It contains
@@ -16,18 +14,16 @@ require 'securerandom'
16
14
  module ServerBase
17
15
  prepend CacheAspect
18
16
  prepend LoggingAspect
19
- prepend PrometheusAspect
20
17
 
21
18
  private
22
19
 
23
- def call_endpoint(name, client_data, session_id, _client_ip)
20
+ def call_endpoint(name, client_data)
24
21
  @macaw.send(
25
22
  name.to_sym,
26
23
  {
27
24
  headers: client_data[:headers],
28
25
  body: client_data[:body],
29
- params: client_data[:params],
30
- client: @session&.dig(session_id)&.dig(0)
26
+ params: client_data[:params]
31
27
  }
32
28
  )
33
29
  end
@@ -37,27 +33,29 @@ module ServerBase
37
33
  end
38
34
 
39
35
  def handle_client(client)
40
- _path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
41
- raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
36
+ apply_socket_timeout(client)
37
+ loop do
38
+ max_body = @macaw.max_body_size || 1_048_576
39
+ _path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes,
40
+ max_body)
41
+ raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
42
42
 
43
- client_data = get_client_data(body, headers, parameters)
44
- session_id = declare_client_session(client_data[:headers], @macaw.secure_header) if @macaw.session
45
-
46
- message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
47
- method_name, client_data, session_id, client.peeraddr[3])
48
- response_headers ||= {}
49
- response_headers[@macaw.secure_header] = session_id if @macaw.session
50
- status ||= 200
51
- message ||= nil
52
- response_headers ||= nil
53
- client.puts ResponseDataFilter.mount_response(status, response_headers, message)
54
- rescue IOError, Errno::EPIPE => e
55
- @macaw_log&.error("Error writing to client: #{e.message}")
56
- rescue EndpointNotMappedError
57
- client.print "HTTP/1.1 404 Not Found\r\n\r\n"
58
- rescue StandardError => e
59
- client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
60
- @macaw_log&.error(e.full_message)
43
+ keep_alive = keep_alive_connection?(headers)
44
+ client.write build_response(method_name, headers, body, parameters, keep_alive)
45
+ break unless keep_alive
46
+ rescue Errno::ECONNRESET, Errno::EPIPE, IOError
47
+ break
48
+ rescue EndpointNotMappedError
49
+ client.print "HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
50
+ break
51
+ rescue PayloadTooLargeError
52
+ client.print "HTTP/1.1 413 Content Too Large\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
53
+ break
54
+ rescue StandardError => e
55
+ client.print "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
56
+ @macaw_log&.error(e.full_message)
57
+ break
58
+ end
61
59
  ensure
62
60
  begin
63
61
  client.close
@@ -66,11 +64,29 @@ module ServerBase
66
64
  end
67
65
  end
68
66
 
69
- def declare_client_session(headers, secure_header_name)
70
- session_id = headers[secure_header_name] || SecureRandom.uuid
71
- session_id = SecureRandom.uuid if @session[session_id].nil?
72
- @session[session_id] ||= [{}, Time.now]
73
- session_id
67
+ def build_response(method_name, headers, body, parameters, keep_alive)
68
+ client_data = get_client_data(body, headers, parameters)
69
+ message, status, response_headers = call_endpoint(@macaw_log, @cache, method_name, client_data)
70
+ response_headers ||= {}
71
+ status ||= 200
72
+ response_headers['Connection'] = keep_alive ? 'keep-alive' : 'close'
73
+ response_headers['Content-Length'] = message.to_s.bytesize
74
+ ResponseDataFilter.mount_response(status, response_headers, message)
75
+ end
76
+
77
+ def apply_socket_timeout(client)
78
+ timeout = @macaw.keep_alive_timeout || 30
79
+ client.timeout = timeout
80
+ rescue NoMethodError
81
+ io = client.respond_to?(:to_io) ? client.to_io : client
82
+ timeval = [timeout, 0].pack('l_2')
83
+ io.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeval)
84
+ rescue StandardError
85
+ nil
86
+ end
87
+
88
+ def keep_alive_connection?(headers)
89
+ headers['Connection']&.downcase != 'close'
74
90
  end
75
91
 
76
92
  def set_ssl
@@ -99,21 +115,8 @@ module ServerBase
99
115
  raise e
100
116
  end
101
117
 
102
- def set_session
103
- return unless @macaw.session
104
-
105
- @session ||= {}
106
- inv = if @macaw.config&.dig('macaw', 'session', 'invalidation_time')
107
- MemoryInvalidationMiddleware.new(@macaw.config['macaw']['session']['invalidation_time'])
108
- else
109
- MemoryInvalidationMiddleware.new
110
- end
111
- inv.cache = @session
112
- end
113
-
114
118
  def set_features
115
119
  @is_shutting_down = false
116
- set_session
117
120
  set_ssl
118
121
  end
119
122
  end
@@ -13,19 +13,13 @@ class ThreadServer
13
13
 
14
14
  attr_reader :context
15
15
 
16
- # rubocop:disable Metrics/ParameterLists
17
-
18
16
  ##
19
17
  # Create a new instance of ThreadServer.
20
18
  # @param {Macaw} macaw
21
- # @param {Logger} logger
22
- # @param {Integer} port
23
- # @param {String} bind
24
- # @param {Integer} num_threads
19
+ # @param {Array} endpoints_to_cache
25
20
  # @param {MemoryInvalidationMiddleware} cache
26
- # @param {Prometheus::Client:Registry} prometheus
27
21
  # @return {Server}
28
- def initialize(macaw, endpoints_to_cache = nil, cache = nil, prometheus = nil, prometheus_mw = nil)
22
+ def initialize(macaw, endpoints_to_cache = nil, cache = nil)
29
23
  @port = macaw.port
30
24
  @bind = macaw.bind
31
25
  @macaw = macaw
@@ -38,13 +32,9 @@ class ThreadServer
38
32
  endpoints_to_cache: endpoints_to_cache || [],
39
33
  cached_methods: macaw.cached_methods
40
34
  }
41
- @prometheus = prometheus
42
- @prometheus_middleware = prometheus_mw
43
35
  @workers = []
44
36
  end
45
37
 
46
- # rubocop:enable Metrics/ParameterLists
47
-
48
38
  ##
49
39
  # Start running the webserver.
50
40
  def run
@@ -81,40 +71,40 @@ class ThreadServer
81
71
  sleep 0.1
82
72
  end
83
73
 
84
- @num_threads.times { @work_queue << :shutdown }
74
+ worker_count = @workers_mutex.synchronize { @workers.size }
75
+ worker_count.times { @work_queue << :shutdown }
85
76
  @workers.each(&:join)
86
- @server.close
77
+ @server&.close
87
78
  end
88
79
 
89
80
  private
90
81
 
91
82
  def spawn_worker
92
- @workers_mutex.synchronize do
93
- t = Thread.new do
94
- loop do
95
- client = @work_queue.pop
96
- break if client == :shutdown
83
+ @workers_mutex.synchronize { create_worker }
84
+ end
97
85
 
98
- handle_client(client)
99
- end
86
+ def create_worker
87
+ t = Thread.new do
88
+ loop do
89
+ client = @work_queue.pop
90
+ break if client == :shutdown
91
+
92
+ handle_client(client)
100
93
  end
101
- @workers << t
102
- t
103
94
  end
95
+ @workers << t
96
+ t
104
97
  end
105
98
 
106
99
  def maintain_worker_pool
107
100
  @workers_mutex.synchronize do
108
- @workers.each_with_index do |worker, index|
109
- unless worker.alive?
110
- if @is_shutting_down
111
- @macaw_log&.info("Worker thread #{index} finished, not respawning due to server shutdown.")
112
- else
113
- @macaw_log&.error("Worker thread #{index} died, respawning...")
114
- @workers.delete_at(index)
115
- spawn_worker
116
- end
117
- end
101
+ return if @is_shutting_down
102
+
103
+ dead_count = @workers.count { |w| !w.alive? }
104
+ @workers.select!(&:alive?)
105
+ dead_count.times do
106
+ @macaw_log&.error('Worker thread died, respawning...')
107
+ create_worker
118
108
  end
119
109
  end
120
110
  end
@@ -1,6 +1,7 @@
1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'digest'
4
5
 
5
6
  ##
6
7
  # Module responsible for sanitizing log data
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi/escape'
3
4
  require_relative '../errors/endpoint_not_mapped_error'
5
+ require_relative '../errors/payload_too_large_error'
4
6
 
5
7
  ##
6
8
  # Module containing methods to filter Strings
@@ -10,14 +12,17 @@ module RequestDataFiltering
10
12
  ##
11
13
  # Method responsible for extracting information
12
14
  # provided by the client like Headers and Body
13
- def self.parse_request_data(client, routes)
14
- path, parameters = extract_url_parameters(client.gets&.gsub('HTTP/1.1', ''))
15
+ def self.parse_request_data(client, routes, max_body_size = 1_048_576)
16
+ first_line = client.gets
17
+ raise EOFError if first_line.nil?
18
+
19
+ path, parameters = extract_url_parameters(first_line.gsub(%r{\s*HTTP/\d+(?:\.\d+)?\s*$}i, ''))
15
20
  parameters = {} if parameters.nil?
16
21
 
17
22
  method_name = sanitize_method_name(path)
18
23
  method_name = select_path(method_name, routes, parameters)
19
24
  body_first_line, headers = extract_headers(client)
20
- body = extract_body(client, body_first_line, headers['Content-Length'].to_i)
25
+ body = extract_body(client, body_first_line, headers['Content-Length'].to_i, max_body_size)
21
26
  [path, method_name, headers, body, parameters]
22
27
  end
23
28
 
@@ -74,8 +79,8 @@ module RequestDataFiltering
74
79
  def self.extract_headers(client)
75
80
  header = client.gets&.delete("\n")&.delete("\r")
76
81
  headers = {}
77
- while header&.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
78
- split_header = header.split(':')
82
+ while header&.match(/\A[^:]+:\s*.+/)
83
+ split_header = header.split(':', 2)
79
84
  headers[split_header[0].strip] = split_header[1].strip
80
85
  header = client.gets&.delete("\n")&.delete("\r")
81
86
  end
@@ -84,7 +89,9 @@ module RequestDataFiltering
84
89
 
85
90
  ##
86
91
  # Method responsible for extracting the body from request
87
- def self.extract_body(client, body_first_line, content_length)
92
+ def self.extract_body(client, body_first_line, content_length, max_body_size = 1_048_576)
93
+ raise PayloadTooLargeError if content_length > max_body_size
94
+
88
95
  body = client&.read(content_length)
89
96
  body_first_line << body.to_s
90
97
  end
@@ -113,9 +120,13 @@ module RequestDataFiltering
113
120
  end
114
121
 
115
122
  ##
116
- # Method responsible for sanitizing the parameter value
123
+ # Method responsible for sanitizing the parameter value.
124
+ # URL-decodes percent-encoded characters (e.g. %40 → @, %20 → space, + → space).
117
125
  def self.sanitize_parameter_value(value)
118
- value&.gsub(/[^\w\s]/, '')
119
- value&.gsub(/\s/, '')
126
+ return nil if value.nil?
127
+
128
+ CGI.unescape(value)
129
+ rescue ArgumentError
130
+ value
120
131
  end
121
132
  end
@@ -12,20 +12,21 @@ module ResponseDataFilter
12
12
  end
13
13
 
14
14
  def self.mount_first_response_line(status, headers)
15
- separator = " \r\n\r\n"
16
- separator = " \r\n" unless headers.nil?
17
-
18
- "HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]}#{separator}"
15
+ reason = HTTP_STATUS_CODE_MAP[status] || 'Unknown'
16
+ separator = headers.nil? ? "\r\n\r\n" : "\r\n"
17
+ "HTTP/1.1 #{status} #{reason}#{separator}"
19
18
  end
20
19
 
21
20
  def self.mount_response_headers(headers)
22
21
  return '' if headers.nil?
23
22
 
24
- response = ''
23
+ response = +''
25
24
  headers.each do |key, value|
26
- response += "#{key}: #{value}\r\n"
25
+ safe_key = key.to_s.gsub(/[\r\n]/, '')
26
+ safe_value = value.to_s.gsub(/[\r\n]/, '')
27
+ response << "#{safe_key}: #{safe_value}\r\n"
27
28
  end
28
- response += "\r\n"
29
+ response << "\r\n"
29
30
  response
30
31
  end
31
32
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Error raised when the request body exceeds the configured max_body_size.
5
+ class PayloadTooLargeError < StandardError; end
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'errors/endpoint_not_mapped_error'
4
- require_relative 'middlewares/prometheus_middleware'
4
+ require_relative 'errors/payload_too_large_error'
5
5
  require_relative 'data_filters/request_data_filtering'
6
6
  require_relative 'middlewares/memory_invalidation_middleware'
7
- require_relative 'core/cron_runner'
8
7
  require_relative 'core/thread_server'
9
8
  require_relative 'version'
10
- require 'prometheus/client'
11
- require 'securerandom'
12
9
  require 'singleton'
13
10
  require 'pathname'
14
11
  require 'logger'
@@ -23,7 +20,7 @@ module MacawFramework; end
23
20
  # Class responsible for creating endpoints and
24
21
  # starting the web server.
25
22
  class MacawFramework::Macaw
26
- attr_reader :routes, :macaw_log, :config, :jobs, :cached_methods, :secure_header, :session
23
+ attr_reader :routes, :macaw_log, :config, :cached_methods, :keep_alive_timeout, :max_body_size
27
24
  attr_accessor :port, :bind, :threads
28
25
 
29
26
  ##
@@ -119,26 +116,6 @@ class MacawFramework::Macaw
119
116
  map_new_endpoint('delete', cache, path, &block)
120
117
  end
121
118
 
122
- ##
123
- # Spawn and start a thread running the defined periodic job.
124
- # @param {Integer} interval
125
- # @param {Integer?} start_delay
126
- # @param {String} job_name
127
- # @param {Proc} block
128
- # @example
129
- #
130
- # macaw = MacawFramework::Macaw.new
131
- # macaw.setup_job(interval: 60, start_delay: 60, job_name: "job 1") do
132
- # puts "I'm a periodic job that runs every minute"
133
- # end
134
- ##
135
- def setup_job(interval: 60, start_delay: 0, job_name: "job_#{SecureRandom.uuid}", &block)
136
- @cron_runner ||= CronRunner.new(self)
137
- @jobs ||= []
138
- @cron_runner.start_cron_job_thread(interval, start_delay, job_name, &block)
139
- @jobs << job_name
140
- end
141
-
142
119
  ##
143
120
  # Starts the web server
144
121
  def start!
@@ -153,7 +130,8 @@ class MacawFramework::Macaw
153
130
  @macaw_log.info("Number of threads: #{@threads}")
154
131
  @macaw_log.info('---------------------------------')
155
132
  end
156
- @server = @server_class.new(self, @endpoints_to_cache, @cache, @prometheus, @prometheus_middleware)
133
+ @server = @server_class.new(self, @endpoints_to_cache, @cache)
134
+ Signal.trap('TERM') { raise Interrupt }
157
135
  server_loop(@server)
158
136
  rescue Interrupt
159
137
  if @macaw_log.nil?
@@ -167,35 +145,19 @@ class MacawFramework::Macaw
167
145
  end
168
146
  end
169
147
 
170
- ##
171
- # This method is intended to start the framework
172
- # without an web server. This can be useful when
173
- # you just want to keep cron jobs running, without
174
- # mapping any HTTP endpoints.
175
- def start_without_server!
176
- @macaw_log.nil? ? puts('Application starting') : @macaw_log.info('Application starting')
177
- loop { sleep(3600) }
178
- rescue Interrupt
179
- @macaw_log.nil? ? puts('Macaw stop flying for some seeds.') : @macaw_log.info('Macaw stop flying for some seeds.')
180
- end
181
-
182
148
  private
183
149
 
184
150
  def setup_default_configs
185
151
  @port ||= 8080
186
- @bind ||= 'localhost'
152
+ @bind ||= '0.0.0.0'
187
153
  @config ||= nil
188
154
  @threads ||= 200
189
155
  @endpoints_to_cache = []
190
- @prometheus ||= nil
191
- @prometheus_middleware ||= nil
192
156
  end
193
157
 
194
158
  def apply_options(custom_log)
195
159
  setup_basic_config(custom_log)
196
- setup_session
197
160
  setup_cache
198
- setup_prometheus
199
161
  rescue StandardError => e
200
162
  @macaw_log&.warn(e.message)
201
163
  end
@@ -203,33 +165,30 @@ class MacawFramework::Macaw
203
165
  def setup_cache
204
166
  return if @config['macaw']['cache'].nil?
205
167
 
206
- @cache = MemoryInvalidationMiddleware.new(@config['macaw']['cache']['cache_invalidation'].to_i || 3_600)
207
- end
208
-
209
- def setup_session
210
- @session = false
211
- return if @config['macaw']['session'].nil?
212
-
213
- @session = true
214
- @secure_header = @config['macaw']['session']['secure_header'] || 'X-Session-ID'
168
+ @cache = MemoryInvalidationMiddleware.new(@config.dig('macaw', 'cache', 'cache_invalidation')&.to_i || 3_600)
215
169
  end
216
170
 
217
171
  def setup_basic_config(custom_log)
218
172
  @routes = []
219
173
  @cached_methods = {}
220
174
  @macaw_log ||= custom_log
221
- @config = JSON.parse(File.read('application.json'))
175
+ config_file = ENV.fetch('MACAW_CONFIG', 'application.json')
176
+ @config = JSON.parse(File.read(config_file))
222
177
  @port = @config['macaw']['port'] || 8080
223
- @bind = @config['macaw']['bind'] || 'localhost'
178
+ @bind = @config['macaw']['bind'] || '0.0.0.0'
224
179
  @threads = @config['macaw']['threads'] || 200
225
- end
226
-
227
- def setup_prometheus
228
- return unless @config['macaw']['prometheus']
229
-
230
- @prometheus = Prometheus::Client::Registry.new
231
- @prometheus_middleware = PrometheusMiddleware.new
232
- @prometheus_middleware&.configure_prometheus(@prometheus, @config, self)
180
+ @keep_alive_timeout = @config['macaw']['keep_alive_timeout'] || 30
181
+ @max_body_size = @config.dig('macaw', 'max_body_size')&.to_i || 1_048_576
182
+ rescue Errno::ENOENT
183
+ @macaw_log&.warn("Config file '#{config_file}' not found, using default settings.")
184
+ @config = { 'macaw' => {} }
185
+ @keep_alive_timeout = 30
186
+ @max_body_size = 1_048_576
187
+ rescue JSON::ParserError => e
188
+ @macaw_log&.warn("Config file '#{config_file}' is not valid JSON: #{e.message}. Using default settings.")
189
+ @config = { 'macaw' => {} }
190
+ @keep_alive_timeout = 30
191
+ @max_body_size = 1_048_576
233
192
  end
234
193
 
235
194
  def server_loop(server)
@@ -17,8 +17,9 @@ class MemoryInvalidationMiddleware
17
17
  @cache.delete(key) if Time.now - value[1] >= inv_time_seconds
18
18
  end
19
19
  end
20
+ rescue StandardError => e
21
+ warn "MemoryInvalidationMiddleware eviction error: #{e.message}"
20
22
  end
21
23
  end
22
- sleep(2)
23
24
  end
24
25
  end
@@ -4,8 +4,6 @@ require 'openssl'
4
4
 
5
5
  module SupportedSSLVersions
6
6
  VERSIONS = {
7
- 'SSL2' => OpenSSL::SSL::SSL2_VERSION,
8
- 'SSL3' => OpenSSL::SSL::SSL3_VERSION,
9
7
  'TLS1.1' => OpenSSL::SSL::TLS1_1_VERSION,
10
8
  'TLS1.2' => OpenSSL::SSL::TLS1_2_VERSION,
11
9
  'TLS1.3' => OpenSSL::SSL::TLS1_3_VERSION
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = '1.4.2'
4
+ VERSION = '1.5.0'
5
5
  end
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Load the C extension when available (built via `rake compile` or `gem install`).
4
+ # Falls back transparently to pure-Ruby mode if the extension has not been compiled.
5
+ begin
6
+ require 'macaw_framework_ext'
7
+ rescue LoadError
8
+ nil
9
+ end
10
+
3
11
  ##
4
12
  # Main module for all Macaw classes
5
13
  module MacawFramework; end
metadata CHANGED
@@ -1,35 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: macaw_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.2
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aria Diniz
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: prometheus-client
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '4.1'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '4.1'
11
+ dependencies: []
26
12
  description: |-
27
13
  A lightweight web framework designed for building efficient backend applications. Initially
28
14
  created for study purposes, now production-ready and open for contributions.
29
15
  email:
30
16
  - aria.diniz.dev@gmail.com
31
17
  executables: []
32
- extensions: []
18
+ extensions:
19
+ - ext/macaw_framework_ext/extconf.rb
33
20
  extra_rdoc_files: []
34
21
  files:
35
22
  - ".rubocop.yml"
@@ -41,21 +28,22 @@ files:
41
28
  - README.md
42
29
  - Rakefile
43
30
  - SECURITY.md
31
+ - ext/macaw_framework_ext/extconf.rb
32
+ - ext/macaw_framework_ext/macaw_framework_ext.c
33
+ - ext/macaw_framework_ext/macaw_framework_ext.h
44
34
  - lib/macaw_framework.rb
45
35
  - lib/macaw_framework/aspects/cache_aspect.rb
46
36
  - lib/macaw_framework/aspects/logging_aspect.rb
47
- - lib/macaw_framework/aspects/prometheus_aspect.rb
48
37
  - lib/macaw_framework/cache.rb
49
38
  - lib/macaw_framework/core/common/server_base.rb
50
- - lib/macaw_framework/core/cron_runner.rb
51
39
  - lib/macaw_framework/core/thread_server.rb
52
40
  - lib/macaw_framework/data_filters/log_data_filter.rb
53
41
  - lib/macaw_framework/data_filters/request_data_filtering.rb
54
42
  - lib/macaw_framework/data_filters/response_data_filter.rb
55
43
  - lib/macaw_framework/errors/endpoint_not_mapped_error.rb
44
+ - lib/macaw_framework/errors/payload_too_large_error.rb
56
45
  - lib/macaw_framework/macaw.rb
57
46
  - lib/macaw_framework/middlewares/memory_invalidation_middleware.rb
58
- - lib/macaw_framework/middlewares/prometheus_middleware.rb
59
47
  - lib/macaw_framework/utils/http_status_code.rb
60
48
  - lib/macaw_framework/utils/supported_ssl_versions.rb
61
49
  - lib/macaw_framework/version.rb
@@ -75,6 +63,7 @@ metadata:
75
63
  documentation_uri: https://rubydoc.info/gems/macaw_framework
76
64
  homepage_uri: https://github.com/ariasdiniz/macaw_framework
77
65
  source_code_uri: https://github.com/ariasdiniz/macaw_framework
66
+ changelog_uri: https://github.com/ariasdiniz/macaw_framework/blob/main/CHANGELOG.md
78
67
  rdoc_options: []
79
68
  require_paths:
80
69
  - lib
@@ -82,7 +71,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
82
71
  requirements:
83
72
  - - ">="
84
73
  - !ruby/object:Gem::Version
85
- version: 3.0.0
74
+ version: 3.2.0
86
75
  required_rubygems_version: !ruby/object:Gem::Requirement
87
76
  requirements:
88
77
  - - ">="
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ##
4
- # Aspect that provides application metrics using prometheus.
5
- module PrometheusAspect
6
- def call_endpoint(prometheus_middleware, *args, **kwargs)
7
- return super(*args, **kwargs) if prometheus_middleware.nil?
8
-
9
- start_time = Time.now
10
-
11
- begin
12
- response = super(*args)
13
- ensure
14
- duration = (Time.now - start_time) * 1_000
15
-
16
- endpoint_name = args[2].split('.').join('/')
17
-
18
- prometheus_middleware.request_duration_milliseconds.with_labels(endpoint: endpoint_name).observe(duration)
19
- prometheus_middleware.request_count.with_labels(endpoint: endpoint_name).increment
20
- if response
21
- prometheus_middleware.response_count.with_labels(endpoint: endpoint_name,
22
- status: response[1]).increment
23
- end
24
- end
25
-
26
- response
27
- end
28
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ##
4
- # This module is responsible to set up a new thread
5
- # for each cron job defined
6
- class CronRunner
7
- def initialize(macaw)
8
- @logger = macaw.macaw_log
9
- @macaw = macaw
10
- end
11
-
12
- ##
13
- # Will start a thread for the defined cron job
14
- # @param {Integer} interval
15
- # @param {Integer?} start_delay
16
- # @param {String} job_name
17
- # @param {Proc} block
18
- def start_cron_job_thread(interval, start_delay, job_name, &block)
19
- start_delay ||= 0
20
- raise "interval can't be <= 0 and start_delay can't be < 0!" if interval <= 0 || start_delay.negative?
21
-
22
- @logger&.info("Starting thread for job #{job_name}")
23
- start_delay ||= 0
24
- thread = Thread.new do
25
- name = job_name
26
- interval_thread = interval
27
- unless start_delay.nil?
28
- @logger&.info("Job #{name} scheduled with delay. Will start running in #{start_delay} seconds.")
29
- sleep(start_delay)
30
- end
31
-
32
- loop do
33
- start_time = Time.now
34
- @logger&.info("Running job #{name}")
35
- block.call
36
- @logger&.info("Job #{name} executed with success. New execution in #{interval_thread} seconds.")
37
-
38
- execution_time = Time.now - start_time
39
- sleep_time = [interval_thread - execution_time, 0].max
40
- sleep(sleep_time)
41
- rescue StandardError => e
42
- @logger&.error("Error executing cron job with name #{name}: #{e.message}")
43
- sleep(interval)
44
- end
45
- end
46
- sleep(1)
47
- @logger&.info("Thread for job #{job_name} started")
48
- thread
49
- end
50
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'prometheus/client'
4
- require 'prometheus/client/formats/text'
5
-
6
- ##
7
- # Middleware responsible to configure prometheus
8
- # defining metrics and an endpoint to access them.
9
- class PrometheusMiddleware
10
- attr_accessor :request_duration_milliseconds, :request_count, :response_count
11
-
12
- def configure_prometheus(prometheus_registry, configurations, macaw)
13
- return nil unless prometheus_registry
14
-
15
- @request_duration_milliseconds = Prometheus::Client::Histogram.new(
16
- :request_duration_milliseconds,
17
- docstring: 'The duration of each request in milliseconds',
18
- labels: [:endpoint],
19
- buckets: (100..1000).step(100).to_a + (2000..10_000).step(1000).to_a
20
- )
21
-
22
- @request_count = Prometheus::Client::Counter.new(
23
- :request_count,
24
- docstring: 'The total number of requests received',
25
- labels: [:endpoint]
26
- )
27
-
28
- @response_count = Prometheus::Client::Counter.new(
29
- :response_count,
30
- docstring: 'The total number of responses sent',
31
- labels: %i[endpoint status]
32
- )
33
-
34
- prometheus_registry.register(@request_duration_milliseconds)
35
- prometheus_registry.register(@request_count)
36
- prometheus_registry.register(@response_count)
37
- prometheus_endpoint(prometheus_registry, configurations, macaw)
38
- end
39
-
40
- private
41
-
42
- def prometheus_endpoint(prometheus_registry, configurations, macaw)
43
- endpoint = configurations['macaw']['prometheus']['endpoint'] || '/metrics'
44
- macaw.get(endpoint) do |_context|
45
- [Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200, { 'Content-Type' => 'plaintext' }]
46
- end
47
- end
48
- end