mayu-live 0.0.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.
Files changed (204) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +661 -0
  3. data/README.md +598 -0
  4. data/exe/mayu +33 -0
  5. data/lib/mayu/app_metrics.rb +93 -0
  6. data/lib/mayu/banner.rb +12 -0
  7. data/lib/mayu/client/README.md +17 -0
  8. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js +1 -0
  9. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.br +0 -0
  10. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map +1 -0
  11. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map.br +0 -0
  12. data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js +1 -0
  13. data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js.map +1 -0
  14. data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js +1 -0
  15. data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js.map +1 -0
  16. data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js +1 -0
  17. data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js.map +1 -0
  18. data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js +1 -0
  19. data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js.map +1 -0
  20. data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js +1 -0
  21. data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js.map +1 -0
  22. data/lib/mayu/client/dist/entries.json +3 -0
  23. data/lib/mayu/client/dist/main-4b49dbc4.js +1 -0
  24. data/lib/mayu/client/dist/main-4b49dbc4.js.br +0 -0
  25. data/lib/mayu/client/dist/main-4b49dbc4.js.map +1 -0
  26. data/lib/mayu/client/dist/main-4b49dbc4.js.map.br +0 -0
  27. data/lib/mayu/client/package.json +39 -0
  28. data/lib/mayu/client/rollup.config.js +81 -0
  29. data/lib/mayu/client/src/DecompressionStream.ts +15 -0
  30. data/lib/mayu/client/src/DecompressionStreamPolyfill.ts +43 -0
  31. data/lib/mayu/client/src/MimeTypes.ts +4 -0
  32. data/lib/mayu/client/src/NodeTree.ts +445 -0
  33. data/lib/mayu/client/src/custom-elements/mayu-alert.html +137 -0
  34. data/lib/mayu/client/src/custom-elements/mayu-alert.ts +62 -0
  35. data/lib/mayu/client/src/custom-elements/mayu-disconnected.html +134 -0
  36. data/lib/mayu/client/src/custom-elements/mayu-disconnected.ts +51 -0
  37. data/lib/mayu/client/src/custom-elements/mayu-exception.html +79 -0
  38. data/lib/mayu/client/src/custom-elements/mayu-exception.ts +28 -0
  39. data/lib/mayu/client/src/custom-elements/mayu-log.html +70 -0
  40. data/lib/mayu/client/src/custom-elements/mayu-log.ts +42 -0
  41. data/lib/mayu/client/src/custom-elements/mayu-ping.html +36 -0
  42. data/lib/mayu/client/src/custom-elements/mayu-ping.ts +53 -0
  43. data/lib/mayu/client/src/custom-elements/mayu-progress-bar.html +44 -0
  44. data/lib/mayu/client/src/custom-elements/mayu-progress-bar.ts +40 -0
  45. data/lib/mayu/client/src/custom-elements/types.d.ts +4 -0
  46. data/lib/mayu/client/src/global.d.ts +26 -0
  47. data/lib/mayu/client/src/h.ts +27 -0
  48. data/lib/mayu/client/src/logger.ts +56 -0
  49. data/lib/mayu/client/src/main.ts +271 -0
  50. data/lib/mayu/client/src/serializeEvent.ts +90 -0
  51. data/lib/mayu/client/src/stream.ts +175 -0
  52. data/lib/mayu/client/src/types.ts +1 -0
  53. data/lib/mayu/client/src/utils.ts +71 -0
  54. data/lib/mayu/client/tsconfig.json +18 -0
  55. data/lib/mayu/colors.rb +34 -0
  56. data/lib/mayu/commands/base.rb +22 -0
  57. data/lib/mayu/commands/build.rb +82 -0
  58. data/lib/mayu/commands.rb +53 -0
  59. data/lib/mayu/component/base.rb +177 -0
  60. data/lib/mayu/component/handler_ref.rb +99 -0
  61. data/lib/mayu/component/helpers.rb +93 -0
  62. data/lib/mayu/component/interface.rb +18 -0
  63. data/lib/mayu/component/wrapper.rb +165 -0
  64. data/lib/mayu/component.rb +54 -0
  65. data/lib/mayu/configuration.rb +195 -0
  66. data/lib/mayu/disable_sorbet.rb +23 -0
  67. data/lib/mayu/environment.rb +151 -0
  68. data/lib/mayu/event_stream.rb +158 -0
  69. data/lib/mayu/fetch.rb +88 -0
  70. data/lib/mayu/html.rb +53 -0
  71. data/lib/mayu/html.yaml +767 -0
  72. data/lib/mayu/message_cipher.rb +172 -0
  73. data/lib/mayu/message_cipher.test.rb +16 -0
  74. data/lib/mayu/metrics/collector.rb +161 -0
  75. data/lib/mayu/metrics/exporter.rb +47 -0
  76. data/lib/mayu/metrics/reporter.rb +187 -0
  77. data/lib/mayu/metrics.rb +82 -0
  78. data/lib/mayu/ref_counter.rb +57 -0
  79. data/lib/mayu/resources/README.md +14 -0
  80. data/lib/mayu/resources/asset.rb +71 -0
  81. data/lib/mayu/resources/assets.rb +76 -0
  82. data/lib/mayu/resources/dependency_graph.rb +306 -0
  83. data/lib/mayu/resources/dot_exporter.rb +167 -0
  84. data/lib/mayu/resources/generators/base.rb +18 -0
  85. data/lib/mayu/resources/generators/copy_file.rb +26 -0
  86. data/lib/mayu/resources/generators/image.rb +106 -0
  87. data/lib/mayu/resources/generators/write_file.rb +39 -0
  88. data/lib/mayu/resources/hot_swap/file_watcher.rb +69 -0
  89. data/lib/mayu/resources/hot_swap.rb +46 -0
  90. data/lib/mayu/resources/mermaid_exporter.rb +210 -0
  91. data/lib/mayu/resources/registry.rb +190 -0
  92. data/lib/mayu/resources/resolver/base.rb +32 -0
  93. data/lib/mayu/resources/resolver/filesystem.rb +94 -0
  94. data/lib/mayu/resources/resolver/static.rb +27 -0
  95. data/lib/mayu/resources/resolver.rb +13 -0
  96. data/lib/mayu/resources/resource.rb +150 -0
  97. data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.in.css +3 -0
  98. data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.out.css +6 -0
  99. data/lib/mayu/resources/transformers/__test__/css/attributes.in.css +3 -0
  100. data/lib/mayu/resources/transformers/__test__/css/attributes.out.css +6 -0
  101. data/lib/mayu/resources/transformers/__test__/css/composes.in.css +6 -0
  102. data/lib/mayu/resources/transformers/__test__/css/composes.out.css +10 -0
  103. data/lib/mayu/resources/transformers/__test__/css/element_selectors.in.css +3 -0
  104. data/lib/mayu/resources/transformers/__test__/css/element_selectors.out.css +6 -0
  105. data/lib/mayu/resources/transformers/__test__/css/has.in.css +7 -0
  106. data/lib/mayu/resources/transformers/__test__/css/has.out.css +10 -0
  107. data/lib/mayu/resources/transformers/__test__/css/media_queries.in.css +8 -0
  108. data/lib/mayu/resources/transformers/__test__/css/media_queries.out.css +12 -0
  109. data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.in.css +5 -0
  110. data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.out.css +6 -0
  111. data/lib/mayu/resources/transformers/__test__/haml/README.md +10 -0
  112. data/lib/mayu/resources/transformers/__test__/haml/case.haml +8 -0
  113. data/lib/mayu/resources/transformers/__test__/haml/case.rb +15 -0
  114. data/lib/mayu/resources/transformers/__test__/haml/class_names.haml +13 -0
  115. data/lib/mayu/resources/transformers/__test__/haml/class_names.rb +26 -0
  116. data/lib/mayu/resources/transformers/__test__/haml/comments.haml +5 -0
  117. data/lib/mayu/resources/transformers/__test__/haml/comments.rb +5 -0
  118. data/lib/mayu/resources/transformers/__test__/haml/css.haml +3 -0
  119. data/lib/mayu/resources/transformers/__test__/haml/css.rb +11 -0
  120. data/lib/mayu/resources/transformers/__test__/haml/dashes.haml +3 -0
  121. data/lib/mayu/resources/transformers/__test__/haml/dashes.rb +11 -0
  122. data/lib/mayu/resources/transformers/__test__/haml/early_return.haml +4 -0
  123. data/lib/mayu/resources/transformers/__test__/haml/early_return.rb +9 -0
  124. data/lib/mayu/resources/transformers/__test__/haml/early_return2.haml +3 -0
  125. data/lib/mayu/resources/transformers/__test__/haml/early_return2.rb +6 -0
  126. data/lib/mayu/resources/transformers/__test__/haml/handlers.haml +6 -0
  127. data/lib/mayu/resources/transformers/__test__/haml/handlers.rb +12 -0
  128. data/lib/mayu/resources/transformers/__test__/haml/if_else.haml +6 -0
  129. data/lib/mayu/resources/transformers/__test__/haml/if_else.rb +12 -0
  130. data/lib/mayu/resources/transformers/__test__/haml/interpolation.haml +8 -0
  131. data/lib/mayu/resources/transformers/__test__/haml/interpolation.rb +11 -0
  132. data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.haml +1 -0
  133. data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.rb +5 -0
  134. data/lib/mayu/resources/transformers/__test__/haml/props.haml +4 -0
  135. data/lib/mayu/resources/transformers/__test__/haml/props.rb +11 -0
  136. data/lib/mayu/resources/transformers/__test__/haml/slots.haml +5 -0
  137. data/lib/mayu/resources/transformers/__test__/haml/slots.rb +9 -0
  138. data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.haml +3 -0
  139. data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.rb +9 -0
  140. data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.haml +3 -0
  141. data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.rb +5 -0
  142. data/lib/mayu/resources/transformers/__test__/haml/spacing.haml +5 -0
  143. data/lib/mayu/resources/transformers/__test__/haml/spacing.rb +14 -0
  144. data/lib/mayu/resources/transformers/__test__/haml/spacing2.haml +10 -0
  145. data/lib/mayu/resources/transformers/__test__/haml/spacing2.rb +11 -0
  146. data/lib/mayu/resources/transformers/__test__/haml/spacing3.haml +3 -0
  147. data/lib/mayu/resources/transformers/__test__/haml/spacing3.rb +10 -0
  148. data/lib/mayu/resources/transformers/css/rouge_lexer.rb +841 -0
  149. data/lib/mayu/resources/transformers/css.rb +100 -0
  150. data/lib/mayu/resources/transformers/css.test.rb +87 -0
  151. data/lib/mayu/resources/transformers/haml.rb +984 -0
  152. data/lib/mayu/resources/transformers/haml.test.rb +114 -0
  153. data/lib/mayu/resources/types/README.md +36 -0
  154. data/lib/mayu/resources/types/base.rb +35 -0
  155. data/lib/mayu/resources/types/component.rb +198 -0
  156. data/lib/mayu/resources/types/image.rb +169 -0
  157. data/lib/mayu/resources/types/javascript.rb +50 -0
  158. data/lib/mayu/resources/types/nil.rb +23 -0
  159. data/lib/mayu/resources/types/stylesheet.rb +119 -0
  160. data/lib/mayu/resources/types/svg.rb +69 -0
  161. data/lib/mayu/resources/types.rb +37 -0
  162. data/lib/mayu/routes.rb +170 -0
  163. data/lib/mayu/routing/builder.rb +108 -0
  164. data/lib/mayu/routing/matcher.rb +58 -0
  165. data/lib/mayu/routing/routes.rb +85 -0
  166. data/lib/mayu/routing.rb +17 -0
  167. data/lib/mayu/server/app.rb +494 -0
  168. data/lib/mayu/server/controller.rb +152 -0
  169. data/lib/mayu/server/errors.rb +110 -0
  170. data/lib/mayu/server/file_server.rb +140 -0
  171. data/lib/mayu/server.rb +63 -0
  172. data/lib/mayu/session.rb +358 -0
  173. data/lib/mayu/state/README.md +6 -0
  174. data/lib/mayu/state/action_creator.rb +191 -0
  175. data/lib/mayu/state/action_wrapper.rb +30 -0
  176. data/lib/mayu/state/loader.rb +220 -0
  177. data/lib/mayu/state/store.rb +82 -0
  178. data/lib/mayu/state.rb +8 -0
  179. data/lib/mayu/state.test.rb +97 -0
  180. data/lib/mayu/utils.rb +114 -0
  181. data/lib/mayu/vdom/children.rb +117 -0
  182. data/lib/mayu/vdom/component_marshaler.rb +53 -0
  183. data/lib/mayu/vdom/css_attributes.rb +131 -0
  184. data/lib/mayu/vdom/descriptor.rb +151 -0
  185. data/lib/mayu/vdom/descriptor.test.rb +26 -0
  186. data/lib/mayu/vdom/dom.rb +239 -0
  187. data/lib/mayu/vdom/h.rb +22 -0
  188. data/lib/mayu/vdom/id_generator.rb +55 -0
  189. data/lib/mayu/vdom/interfaces.rb +186 -0
  190. data/lib/mayu/vdom/marshalling.rb +78 -0
  191. data/lib/mayu/vdom/reconciliation.rb +205 -0
  192. data/lib/mayu/vdom/reconciliation.test.rb +56 -0
  193. data/lib/mayu/vdom/special_elements.rb +108 -0
  194. data/lib/mayu/vdom/update_context.rb +180 -0
  195. data/lib/mayu/vdom/vdom.perf.test.rb +146 -0
  196. data/lib/mayu/vdom/vnode.rb +266 -0
  197. data/lib/mayu/vdom/vtree.rb +672 -0
  198. data/lib/mayu/vdom/vtree.test.rb +68 -0
  199. data/lib/mayu/vdom.rb +8 -0
  200. data/lib/mayu/vdom.test.rb +73 -0
  201. data/lib/mayu/version.rb +6 -0
  202. data/lib/mayu.rb +8 -0
  203. data/mayu-live.gemspec +70 -0
  204. metadata +612 -0
