macaw_framework 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d900a18480da4792dfa03608ca8c85c0d0ba4084a3430c39008f4ca026ae784c
4
- data.tar.gz: e3bc35f7b78bc0a6e995c0abd906879dda434e4c12306d3d55713c35dc45065c
3
+ metadata.gz: b2191d627ee06b38338eeef5d88e16791ad96c9ac94fd3b4f8a917cb7a4fd1d6
4
+ data.tar.gz: 8572db3e03300d28013c29f2bc5238d2f2a4907b087cfc1fe60ff6a8b2369026
5
5
  SHA512:
6
- metadata.gz: 6ac4eb646bb2510fbc67ddec9ccfca45bea8dfcb6833f9e55b60f0f18d5edebaf392645c53dae7745eec797c926af1700c273516682bd6f10e66c2b018f1cea6
7
- data.tar.gz: 0ecb2e16a9f1762833751bb403d824e04571c4f3b79f01355ffb6f1f29eb29c234befc759c7d76d7b939f05d5f9f3c252d8a5828198e9c7674b7854fe2edcdf1
6
+ metadata.gz: 58cc6a8c9fe40d4f2fead09cf8188ac440dce1444b387a737283285ba1d823b9604ada962305d7c9279bfe0195215b891ecf417715dc755f1b027f75ffa07c13
7
+ data.tar.gz: 6e8b48c28f83a5eb33f5166c37dc1bbacc2831a3fd31f494e75a5ded27410d006475f12b684da06051b4a6ffe27a3067b1c8dfd5ab0acdaf12ecaba8b444ca60
data/.rubocop.yml CHANGED
@@ -16,7 +16,13 @@ Metrics/MethodLength:
16
16
  Max: 30
17
17
 
18
18
  Metrics/AbcSize:
19
- Max: 35
19
+ Enabled: false
20
20
 
21
21
  Metrics/CyclomaticComplexity:
22
- Max: 10
22
+ Enabled: false
23
+
24
+ Metrics/ParameterLists:
25
+ Max: 15
26
+
27
+ Metrics/PerceivedComplexity:
28
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -24,3 +24,13 @@
24
24
  - Adding log by aspect on endpoint calls to improve observability
25
25
  - Moving the server for a new separate class to respect single responsibility
26
26
  - Improved the data filtering middleware to sanitize inputs
27
+
28
+ ## [0.1.5] - 2023-04-16
29
+
30
+ - Adding support to path variables
31
+
32
+ ## [0.2.0] - 2023-04-22
33
+
34
+ - Adding middleware for integration with Prometheus to collect metrics
35
+ - Adding a simple caching mechanism that can be enabled separately for each endpoint
36
+ - Performance and functional optimizations
data/Gemfile CHANGED
@@ -12,3 +12,5 @@ gem "minitest", "~> 5.0"
12
12
  gem "rubocop", "~> 1.21"
13
13
 
14
14
  gem "simplecov", "~> 0.22.0"
15
+
16
+ gem "prometheus-client", "~> 4.1"
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <img src="macaw_logo.png" alt= “” style="width: 30%;height: 30%;margin-left: 35%">
4
4
 
5
5
  This is a framework for developing web applications. Please have in mind that this is still a work in progress and
6
- it is strongly advised to not use it for production purposes for now. Actualy it supports only HTTP. HTTPS and SSL
6
+ it is strongly advised to not use it for production purposes for now. Actually it supports only HTTP. and HTTPS/SSL
7
7
  support will be implemented soon. Anyone who wishes to contribute is welcome.
8
8
 
9
9
  ## Installation
@@ -19,7 +19,6 @@ If bundler is not being used to manage dependencies, install the gem by executin
19
19
  ## Usage
20
20
 
21
21
  The usage of the framework still very simple. Actually it support 5 HTTP verbs: GET, POST, PUT, PATCH and DELETE.
22
- For now, the framework can't resolve client request body and headers. The support for this will be included soon.
23
22
 
24
23
  The default server port is 8080. To choose a different port, create a file with the name `application.json`
25
24
  in the same directory of the script that will start the application with the following content:
@@ -29,11 +28,17 @@ in the same directory of the script that will start the application with the fol
29
28
  "macaw": {
30
29
  "port": 8080,
31
30
  "bind": "localhost",
32
- "threads": 10
31
+ "threads": 10,
32
+ "cache": {
33
+ "cache_invalidation": 3600
34
+ }
33
35
  }
34
36
  }
35
37
  ```
36
38
 
39
+ Cache invalidation time should be specified in seconds. In order to enable caching, The application.json file
40
+ should exist in the app main directory and it need the `cache_invalidation` config set.
41
+
37
42
  Example of usage:
38
43
 
39
44
  ```ruby
@@ -42,20 +47,32 @@ require 'json'
42
47
 
