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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6665ca6f0d74295cacf6bea9ee921b3dba5decc6b5cb3775bbaa0392fd65b86
4
- data.tar.gz: 700cae8005557d25ff41c3f9583725686485eff37ccbb0eec11ff07473ad9077
3
+ metadata.gz: 9b46793e55bdd7e53d2e98e3e792972ec67a86048074aeec1bc9ffdde8028965
4
+ data.tar.gz: 44d5748d6fb445709add87f4bea42c9c19516ff9bb81be8b4112b716c17469c9
5
5
  SHA512:
6
- metadata.gz: 97dac9ae9946f9398bd62b2fda16acbd978c1e4e153e23884ab78ac3a055dec7d201fcbf3a98ed4b65403324af2b03d034a6073f30d15c20690699021fccae74
7
- data.tar.gz: f9ed89dff91778420037ad50e7531af48d9b7b6d11c7d92afb1d05cdc31192498310520da51d35aa32e9ed77041e808137f5d9b699fcea24d495f2d17379b475
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.1.0...HEAD
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.1.0)
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.1.0)
14
- barkibu-kb (= 1.1.0)
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
- options = request_options(read_timeout)
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
- KB::Cache.fetch(cache_key) do
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 = "#{@base_url}/#{filters.sort.to_h}"
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
- KB::Cache.fetch("#{@base_url}/#{key}") do
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
- connection.post('', attributes_to_json(attributes)).body
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
- connection.patch(key.to_s, attributes_to_json(attributes)).body
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
- connection.delete(key.to_s).body
46
+ perform(:delete, key.to_s)
51
47
  end
52
48
 
53
49
  def upsert(attributes)
54
- connection.put('', attributes_to_json(attributes)).body
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
@@ -1,3 +1,3 @@
1
1
  module KB
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '1.2.0'.freeze
3
3
  end
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.1.0
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