rspec-otel 0.0.7 → 0.1.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 +4 -4
- data/README.md +41 -0
- data/lib/rspec_otel/matchers/emit_metric.rb +170 -0
- data/lib/rspec_otel/matchers/emit_span.rb +42 -62
- data/lib/rspec_otel/matchers/metric_details.rb +38 -0
- data/lib/rspec_otel/matchers/span_details.rb +64 -0
- data/lib/rspec_otel/matchers.rb +7 -0
- data/lib/rspec_otel/version.rb +1 -1
- data/lib/rspec_otel.rb +11 -0
- metadata +33 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34bc15bc0e142300b4a3d26488b19cf53567d7398377699267c30092a4c81f24
|
|
4
|
+
data.tar.gz: 3d7b067208db46d4c30d8768f16de46681b5bbfed4d841d4e426ed558f17ae98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd70b47b5b70a3b3eb9c6ef8f73bc5ea6e3bc5ab9e834ec94e3defce8f0ac34912a16df748a15757a1c46208b11a9f77614a2e1acfdffdadc3bc025847c0f812
|
|
7
|
+
data.tar.gz: f224136edc1819bff811c66cba4b2d6b449bd286e99272b32a84c2247746882956a3d2b62fd3319978a7150719a94c93e57635c1960d45c0b65b194ac2653ca5
|
data/README.md
CHANGED
|
@@ -73,6 +73,47 @@ Several conditions can be added to the matcher:
|
|
|
73
73
|
_The `*_event` condition can be called multiple times with different events._
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
### Matching the presence of a metric
|
|
77
|
+
|
|
78
|
+
You can match the emission of a metric with the `emit_metric` matcher:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
require 'spec_helper'
|
|
82
|
+
|
|
83
|
+
RSpec.describe 'User API' do
|
|
84
|
+
it 'emits a metric' do
|
|
85
|
+
expect do
|
|
86
|
+
get :user, id: 1
|
|
87
|
+
end.to emit_metric('http.server.duration')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`emit_metric` will also match a regular expression:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
expect do
|
|
96
|
+
get :user, id: 1
|
|
97
|
+
end.to emit_metric(/^http\.server\./)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Several conditions can be added to the matcher:
|
|
101
|
+
|
|
102
|
+
* `of_type` - Will match only metrics of the specified instrument kind (`:counter`, `:histogram`, `:gauge`, `:up_down_counter`, `:observable_counter`, `:observable_gauge`, `:observable_up_down_counter`).
|
|
103
|
+
* `with_attributes` - Will match only the metrics with the specified attributes on a data point.
|
|
104
|
+
* `without_attributes` - Will only match the metrics that do not have the specified attributes on any data point.
|
|
105
|
+
* `with_value` - Will match only the metrics where a data point has the specified value (applies to counters, gauges, and up-down counters).
|
|
106
|
+
* `with_count` - Will match only the metrics where a histogram data point has the specified recording count (applies to histograms only).
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
expect do
|
|
110
|
+
get :user, id: 1
|
|
111
|
+
end.to emit_metric('http.server.duration')
|
|
112
|
+
.of_type(:histogram)
|
|
113
|
+
.with_attributes({ 'http.request.method' => 'GET' })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
|
|
76
117
|
### Disabling
|
|
77
118
|
|
|
78
119
|
We wrap every example in a new OpenTelemetry SDK configuration by default, if you wish to disable this you can tag your example with `:rspec_otel_disable_tracing`:
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RspecOtel
|
|
4
|
+
module Matchers
|
|
5
|
+
class EmitMetric
|
|
6
|
+
def initialize(name)
|
|
7
|
+
@name = name
|
|
8
|
+
@kind = nil
|
|
9
|
+
@filters = []
|
|
10
|
+
@before_count = 0
|
|
11
|
+
@pre_snapshot = {}
|
|
12
|
+
@closest_metric = nil
|
|
13
|
+
@closest_filter_count = 0
|
|
14
|
+
@emitted_outside_block = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def matches?(block)
|
|
18
|
+
execute_block(block) if block.respond_to?(:call)
|
|
19
|
+
matching_metric?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def of_type(kind)
|
|
23
|
+
@kind = kind
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def with_attributes(attributes)
|
|
28
|
+
@filters << ->(dp) { attributes_match?(dp.attributes || {}, attributes) }
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def without_attributes(attributes)
|
|
33
|
+
@filters << ->(dp) { !attributes_match?(dp.attributes || {}, attributes) }
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_value(value)
|
|
38
|
+
@filters << lambda { |dp|
|
|
39
|
+
raise ArgumentError, 'with_value is not supported for histogram data points' unless dp.respond_to?(:value)
|
|
40
|
+
|
|
41
|
+
dp.value == value
|
|
42
|
+
}
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_count(count)
|
|
47
|
+
@filters << lambda { |dp|
|
|
48
|
+
raise ArgumentError, 'with_count is only supported for histogram data points' if dp.respond_to?(:value)
|
|
49
|
+
|
|
50
|
+
dp.count == count
|
|
51
|
+
}
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def failure_message
|
|
56
|
+
closest = @closest_metric || new_snapshots.first
|
|
57
|
+
if closest.nil?
|
|
58
|
+
"expected metric #{printable_name} to have been emitted, but no metrics were emitted at all"
|
|
59
|
+
elsif @emitted_outside_block
|
|
60
|
+
"expected metric #{printable_name} to have been emitted within the block, but it was already emitted before"
|
|
61
|
+
else
|
|
62
|
+
"expected metric #{printable_name} to have been emitted, but it couldn't be found. " \
|
|
63
|
+
"Found a close matching metric named `#{closest.name}`#{MetricDetails.new(closest)}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def failure_message_when_negated
|
|
68
|
+
"expected metric #{printable_name} to not have been emitted"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def supports_block_expectations?
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def execute_block(block)
|
|
78
|
+
RspecOtel.metric_exporter.pull
|
|
79
|
+
@pre_snapshot = build_pre_snapshot(RspecOtel.metric_exporter.metric_snapshots)
|
|
80
|
+
@before_count = RspecOtel.metric_exporter.metric_snapshots.length
|
|
81
|
+
block.call
|
|
82
|
+
RspecOtel.metric_exporter.pull
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def matching_metric?
|
|
86
|
+
new_snapshots.each do |metric_data|
|
|
87
|
+
next unless name_matches?(metric_data.name)
|
|
88
|
+
next unless kind_matches?(metric_data.instrument_kind)
|
|
89
|
+
|
|
90
|
+
return true if matching_data_point?(metric_data)
|
|
91
|
+
|
|
92
|
+
@closest_metric ||= metric_data
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def matching_data_point?(metric_data)
|
|
99
|
+
any_changed = false
|
|
100
|
+
metric_data.data_points.each do |data_point|
|
|
101
|
+
next unless data_point_changed?(metric_data.name, metric_data.instrument_kind, data_point)
|
|
102
|
+
|
|
103
|
+
any_changed = true
|
|
104
|
+
return true if all_filters_match?(metric_data, data_point)
|
|
105
|
+
end
|
|
106
|
+
@emitted_outside_block ||= !any_changed
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def all_filters_match?(metric_data, data_point)
|
|
111
|
+
count = @filters.count { |f| f.call(data_point) }
|
|
112
|
+
if count > @closest_filter_count
|
|
113
|
+
@closest_metric = metric_data
|
|
114
|
+
@closest_filter_count = count
|
|
115
|
+
end
|
|
116
|
+
count == @filters.length
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def data_point_changed?(metric_name, instrument_kind, data_point)
|
|
120
|
+
pre = @pre_snapshot.dig(metric_name, data_point.attributes)
|
|
121
|
+
return true if pre.nil?
|
|
122
|
+
return false if observable_instrument?(instrument_kind)
|
|
123
|
+
|
|
124
|
+
pre != data_point_magnitude(data_point)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def observable_instrument?(instrument_kind)
|
|
128
|
+
%i[observable_counter observable_gauge observable_up_down_counter].include?(instrument_kind)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def data_point_magnitude(data_point)
|
|
132
|
+
data_point.respond_to?(:value) ? data_point.value : data_point.count
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def build_pre_snapshot(snapshots)
|
|
136
|
+
# metric_snapshots accumulates across pulls; if the same metric name appears multiple
|
|
137
|
+
# times, to_h keeps the last entry — which is the most recent (and correct) baseline.
|
|
138
|
+
snapshots.to_h do |metric_data|
|
|
139
|
+
[metric_data.name, metric_data.data_points.to_h { |dp| [dp.attributes, data_point_magnitude(dp)] }]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def new_snapshots
|
|
144
|
+
RspecOtel.metric_exporter.metric_snapshots[@before_count..]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def kind_matches?(instrument_kind)
|
|
148
|
+
@kind.nil? || instrument_kind == @kind
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def name_matches?(metric_name)
|
|
152
|
+
case @name
|
|
153
|
+
when String then metric_name == @name
|
|
154
|
+
when Regexp then metric_name.match?(@name)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def printable_name
|
|
159
|
+
case @name
|
|
160
|
+
when String then "'#{@name}'"
|
|
161
|
+
when Regexp then @name.inspect
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def attributes_match?(actual, expected)
|
|
166
|
+
expected.all? { |k, v| actual[k] == v }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module RspecOtel
|
|
4
4
|
module Matchers
|
|
5
|
-
class EmitSpan
|
|
5
|
+
class EmitSpan
|
|
6
6
|
attr_reader :name
|
|
7
7
|
|
|
8
8
|
def initialize(name = nil)
|
|
@@ -14,95 +14,55 @@ module RspecOtel
|
|
|
14
14
|
@filters << name_filter
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def matches?(block)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
block.call
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
closest_count = 0
|
|
24
|
-
(RspecOtel.exporter.finished_spans - @before_spans).each do |span|
|
|
25
|
-
count = @filters.count { |f| f.call(span) }
|
|
26
|
-
@closest_span = span if count > closest_count
|
|
27
|
-
closest_count = count
|
|
28
|
-
return true if count == @filters.count
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
false
|
|
17
|
+
def matches?(block)
|
|
18
|
+
capture_before_spans(block)
|
|
19
|
+
matching_span?
|
|
32
20
|
end
|
|
33
21
|
|
|
34
22
|
def as_child
|
|
35
|
-
@filters <<
|
|
36
|
-
span.parent_span_id && span.parent_span_id != OpenTelemetry::Trace::INVALID_SPAN_ID
|
|
37
|
-
end
|
|
38
|
-
|
|
23
|
+
@filters << ->(span) { span.parent_span_id && span.parent_span_id != OpenTelemetry::Trace::INVALID_SPAN_ID }
|
|
39
24
|
self
|
|
40
25
|
end
|
|
41
26
|
|
|
42
27
|
def as_root
|
|
43
|
-
@filters <<
|
|
44
|
-
span.parent_span_id == OpenTelemetry::Trace::INVALID_SPAN_ID
|
|
45
|
-
end
|
|
46
|
-
|
|
28
|
+
@filters << ->(span) { span.parent_span_id == OpenTelemetry::Trace::INVALID_SPAN_ID }
|
|
47
29
|
self
|
|
48
30
|
end
|
|
49
31
|
|
|
50
32
|
def with_attributes(attributes)
|
|
51
|
-
@filters <<
|
|
52
|
-
attributes_match?(span.attributes, attributes)
|
|
53
|
-
end
|
|
54
|
-
|
|
33
|
+
@filters << ->(span) { attributes_match?(span.attributes, attributes) }
|
|
55
34
|
self
|
|
56
35
|
end
|
|
57
36
|
|
|
58
37
|
def without_attributes(attributes)
|
|
59
|
-
@filters <<
|
|
60
|
-
!attributes_match?(span.attributes, attributes)
|
|
61
|
-
end
|
|
62
|
-
|
|
38
|
+
@filters << ->(span) { !attributes_match?(span.attributes, attributes) }
|
|
63
39
|
self
|
|
64
40
|
end
|
|
65
41
|
|
|
66
42
|
def with_link(attributes = {})
|
|
67
|
-
@filters <<
|
|
68
|
-
span.links &&
|
|
69
|
-
link_match?(span.links, attributes)
|
|
70
|
-
end
|
|
71
|
-
|
|
43
|
+
@filters << ->(span) { span.links && link_match?(span.links, attributes) }
|
|
72
44
|
self
|
|
73
45
|
end
|
|
74
46
|
|
|
75
47
|
def without_link(attributes = {})
|
|
76
|
-
@filters <<
|
|
77
|
-
span.links.nil? ||
|
|
78
|
-
!link_match?(span.links, attributes)
|
|
79
|
-
end
|
|
80
|
-
|
|
48
|
+
@filters << ->(span) { span.links.nil? || !link_match?(span.links, attributes) }
|
|
81
49
|
self
|
|
82
50
|
end
|
|
83
51
|
|
|
84
52
|
def with_event(name, attributes = {})
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
event_match?(span.events, OpenTelemetry::SDK::Trace::Event.new(name, attributes))
|
|
88
|
-
end
|
|
89
|
-
|
|
53
|
+
event = OpenTelemetry::SDK::Trace::Event.new(name, attributes)
|
|
54
|
+
@filters << ->(span) { span.events && event_match?(span.events, event) }
|
|
90
55
|
self
|
|
91
56
|
end
|
|
92
57
|
|
|
93
58
|
def without_event(name, attributes = {})
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
!event_match?(span.events, OpenTelemetry::SDK::Trace::Event.new(name, attributes))
|
|
97
|
-
end
|
|
98
|
-
|
|
59
|
+
event = OpenTelemetry::SDK::Trace::Event.new(name, attributes)
|
|
60
|
+
@filters << ->(span) { span.events.nil? || !event_match?(span.events, event) }
|
|
99
61
|
self
|
|
100
62
|
end
|
|
101
63
|
|
|
102
64
|
def with_status(code, description)
|
|
103
|
-
@filters <<
|
|
104
|
-
status_match?(span.status, code, description)
|
|
105
|
-
end
|
|
65
|
+
@filters << ->(span) { status_match?(span.status, code, description) }
|
|
106
66
|
self
|
|
107
67
|
end
|
|
108
68
|
|
|
@@ -116,15 +76,12 @@ module RspecOtel
|
|
|
116
76
|
|
|
117
77
|
def failure_message
|
|
118
78
|
closest = closest_span
|
|
119
|
-
|
|
79
|
+
prefix = "expected span #{failure_match_description} #{printable_name} to have been emitted"
|
|
120
80
|
|
|
121
81
|
case closest
|
|
122
|
-
when nil
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"#{expect_content}, but it couldn't be found. Found a close matching span named `#{closest.name}`"
|
|
126
|
-
else
|
|
127
|
-
raise "I don't know what to do with a #{closest.class} span"
|
|
82
|
+
when nil then "#{prefix}, but there were no spans emitted at all"
|
|
83
|
+
when OpenTelemetry::SDK::Trace::SpanData then closest_not_found_message(prefix, closest)
|
|
84
|
+
else raise "I don't know what to do with a #{closest.class} span"
|
|
128
85
|
end
|
|
129
86
|
end
|
|
130
87
|
|
|
@@ -138,6 +95,24 @@ module RspecOtel
|
|
|
138
95
|
|
|
139
96
|
private
|
|
140
97
|
|
|
98
|
+
def capture_before_spans(block)
|
|
99
|
+
return unless block.respond_to?(:call)
|
|
100
|
+
|
|
101
|
+
@before_spans = RspecOtel.exporter.finished_spans
|
|
102
|
+
block.call
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def matching_span?
|
|
106
|
+
closest_count = 0
|
|
107
|
+
(RspecOtel.exporter.finished_spans - @before_spans).each do |span|
|
|
108
|
+
count = @filters.count { |f| f.call(span) }
|
|
109
|
+
@closest_span = span if count > closest_count
|
|
110
|
+
closest_count = count
|
|
111
|
+
return true if count == @filters.count
|
|
112
|
+
end
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
|
|
141
116
|
def closest_span
|
|
142
117
|
return @closest_span unless @closest_span.nil?
|
|
143
118
|
|
|
@@ -219,6 +194,11 @@ module RspecOtel
|
|
|
219
194
|
|
|
220
195
|
!link.empty?
|
|
221
196
|
end
|
|
197
|
+
|
|
198
|
+
def closest_not_found_message(prefix, span)
|
|
199
|
+
"#{prefix}, but it couldn't be found. " \
|
|
200
|
+
"Found a close matching span named `#{span.name}`#{SpanDetails.new(span)}"
|
|
201
|
+
end
|
|
222
202
|
end
|
|
223
203
|
end
|
|
224
204
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RspecOtel
|
|
4
|
+
module Matchers
|
|
5
|
+
class MetricDetails
|
|
6
|
+
def initialize(metric)
|
|
7
|
+
@metric = metric
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_s
|
|
11
|
+
"\n#{[type_details, data_points_details].compact.join("\n")}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def type_details
|
|
17
|
+
" type: #{@metric.instrument_kind}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def data_points_details
|
|
21
|
+
return unless @metric.data_points&.any?
|
|
22
|
+
|
|
23
|
+
format_collection('data_points', @metric.data_points) { |dp| format_data_point(dp) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def format_collection(header, collection, &)
|
|
27
|
+
item_lines = collection.map(&)
|
|
28
|
+
" #{header}:\n#{item_lines.join("\n")}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def format_data_point(data_point)
|
|
32
|
+
magnitude_label = data_point.respond_to?(:value) ? "value: #{data_point.value}" : "count: #{data_point.count}"
|
|
33
|
+
attrs = data_point.attributes
|
|
34
|
+
attrs&.any? ? " - #{magnitude_label} #{attrs.inspect}" : " - #{magnitude_label}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RspecOtel
|
|
4
|
+
module Matchers
|
|
5
|
+
class SpanDetails
|
|
6
|
+
def initialize(span)
|
|
7
|
+
@span = span
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_s
|
|
11
|
+
lines = [attributes_details, events_details, links_details, status_details].compact
|
|
12
|
+
return '' if lines.empty?
|
|
13
|
+
|
|
14
|
+
"\n#{lines.join("\n")}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def attributes_details
|
|
20
|
+
return unless @span.attributes&.any?
|
|
21
|
+
|
|
22
|
+
" attributes: #{@span.attributes.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def events_details
|
|
26
|
+
return unless @span.events&.any?
|
|
27
|
+
|
|
28
|
+
format_collection('events', @span.events) { |e| format_item(e.name, e.attributes) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def links_details
|
|
32
|
+
return unless @span.links&.any?
|
|
33
|
+
|
|
34
|
+
format_collection('links', @span.links) { |l| format_item('link', l.attributes) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def status_details
|
|
38
|
+
return if @span.status.nil? || @span.status.code == OpenTelemetry::Trace::Status::UNSET
|
|
39
|
+
|
|
40
|
+
label = status_label(@span.status.code)
|
|
41
|
+
desc = @span.status.description
|
|
42
|
+
status_str = desc.to_s.empty? ? label : "#{label} (#{desc})"
|
|
43
|
+
" status: #{status_str}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format_collection(header, collection, &)
|
|
47
|
+
item_lines = collection.map(&)
|
|
48
|
+
" #{header}:\n#{item_lines.join("\n")}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def format_item(label, attributes)
|
|
52
|
+
attributes&.any? ? " - #{label} #{attributes.inspect}" : " - #{label}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def status_label(code)
|
|
56
|
+
case code
|
|
57
|
+
when OpenTelemetry::Trace::Status::OK then 'ok'
|
|
58
|
+
when OpenTelemetry::Trace::Status::ERROR then 'error'
|
|
59
|
+
else 'unknown'
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/rspec_otel/matchers.rb
CHANGED
|
@@ -5,7 +5,14 @@ module RspecOtel
|
|
|
5
5
|
def emit_span(name)
|
|
6
6
|
EmitSpan.new(name)
|
|
7
7
|
end
|
|
8
|
+
|
|
9
|
+
def emit_metric(name)
|
|
10
|
+
EmitMetric.new(name)
|
|
11
|
+
end
|
|
8
12
|
end
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
require 'rspec_otel/matchers/emit_span'
|
|
16
|
+
require 'rspec_otel/matchers/emit_metric'
|
|
17
|
+
require 'rspec_otel/matchers/span_details'
|
|
18
|
+
require 'rspec_otel/matchers/metric_details'
|
data/lib/rspec_otel/version.rb
CHANGED
data/lib/rspec_otel.rb
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require 'opentelemetry/sdk'
|
|
4
4
|
require 'opentelemetry-test-helpers'
|
|
5
|
+
require 'opentelemetry-metrics-sdk'
|
|
5
6
|
|
|
6
7
|
module RspecOtel
|
|
7
8
|
def self.exporter
|
|
8
9
|
@exporter ||= OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new
|
|
9
10
|
end
|
|
10
11
|
|
|
12
|
+
def self.metric_exporter
|
|
13
|
+
@metric_exporter ||= OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
def self.record
|
|
12
17
|
span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
|
|
13
18
|
|
|
@@ -15,6 +20,10 @@ module RspecOtel
|
|
|
15
20
|
c.add_span_processor span_processor
|
|
16
21
|
end
|
|
17
22
|
|
|
23
|
+
meter_provider = OpenTelemetry::SDK::Metrics::MeterProvider.new
|
|
24
|
+
meter_provider.add_metric_reader(metric_exporter)
|
|
25
|
+
OpenTelemetry.meter_provider = meter_provider
|
|
26
|
+
|
|
18
27
|
yield
|
|
19
28
|
ensure
|
|
20
29
|
reset
|
|
@@ -22,7 +31,9 @@ module RspecOtel
|
|
|
22
31
|
|
|
23
32
|
def self.reset
|
|
24
33
|
OpenTelemetry::TestHelpers.reset_opentelemetry
|
|
34
|
+
OpenTelemetry.meter_provider = OpenTelemetry::Internal::ProxyMeterProvider.new
|
|
25
35
|
@exporter = nil
|
|
36
|
+
@metric_exporter = nil
|
|
26
37
|
end
|
|
27
38
|
end
|
|
28
39
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-otel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Damien MATHIEU
|
|
@@ -9,6 +9,20 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: opentelemetry-api
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +37,20 @@ dependencies:
|
|
|
23
37
|
- - "~>"
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
39
|
version: '1.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: opentelemetry-metrics-sdk
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.12'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.12'
|
|
26
54
|
- !ruby/object:Gem::Dependency
|
|
27
55
|
name: opentelemetry-sdk
|
|
28
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -76,7 +104,10 @@ files:
|
|
|
76
104
|
- README.md
|
|
77
105
|
- lib/rspec_otel.rb
|
|
78
106
|
- lib/rspec_otel/matchers.rb
|
|
107
|
+
- lib/rspec_otel/matchers/emit_metric.rb
|
|
79
108
|
- lib/rspec_otel/matchers/emit_span.rb
|
|
109
|
+
- lib/rspec_otel/matchers/metric_details.rb
|
|
110
|
+
- lib/rspec_otel/matchers/span_details.rb
|
|
80
111
|
- lib/rspec_otel/rspec.rb
|
|
81
112
|
- lib/rspec_otel/version.rb
|
|
82
113
|
homepage: https://github.com/dmathieu/rspec-otel
|
|
@@ -98,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
98
129
|
- !ruby/object:Gem::Version
|
|
99
130
|
version: '0'
|
|
100
131
|
requirements: []
|
|
101
|
-
rubygems_version:
|
|
132
|
+
rubygems_version: 4.0.6
|
|
102
133
|
specification_version: 4
|
|
103
134
|
summary: RSpec matchers for the OpenTelemetry framework
|
|
104
135
|
test_files: []
|