stoplight-statsd 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 10b6fa4537afd4535b2f688c867e9696ca0e1e533493ce4eefdbb83ae693bee4
4
+ data.tar.gz: 62f42990dc70ce1b679bf4b8626d0225463277cbc67dd8f4a2dcc7502081fe21
5
+ SHA512:
6
+ metadata.gz: 4377451407e84fc48d17e5c9245871fc55cbbf1ea2f697aa7e3e6693bb7c4cac6b4a17afd1e68cc3c565aec5aca1bc339e90ce87ebeca42be0924360483472e9
7
+ data.tar.gz: 584de22665a30ee2076d71a184e684294eceaedbcbc0d85fc08a57fb568e2a04ff2dc31abf78177c4589a2d564ce0082426f139a6ce405c5d8ef288f5abf42da
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Stoplight::Statsd
2
+
3
+ A [DogStatsD] exporter for [Stoplight]'s telemetry bus.
4
+
5
+ [Stoplight] is a Ruby circuit breaker: you wrap a risky call (a flaky API, a slow database) in a
6
+ "light", and once it fails too often, the light turns red and stops making the call for a while,
7
+ giving the dependency time to recover. Stoplight already emits detailed telemetry about what its
8
+ lights are doing internally - this gem listens to that telemetry and forwards it to DogStatsD, so
9
+ those events show up as metrics and events in Datadog (or any other StatsD-compatible receiver)
10
+ without you having to wire that up yourself.
11
+
12
+ 🚧 **Status: under development, not yet released.** Nothing in this README is installable yet.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ bundle add stoplight-statsd
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ Call `Stoplight::Statsd.subscribe` once, wherever you set up Stoplight, and pass it a DogStatsD
23
+ client:
24
+
25
+ ```ruby
26
+ require "datadog/statsd"
27
+ require "stoplight/statsd"
28
+
29
+ statsd = Datadog::Statsd.new("localhost", 8125)
30
+ Stoplight::Statsd.subscribe(Stoplight.telemetry, statsd:)
31
+ ```
32
+
33
+ That's it - from then on, every light reports its runs, recovery attempts, and state changes to
34
+ `statsd` automatically. See [Events](#events) below for the full list of what gets sent.
35
+
36
+ `subscribe` returns a handle you can use to stop forwarding later, e.g. when tearing down in tests:
37
+
38
+ ```ruby
39
+ subscription = Stoplight::Statsd.subscribe(Stoplight.telemetry, statsd:)
40
+ subscription.unsubscribe
41
+ ```
42
+
43
+ ### Sampling busy traffic
44
+
45
+ `Light#run` fires on every call made through a light, so on a busy fleet it can dominate metric
46
+ volume. Pass `sample_rate:` (`0.0`-`1.0`, default `1.0`) to forward only a fraction of
47
+ `stoplight.run.completed`/`stoplight.run.duration` - the only two metrics whose volume scales with
48
+ traffic (each sampled independently, so a `0.1` rate isn't guaranteed to keep the same runs in both
49
+ series, just the same average share). Everything else stays low-volume on its own, so it always
50
+ sends at full rate regardless of `sample_rate`.
51
+
52
+ ```ruby
53
+ Stoplight::Statsd.subscribe(Stoplight.telemetry, statsd:, sample_rate: 0.1)
54
+ ```
55
+
56
+ ## Events
57
+
58
+ "Count" metrics are plain counters; "histogram" metrics record a distribution of values (here,
59
+ milliseconds) so you can graph percentiles instead of just an average.
60
+
61
+ - `stoplight.run.completed` (count) - every call made through a light, tagged with the outcome
62
+ (`success`/`failure`/`blocked`). Respects `sample_rate`.
63
+ - `stoplight.run.duration` (histogram, ms) - how long that call took, when known. Respects
64
+ `sample_rate`.
65
+ - `stoplight.recovery_probe.completed` (count) - every recovery probe, i.e. the trial call
66
+ Stoplight makes while a light is yellow to decide whether it can go back to green. Always sent
67
+ at full rate.
68
+ - `stoplight.recovery_probe.duration` (histogram, ms) - how long that probe took, when known.
69
+ Always sent at full rate.
70
+ - `stoplight.state.transitioned` (count, plus a matching Datadog event) - a light trips to red, a
71
+ recovery attempt starts, succeeds, or fails, or a light gets locked/unlocked. Always sent at
72
+ full rate.
73
+ - `stoplight.light.registered` (count) - a light is registered for the first time. Always sent at
74
+ full rate.
75
+
76
+ ## Development
77
+
78
+ After checking out the repo, run `bin/setup` to install dependencies. Then run `rake spec` to run
79
+ the tests. `bin/console` gives you an interactive prompt.
80
+
81
+ To install this gem onto your local machine, run `bundle exec rake install`.
82
+
83
+ ## Contributing
84
+
85
+ Bug reports and pull requests are welcome on GitHub at
86
+ https://github.com/bolshakov/stoplight-statsd.
87
+
88
+ ## License
89
+
90
+ The gem is available as open source under the terms of the
91
+ [MIT License](https://opensource.org/licenses/MIT).
92
+
93
+
94
+ [DogStatsD]: https://docs.datadoghq.com/developers/dogstatsd/
95
+ [Stoplight]: https://github.com/bolshakov/stoplight
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Statsd
5
+ # Aggregates multiple bus subscriptions behind one caller-facing handle.
6
+ class SubscriptionGroup
7
+ def initialize(telemetry:, subscriptions:)
8
+ @telemetry = telemetry
9
+ @subscriptions = subscriptions
10
+ end
11
+
12
+ def unsubscribe
13
+ @subscriptions.each { |subscription| @telemetry.unsubscribe(subscription) }
14
+ nil
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Statsd
5
+ VERSION = "0.0.0"
6
+ end
7
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stoplight"
4
+ require "zeitwerk"
5
+
6
+ # steep:ignore:start
7
+ loader = Zeitwerk::Loader.for_gem_extension(Stoplight)
8
+ loader.setup
9
+ # steep:ignore:end
10
+
11
+ module Stoplight
12
+ module Statsd
13
+ class Error < StandardError; end
14
+
15
+ def self.subscribe(telemetry, statsd:, sample_rate: 1.0)
16
+ unless (0.0..1.0).cover?(sample_rate)
17
+ raise Error, "sample_rate must be between 0.0 and 1.0, got #{sample_rate.inspect}"
18
+ end
19
+
20
+ subscriptions = [subscribe_run_completed(telemetry, statsd: statsd, sample_rate:),
21
+ subscribe_recovery_probe_completed(telemetry, statsd: statsd),
22
+ subscribe_state_transitioned(telemetry, statsd: statsd), subscribe_light_registered(telemetry, statsd: statsd)]
23
+ SubscriptionGroup.new(telemetry:, subscriptions:)
24
+ end
25
+
26
+ def self.envelope_tags(envelope)
27
+ ["system_name:#{envelope.system_name}", "light_name:#{envelope.light_name}"]
28
+ end
29
+
30
+ def self.subscribe_run_completed(telemetry, statsd:, sample_rate:)
31
+ telemetry.subscribe(Stoplight::Telemetry::RunCompleted) do |envelope|
32
+ record_run_completed(envelope, statsd: statsd, sample_rate: sample_rate)
33
+ end
34
+ end
35
+
36
+ def self.record_run_completed(envelope, statsd:, sample_rate:)
37
+ identity_tags = envelope_tags(envelope)
38
+ outcome_tag = "outcome:#{envelope.payload.outcome}"
39
+
40
+ statsd.increment("stoplight.run.completed", tags: [
41
+ *identity_tags, outcome_tag,
42
+ "color:#{envelope.payload.color}",
43
+ "fallback_used:#{envelope.payload.fallback_used}"
44
+ ], sample_rate:)
45
+ return unless envelope.payload.duration_ms
46
+
47
+ statsd.histogram("stoplight.run.duration", envelope.payload.duration_ms,
48
+ tags: [*identity_tags, outcome_tag], sample_rate:)
49
+ end
50
+
51
+ def self.subscribe_recovery_probe_completed(telemetry, statsd:)
52
+ telemetry.subscribe(Stoplight::Telemetry::RecoveryProbeCompleted) do |envelope|
53
+ identity_tags = envelope_tags(envelope)
54
+ outcome_tag = "outcome:#{envelope.payload.outcome}"
55
+
56
+ statsd.increment("stoplight.recovery_probe.completed", tags: [*identity_tags, outcome_tag], sample_rate: 1.0)
57
+ statsd.histogram("stoplight.recovery_probe.duration", envelope.payload.duration_ms,
58
+ tags: [*identity_tags, outcome_tag], sample_rate: 1.0)
59
+ end
60
+ end
61
+
62
+ # The event's text is always the color transition, never the failure's exception message - that
63
+ # message is the host application's own text, written for its own logs, and can carry data (a
64
+ # failed URL, a DB value, echoed user input) never meant to leave the process to a third-party
65
+ # service. state_transitioned_variant_tags already exposes the bounded, developer-controlled
66
+ # exception_class as a tag, which is safe to export.
67
+ def self.subscribe_state_transitioned(telemetry, statsd:)
68
+ telemetry.subscribe(Stoplight::Telemetry::StateTransitioned) do |envelope|
69
+ record_state_transitioned(envelope, statsd: statsd)
70
+ end
71
+ end
72
+
73
+ def self.record_state_transitioned(envelope, statsd:)
74
+ payload = envelope.payload
75
+ event_name = state_transitioned_event_name(payload)
76
+ tags = state_transitioned_tags(envelope, payload, event_name)
77
+
78
+ statsd.increment("stoplight.state.transitioned", tags: tags, sample_rate: 1.0)
79
+ statsd.event("#{envelope.system_name}/#{envelope.light_name}: #{event_name}",
80
+ "#{payload.from_color} -> #{payload.to_color}", alert_type: state_transitioned_alert_type(payload),
81
+ tags: tags, truncate_if_too_long: true)
82
+ end
83
+
84
+ def self.state_transitioned_tags(envelope, payload, event_name)
85
+ [
86
+ *envelope_tags(envelope),
87
+ "event:#{event_name}",
88
+ "from_color:#{payload.from_color}",
89
+ "to_color:#{payload.to_color}"
90
+ ] + state_transitioned_variant_tags(payload)
91
+ end
92
+
93
+ def self.state_transitioned_event_name(payload)
94
+ case payload
95
+ when Stoplight::Telemetry::TrafficBreached then "traffic_breached"
96
+ when Stoplight::Telemetry::RecoveryStarted then "recovery_started"
97
+ when Stoplight::Telemetry::RecoverySucceeded then "recovery_succeeded"
98
+ when Stoplight::Telemetry::RecoveryFailed then "recovery_failed"
99
+ when Stoplight::Telemetry::LockChanged then "lock_changed"
100
+ else raise Error, "unhandled state_transitioned payload: #{payload.class}"
101
+ end
102
+ end
103
+
104
+ def self.state_transitioned_alert_type(payload)
105
+ case payload
106
+ when Stoplight::Telemetry::TrafficBreached, Stoplight::Telemetry::RecoveryFailed then "error"
107
+ when Stoplight::Telemetry::RecoverySucceeded then "success"
108
+ when Stoplight::Telemetry::RecoveryStarted, Stoplight::Telemetry::LockChanged then "info"
109
+ else raise Error, "unhandled state_transitioned payload: #{payload.class}"
110
+ end
111
+ end
112
+
113
+ # Tags beyond system_name/light_name/event/from_color/to_color, which every variant carries.
114
+ def self.state_transitioned_variant_tags(payload)
115
+ case payload
116
+ when Stoplight::Telemetry::TrafficBreached, Stoplight::Telemetry::RecoveryFailed
117
+ ["policy:#{payload.policy}"] + state_transitioned_failure_tags(payload.failure)
118
+ when Stoplight::Telemetry::RecoverySucceeded then ["policy:#{payload.policy}"]
119
+ when Stoplight::Telemetry::LockChanged then ["from_state:#{payload.from_state}", "to_state:#{payload.to_state}"]
120
+ when Stoplight::Telemetry::RecoveryStarted then []
121
+ end
122
+ end
123
+
124
+ def self.state_transitioned_failure_tags(failure)
125
+ failure ? ["tracked:#{failure.tracked}", "exception_class:#{failure.exception.class}"] : []
126
+ end
127
+
128
+ def self.subscribe_light_registered(telemetry, statsd:)
129
+ telemetry.subscribe(Stoplight::Telemetry::LightRegistered) do |envelope|
130
+ statsd.increment("stoplight.light.registered", tags: envelope_tags(envelope), sample_rate: 1.0)
131
+ end
132
+ end
133
+
134
+ private_class_method :subscribe_run_completed, :record_run_completed, :subscribe_recovery_probe_completed,
135
+ :subscribe_state_transitioned, :record_state_transitioned, :state_transitioned_tags,
136
+ :state_transitioned_event_name, :state_transitioned_alert_type, :state_transitioned_variant_tags,
137
+ :state_transitioned_failure_tags, :subscribe_light_registered
138
+ end
139
+ end
@@ -0,0 +1,31 @@
1
+ module Stoplight
2
+ module Statsd
3
+ VERSION: String
4
+
5
+ class Error < StandardError
6
+ end
7
+
8
+ class SubscriptionGroup
9
+ @telemetry: Stoplight::Domain::_Telemetry
10
+ @subscriptions: Array[Stoplight::Domain::Telemetry::Subscription]
11
+
12
+ def initialize: (telemetry: Stoplight::Domain::_Telemetry, subscriptions: Array[Stoplight::Domain::Telemetry::Subscription]) -> void
13
+ def unsubscribe: () -> void
14
+ end
15
+
16
+ def self.subscribe: (Stoplight::Domain::_Telemetry, statsd: Datadog::Statsd, ?sample_rate: Float) -> SubscriptionGroup
17
+
18
+ private def self.envelope_tags: [P < Stoplight::Domain::Telemetry::event] (Stoplight::Domain::Telemetry::Envelope[P]) -> Array[String]
19
+ private def self.subscribe_run_completed: (Stoplight::Domain::_Telemetry, statsd: Datadog::Statsd, sample_rate: Float) -> Stoplight::Domain::Telemetry::Subscription
20
+ private def self.record_run_completed: (Stoplight::Domain::Telemetry::Envelope[Stoplight::Domain::Telemetry::RunCompleted], statsd: Datadog::Statsd, sample_rate: Float) -> void
21
+ private def self.subscribe_recovery_probe_completed: (Stoplight::Domain::_Telemetry, statsd: Datadog::Statsd) -> Stoplight::Domain::Telemetry::Subscription
22
+ private def self.subscribe_state_transitioned: (Stoplight::Domain::_Telemetry, statsd: Datadog::Statsd) -> Stoplight::Domain::Telemetry::Subscription
23
+ private def self.record_state_transitioned: (Stoplight::Domain::Telemetry::Envelope[Stoplight::Domain::Telemetry::state_transitioned], statsd: Datadog::Statsd) -> void
24
+ private def self.state_transitioned_tags: (Stoplight::Domain::Telemetry::Envelope[Stoplight::Domain::Telemetry::state_transitioned], Stoplight::Domain::Telemetry::state_transitioned, String) -> Array[String]
25
+ private def self.state_transitioned_event_name: (Stoplight::Domain::Telemetry::state_transitioned) -> String
26
+ private def self.state_transitioned_alert_type: (Stoplight::Domain::Telemetry::state_transitioned) -> String
27
+ private def self.state_transitioned_variant_tags: (Stoplight::Domain::Telemetry::state_transitioned) -> Array[String]
28
+ private def self.state_transitioned_failure_tags: (Stoplight::Domain::Telemetry::Failure?) -> Array[String]
29
+ private def self.subscribe_light_registered: (Stoplight::Domain::_Telemetry, statsd: Datadog::Statsd) -> Stoplight::Domain::Telemetry::Subscription
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stoplight-statsd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tëma Bolshakov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dogstatsd-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: stoplight
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: zeitwerk
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: Subscribes to Stoplight's telemetry bus and forwards circuit breaker
55
+ events as DogStatsD metrics to whatever StatsD/Datadog-compatible receiver you already
56
+ run.
57
+ email:
58
+ - tema@bolshakov.dev
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - README.md
64
+ - lib/stoplight/statsd.rb
65
+ - lib/stoplight/statsd/subscription_group.rb
66
+ - lib/stoplight/statsd/version.rb
67
+ - sig/stoplight/statsd.rbs
68
+ homepage: https://github.com/bolshakov/stoplight-statsd
69
+ licenses:
70
+ - MIT
71
+ metadata:
72
+ homepage_uri: https://github.com/bolshakov/stoplight-statsd
73
+ source_code_uri: https://github.com/bolshakov/stoplight-statsd
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '3.2'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 4.0.16
89
+ specification_version: 4
90
+ summary: DogStatsD exporter for Stoplight's telemetry bus
91
+ test_files: []