atatus 1.4.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/LICENSE +1 -1
- data/lib/atatus/collector/base.rb +118 -33
- data/lib/atatus/collector/builder.rb +91 -4
- data/lib/atatus/collector/layer.rb +1 -0
- data/lib/atatus/collector/transport.rb +20 -0
- data/lib/atatus/error_builder.rb +4 -0
- data/lib/atatus/version.rb +1 -1
- data/lib/atatus.rb +8 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48a2777c09d7af196314fb322c9a55feadb46f50d90ffd88976ff978eb5039a2
|
4
|
+
data.tar.gz: 59f1f63ebd4ad84f23359cc2c96ccb62e0c0eabceaa6b48bf63c711c3383bf64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a705d56af3228c31ce400a16eae3d961fe2bbcae949db3effc4828682563bd4ce3070adb14613cb096caa48d71e8ccce2969dc21f4b0c910c73ca99b28bf566
|
7
|
+
data.tar.gz: 30461593c406cf6f7b8f2e08c9bef4aa64b9f60f0f4c2b3f0bff9de1cff8a26f5cf25e07507b263e8638a37345916f15d479eb11bcb3e58f69a7f34d79d51d70
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
|
5
5
|
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## 1.6.2 (Mon, 31 Jan 2022)
|
8
|
+
|
9
|
+
- Fixed config environment.
|
10
|
+
- Supported ignore failure patterns, ignore exceptions and ignore transactions.
|
11
|
+
|
12
|
+
|
13
|
+
## 1.6.1 (Wed, 29 Dec 2021)
|
14
|
+
|
15
|
+
- Fixed a specific case where span duration goes less than 0.
|
16
|
+
|
17
|
+
|
18
|
+
## 1.6.0 (Fri, 24 Dec 2021)
|
19
|
+
|
20
|
+
- Added support for set custom data.
|
21
|
+
- Fixed the span timing issue.
|
22
|
+
|
23
|
+
|
24
|
+
## 1.5.0 (Thu, 27 May 2021)
|
25
|
+
|
26
|
+
- Fixed issue in status code conversion.
|
27
|
+
|
7
28
|
|
8
29
|
## 1.4.0 (Tue, 15 Sept 2020)
|
9
30
|
|
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
All components of this product are Copyright (c)
|
1
|
+
All components of this product are Copyright (c) 2022 Atatus. All rights reserved.
|
2
2
|
|
3
3
|
Except as otherwise expressly provided in this Agreement,
|
4
4
|
End User shall not (and shall not permit any third party to):
|
@@ -7,7 +7,7 @@ require 'atatus/collector/hist'
|
|
7
7
|
require 'atatus/collector/layer'
|
8
8
|
require 'atatus/collector/transport'
|
9
9
|
|
10
|
-
SpanTiming = Struct.new(:start, :end)
|
10
|
+
SpanTiming = Struct.new(:name, :type, :subtype, :start, :end, :duration, :id, :transaction_id)
|
11
11
|
|
12
12
|
module Atatus
|
13
13
|
module Collector
|
@@ -42,6 +42,7 @@ module Atatus
|
|
42
42
|
@metrics_lock = Mutex.new
|
43
43
|
@metrics_agg = []
|
44
44
|
|
45
|
+
@hostinfo_response = {}
|
45
46
|
@transport = Atatus::BaseTransport.new(config)
|
46
47
|
@collect_counter = 0
|
47
48
|
@running = false
|
@@ -79,13 +80,38 @@ module Atatus
|
|
79
80
|
|
80
81
|
def add_error(error)
|
81
82
|
ensure_worker_running
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
83
|
+
ignore_error = false
|
84
|
+
if
|
85
|
+
!error.exception.nil? &&
|
86
|
+
!error.exception.type.nil? &&
|
87
|
+
!error.exception.message.nil?
|
88
|
+
then
|
89
|
+
if @hostinfo_response.key?("ignoreExceptionPatterns")
|
90
|
+
@hostinfo_response['ignoreExceptionPatterns'].each do |k, v|
|
91
|
+
if error.exception.type.match(k)
|
92
|
+
exception_values = v
|
93
|
+
if exception_values.length == 0
|
94
|
+
ignore_error = true
|
95
|
+
else
|
96
|
+
exception_values.each do |value|
|
97
|
+
if error.exception.message.include?(value)
|
98
|
+
ignore_error = true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
break
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
if ignore_error == false
|
108
|
+
@errors_lock.synchronize do
|
109
|
+
if @errors_aggs.length < 20
|
110
|
+
@errors_aggs.push(error)
|
111
|
+
else
|
112
|
+
i = rand(20)
|
113
|
+
@errors_aggs[i] = error
|
114
|
+
end
|
89
115
|
end
|
90
116
|
end
|
91
117
|
end
|
@@ -156,6 +182,19 @@ module Atatus
|
|
156
182
|
return
|
157
183
|
end
|
158
184
|
|
185
|
+
ignore_txn = false
|
186
|
+
if @hostinfo_response.key?("ignoreTxnNamePatterns")
|
187
|
+
@hostinfo_response['ignoreTxnNamePatterns'].each do |k|
|
188
|
+
if txn.name.match(k)
|
189
|
+
ignore_txn = true
|
190
|
+
break
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
if ignore_txn == true
|
195
|
+
return
|
196
|
+
end
|
197
|
+
|
159
198
|
return if txn.name.empty?
|
160
199
|
return if txn.duration <= 0
|
161
200
|
|
@@ -204,16 +243,7 @@ module Atatus
|
|
204
243
|
|
205
244
|
if span.timestamp >= txn.timestamp
|
206
245
|
start = Util.ms(span.timestamp - txn.timestamp)
|
207
|
-
spans_tuple.push(SpanTiming.new(start, start + Util.ms(span.duration)))
|
208
|
-
if !@txns_agg[txn.name].spans.key?(span.name)
|
209
|
-
kind = Layer.span_kind(span.type)
|
210
|
-
type = Layer.span_type(span.subtype)
|
211
|
-
@txns_agg[txn.name].spans[span.name] = Layer.new(type, kind, span.duration)
|
212
|
-
@txns_agg[txn.name].spans[span.name].id = span.id
|
213
|
-
@txns_agg[txn.name].spans[span.name].pid = span.transaction_id
|
214
|
-
else
|
215
|
-
@txns_agg[txn.name].spans[span.name].aggregate! span.duration
|
216
|
-
end
|
246
|
+
spans_tuple.push(SpanTiming.new(span.name, span.type, span.subtype, start, start + Util.ms(span.duration), span.duration, span.id, span.transaction_id))
|
217
247
|
end
|
218
248
|
end
|
219
249
|
end
|
@@ -222,6 +252,40 @@ module Atatus
|
|
222
252
|
ruby_time = Util.ms(txn.duration)
|
223
253
|
else
|
224
254
|
spans_tuple.sort! {| a, b | a[:start] <=> b[:start] }
|
255
|
+
j = 0
|
256
|
+
while j < spans_tuple.length
|
257
|
+
if spans_tuple[j].subtype == 'controller' || spans_tuple[j].subtype == 'view' || spans_tuple[j].subtype == 'tilt'
|
258
|
+
k = j+1
|
259
|
+
while k < spans_tuple.length
|
260
|
+
if spans_tuple[k].start >= spans_tuple[j].end
|
261
|
+
break
|
262
|
+
else
|
263
|
+
if spans_tuple[k].end <= spans_tuple[j].end
|
264
|
+
spans_tuple[j].duration -= Util.us(spans_tuple[k].end - spans_tuple[k].start)
|
265
|
+
else
|
266
|
+
spans_tuple[j].duration -= Util.us(spans_tuple[j].end - spans_tuple[k].start)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
k += 1
|
270
|
+
end
|
271
|
+
if spans_tuple[j].duration <= 0
|
272
|
+
spans_tuple[j].duration = 1
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
if !@txns_agg[txn.name].spans.key?(spans_tuple[j].name)
|
277
|
+
kind = Layer.span_kind(spans_tuple[j].type)
|
278
|
+
type = Layer.span_type(spans_tuple[j].subtype)
|
279
|
+
@txns_agg[txn.name].spans[spans_tuple[j].name] = Layer.new(type, kind, spans_tuple[j].duration)
|
280
|
+
@txns_agg[txn.name].spans[spans_tuple[j].name].id = spans_tuple[j].id
|
281
|
+
@txns_agg[txn.name].spans[spans_tuple[j].name].pid = spans_tuple[j].transaction_id
|
282
|
+
else
|
283
|
+
@txns_agg[txn.name].spans[spans_tuple[j].name].aggregate! spans_tuple[j].duration
|
284
|
+
end
|
285
|
+
|
286
|
+
j += 1
|
287
|
+
end
|
288
|
+
|
225
289
|
ruby_time = spans_tuple[0].start
|
226
290
|
span_end = spans_tuple[0].end
|
227
291
|
j = 0
|
@@ -270,24 +334,45 @@ module Atatus
|
|
270
334
|
!txn.context.response.nil? &&
|
271
335
|
!txn.context.response.status_code.nil?
|
272
336
|
then
|
273
|
-
status_code = txn.context.response.status_code
|
274
|
-
|
337
|
+
status_code = txn.context.response.status_code.to_i
|
338
|
+
ignore_status_code = false
|
275
339
|
if status_code >= 400 && status_code != 404
|
276
|
-
if
|
277
|
-
@
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
340
|
+
if @hostinfo_response.key?("ignoreHTTPFailurePatterns")
|
341
|
+
@hostinfo_response['ignoreHTTPFailurePatterns'].each do |k, v|
|
342
|
+
if txn.name.match(k)
|
343
|
+
status_code_array_s = v
|
344
|
+
if status_code_array_s.length == 0
|
345
|
+
ignore_status_code = true
|
346
|
+
else
|
347
|
+
status_code_array_s.each do |code|
|
348
|
+
if code == txn.context.response.status_code.to_s
|
349
|
+
ignore_status_code = true
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
break
|
354
|
+
end
|
283
355
|
end
|
284
356
|
end
|
357
|
+
if ignore_status_code == false
|
358
|
+
if !@error_metrics_agg.key?(txn.name)
|
359
|
+
@error_metrics_agg[txn.name] = {status_code => 1}
|
360
|
+
else
|
361
|
+
if !@error_metrics_agg[txn.name].key?(status_code)
|
362
|
+
@error_metrics_agg[txn.name][status_code] = 1
|
363
|
+
else
|
364
|
+
@error_metrics_agg[txn.name][status_code] += 1
|
365
|
+
end
|
366
|
+
end
|
285
367
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
368
|
+
trace_id = ""
|
369
|
+
trace_id = txn.trace_id if !txn.trace_id.nil?
|
370
|
+
if @error_requests_agg.length < 20
|
371
|
+
@error_requests_agg.push({'name' => txn.name, 'txn_id' => txn.id, 'trace_id' => trace_id, 'context' => txn.context})
|
372
|
+
else
|
373
|
+
i = rand(20)
|
374
|
+
@error_requests_agg[i] = {'name' => txn.name, 'txn_id' => txn.id, 'trace_id' => trace_id, 'context' => txn.context}
|
375
|
+
end
|
291
376
|
end
|
292
377
|
end
|
293
378
|
end
|
@@ -329,7 +414,7 @@ module Atatus
|
|
329
414
|
end
|
330
415
|
|
331
416
|
if @collect_counter % 30 == 0
|
332
|
-
@transport.hostinfo(start_time)
|
417
|
+
@hostinfo_response = @transport.hostinfo(start_time)
|
333
418
|
@collect_counter = 0
|
334
419
|
end
|
335
420
|
@collect_counter += 1
|
@@ -29,7 +29,8 @@ module Atatus
|
|
29
29
|
version: VERSION
|
30
30
|
},
|
31
31
|
hostname: @metadata.hostname,
|
32
|
-
hostId: @metadata.hwinfo.hostid
|
32
|
+
hostId: @metadata.hwinfo.hostid,
|
33
|
+
releaseStage: @config.environment
|
33
34
|
}
|
34
35
|
if !@container_id.nil?
|
35
36
|
common[:containerId] = @container_id
|
@@ -195,7 +196,7 @@ module Atatus
|
|
195
196
|
|
196
197
|
if !context.response.nil?
|
197
198
|
if !context.response.status_code.nil?
|
198
|
-
request[:statusCode] = context.response.status_code
|
199
|
+
request[:statusCode] = context.response.status_code.to_i
|
199
200
|
end
|
200
201
|
end
|
201
202
|
|
@@ -237,11 +238,17 @@ module Atatus
|
|
237
238
|
then
|
238
239
|
next
|
239
240
|
end
|
241
|
+
txn_id = ""
|
242
|
+
txn_id = txn.id if !txn.id.nil?
|
243
|
+
trace_id = ""
|
244
|
+
trace_id = txn.trace_id if !txn.trace_id.nil?
|
240
245
|
|
241
246
|
trace = {}
|
242
247
|
trace[:name] = txn.name
|
243
248
|
trace[:type] = @config.framework_name || AGENT_NAME
|
244
249
|
trace[:kind] = AGENT_NAME
|
250
|
+
trace[:txnId] = txn_id
|
251
|
+
trace[:traceId] = trace_id
|
245
252
|
trace[:start] = txn.timestamp
|
246
253
|
trace[:duration] = Util.ms(txn.duration)
|
247
254
|
if !txn.context.nil?
|
@@ -249,6 +256,28 @@ module Atatus
|
|
249
256
|
end
|
250
257
|
trace[:entries] = []
|
251
258
|
trace[:funcs] = []
|
259
|
+
|
260
|
+
if
|
261
|
+
!txn.context.nil? &&
|
262
|
+
defined?(txn.context.custom) &&
|
263
|
+
!txn.context.custom.nil? &&
|
264
|
+
!txn.context.custom.empty?
|
265
|
+
then
|
266
|
+
trace[:customData] = txn.context.custom
|
267
|
+
end
|
268
|
+
|
269
|
+
if
|
270
|
+
!txn.context.nil? &&
|
271
|
+
defined?(txn.context.user) &&
|
272
|
+
!txn.context.user.nil? &&
|
273
|
+
!txn.context.user.empty?
|
274
|
+
then
|
275
|
+
trace[:user] = {}
|
276
|
+
trace[:user][:id] = txn.context.user.id
|
277
|
+
trace[:user][:email] = txn.context.user.email
|
278
|
+
trace[:user][:username] = txn.context.user.username
|
279
|
+
end
|
280
|
+
|
252
281
|
i = 0
|
253
282
|
|
254
283
|
if
|
@@ -286,7 +315,7 @@ module Atatus
|
|
286
315
|
entry[:dt] = {}
|
287
316
|
entry[:dt][:url] = span.context.http.url
|
288
317
|
entry[:dt][:method] = span.context.http.method if defined?(span.context.http.method) && !span.context.http.method.nil?
|
289
|
-
entry[:dt][:status_code] = span.context.http.status_code if defined?(span.context.http.status_code) && !span.context.http.status_code.nil?
|
318
|
+
entry[:dt][:status_code] = span.context.http.status_code.to_s if defined?(span.context.http.status_code) && !span.context.http.status_code.nil?
|
290
319
|
end
|
291
320
|
trace[:entries] << entry
|
292
321
|
func_index = trace[:funcs].index(span.name)
|
@@ -347,11 +376,39 @@ module Atatus
|
|
347
376
|
then
|
348
377
|
next
|
349
378
|
end
|
379
|
+
txn_id = ""
|
380
|
+
txn_id = v['txn_id'] if v.key?('txn_id')
|
381
|
+
trace_id = ""
|
382
|
+
trace_id = v['trace_id'] if v.key?('trace_id')
|
350
383
|
error_request = {}
|
351
384
|
error_request[:name] = v['name']
|
352
385
|
error_request[:type] = @config.framework_name || AGENT_NAME
|
353
386
|
error_request[:kind] = AGENT_NAME
|
387
|
+
error_request[:txnId] = txn_id
|
388
|
+
error_request[:traceId] = trace_id
|
354
389
|
error_request[:request] = build_request(v['context'])
|
390
|
+
|
391
|
+
if
|
392
|
+
!v['context'].nil? &&
|
393
|
+
defined?(v['context'].custom) &&
|
394
|
+
!v['context'].custom.nil? &&
|
395
|
+
!v['context'].custom.empty?
|
396
|
+
then
|
397
|
+
error_request[:customData] = v['context'].custom
|
398
|
+
end
|
399
|
+
|
400
|
+
if
|
401
|
+
!v['context'].nil? &&
|
402
|
+
defined?(v['context'].user) &&
|
403
|
+
!v['context'].user.nil? &&
|
404
|
+
!v['context'].user.empty?
|
405
|
+
then
|
406
|
+
error_request[:user] = {}
|
407
|
+
error_request[:user][:id] = v['context'].user.id
|
408
|
+
error_request[:user][:email] = v['context'].user.email
|
409
|
+
error_request[:user][:username] = v['context'].user.username
|
410
|
+
end
|
411
|
+
|
355
412
|
error_requests << error_request
|
356
413
|
end
|
357
414
|
error_requests
|
@@ -384,9 +441,39 @@ module Atatus
|
|
384
441
|
error[:transaction] = v.transaction[:name]
|
385
442
|
end
|
386
443
|
|
444
|
+
txn_id = ""
|
445
|
+
txn_id = v.transaction_id if !v.transaction_id.nil?
|
446
|
+
trace_id = ""
|
447
|
+
trace_id = v.trace_id if !v.trace_id.nil?
|
448
|
+
|
449
|
+
error[:txnId] = txn_id
|
450
|
+
error[:traceId] = trace_id
|
451
|
+
|
387
452
|
if !v.context.nil?
|
388
453
|
error[:request] = build_request(v.context)
|
389
454
|
end
|
455
|
+
|
456
|
+
if
|
457
|
+
!v.context.nil? &&
|
458
|
+
defined?(v.context.custom) &&
|
459
|
+
!v.context.custom.nil? &&
|
460
|
+
!v.context.custom.empty?
|
461
|
+
then
|
462
|
+
error[:customData] = v.context.custom
|
463
|
+
end
|
464
|
+
|
465
|
+
if
|
466
|
+
!v.context.nil? &&
|
467
|
+
defined?(v.context.user) &&
|
468
|
+
!v.context.user.nil? &&
|
469
|
+
!v.context.user.empty?
|
470
|
+
then
|
471
|
+
error[:user] = {}
|
472
|
+
error[:user][:id] = v.context.user.id
|
473
|
+
error[:user][:email] = v.context.user.email
|
474
|
+
error[:user][:username] = v.context.user.username
|
475
|
+
end
|
476
|
+
|
390
477
|
error[:exceptions] = []
|
391
478
|
exception = {}
|
392
479
|
exception[:class] = v.exception.type
|
@@ -409,7 +496,7 @@ module Atatus
|
|
409
496
|
frame = {}
|
410
497
|
frame[:f] = f.filename
|
411
498
|
frame[:m] = f.function
|
412
|
-
frame[:ln] = f.lineno
|
499
|
+
frame[:ln] = f.lineno.to_i
|
413
500
|
if f.library_frame == false
|
414
501
|
frame[:inp] = true
|
415
502
|
end
|
@@ -30,11 +30,13 @@ module Atatus
|
|
30
30
|
|
31
31
|
@blocked = false
|
32
32
|
@capture_percentiles = false
|
33
|
+
@hostinfo_response = {}
|
33
34
|
end
|
34
35
|
|
35
36
|
def hostinfo(start_time)
|
36
37
|
payload = @builder.hostinfo(start_time)
|
37
38
|
post(HOSTINFO_ENDPOINT, payload)
|
39
|
+
@hostinfo_response
|
38
40
|
end
|
39
41
|
|
40
42
|
def txns(start_time, end_time, data)
|
@@ -109,7 +111,25 @@ module Atatus
|
|
109
111
|
if resp
|
110
112
|
if resp.key?("capturePercentiles")
|
111
113
|
@capture_percentiles = resp["capturePercentiles"]
|
114
|
+
@hostinfo_response['capturePercentiles'] = @capture_percentiles
|
112
115
|
end
|
116
|
+
|
117
|
+
if resp.key?("extRequestPatterns")
|
118
|
+
@hostinfo_response['extRequestPatterns'] = resp["extRequestPatterns"]
|
119
|
+
end
|
120
|
+
|
121
|
+
if resp.key?("ignoreTxnNamePatterns")
|
122
|
+
@hostinfo_response['ignoreTxnNamePatterns'] = resp["ignoreTxnNamePatterns"]
|
123
|
+
end
|
124
|
+
|
125
|
+
if resp.key?("ignoreHTTPFailurePatterns")
|
126
|
+
@hostinfo_response['ignoreHTTPFailurePatterns'] = resp["ignoreHTTPFailurePatterns"]
|
127
|
+
end
|
128
|
+
|
129
|
+
if resp.key?("ignoreExceptionPatterns")
|
130
|
+
@hostinfo_response['ignoreExceptionPatterns'] = resp["ignoreExceptionPatterns"]
|
131
|
+
end
|
132
|
+
|
113
133
|
end
|
114
134
|
else
|
115
135
|
true
|
data/lib/atatus/error_builder.rb
CHANGED
@@ -86,6 +86,10 @@ module Atatus
|
|
86
86
|
|
87
87
|
Util.reverse_merge!(error.context.labels, transaction.context.labels)
|
88
88
|
Util.reverse_merge!(error.context.custom, transaction.context.custom)
|
89
|
+
|
90
|
+
return unless transaction.context.user
|
91
|
+
|
92
|
+
error.context.user = transaction.context.user
|
89
93
|
end
|
90
94
|
end
|
91
95
|
end
|
data/lib/atatus/version.rb
CHANGED
data/lib/atatus.rb
CHANGED
@@ -352,6 +352,14 @@ module Atatus
|
|
352
352
|
end
|
353
353
|
end
|
354
354
|
|
355
|
+
# Provide further context for the current transaction
|
356
|
+
#
|
357
|
+
# @param custom [Hash] A hash with custom information. Can be nested.
|
358
|
+
# @return [Hash] The current custom context
|
359
|
+
def set_custom_data(custom)
|
360
|
+
agent&.set_custom_context(custom)
|
361
|
+
end
|
362
|
+
|
355
363
|
# Provide further context for the current transaction
|
356
364
|
#
|
357
365
|
# @param custom [Hash] A hash with custom information. Can be nested.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: atatus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Atatus
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|