aho 0.1.0 → 0.1.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +34 -7
  3. data/.ruby-version +1 -1
  4. data/README.md +2 -2
  5. data/Rakefile +3 -3
  6. data/aho.gemspec +41 -0
  7. data/app/controllers/dsp/api/v1/agent_controller.rb +17 -0
  8. data/app/controllers/dsp/api/v1/base_controller.rb +32 -0
  9. data/app/controllers/dsp/api/v1/config_controller.rb +20 -0
  10. data/app/controllers/dsp/api/v1/healthcheck_controller.rb +29 -0
  11. data/app/controllers/dsp/api/v1/metrics_controller.rb +13 -0
  12. data/config/default.aho.yml +36 -0
  13. data/config/puma.rb +5 -0
  14. data/config/routes.rb +10 -0
  15. data/config.ru +5 -0
  16. data/exe/aho +6 -3
  17. data/lib/aho/configuration.rb +43 -0
  18. data/lib/aho/errors.rb +19 -0
  19. data/lib/aho/metrics/active.rb +58 -0
  20. data/lib/aho/metrics/ignore.rb +23 -0
  21. data/lib/aho/metrics_aggregator.rb +29 -0
  22. data/lib/aho/metrics_builder.rb +14 -0
  23. data/lib/aho/metrics_formatter.rb +7 -0
  24. data/lib/aho/metrics_log_device.rb +10 -0
  25. data/lib/aho/metrics_logger/period.rb +27 -0
  26. data/lib/aho/metrics_logger.rb +24 -0
  27. data/lib/aho/schema/parser.rb +45 -0
  28. data/lib/aho/schema/validator.rb +55 -0
  29. data/lib/aho/version.rb +1 -1
  30. data/lib/aho/web_server/application.rb +44 -0
  31. data/lib/aho/web_server/router.rb +34 -0
  32. data/lib/aho.rb +49 -7
  33. data/lib/extensions/dot_hash.rb +29 -0
  34. data/lib/extensions/hash/deep_mergeable.rb +24 -0
  35. data/lib/extensions/hash.rb +43 -0
  36. data/openapi.yml +386 -0
  37. metadata +100 -15
  38. data/.idea/.gitignore +0 -8
  39. data/.idea/aho.iml +0 -77
  40. data/.idea/misc.xml +0 -4
  41. data/.idea/modules.xml +0 -8
  42. data/.idea/vcs.xml +0 -6
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aho
4
+ module Schema
5
+ class Validator
6
+ TYPES = {
7
+ 'string' => String,
8
+ 'number' => Numeric,
9
+ 'integer' => Integer,
10
+ 'boolean' => [TrueClass, FalseClass],
11
+ 'array' => Array,
12
+ 'object' => Hash
13
+ }.freeze
14
+
15
+ def initialize(body, schema, parent_key = nil)
16
+ @body = body
17
+ @schema = schema
18
+ @parent_key = parent_key
19
+ end
20
+
21
+ def validate!
22
+ validate_required_fields!
23
+ validate_properties!
24
+ true
25
+ end
26
+
27
+ private
28
+
29
+ def validate_required_fields!
30
+ @schema['required']&.each do |required_field|
31
+ raise Api::MissingFieldError, full_key_for(required_field) unless @body.key?(required_field)
32
+ end
33
+ end
34
+
35
+ def validate_properties!
36
+ @schema['properties'].each do |key, property_schema|
37
+ value = @body[key]
38
+ type = property_schema['type']
39
+ expected_class = TYPES[type]
40
+ full_key = full_key_for(key)
41
+
42
+ unless [*expected_class].any? { |klass| value.is_a?(klass) }
43
+ raise Api::InvalidTypeError.new(full_key, type.capitalize, value)
44
+ end
45
+
46
+ self.class.new(value, property_schema, full_key).validate! if type == 'object'
47
+ end
48
+ end
49
+
50
+ def full_key_for(key)
51
+ [@parent_key, key].compact.join('.')
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/aho/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aho
4
- VERSION = "0.1.0"
4
+ VERSION = '0.1.1'
5
5
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ module Aho
6
+ module WebServer
7
+ class Application
8
+ def call(env)
9
+ Aho.router.route_for(env) || [404, {}, []]
10
+ rescue Api::MissingFieldError,
11
+ Api::InvalidTypeError => e
12
+ bad_request_error(error: e)
13
+ rescue JSON::ParserError => e
14
+ common_error(error: e, status_code: 400)
15
+ rescue StandardError => e
16
+ common_error(error: e, status_code: 500)
17
+ end
18
+
19
+ private
20
+
21
+ def common_error(error:, status_code:)
22
+ [
23
+ status_code,
24
+ { 'Content-Type' => 'application/json' },
25
+ [
26
+ {
27
+ status: 'ERROR',
28
+ message: "Internal server error (#{error.class}): #{error.message}",
29
+ details: error.backtrace.first
30
+ }.to_json
31
+ ]
32
+ ]
33
+ end
34
+
35
+ def bad_request_error(error:)
36
+ [
37
+ 422,
38
+ { 'Content-Type' => 'application/json' },
39
+ [{ status: 'ERROR', message: error.message }.to_json]
40
+ ]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aho
4
+ module WebServer
5
+ class Router
6
+ attr_reader :routes
7
+
8
+ def initialize
9
+ @routes = Hash.new { |hash, key| hash[key] = {} }
10
+ end
11
+
12
+ def route_for(env)
13
+ path = env['PATH_INFO']
14
+ method = env['REQUEST_METHOD'].downcase
15
+ destination = routes.dig(method.to_sym, path)
16
+ return if destination.nil?
17
+
18
+ controller = Module.const_get(destination[:controller]).new(env)
19
+ controller.public_send(destination[:action])
20
+ end
21
+
22
+ %w[get post patch delete].each do |method|
23
+ define_method(method) do |path, to:|
24
+ endpoint = Aho::Schema::Parser.new(method, path, to)
25
+ routes[method.to_sym][path] = {
26
+ controller: endpoint.controller,
27
+ action: endpoint.action,
28
+ schema: endpoint.schema
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/aho.rb CHANGED
@@ -1,15 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "aho/version"
3
+ require 'logger'
4
+ require 'time'
5
+ require 'yaml'
6
+ require 'json'
7
+ require 'rack'
8
+
9
+ require_relative 'aho/version'
10
+ require_relative 'aho/configuration'
11
+
12
+ require_relative 'aho/metrics_logger'
13
+ require_relative 'aho/metrics_formatter'
14
+ require_relative 'aho/metrics_logger/period'
15
+ require_relative 'aho/schema/parser'
16
+ require_relative 'aho/schema/validator'
17
+ require_relative 'extensions/hash'
18
+ require_relative 'extensions/dot_hash'
19
+
20
+ require_relative 'aho/web_server/application'
21
+ require_relative 'aho/web_server/router'
22
+
23
+ require_relative '../app/controllers/dsp/api/v1/base_controller'
24
+ require_relative '../app/controllers/dsp/api/v1/agent_controller'
25
+ require_relative '../app/controllers/dsp/api/v1/config_controller'
26
+ require_relative '../app/controllers/dsp/api/v1/metrics_controller'
27
+ require_relative '../app/controllers/dsp/api/v1/healthcheck_controller'
28
+
29
+ require_relative 'aho/metrics_aggregator'
4
30
 
5
31
  module Aho
6
- class Error < StandardError; end
32
+ class << self
33
+ def root_path
34
+ File.dirname __dir__
35
+ end
7
36
 
8
- def self.root
9
- File.dirname __dir__
10
- end
37
+ def logs_path
38
+ FileUtils.mkdir_p("#{root_path}/logs").first
39
+ end
40
+
41
+ def config
42
+ @config ||= Configuration.new
43
+ end
44
+
45
+ def logger
46
+ @logger ||= MetricsLogger.new
47
+ end
48
+
49
+ def router
50
+ @router ||= Aho::WebServer::Router.new
51
+ end
11
52
 
12
- def self.logs
13
- File.join root, "logs"
53
+ def metrics
54
+ Aho::MetricsAggregator.new.metrics
55
+ end
14
56
  end
15
57
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DotHash < Hash
4
+ def initialize(hash = {})
5
+ super()
6
+ hash.each do |key, value|
7
+ self[key] = value.is_a?(Hash) ? DotHash.new(value) : value
8
+ end
9
+ end
10
+
11
+ def to_h
12
+ transform_values { |value| value.is_a?(DotHash) ? value.to_h : value }
13
+ end
14
+
15
+ def method_missing(method_name, *args, &)
16
+ key = method_name.to_s
17
+ if key?(key)
18
+ self[key]
19
+ elsif key?(method_name)
20
+ self[method_name]
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def respond_to_missing?(method_name, include_private = false)
27
+ key?(method_name.to_s) || key?(method_name) || super
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepMergeable
4
+ def deep_merge(other, &)
5
+ dup.deep_merge!(other, &)
6
+ end
7
+
8
+ # Same as #deep_merge, but modifies +self+.
9
+ def deep_merge!(other, &block)
10
+ merge!(other) do |key, this_val, other_val|
11
+ if this_val.is_a?(DeepMergeable) && this_val.deep_merge?(other_val)
12
+ this_val.deep_merge(other_val, &block)
13
+ elsif block_given?
14
+ yield(key, this_val, other_val)
15
+ else
16
+ other_val
17
+ end
18
+ end
19
+ end
20
+
21
+ def deep_merge?(other)
22
+ other.is_a?(self.class)
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'hash/deep_mergeable'
4
+
5
+ class Hash
6
+ include DeepMergeable
7
+
8
+ def deep_transform_values(&)
9
+ _deep_transform_values_in_object(self, &)
10
+ end
11
+
12
+ def deep_transform_values!(&)
13
+ _deep_transform_values_in_object!(self, &)
14
+ end
15
+
16
+ def stringify_keys
17
+ transform_keys(&:to_s)
18
+ end
19
+
20
+ private
21
+
22
+ def _deep_transform_values_in_object(object, &block)
23
+ case object
24
+ when Hash
25
+ object.transform_values { |value| _deep_transform_values_in_object(value, &block) }
26
+ when Array
27
+ object.map { |e| _deep_transform_values_in_object(e, &block) }
28
+ else
29
+ yield(object)
30
+ end
31
+ end
32
+
33
+ def _deep_transform_values_in_object!(object, &block)
34
+ case object
35
+ when Hash
36
+ object.transform_values! { |value| _deep_transform_values_in_object!(value, &block) }
37
+ when Array
38
+ object.map! { |e| _deep_transform_values_in_object!(e, &block) }
39
+ else
40
+ yield(object)
41
+ end
42
+ end
43
+ end
data/openapi.yml ADDED
@@ -0,0 +1,386 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: Aho API
4
+ version: 1.0.0
5
+
6
+ paths:
7
+ /dsp/api/v1/config:
8
+ post:
9
+ summary: Configure the agent
10
+ description: Configures the agent with specified settings.
11
+ operationId: configureAgent
12
+ requestBody:
13
+ required: true
14
+ content:
15
+ application/json:
16
+ schema:
17
+ type: object
18
+ properties:
19
+ server_id:
20
+ type: string
21
+ description: ID сервера в Студии
22
+ jwt:
23
+ type: string
24
+ description: Токен авторизации
25
+ ignore_metrics:
26
+ type: integer
27
+ description: Ограничение сбора метрик
28
+ upload:
29
+ type: object
30
+ description: Периодичность сброса данных
31
+ properties:
32
+ limit_time:
33
+ type: integer
34
+ description: По времени
35
+ limit_lines:
36
+ type: integer
37
+ description: По количеству событий
38
+ limit_size:
39
+ type: integer
40
+ description: По размеру файла
41
+ studio_settings:
42
+ type: object
43
+ description: Настройки API студии для сброса данных
44
+ properties:
45
+ url:
46
+ type: string
47
+ description: Протокол, адрес, порт
48
+ agent_settings:
49
+ type: object
50
+ description: Настройки агента
51
+ properties:
52
+ port:
53
+ type: integer
54
+ description: API порт агента
55
+ storage:
56
+ type: object
57
+ description: Настройки хранилища
58
+ properties:
59
+ limit_size:
60
+ type: integer
61
+ description: Ограничение на размер хранимых данных
62
+ limit_time:
63
+ type: integer
64
+ description: Ограничение на время хранимых файлов
65
+ directory:
66
+ type: string
67
+ description: Расположение файлов агента
68
+ file:
69
+ type: object
70
+ description: Ограничение одного файла ротации
71
+ properties:
72
+ limit_size:
73
+ type: integer
74
+ description: По размеру
75
+ limit_time:
76
+ type: integer
77
+ description: По времени
78
+ archive:
79
+ type: object
80
+ description: Настройки архивации
81
+ properties:
82
+ on_files:
83
+ type: integer
84
+ description: Начинается при создании n-го файла ротации
85
+ on_size:
86
+ type: integer
87
+ description: Начинается при достижении размера данных X mb
88
+ limit_files:
89
+ type: integer
90
+ description: Количество файлов данных, которые архивируется за 1 шаг архивации
91
+ compression_level:
92
+ type: integer
93
+ description: Уровень сжатия
94
+ aggregation:
95
+ type: object
96
+ description: Настройки агрегации
97
+ properties:
98
+ on_files:
99
+ type: integer
100
+ description: Начинается при создании n-го файла ротации
101
+ on_size:
102
+ type: integer
103
+ description: Начинается при достижении размера данных X mb
104
+ level:
105
+ type: integer
106
+ description: Уровень агрегации данных
107
+ required:
108
+ - server_id
109
+ - jwt
110
+ - ignore_metrics
111
+ - upload
112
+ - studio_settings
113
+ - agent_settings
114
+ - storage
115
+ - file
116
+ - archive
117
+ - aggregation
118
+ examples:
119
+ validConfig:
120
+ summary: Example agent configuration
121
+ value:
122
+ server_id: "Server1"
123
+ jwt: "1q2w3e4r5t6y"
124
+ ignore_metrics: 0
125
+ upload:
126
+ limit_time: 9999
127
+ limit_lines: 100
128
+ limit_size: 1
129
+ studio_settings:
130
+ url: "http://studio.example.com:8088/dsp/api/v1/"
131
+ agent_settings:
132
+ port: 16888
133
+ storage:
134
+ limit_size: 0
135
+ limit_time: 86400
136
+ directory: "/opt/dtms-billing-agent/"
137
+ file:
138
+ limit_size: 1
139
+ limit_time: 0
140
+ archive:
141
+ on_files: 500
142
+ on_size: 0
143
+ limit_files: 10
144
+ compression_level: -1
145
+ aggregation:
146
+ on_files: 10
147
+ on_size: 0
148
+ level: 1
149
+ invalidType:
150
+ summary: Example agent configuration
151
+ value:
152
+ server_id: "Server1"
153
+ jwt: "1q2w3e4r5t6y"
154
+ ignore_metrics: 0
155
+ upload:
156
+ limit_time: 3600
157
+ limit_lines: 100
158
+ limit_size: 1
159
+ studio_settings:
160
+ url: "http://studio.example.com:8088/dsp/api/v1/"
161
+ agent_settings:
162
+ port: 16888
163
+ storage:
164
+ limit_size: 0
165
+ limit_time: 86400
166
+ directory: "/opt/dtms-billing-agent/"
167
+ file:
168
+ limit_size: 1
169
+ limit_time: 'hello_world'
170
+ archive:
171
+ on_files: 500
172
+ on_size: 0
173
+ limit_files: 10
174
+ compression_level: -1
175
+ aggregation:
176
+ on_files: 10
177
+ on_size: 0
178
+ level: 1
179
+ missingField:
180
+ summary: Missing field
181
+ value:
182
+ server_id: "Server1"
183
+ responses:
184
+ '200':
185
+ description: Successful configuration
186
+ content:
187
+ application/json:
188
+ schema:
189
+ type: object
190
+ properties:
191
+ status:
192
+ type: string
193
+ message:
194
+ type: string
195
+ examples:
196
+ success:
197
+ summary: Successful configuration update
198
+ value:
199
+ status: "OK"
200
+ message: "Configuration applied successfully"
201
+ '400': # TODO: write rspec request example
202
+ description: Invalid request or permission error
203
+ content:
204
+ application/json:
205
+ schema:
206
+ type: object
207
+ properties:
208
+ status:
209
+ type: string
210
+ message:
211
+ type: string
212
+ examples:
213
+ permissionError:
214
+ summary: Permission error
215
+ value:
216
+ status: "ERROR"
217
+ message: "You don't have permission to access"
218
+ '403': # TODO: write rspec request example
219
+ description: Invalid server ID
220
+ content:
221
+ application/json:
222
+ schema:
223
+ type: object
224
+ properties:
225
+ status:
226
+ type: string
227
+ message:
228
+ type: string
229
+ examples:
230
+ invalidServerID:
231
+ summary: Invalid server ID
232
+ value:
233
+ status: "ERROR"
234
+ message: "Invalid server ID"
235
+ '422':
236
+ description: Invalid configuration
237
+ content:
238
+ application/json:
239
+ schema:
240
+ type: object
241
+ properties:
242
+ status:
243
+ type: string
244
+ message:
245
+ type: string
246
+ examples:
247
+ missingField:
248
+ summary: Missing required field
249
+ value:
250
+ status: "ERROR"
251
+ message: "Missing required field 'jwt'"
252
+ invalidType:
253
+ summary: Invalid type for field
254
+ value:
255
+ status: "ERROR"
256
+ message: "Invalid type for 'file.limit_time'. Expected Integer, got String"
257
+ '500': # TODO: write rspec request example
258
+ description: Internal server error
259
+ content:
260
+ application/json:
261
+ schema:
262
+ type: object
263
+ properties:
264
+ status:
265
+ type: string
266
+ message:
267
+ type: string
268
+ examples:
269
+ internalError:
270
+ summary: Example of an internal server error
271
+ value:
272
+ status: "ERROR"
273
+ message: >
274
+ Internal server error (NameError): undefined local variable or method `hello_world'
275
+ for an instance of Aho::Schema::Validator
276
+ details: "/aho/lib/aho/schema/validator.rb:22:in `validate!'"
277
+ get:
278
+ summary: Retrieve the current agent configuration
279
+ description: Returns the current configuration of the agent.
280
+ operationId: getAgentConfig
281
+ responses:
282
+ '200':
283
+ description: Current agent configuration
284
+ content:
285
+ application/json:
286
+ schema:
287
+ type: object
288
+ properties:
289
+ server_id:
290
+ type: string
291
+ ignore_metrics:
292
+ type: integer
293
+ life_jwt_time:
294
+ type: integer
295
+ scrape_interval_time:
296
+ type: integer
297
+ enable_agent:
298
+ type: integer
299
+ upload:
300
+ type: object
301
+ properties:
302
+ limit_time:
303
+ type: integer
304
+ limit_lines:
305
+ type: integer
306
+ limit_size:
307
+ type: integer
308
+ studio_settings:
309
+ type: object
310
+ properties:
311
+ url:
312
+ type: string
313
+ agent_settings:
314
+ type: object
315
+ properties:
316
+ port:
317
+ type: integer
318
+ storage:
319
+ type: object
320
+ properties:
321
+ limit_size:
322
+ type: integer
323
+ limit_time:
324
+ type: integer
325
+ directory:
326
+ type: string
327
+ file:
328
+ type: object
329
+ properties:
330
+ limit_size:
331
+ type: integer
332
+ limit_time:
333
+ type: integer
334
+ archive:
335
+ type: object
336
+ properties:
337
+ files_on:
338
+ type: integer
339
+ size_on:
340
+ type: integer
341
+ files_limit:
342
+ type: integer
343
+ compression_level:
344
+ type: integer
345
+ aggregation:
346
+ type: object
347
+ properties:
348
+ files_on:
349
+ type: integer
350
+ size_on:
351
+ type: integer
352
+ level:
353
+ type: integer
354
+ examples:
355
+ success:
356
+ summary: Current agent configuration
357
+ value:
358
+ server_id: "STUDIO_SERVER_ID"
359
+ ignore_metrics: -1
360
+ life_jwt_time: 3600
361
+ scrape_interval_time: 60
362
+ enable_agent: -1
363
+ upload:
364
+ limit_time: 3600
365
+ limit_lines: 100
366
+ limit_size: 1
367
+ studio_settings:
368
+ url: "http://localhost:8088/dsp/api/v1/vmagent"
369
+ agent_settings:
370
+ port: 16888
371
+ storage:
372
+ limit_size: 0
373
+ limit_time: 86400
374
+ directory: "/opt/dtms-billing-agent/"
375
+ file:
376
+ limit_size: 1
377
+ limit_time: 86400
378
+ archive:
379
+ files_on: 0
380
+ size_on: 0
381
+ files_limit: 10
382
+ compression_level: -1
383
+ aggregation:
384
+ files_on: 0
385
+ size_on: 0
386
+ level: 1