43
48
  m = MacawFramework::Macaw.new
44
49
 
45
- m.get('/hello_world') do |context|
50
+ m.get('/hello_world', cache: true) do |context|
51
+ context[:body] # Returns the request body as string
52
+ context[:params] # Returns query parameters and path variables as a hash
53
+ context[:headers] # Returns headers as a hash
54
+ return JSON.pretty_generate({ hello_message: 'Hello World!' }), 200
55
+ end
56
+
57
+ m.post('/hello_world/:path_variable') do |context|
46
58
  context[:body] # Returns the request body as string
47
59
  context[:params] # Returns query parameters and path variables as a hash
48
60
  context[:headers] # Returns headers as a hash
61
+ context[:params][:path_variable] # The defined path variable can be found in :params
49
62
  return JSON.pretty_generate({ hello_message: 'Hello World!' }), 200
50
63
  end
51
64
 
52
65
  m.start!
53
66
  ```
54
67
 
55
- The above example will start a server and will create a GET endpoint at localhost/hello_world.
68
+ The example above starts a server and creates a GET endpoint at localhost/hello_world.
69
+
70
+ If prometheus is enabled, a get endpoint will be defined at path `/metrics` to collect prometheus metrics. This path
71
+ is configurable via the `application.json` file.
56
72
 
57
- The verb methods must always return a String or nil (Used as response) and a number corresponding the
58
- HTTP Status Code to be returned to the client.
73
+ The verb methods must always return a string or nil (used as the response) and a number corresponding to the HTTP status
74
+ code to be returned to the client. If an endpoint doesn't return a value or returns nil for both the string and the
75
+ code, a default 200 OK status will be sent as the response.
59
76
 
60
77
  ## Contributing
61
78
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Aspect that provide cache for the endpoints.
5
+ module CacheAspect
6
+ def call_endpoint(cache, endpoints_to_cache, *args)
7
+ return super(*args) unless endpoints_to_cache.include?(args[0]) && !cache.nil?
8
+ return cache.cache[args[2..].to_s.to_sym][0] unless cache.cache[args[2..].to_s.to_sym].nil?
9
+
10
+ response = super(*args)
11
+ cache.cache[args[2..].to_s.to_sym] = [response, Time.now]
12
+ response
13
+ end
14
+ end
@@ -8,9 +8,17 @@ require "logger"
8
8
  # in the framework.
9
9
  module LoggingAspect
10
10
  def call_endpoint(logger, *args)
11
- logger.info("Input of #{args[0]}: #{args}")
12
- response = super(*args)
13
- logger.info("Output of #{args[0]} #{response}")
11
+ endpoint_name = args[2].split(".")[1..].join("/")
12
+ logger.info("Request received for #{endpoint_name} with arguments: #{args[3..]}")
13
+
14
+ begin
15
+ response = super(*args)
16
+ logger.info("Response for #{endpoint_name}: #{response}")
17
+ rescue StandardError => e
18
+ logger.error("Error processing #{endpoint_name}: #{e.message}\n#{e.backtrace.join("\n")}")
19
+ raise e
20
+ end
21
+
14
22
  response
15
23
  end
16
24
  end
@@ -0,0 +1,28 @@
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)
7
+ return super(*args) 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[3].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,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../aspects/prometheus_aspect"
3
4
  require_relative "../aspects/logging_aspect"
4
5
  require_relative "../utils/http_status_code"
6
+ require_relative "../aspects/cache_aspect"
5
7
 
6
8
  ##
7
9
  # Class responsible for providing a default
8
10
  # webserver.
9
11
  class Server
12
+ prepend CacheAspect
10
13
  prepend LoggingAspect
14
+ prepend PrometheusAspect
11
15
  include HttpStatusCode
16
+ # rubocop:disable Metrics/ParameterLists
12
17
 
13
18
  ##
14
19
  # Create a new instance of Server.
@@ -17,17 +22,26 @@ class Server
17
22
  # @param {Integer} port
18
23
  # @param {String} bind
19
24
  # @param {Integer} num_threads
25
+ # @param {CachingMiddleware} cache
26
+ # @param {Prometheus::Client:Registry} prometheus
20
27
  # @return {Server}
21
- def initialize(macaw, logger, port, bind, num_threads)
28
+ def initialize(macaw, logger, port, bind, num_threads, endpoints_to_cache = nil, cache = nil, prometheus = nil,
29
+ prometheus_middleware = nil)
22
30
  @port = port
23
31
  @bind = bind
24
32
  @macaw = macaw
25
33
  @macaw_log = logger
26
34
  @num_threads = num_threads
27
35
  @work_queue = Queue.new
36
+ @endpoints_to_cache = endpoints_to_cache || []
37
+ @cache = cache
38
+ @prometheus = prometheus
39
+ @prometheus_middleware = prometheus_middleware
28
40
  @workers = []
29
41
  end
30
42
 
43
+ # rubocop:enable Metrics/ParameterLists
44
+
31
45
  ##
32
46
  # Start running the webserver.
33
47
  def run
@@ -65,17 +79,17 @@ class Server
65
79
  raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
66
80
 
67
81
  @macaw_log.info("Running #{path.gsub("\n", "").gsub("\r", "")}")
68
- message, status = call_endpoint(@macaw_log, method_name, headers, body, parameters)
82
+ message, status = call_endpoint(@prometheus_middleware, @macaw_log, @cache, @endpoints_to_cache,
83
+ method_name, headers, body, parameters)
69
84
  status ||= 200
70
- message ||= "Ok"
85
+ message ||= nil
71
86
  client.puts "HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]} \r\n\r\n#{message}"
72
- client.close
73
87
  rescue EndpointNotMappedError
74
88
  client.print "HTTP/1.1 404 Not Found\r\n\r\n"
75
- client.close
76
89
  rescue StandardError => e
77
90
  client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
78
91
  @macaw_log.info("Error: #{e}")
92
+ ensure
79
93
  client.close
80
94
  end
81
95
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Middleware responsible for storing and
5
+ # invalidating cache.
6
+ class CachingMiddleware
7
+ attr_accessor :cache
8
+
9
+ def initialize(inv_time_seconds = 3_600)
10
+ @cache = {}
11
+ Thread.new do
12
+ loop do
13
+ sleep(1)
14
+ @cache.each_pair do |key, value|
15
+ @cache.delete(key) if Time.now - value[1] >= inv_time_seconds
16
+ end
17
+ end
18
+ end
19
+ sleep(2)
20
+ end
21
+ end
@@ -0,0 +1,48 @@
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]
46
+ end
47
+ end
48
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = "0.1.5"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "macaw_framework/errors/endpoint_not_mapped_error"
4
+ require_relative "macaw_framework/middlewares/prometheus_middleware"
4
5
  require_relative "macaw_framework/middlewares/request_data_filtering"
5
- require_relative "macaw_framework/middlewares/server"
6
+ require_relative "macaw_framework/middlewares/caching_middleware"
7
+ require_relative "macaw_framework/core/server"
6
8
  require_relative "macaw_framework/version"
9
+ require "prometheus/client"
7
10
  require "logger"
8
11
  require "socket"
9
12
  require "json"
@@ -24,16 +27,26 @@ module MacawFramework
24
27
  @routes = []
25
28
  @macaw_log ||= custom_log.nil? ? Logger.new($stdout) : custom_log
26
29
  config = JSON.parse(File.read("application.json"))
27
- @port = config["macaw"]["port"]
28
- @bind = config["macaw"]["bind"]
29
- @threads = config["macaw"]["threads"].to_i
30
+ @port = config["macaw"]["port"] || 8080
31
+ @bind = config["macaw"]["bind"] || "localhost"
32
+ @threads = config["macaw"]["threads"] || 5
33
+ unless config["macaw"]["cache"].nil?
34
+ @cache = CachingMiddleware.new(config["macaw"]["cache"]["cache_invalidation"].to_i || 3_600)
35
+ end
36
+ @prometheus = Prometheus::Client::Registry.new if config["macaw"]["prometheus"]
37
+ @prometheus_middleware = PrometheusMiddleware.new if config["macaw"]["prometheus"]
38
+ @prometheus_middleware.configure_prometheus(@prometheus, config, self) if config["macaw"]["prometheus"]
30
39
  rescue StandardError => e
31
40
  @macaw_log.error(e.message)
32
41
  end
33
42
  @port ||= 8080
34
43
  @bind ||= "localhost"
35
44
  @threads ||= 5
36
- @server = server.new(self, @macaw_log, @port, @bind, @threads)
45
+ @endpoints_to_cache = []
46
+ @prometheus ||= nil
47
+ @prometheus_middleware ||= nil
48
+ @server = server.new(self, @macaw_log, @port, @bind, @threads, @endpoints_to_cache, @cache, @prometheus,
49
+ @prometheus_middleware)
37
50
  end
38
51
 
39
52
  ##
@@ -42,18 +55,19 @@ module MacawFramework
42
55
  # @param {String} path
43
56
  # @param {Proc} block
44
57
  # @return {Integer, String}
45
- def get(path, &block)
46
- map_new_endpoint("get", path, &block)
58
+ def get(path, cache: false, &block)
59
+ map_new_endpoint("get", cache, path, &block)
47
60
  end
48
61
 
49
62
  ##
50
63
  # Creates a POST endpoint associated
51
64
  # with the respective path.
52
65
  # @param {String} path
66
+ # @param {Boolean} cache
53
67
  # @param {Proc} block
54
68
  # @return {String, Integer}
55
- def post(path, &block)
56
- map_new_endpoint("post", path, &block)
69
+ def post(path, cache: false, &block)
70
+ map_new_endpoint("post", cache, path, &block)
57
71
  end
58
72
 
59
73
  ##
@@ -62,8 +76,8 @@ module MacawFramework
62
76
  # @param {String} path
63
77
  # @param {Proc} block
64
78
  # @return {String, Integer}
65
- def put(path, &block)
66
- map_new_endpoint("put", path, &block)
79
+ def put(path, cache: false, &block)
80
+ map_new_endpoint("put", cache, path, &block)
67
81
  end
68
82
 
69
83
  ##
@@ -72,8 +86,8 @@ module MacawFramework
72
86
  # @param {String} path
73
87
  # @param {Proc} block
74
88
  # @return {String, Integer}
75
- def patch(path, &block)
76
- map_new_endpoint("patch", path, &block)
89
+ def patch(path, cache: false, &block)
90
+ map_new_endpoint("patch", cache, path, &block)
77
91
  end
78
92
 
79
93
  ##
@@ -82,8 +96,8 @@ module MacawFramework
82
96
  # @param {String} path
83
97
  # @param {Proc} block
84
98
  # @return {String, Integer}
85
- def delete(path, &block)
86
- map_new_endpoint("delete", path, &block)
99
+ def delete(path, cache: false, &block)
100
+ map_new_endpoint("delete", cache, path, &block)
87
101
  end
88
102
 
89
103
  ##
@@ -106,7 +120,8 @@ module MacawFramework
106
120
  server.run
107
121
  end
108
122
 
109
- def map_new_endpoint(prefix, path, &block)
123
+ def map_new_endpoint(prefix, cache, path, &block)
124
+ @endpoints_to_cache << "#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}" if cache
110
125
  path_clean = RequestDataFiltering.extract_path(path)
111
126
  @macaw_log.info("Defining #{prefix.upcase} endpoint at /#{path}")
112
127
  define_singleton_method("#{prefix}.#{path_clean}", block || lambda {
@@ -0,0 +1,5 @@
1
+ class CachingMiddleware
2
+ @cache: Hash[String, Array[string]]
3
+
4
+ attr_accessor cache: Hash[String, Array[string]]
5
+ end
@@ -1,9 +1,12 @@
1
1
  module MacawFramework
2
2
  class Macaw
3
3
  @bind: string
4
+ @cache: untyped
5
+ @endpoints_to_cache: Array[String]
4
6
  @macaw_log: Logger
5
7
  @port: int
6
8
 
9
+ @prometheus: untyped
7
10
  @server: Server
8
11
 
9
12
  @threads: Integer
data/sig/server.rbs CHANGED
@@ -1,10 +1,14 @@
1
1
  class Server
2
2
  @bind: String
3
+ @cache: CachingMiddleware
4
+ @endpoints_to_cache: Array[String]
3
5
  @macaw: MacawFramework::Macaw
4
6
  @macaw_log: Logger
5
7
  @num_threads: Integer
6
8
  @port: Integer
7
9
 
10
+ @prometheus: untyped
11
+ @prometheus_middleware: untyped
8
12
  @server: TCPServer
9
13
 
10
14
  @threads: Integer
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: macaw_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aria Diniz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-16 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2023-04-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prometheus-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.1'
13
27
  description: A project started for study purpose that I intend to keep working on.
14
28
  email:
15
29
  - aria.diniz.dev@gmail.com
@@ -25,13 +39,18 @@ files:
25
39
  - README.md
26
40
  - Rakefile
27
41
  - lib/macaw_framework.rb
42
+ - lib/macaw_framework/aspects/cache_aspect.rb
28
43
  - lib/macaw_framework/aspects/logging_aspect.rb
44
+ - lib/macaw_framework/aspects/prometheus_aspect.rb
45
+ - lib/macaw_framework/core/server.rb
29
46
  - lib/macaw_framework/errors/endpoint_not_mapped_error.rb
47
+ - lib/macaw_framework/middlewares/caching_middleware.rb
48
+ - lib/macaw_framework/middlewares/prometheus_middleware.rb
30
49
  - lib/macaw_framework/middlewares/request_data_filtering.rb
31
- - lib/macaw_framework/middlewares/server.rb
32
50
  - lib/macaw_framework/utils/http_status_code.rb
33
51
  - lib/macaw_framework/version.rb
34
52
  - macaw_logo.png
53
+ - sig/caching_middleware.rbs
35
54
  - sig/http_status_code.rbs
36
55
  - sig/logging_aspect.rbs
37
56
  - sig/macaw_framework.rbs