barkibu-kb 1.1.0 → 1.2.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/CHANGELOG.md +5 -1
- data/Gemfile.lock +18 -3
- data/README.md +43 -0
- data/barkibu-kb.gemspec +1 -0
- data/lib/kb/client.rb +39 -17
- data/lib/kb/instrumentation/datadog.rb +81 -0
- data/lib/kb/version.rb +1 -1
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b46793e55bdd7e53d2e98e3e792972ec67a86048074aeec1bc9ffdde8028965
|
|
4
|
+
data.tar.gz: 44d5748d6fb445709add87f4bea42c9c19516ff9bb81be8b4112b716c17469c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38cb22fa193c859a72448aea1cecfbad17671a4deae2dbd5763881c05f3ffa7338ab80b6218de5e041463c1e0575d190bba204fcf6851a6655107b1894eb1ca6
|
|
7
|
+
data.tar.gz: f66b4353ac4cc0e3432310ecf7d54b212dfe8b1b245ca6f9f95062b49185acc5028ac1b5ab69503f9099de9cfc89f2ff39e3c62df12531e8c4f593a3bef2e034
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
8
|
## [unreleased]
|
|
9
|
-
- See diff: https://github.com/barkibu/kb-ruby/compare/v1.
|
|
9
|
+
- See diff: https://github.com/barkibu/kb-ruby/compare/v1.2.0...HEAD
|
|
10
|
+
|
|
11
|
+
## [1.2.0]
|
|
12
|
+
- `KB::Client` emits one `request.kb_client` `ActiveSupport::Notifications` event per KB call (`KB::Client::REQUEST_EVENT`), wrapping cache lookup, connect, TLS, write, read and parsing. Payload: `verb`, `path`, `base_url`, `cache_hit` (GET only), `status`, plus ActiveSupport's `exception`/`exception_object` when the call raised. Every public method now goes through one private `perform` seam; no behaviour change (same cache keys, params and error classes).
|
|
13
|
+
- Add an opt-in Datadog subscriber: `require 'kb/instrumentation/datadog'` + `KB::Instrumentation::Datadog.subscribe!` turns each event into a `kb.client.request` APM span, opened on event start and closed on finish so the tracer's Net::HTTP spans nest under it. The span inherits the app's service and stays there (no `span.kind:client`/`peer.service`, so it is not attributed to the knowledge-base service), tags `peer.hostname`, `kb.method`, `kb.cache_hit`, `http.status_code`, and uses low-cardinality resources (`GET /v1/pets/?/contracts`). Motivation: connect timeouts happen before `Net::HTTP#request`, so the Datadog Net::HTTP tracer never sees them and they were invisible on our dashboards. Works with `ddtrace` 1.x and `datadog` 2.x; the tracer stays the app's dependency.
|
|
10
14
|
|
|
11
15
|
## [1.1.0]
|
|
12
16
|
- Add `read_timeout:` to `KB::Client#request` to raise the read budget for a single call (e.g. `GET /v1/pets/birthdays`, whose server-side work runs for seconds). Connect and write budgets stay global; the override does not leak into later calls on the same connection.
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
barkibu-kb (1.
|
|
4
|
+
barkibu-kb (1.2.0)
|
|
5
5
|
activemodel (>= 4.0.2)
|
|
6
6
|
activerecord
|
|
7
7
|
activesupport (>= 3.0.0)
|
|
@@ -10,8 +10,8 @@ PATH
|
|
|
10
10
|
faraday-net_http (~> 1.0)
|
|
11
11
|
faraday_middleware
|
|
12
12
|
i18n
|
|
13
|
-
barkibu-kb-fake (1.
|
|
14
|
-
barkibu-kb (= 1.
|
|
13
|
+
barkibu-kb-fake (1.2.0)
|
|
14
|
+
barkibu-kb (= 1.2.0)
|
|
15
15
|
countries
|
|
16
16
|
sinatra
|
|
17
17
|
webmock
|
|
@@ -44,12 +44,21 @@ GEM
|
|
|
44
44
|
base64 (0.3.0)
|
|
45
45
|
bigdecimal (4.1.2)
|
|
46
46
|
byebug (11.1.3)
|
|
47
|
+
cgi (0.5.2)
|
|
47
48
|
concurrent-ruby (1.3.7)
|
|
48
49
|
connection_pool (3.0.2)
|
|
49
50
|
countries (5.3.1)
|
|
50
51
|
unaccent (~> 0.3)
|
|
51
52
|
crack (0.4.5)
|
|
52
53
|
rexml
|
|
54
|
+
datadog (2.43.0)
|
|
55
|
+
cgi
|
|
56
|
+
datadog-ruby_core_source (~> 3.5, >= 3.5.5)
|
|
57
|
+
libdatadog (~> 40.0.0.2.0)
|
|
58
|
+
libddwaf (~> 1.30.0.0.0)
|
|
59
|
+
logger
|
|
60
|
+
msgpack
|
|
61
|
+
datadog-ruby_core_source (3.5.5)
|
|
53
62
|
diff-lcs (1.4.4)
|
|
54
63
|
docile (1.4.0)
|
|
55
64
|
drb (2.2.3)
|
|
@@ -84,14 +93,19 @@ GEM
|
|
|
84
93
|
faraday-retry (1.0.3)
|
|
85
94
|
faraday_middleware (1.2.0)
|
|
86
95
|
faraday (~> 1.0)
|
|
96
|
+
ffi (1.17.4)
|
|
87
97
|
hashdiff (1.0.1)
|
|
88
98
|
i18n (1.15.2)
|
|
89
99
|
concurrent-ruby (~> 1.0)
|
|
90
100
|
json (2.20.0)
|
|
101
|
+
libdatadog (40.0.0.2.0)
|
|
102
|
+
libddwaf (1.30.0.0.2)
|
|
103
|
+
ffi (~> 1.0)
|
|
91
104
|
logger (1.7.0)
|
|
92
105
|
minitest (6.0.6)
|
|
93
106
|
drb (~> 2.0)
|
|
94
107
|
prism (~> 1.5)
|
|
108
|
+
msgpack (1.8.5)
|
|
95
109
|
multipart-post (2.3.0)
|
|
96
110
|
mustermann (3.0.0)
|
|
97
111
|
ruby2_keywords (~> 0.0.1)
|
|
@@ -173,6 +187,7 @@ DEPENDENCIES
|
|
|
173
187
|
bigdecimal
|
|
174
188
|
bundler
|
|
175
189
|
byebug
|
|
190
|
+
datadog (~> 2.0)
|
|
176
191
|
rake (>= 12.3.3)
|
|
177
192
|
rspec (~> 3.0)
|
|
178
193
|
rubocop
|
data/README.md
CHANGED
|
@@ -74,6 +74,49 @@ and write budgets stay global:
|
|
|
74
74
|
KB::Pet.kb_client.request('birthdays', filters: { month: 9, day: 22, size: 1000 }, read_timeout: 30)
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
#### Instrumentation
|
|
78
|
+
|
|
79
|
+
Every KB call emits one `request.kb_client` event through
|
|
80
|
+
`ActiveSupport::Notifications`, wrapping the whole call: cache lookup, TCP
|
|
81
|
+
connect, TLS, write, read and JSON parsing. The payload carries `verb`, `path`,
|
|
82
|
+
`base_url`, `cache_hit` (GET calls only), `status` (when a response arrived) and
|
|
83
|
+
ActiveSupport's `exception` / `exception_object` when the call raised. Subscribe
|
|
84
|
+
to it for logging, metrics or anything else:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
ActiveSupport::Notifications.subscribe(KB::Client::REQUEST_EVENT) do |event|
|
|
88
|
+
Rails.logger.info("KB #{event.payload[:verb]} #{event.payload[:path]} #{event.duration.round}ms")
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
##### Datadog
|
|
93
|
+
|
|
94
|
+
A ready-made subscriber turns each event into a `kb.client.request` APM span.
|
|
95
|
+
Opt in from the app's Datadog initializer, after `Datadog.configure`. Works with
|
|
96
|
+
both `ddtrace` 1.x and `datadog` 2.x; the tracer gem is the app's dependency.
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# config/initializers/datadog_tracer.rb
|
|
100
|
+
Datadog.configure { |c| ... }
|
|
101
|
+
|
|
102
|
+
require 'kb/instrumentation/datadog'
|
|
103
|
+
KB::Instrumentation::Datadog.subscribe!
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The span opens when the event starts and closes when it finishes, so the
|
|
107
|
+
tracer's own Net::HTTP spans nest under it. It inherits the app's service
|
|
108
|
+
(`c.service`), so nothing new appears in the APM service list, and it carries no
|
|
109
|
+
`span.kind:client` or `peer.service`, so Datadog does not attribute it to the
|
|
110
|
+
knowledge-base service either: it measures the client's whole call, not a KB
|
|
111
|
+
operation. Resources are low-cardinality (`GET /v1/pets/birthdays`,
|
|
112
|
+
`GET /v1/pets/?/contracts`). Tags: `peer.hostname` (the KB host used),
|
|
113
|
+
`kb.method`, `kb.cache_hit` (GET calls only), `http.status_code`, plus the
|
|
114
|
+
standard `error.type`/`error.message` when the call raises.
|
|
115
|
+
|
|
116
|
+
Why not rely on the Net::HTTP tracer alone: faraday-net_http opens the socket
|
|
117
|
+
before `Net::HTTP#request`, the method that tracer patches, so a connect timeout
|
|
118
|
+
produces no http span at all. This span sees every phase.
|
|
119
|
+
|
|
77
120
|
### Exposed Entities
|
|
78
121
|
|
|
79
122
|
#### Pet Parent 🧍🏾
|
data/barkibu-kb.gemspec
CHANGED
|
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec|
|
|
|
39
39
|
spec.add_dependency 'dry-configurable', '~> 0.9'
|
|
40
40
|
spec.add_development_dependency 'bundler'
|
|
41
41
|
spec.add_development_dependency 'byebug'
|
|
42
|
+
spec.add_development_dependency 'datadog', '~> 2.0'
|
|
42
43
|
spec.add_development_dependency 'rake', '>= 12.3.3'
|
|
43
44
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
44
45
|
spec.add_development_dependency 'rubocop'
|
data/lib/kb/client.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
module KB
|
|
2
2
|
class Client
|
|
3
|
+
# Emitted once per KB call, wrapping cache lookup and the HTTP request.
|
|
4
|
+
# Payload: verb, path, base_url, cache_hit (GET only), status (when a response arrived),
|
|
5
|
+
# plus ActiveSupport's exception/exception_object when the call raised.
|
|
6
|
+
REQUEST_EVENT = 'request.kb_client'.freeze
|
|
7
|
+
|
|
3
8
|
attr_reader :api_key, :base_url
|
|
4
9
|
|
|
5
10
|
def initialize(base_url, api_key: ENV['KB_API_KEY'])
|
|
@@ -11,47 +16,38 @@ module KB
|
|
|
11
16
|
# for the few endpoints whose server-side work legitimately runs for seconds
|
|
12
17
|
# (e.g. GET /v1/pets/birthdays). Connect and write budgets stay global.
|
|
13
18
|
def request(sub_path, filters: nil, method: :get, read_timeout: nil)
|
|
14
|
-
|
|
15
|
-
return connection.public_send(method, sub_path, attributes_to_json(filters), &options).body if method != :get
|
|
19
|
+
return perform(method, sub_path, attributes_to_json(filters), read_timeout: read_timeout) if method != :get
|
|
16
20
|
|
|
17
21
|
cache_key = "#{@base_url}/#{sub_path}/#{(filters || {}).sort.to_h}"
|
|
18
|
-
|
|
19
|
-
connection.public_send(method, sub_path, filters, &options).body
|
|
20
|
-
end
|
|
22
|
+
perform(:get, sub_path, filters, cache_key: cache_key, read_timeout: read_timeout)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def all(filters = {})
|
|
24
|
-
cache_key
|
|
25
|
-
|
|
26
|
-
KB::Cache.fetch(cache_key) do
|
|
27
|
-
connection.get('', attributes_case_transform(filters)).body
|
|
28
|
-
end
|
|
26
|
+
perform(:get, '', attributes_case_transform(filters), cache_key: "#{@base_url}/#{filters.sort.to_h}")
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
def find(key, params = {})
|
|
32
30
|
raise Faraday::ResourceNotFound, {} if key.blank?
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
connection.get(key, attributes_case_transform(params)).body
|
|
36
|
-
end
|
|
32
|
+
perform(:get, key, attributes_case_transform(params), cache_key: "#{@base_url}/#{key}")
|
|
37
33
|
end
|
|
38
34
|
|
|
39
35
|
def create(attributes)
|
|
40
|
-
|
|
36
|
+
perform(:post, '', attributes_to_json(attributes))
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
def update(key, attributes)
|
|
44
40
|
clear_cache_for(key)
|
|
45
|
-
|
|
41
|
+
perform(:patch, key.to_s, attributes_to_json(attributes))
|
|
46
42
|
end
|
|
47
43
|
|
|
48
44
|
def destroy(key)
|
|
49
45
|
clear_cache_for(key)
|
|
50
|
-
|
|
46
|
+
perform(:delete, key.to_s)
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
def upsert(attributes)
|
|
54
|
-
|
|
50
|
+
perform(:put, '', attributes_to_json(attributes))
|
|
55
51
|
end
|
|
56
52
|
|
|
57
53
|
def clear_cache_for(key)
|
|
@@ -60,6 +56,32 @@ module KB
|
|
|
60
56
|
|
|
61
57
|
private
|
|
62
58
|
|
|
59
|
+
# Every public method ends up here, so this is the one place a KB call is
|
|
60
|
+
# observable as a whole: cache lookup, connect, TLS, write, read, parse.
|
|
61
|
+
def perform(verb, path, payload = nil, cache_key: nil, read_timeout: nil)
|
|
62
|
+
event = { verb: verb, path: path, base_url: base_url }
|
|
63
|
+
ActiveSupport::Notifications.instrument(REQUEST_EVENT, event) do
|
|
64
|
+
if cache_key
|
|
65
|
+
event[:cache_hit] = true
|
|
66
|
+
KB::Cache.fetch(cache_key) do
|
|
67
|
+
event[:cache_hit] = false
|
|
68
|
+
http(event, payload, read_timeout)
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
http(event, payload, read_timeout)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def http(event, payload, read_timeout)
|
|
77
|
+
response = connection.public_send(event[:verb], event[:path], payload, &request_options(read_timeout))
|
|
78
|
+
event[:status] = response.status
|
|
79
|
+
response.body
|
|
80
|
+
rescue Faraday::ClientError, Faraday::ServerError => e
|
|
81
|
+
event[:status] = e.response && e.response[:status]
|
|
82
|
+
raise
|
|
83
|
+
end
|
|
84
|
+
|
|
63
85
|
def headers
|
|
64
86
|
{
|
|
65
87
|
'Content-Type': 'application/json',
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'active_support/notifications'
|
|
3
|
+
|
|
4
|
+
module KB
|
|
5
|
+
module Instrumentation
|
|
6
|
+
# Opt-in Datadog APM tracing for every KB call, as a subscriber to the
|
|
7
|
+
# client's `request.kb_client` notification.
|
|
8
|
+
#
|
|
9
|
+
# # config/initializers/datadog_tracer.rb, after Datadog.configure
|
|
10
|
+
# require 'kb/instrumentation/datadog'
|
|
11
|
+
# KB::Instrumentation::Datadog.subscribe!
|
|
12
|
+
#
|
|
13
|
+
# One `kb.client.request` span per call, opened when the event starts and
|
|
14
|
+
# closed when it finishes, so it wraps cache lookup, TCP connect, TLS,
|
|
15
|
+
# write, read and JSON parsing, and the tracer's own Net::HTTP spans nest
|
|
16
|
+
# under it. No service is given, so the span inherits the app's, and it stays
|
|
17
|
+
# there: the KB host is a plain `peer.hostname` tag, with no `span.kind:client`
|
|
18
|
+
# or `peer.service` that would attribute it to KB. Works with
|
|
19
|
+
# `ddtrace` 1.x and `datadog` 2.x; the tracer gem is the app's dependency.
|
|
20
|
+
module Datadog
|
|
21
|
+
OPERATION = 'kb.client.request'.freeze
|
|
22
|
+
# Path segments that are identifiers, collapsed to `?` so resources stay
|
|
23
|
+
# low-cardinality: `GET /v1/pets/?/contracts` rather than one per pet.
|
|
24
|
+
IDENTIFIER_SEGMENT = /\A(?:\h{8}-\h{4}-\h{4}-\h{4}-\h{12}|\d+)\z/.freeze
|
|
25
|
+
SPAN_KEY = :datadog_span
|
|
26
|
+
|
|
27
|
+
class TracerMissing < StandardError; end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def subscribe!
|
|
31
|
+
unless defined?(::Datadog::Tracing)
|
|
32
|
+
raise TracerMissing, "Datadog tracing is not loaded; require 'ddtrace' or 'datadog' first"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
return @subscriber if @subscriber
|
|
36
|
+
|
|
37
|
+
@subscriber = ActiveSupport::Notifications.subscribe(KB::Client::REQUEST_EVENT, Subscriber.new)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unsubscribe!
|
|
41
|
+
ActiveSupport::Notifications.unsubscribe(@subscriber) if @subscriber
|
|
42
|
+
@subscriber = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def subscribed?
|
|
46
|
+
!@subscriber.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resource_for(base_url, verb, path)
|
|
50
|
+
segments = (URI(base_url).path.split('/') + path.to_s.split('/')).reject(&:empty?)
|
|
51
|
+
template = segments.map { |segment| segment.match?(IDENTIFIER_SEGMENT) ? '?' : segment }
|
|
52
|
+
"#{verb.to_s.upcase} /#{template.join('/')}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Subscriber
|
|
57
|
+
def start(_name, _id, payload)
|
|
58
|
+
span = ::Datadog::Tracing.trace(OPERATION, type: 'http',
|
|
59
|
+
resource: Datadog.resource_for(payload[:base_url], payload[:verb],
|
|
60
|
+
payload[:path]))
|
|
61
|
+
# No span.kind:client / peer.service on purpose: the span covers the
|
|
62
|
+
# client's whole call (cache lookup, connect, parse), so it must not be
|
|
63
|
+
# inferred onto the knowledge-base service page as one of KB's operations.
|
|
64
|
+
span.set_tag('peer.hostname', URI(payload[:base_url]).host)
|
|
65
|
+
span.set_tag('kb.method', payload[:verb].to_s.upcase)
|
|
66
|
+
payload[SPAN_KEY] = span
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def finish(_name, _id, payload)
|
|
70
|
+
span = payload.delete(SPAN_KEY)
|
|
71
|
+
return unless span
|
|
72
|
+
|
|
73
|
+
span.set_tag('kb.cache_hit', payload[:cache_hit].to_s) if payload.key?(:cache_hit)
|
|
74
|
+
span.set_tag('http.status_code', payload[:status].to_s) if payload[:status]
|
|
75
|
+
span.set_error(payload[:exception_object]) if payload[:exception_object]
|
|
76
|
+
span.finish
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/kb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: barkibu-kb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Léo Figea
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: datadog
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.0'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: rake
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -313,6 +327,7 @@ files:
|
|
|
313
327
|
- lib/kb/fake/bounded_context/pet_family/products.rb
|
|
314
328
|
- lib/kb/fake/bounded_context/rest_resource.rb
|
|
315
329
|
- lib/kb/inflections.rb
|
|
330
|
+
- lib/kb/instrumentation/datadog.rb
|
|
316
331
|
- lib/kb/models.rb
|
|
317
332
|
- lib/kb/models/assessment.rb
|
|
318
333
|
- lib/kb/models/base_model.rb
|