@@ -0,0 +1,172 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "time"
6
+ require "digest/sha2"
7
+ require "openssl"
8
+ require "securerandom"
9
+ require "brotli"
10
+
11
+ module Mayu
12
+ class MessageCipher
13
+ extend T::Sig
14
+
15
+ DEFAULT_TTL_SECONDS = T.let(10, Integer)
16
+
17
+ Message = T.type_alias { { iss: Float, exp: Float, payload: T.untyped } }
18
+
19
+ class Error < StandardError
20
+ end
21
+ class ExpiredError < Error
22
+ end
23
+ class IssuedInTheFutureError < Error
24
+ end
25
+ class EncryptError < Error
26
+ end
27
+ class DecryptError < Error
28
+ end
29
+ class InvalidHMACError < Error
30
+ end
31
+
32
+ sig { params(key: String, ttl: Integer).void }
33
+ def initialize(key:, ttl: DEFAULT_TTL_SECONDS)
34
+ raise ArgumentError, "ttl must be positive" unless ttl.positive?
35
+ @default_ttl_seconds = ttl
36
+ @key = T.let(Digest::SHA256.digest(key), String)
37
+ end
38
+
39
+ sig do
40
+ params(payload: T.untyped, auth_data: String, ttl: Integer).returns(
41
+ String
42
+ )
43
+ end
44
+ def dump(payload, auth_data: "", ttl: @default_ttl_seconds)
45
+ raise ArgumentError, "ttl must be positive" unless ttl.positive?
46
+ now = Time.now.to_f
47
+ message = { iss: now, exp: now + ttl, payload: Marshal.dump(payload) }
48
+ encode_message(message, auth_data:)
49
+ end
50
+
51
+ sig { params(data: String, auth_data: String).returns(T.untyped) }
52
+ def load(data, auth_data: "")
53
+ Marshal.load(decode_message(data, auth_data:))
54
+ end
55
+
56
+ private
57
+
58
+ sig { params(message: Message, auth_data: String).returns(String) }
59
+ def encode_message(message, auth_data: "")
60
+ message
61
+ .then { Marshal.dump(_1) }
62
+ .then { prepend_hmac(_1) }
63
+ .then { Brotli.deflate(_1) }
64
+ .then { encrypt(_1, auth_data:) }
65
+ end
66
+
67
+ sig { params(message: String, auth_data: String).returns(String) }
68
+ def decode_message(message, auth_data: "")
69
+ message
70
+ .then { decrypt(_1, auth_data:) }
71
+ .then { Brotli.inflate(_1) }
72
+ .then { validate_hmac(_1) }
73
+ .then { Marshal.load(_1) }
74
+ .tap { validate_times(_1) }
75
+ .fetch(:payload)
76
+ end
77
+
78
+ sig { params(input: String).returns(String) }
79
+ def prepend_hmac(input)
80
+ hmac = Digest::SHA256.digest(input)
81
+ input.prepend(hmac)
82
+ end
83
+
84
+ sig { params(input: String).returns(String) }
85
+ def validate_hmac(input)
86
+ hmac, message = input.unpack("a32 a*")
87
+
88
+ unless OpenSSL.fixed_length_secure_compare(
89
+ hmac,
90
+ Digest::SHA256.digest(message.to_s)
91
+ )
92
+ raise InvalidHMACError
93
+ end
94
+
95
+ message.to_s
96
+ end
97
+
98
+ sig { params(message: { iss: Float, exp: Float, payload: String }).void }
99
+ def validate_times(message)
100
+ message => { iss:, exp: }
101
+ now = Time.now.to_f
102
+ validate_iss(now, iss)
103
+ validate_exp(now, exp)
104
+ end
105
+
106
+ sig { params(now: Float, iss: Float).void }
107
+ def validate_iss(now, iss)
108
+ return if iss < now
109
+
110
+ raise IssuedInTheFutureError,
111
+ "The message was issued at #{Time.at(iss).iso8601}, which is in the future"
112
+ end
113
+
114
+ sig { params(now: Float, exp: Float).void }
115
+ def validate_exp(now, exp)
116
+ return if exp > now
117
+
118
+ raise ExpiredError,
119
+ "The message expired at #{Time.at(exp).iso8601}, which is in the past"
120
+ end
121
+
122
+ sig { params(message: String, auth_data: String).returns(String) }
123
+ def encrypt(message, auth_data: "")
124
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
125
+ cipher.encrypt
126
+ salt = SecureRandom.random_bytes(8)
127
+ cipher.key = generate_key(salt)
128
+ cipher.iv = iv = cipher.random_iv
129
+ cipher.auth_data = auth_data
130
+ cipher_text = cipher.update(message) + cipher.final
131
+ auth_tag = cipher.auth_tag
132
+ [auth_tag.bytesize, auth_tag, salt, iv, cipher_text].pack("C a* a* a* a*")
133
+ rescue OpenSSL::Cipher::CipherError
134
+ raise EncryptError
135
+ end
136
+
137
+ sig { params(data: String, auth_data: String).returns(String) }
138
+ def decrypt(data, auth_data: "")
139
+ data.unpack("C a*") => [Integer => auth_tag_len, String => data]
140
+
141
+ data.unpack("a#{auth_tag_len} a8 a12 a*") => [
142
+ auth_tag,
143
+ salt,
144
+ iv,
145
+ cipher_text
146
+ ]
147
+
148
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
149
+ cipher.iv = iv
150
+ cipher.key = generate_key(salt)
151
+ cipher.auth_data = auth_data
152
+ cipher.auth_tag = auth_tag
153
+ cipher.update(cipher_text) + cipher.final
154
+ rescue NoMatchingPatternError
155
+ raise DecryptError
156
+ rescue OpenSSL::Cipher::CipherError
157
+ raise DecryptError
158
+ end
159
+
160
+ sig { params(salt: String).returns(String) }
161
+ def generate_key(salt)
162
+ OpenSSL::KDF.scrypt(
163
+ @key,
164
+ salt:, # Salt.
165
+ N: 2**14, # CPU/memory cost parameter. This must be a power of 2.
166
+ r: 8, # Block size parameter.
167
+ p: 1, # Parallelization parameter
168
+ length: 32 # Length in octets of the derived key
169
+ )
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,16 @@
1
+ # typed: true
2
+
3
+ require "minitest/autorun"
4
+ require "test_helper"
5
+
6
+ require_relative "message_cipher"
7
+
8
+ class TestMessageCipher < Minitest::Test
9
+ def test_dump_and_load
10
+ message_cipher = Mayu::MessageCipher.new(key: "test")
11
+
12
+ dumped = message_cipher.dump("hello")
13
+ loaded = message_cipher.load(dumped)
14
+ assert(loaded == "hello")
15
+ end
16
+ end
@@ -0,0 +1,161 @@
1
+ # typed: strict
2
+
3
+ module Mayu
4
+ module Metrics
5
+ module Collector
6
+ class DataStore
7
+ class MetricStore
8
+ extend T::Sig
9
+
10
+ sig { returns(DataStore) }
11
+ attr_reader :store
12
+
13
+ sig do
14
+ params(
15
+ store: DataStore,
16
+ metric_name: Symbol,
17
+ metric_type: Symbol,
18
+ metric_settings: T::Hash[Symbol, T.untyped]
19
+ ).void
20
+ end
21
+ def initialize(store, metric_name, metric_type:, metric_settings: {})
22
+ @store = store
23
+ @metric_name = metric_name
24
+ @metric_type = metric_type
25
+ @metric_settings = metric_settings
26
+ @aggregation_mode =
27
+ T.let(metric_settings.fetch(:aggregation, :sum), Symbol)
28
+ end
29
+
30
+ sig do
31
+ params(
32
+ val: T.any(Integer, Float),
33
+ labels: T::Hash[Symbol, T.untyped]
34
+ ).void
35
+ end
36
+ def set(val:, labels: {})
37
+ end
38
+
39
+ sig { returns(T::Hash[T::Array[Symbol], Float]) }
40
+ def all_values
41
+ @store
42
+ .values_for_metric(@metric_name)
43
+ .transform_values { aggregate(_1) }
44
+ end
45
+
46
+ private
47
+
48
+ sig { params(values: T::Array[Float]).returns(Float) }
49
+ def aggregate(values)
50
+ case @aggregation_mode
51
+ when :min
52
+ values.min
53
+ when :max
54
+ values.max
55
+ when :sum
56
+ values.sum
57
+ else
58
+ raise "Invalid aggregation setting"
59
+ end.to_f
60
+ end
61
+ end
62
+
63
+ extend T::Sig
64
+
65
+ sig { returns(InternalStore) }
66
+ attr_reader :internal_store
67
+
68
+ sig { params(internal_store: InternalStore).void }
69
+ def initialize(internal_store = {})
70
+ @internal_store = internal_store
71
+ end
72
+
73
+ sig do
74
+ params(metric_name: Symbol).returns(
75
+ T::Hash[T::Array[Symbol], T::Array[Float]]
76
+ )
77
+ end
78
+ def values_for_metric(metric_name)
79
+ @internal_store
80
+ .values
81
+ .map { _1[metric_name] }
82
+ .compact
83
+ .each_with_object(Hash.new { |h, k| h[k] = [] }) do |entries, obj|
84
+ entries.each { |labels, value| obj[labels] << value }
85
+ end
86
+ end
87
+
88
+ sig do
89
+ params(
90
+ metric_name: Symbol,
91
+ metric_type: Symbol,
92
+ metric_settings: T::Hash[Symbol, T.untyped]
93
+ ).returns(MetricStore)
94
+ end
95
+ def for_metric(metric_name, metric_type:, metric_settings: {})
96
+ MetricStore.new(self, metric_name, metric_type:, metric_settings: {})
97
+ end
98
+ end
99
+
100
+ class Server
101
+ extend T::Sig
102
+
103
+ sig { returns(Async::IO::Endpoint) }
104
+ attr_reader :endpoint
105
+
106
+ sig { params(endpoint: Async::IO::UNIXEndpoint).void }
107
+ def initialize(endpoint)
108
+ @endpoint = endpoint
109
+ end
110
+
111
+ sig { params(metric_name: Symbol).void }
112
+ def all_values(metric_name)
113
+ raise NotImplementedError, "Should this even be implemented?"
114
+ end
115
+
116
+ sig { void }
117
+ def start
118
+ end
119
+
120
+ sig { void }
121
+ def stop
122
+ end
123
+
124
+ sig do
125
+ params(
126
+ internal_store: InternalStore,
127
+ name: String,
128
+ restart: T::Boolean
129
+ ).void
130
+ end
131
+ def run(internal_store, name: self.class.name.to_s, restart: true)
132
+ wrapper = Wrapper.new
133
+
134
+ Console.logger.info(
135
+ self,
136
+ "Starting metrics collection on #{File.expand_path(@endpoint.path)}"
137
+ )
138
+
139
+ @endpoint.accept do |peer|
140
+ store = internal_store.store(peer, {})
141
+
142
+ unpacker = wrapper.unpacker(peer)
143
+
144
+ unpacker.each do |message|
145
+ case message
146
+ in [:store, data]
147
+ store.merge!(data)
148
+ else
149
+ Console
150
+ .logger
151
+ .warn(self) { "Unhandled mesage: #{message.inspect}" }
152
+ end
153
+ end
154
+ ensure
155
+ internal_store.delete(peer)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,47 @@
1
+ # typed: strict
2
+
3
+ module Mayu
4
+ module Metrics
5
+ class Exporter
6
+ class Server
7
+ extend T::Sig
8
+
9
+ sig do
10
+ params(
11
+ endpoint: Async::HTTP::Endpoint,
12
+ registry: Prometheus::Client::Registry
13
+ ).returns(Async::HTTP::Server)
14
+ end
15
+ def self.setup(endpoint:, registry:)
16
+ Console.logger.info(
17
+ self,
18
+ "Starting metrics exporter on #{endpoint.to_url}"
19
+ )
20
+
21
+ Async::HTTP::Server.for(
22
+ endpoint,
23
+ protocol: Async::HTTP::Protocol::HTTP11
24
+ ) do |request|
25
+ if request.path == "/favicon.ico"
26
+ next(
27
+ Protocol::HTTP::Response[
28
+ 404,
29
+ { "content-type": "text/plain" },
30
+ ["Not found"]
31
+ ]
32
+ )
33
+ end
34
+
35
+ body = Prometheus::Client::Formats::Text.marshal(registry)
36
+
37
+ Protocol::HTTP::Response[
38
+ 200,
39
+ { "content-type": "text/plain" },
40
+ [body]
41
+ ]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,187 @@
1
+ # typed: strict
2
+
3
+ module Mayu
4
+ module Metrics
5
+ module Reporter
6
+ extend T::Sig
7
+
8
+ sig do
9
+ type_parameters(:M)
10
+ .params(
11
+ collector_endpoint: Async::IO::UNIXEndpoint,
12
+ block:
13
+ T
14
+ .proc
15
+ .params(arg0: Prometheus::Client::Registry)
16
+ .returns(T.type_parameter(:M))
17
+ )
18
+ .returns(T.type_parameter(:M))
19
+ end
20
+ def self.run(collector_endpoint, &block)
21
+ data_store = DataStore.new
22
+ Prometheus::Client.config.data_store = data_store
23
+ metrics = yield(Prometheus::Client::Registry.new)
24
+ Client.connect_and_sync(collector_endpoint:, data_store:, interval: 1)
25
+ metrics
26
+ end
27
+
28
+ class Client
29
+ extend T::Sig
30
+
31
+ sig do
32
+ params(
33
+ collector_endpoint: Async::IO::UNIXEndpoint,
34
+ block: T.proc.params(arg0: Client).void
35
+ ).void
36
+ end
37
+ def self.connect(collector_endpoint, &block)
38
+ Console.logger.info(
39
+ self,
40
+ "Connecting to #{File.expand_path(collector_endpoint.path)}"
41
+ )
42
+
43
+ collector_endpoint.connect { |peer| yield new(peer) }
44
+ end
45
+
46
+ sig do
47
+ params(
48
+ collector_endpoint: Async::IO::UNIXEndpoint,
49
+ data_store: DataStore,
50
+ interval: Integer,
51
+ task: Async::Task
52
+ ).returns(Async::Task)
53
+ end
54
+ def self.connect_and_sync(
55
+ collector_endpoint:,
56
+ data_store:,
57
+ interval: 1,
58
+ task: Async::Task.current
59
+ )
60
+ task.async do
61
+ connect(collector_endpoint) do |client|
62
+ loop do
63
+ client.sync(data_store)
64
+ sleep(interval)
65
+ end
66
+ end
67
+ rescue Errno::EPIPE
68
+ Console.logger.error(self, "Broken pipe")
69
+ rescue Errno::ECONNREFUSED
70
+ Console.logger.error(self, "Connection refused")
71
+ end
72
+ end
73
+
74
+ sig { params(peer: Async::IO::Peer).void }
75
+ def initialize(peer)
76
+ wrapper = Wrapper.new
77
+ @packer = T.let(wrapper.packer(peer), MessagePack::Packer)
78
+ end
79
+
80
+ sig { params(data_store: DataStore, task: Async::Task).void }
81
+ def sync(data_store, task: Async::Task.current)
82
+ send(:store, data_store.store)
83
+ end
84
+
85
+ private
86
+
87
+ sig { params(args: T.untyped).void }
88
+ def send(*args)
89
+ @packer.write(args)
90
+ @packer.flush
91
+ end
92
+ end
93
+
94
+ class DataStore
95
+ class MetricStore
96
+ extend T::Sig
97
+
98
+ sig do
99
+ params(
100
+ store: ValueHash,
101
+ metric_name: Symbol,
102
+ metric_type: Symbol,
103
+ metric_settings: T::Hash[Symbol, T.untyped]
104
+ ).void
105
+ end
106
+ def initialize(store, metric_name:, metric_type:, metric_settings:)
107
+ @store = store
108
+ @metric_name = metric_name
109
+ @semaphore = T.let(Async::Semaphore.new, Async::Semaphore)
110
+ end
111
+
112
+ sig do
113
+ type_parameters(:T)
114
+ .params(block: T.proc.returns(T.type_parameter(:T)))
115
+ .returns(T.type_parameter(:T))
116
+ end
117
+ def synchronize(&block)
118
+ @semaphore.async { yield }.wait
119
+ end
120
+
121
+ sig do
122
+ params(
123
+ val: T.any(Integer, Float),
124
+ labels: T::Hash[Symbol, T.untyped]
125
+ ).void
126
+ end
127
+ def set(val:, labels: {})
128
+ @store.store(labels, val.to_f)
129
+ end
130
+
131
+ sig do
132
+ params(
133
+ by: T.any(Integer, Float),
134
+ labels: T::Hash[Symbol, T.untyped]
135
+ ).void
136
+ end
137
+ def increment(by: 1, labels: {})
138
+ @store.store(labels, @store.fetch(labels, 0.0) + by.to_f)
139
+ end
140
+
141
+ sig { params(labels: T::Hash[Symbol, T.untyped]).void }
142
+ def get(labels:)
143
+ @store.fetch(labels)
144
+ end
145
+ end
146
+
147
+ extend T::Sig
148
+
149
+ sig { returns(MetricHash) }
150
+ attr_reader :store
151
+
152
+ sig { void }
153
+ def initialize
154
+ @store = T.let(init_metric_hash, MetricHash)
155
+ end
156
+
157
+ sig do
158
+ params(
159
+ metric_name: Symbol,
160
+ metric_type: Symbol,
161
+ metric_settings: T::Hash[Symbol, T.untyped]
162
+ ).returns(MetricStore)
163
+ end
164
+ def for_metric(metric_name, metric_type:, metric_settings: {})
165
+ MetricStore.new(
166
+ T.must(@store[metric_name]),
167
+ metric_name:,
168
+ metric_type:,
169
+ metric_settings:
170
+ )
171
+ end
172
+
173
+ private
174
+
175
+ sig { returns(MetricHash) }
176
+ def init_metric_hash
177
+ Hash.new { |hash, metric_name| hash[metric_name] = init_value_hash }
178
+ end
179
+
180
+ sig { returns(ValueHash) }
181
+ def init_value_hash
182
+ Hash.new { |hash, labels| hash[labels] = 0 }
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,82 @@
1
+ # typed: strict
2
+
3
+ require "bundler/setup"
4
+ require "async"
5
+ require "async/container"
6
+ require "async/semaphore"
7
+ require "async/http"
8
+ require "async/io/unix_endpoint"
9
+ require "async/io/shared_endpoint"
10
+ require "msgpack"
11
+ require "nanoid"
12
+ require "prometheus/client"
13
+ require "prometheus/client/formats/text"
14
+
15
+ require_relative "metrics/collector"
16
+ require_relative "metrics/exporter"
17
+ require_relative "metrics/reporter"
18
+
19
+ module Mayu
20
+ module Metrics
21
+ InternalStore = T.type_alias { T::Hash[Symbol, MetricHash] }
22
+ MetricHash = T.type_alias { T::Hash[Symbol, ValueHash] }
23
+ ValueHash = T.type_alias { T::Hash[LabelsHash, Float] }
24
+ LabelsHash = T.type_alias { T::Hash[Symbol, T.untyped] }
25
+
26
+ class Wrapper < MessagePack::Factory
27
+ extend T::Sig
28
+
29
+ sig { void }
30
+ def initialize
31
+ super()
32
+
33
+ self.register_type(0x01, Symbol)
34
+ end
35
+ end
36
+
37
+ extend T::Sig
38
+
39
+ sig do
40
+ params(
41
+ container: Async::Container::Generic,
42
+ exporter_endpoint: Async::HTTP::Endpoint,
43
+ collector_endpoint: Async::IO::UNIXEndpoint,
44
+ block: T.proc.params(arg0: Prometheus::Client::Registry).void
45
+ ).void
46
+ end
47
+ def self.start_collect_and_export(
48
+ container,
49
+ exporter_endpoint:,
50
+ collector_endpoint:,
51
+ &block
52
+ )
53
+ collector = Metrics::Collector::Server.new(collector_endpoint)
54
+ collector.start
55
+
56
+ container.spawn(
57
+ name: "Metrics collector/exporter",
58
+ restart: true
59
+ ) do |instance|
60
+ Async do
61
+ internal_store = {}
62
+
63
+ Prometheus::Client.config.data_store =
64
+ Metrics::Collector::DataStore.new(internal_store)
65
+
66
+ registry = Prometheus::Client::Registry.new
67
+
68
+ yield registry
69
+
70
+ Metrics::Exporter::Server.setup(
71
+ endpoint: exporter_endpoint,
72
+ registry:
73
+ ).run
74
+
75
+ collector.run(internal_store)
76
+
77
+ instance.ready!
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end