barkibu-kb 1.0.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: 5eb3a704f422c511e2e30d584cd6a80a7d91a090cfa5452dd197e65496b52eee
4
- data.tar.gz: 05d766aeca3b869601193bd1a6310ced1c07f96d6f24cd5424c290a33b1edaf9
3
+ metadata.gz: 9b46793e55bdd7e53d2e98e3e792972ec67a86048074aeec1bc9ffdde8028965
4
+ data.tar.gz: 44d5748d6fb445709add87f4bea42c9c19516ff9bb81be8b4112b716c17469c9
5
5
  SHA512:
6
- metadata.gz: 994f4d86e1c2d1aa40a173ed0b77a2fcff39cbfe1e0390c728f92887be3817e3851610f85796c24a61919c05789a718c3939ee965866a1fda2f72ed2e68492b1
7
- data.tar.gz: c9de80ede2701f7e58bde7f803b473980383d6ec9ac3d8509f2fe0e74ff15a200e48025ecc2535bc7102b82f1199228806d1e1999de32745ae07123123cb7edd
6
+ metadata.gz: 38cb22fa193c859a72448aea1cecfbad17671a4deae2dbd5763881c05f3ffa7338ab80b6218de5e041463c1e0575d190bba204fcf6851a6655107b1894eb1ca6
7
+ data.tar.gz: f66b4353ac4cc0e3432310ecf7d54b212dfe8b1b245ca6f9f95062b49185acc5028ac1b5ab69503f9099de9cfc89f2ff39e3c62df12531e8c4f593a3bef2e034
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [master]
7
+
8
+ jobs:
9
+ rubocop:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: '3.4'
16
+ bundler-cache: true
17
+ - run: bundle exec rubocop lib spec
18
+
19
+ rspec:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v5
23
+ - uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: '3.4'
26
+ bundler-cache: true
27
+ - run: bundle exec rspec
data/CHANGELOG.md CHANGED
@@ -6,7 +6,14 @@ 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.0.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.
14
+
15
+ ## [1.1.0]
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.
10
17
 
11
18
  ## [1.0.0]
12
19
  - [Breaking changes] Split the single global request timeout into per-phase budgets: `KB.config.request.connect_timeout` (default 1s, bounds TCP connect + TLS handshake), `write_timeout` (default 3s), `read_timeout` (default 5s). `KB.config.request.timeout` is removed — assigning it now raises `NoMethodError` at boot. Migration: a previous global `timeout` maps to `read_timeout` (e.g. `KB_REQUEST_TIMEOUT_SECONDS=12` → `read_timeout = 12`).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- barkibu-kb (1.0.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.0.0)
14
- barkibu-kb (= 1.0.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
@@ -18,6 +18,15 @@ Or install it yourself as:
18
18
 
19
19
  $ gem install kb
20
20
 
21
+ ## Development
22
+
23
+ Specs and RuboCop run on every pull request (`.github/workflows/ci.yml`) on Ruby 3.4. Locally:
24
+
25
+ ```sh
26
+ docker compose run --rm kb bundle exec rspec
27
+ docker compose run --rm kb bundle exec rubocop lib spec
28
+ ```
29
+
21
30
  ## Usage
22
31
 
23
32
  This gem wraps the Knowledge Base Api and exposes CRUD-_able_ entities into the requiring application.
@@ -57,6 +66,57 @@ KB.config.request.write_timeout = 4 # 3 by default
57
66
  KB.config.request.read_timeout = 10 # 5 by default
58
67
  ```
59
68
 
69
+ The read budget can be raised for a single call through `KB::Client#request`, for
70
+ the few endpoints whose server-side work legitimately runs for seconds. Connect
71
+ and write budgets stay global:
72
+
73
+ ```ruby
74
+ KB::Pet.kb_client.request('birthdays', filters: { month: 9, day: 22, size: 1000 }, read_timeout: 30)
75
+ ```
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
+
60
120
  ### Exposed Entities
61
121
 
62
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'])
@@ -7,47 +12,42 @@ module KB
7
12
  @base_url = base_url
8
13
  end
9
14
 
10
- def request(sub_path, filters: nil, method: :get)
11
- return connection.public_send(method, sub_path, attributes_to_json(filters)).body if method != :get
15
+ # `read_timeout` overrides KB.config.request.read_timeout for this one call only,
16
+ # for the few endpoints whose server-side work legitimately runs for seconds
17
+ # (e.g. GET /v1/pets/birthdays). Connect and write budgets stay global.
18
+ def request(sub_path, filters: nil, method: :get, read_timeout: nil)
19
+ return perform(method, sub_path, attributes_to_json(filters), read_timeout: read_timeout) if method != :get
12
20
 
13
21
  cache_key = "#{@base_url}/#{sub_path}/#{(filters || {}).sort.to_h}"
14
- KB::Cache.fetch(cache_key) do
15
- connection.public_send(method, sub_path, filters).body
16
- end
22
+ perform(:get, sub_path, filters, cache_key: cache_key, read_timeout: read_timeout)
17
23
  end
18
24
 
19
25
  def all(filters = {})
20
- cache_key = "#{@base_url}/#{filters.sort.to_h}"
21
-
22
- KB::Cache.fetch(cache_key) do
23
- connection.get('', attributes_case_transform(filters)).body
24
- end
26
+ perform(:get, '', attributes_case_transform(filters), cache_key: "#{@base_url}/#{filters.sort.to_h}")
25
27
  end
26
28
 
27
29
  def find(key, params = {})
28
30
  raise Faraday::ResourceNotFound, {} if key.blank?
29
31
 
30
- KB::Cache.fetch("#{@base_url}/#{key}") do
31
- connection.get(key, attributes_case_transform(params)).body
32
- end
32
+ perform(:get, key, attributes_case_transform(params), cache_key: "#{@base_url}/#{key}")
33
33
  end
34
34
 
35
35
  def create(attributes)
36
- connection.post('', attributes_to_json(attributes)).body
36
+ perform(:post, '', attributes_to_json(attributes))
37
37
  end
38
38
 
39
39
  def update(key, attributes)
40
40
  clear_cache_for(key)
41
- connection.patch(key.to_s, attributes_to_json(attributes)).body
41
+ perform(:patch, key.to_s, attributes_to_json(attributes))
42
42
  end
43
43
 
44
44
  def destroy(key)
45
45
  clear_cache_for(key)
46
- connection.delete(key.to_s).body
46
+ perform(:delete, key.to_s)
47
47
  end
48
48
 
49
49
  def upsert(attributes)
50
- connection.put('', attributes_to_json(attributes)).body
50
+ perform(:put, '', attributes_to_json(attributes))
51
51
  end
52
52
 
53
53
  def clear_cache_for(key)
@@ -56,6 +56,32 @@ module KB
56
56
 
57
57
  private
58
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
+
59
85
  def headers
60
86
  {
61
87
  'Content-Type': 'application/json',
@@ -86,6 +112,12 @@ module KB
86
112
  end
87
113
  end
88
114
 
115
+ def request_options(read_timeout)
116
+ return nil if read_timeout.nil?
117
+
118
+ ->(req) { req.options.read_timeout = read_timeout }
119
+ end
120
+
89
121
  def request_timeouts
90
122
  {
91
123
  open_timeout: KB.config.request.connect_timeout,
@@ -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.0.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.0.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
@@ -271,6 +285,7 @@ extra_rdoc_files: []
271
285
  files:
272
286
  - ".env.example"
273
287
  - ".github/pull_request_template.md"
288
+ - ".github/workflows/ci.yml"
274
289
  - ".github/workflows/release.yaml"
275
290
  - ".gitignore"
276
291
  - ".rspec"
@@ -312,6 +327,7 @@ files:
312
327
  - lib/kb/fake/bounded_context/pet_family/products.rb
313
328
  - lib/kb/fake/bounded_context/rest_resource.rb
314
329
  - lib/kb/inflections.rb
330
+ - lib/kb/instrumentation/datadog.rb
315
331
  - lib/kb/models.rb
316
332
  - lib/kb/models/assessment.rb
317
333
  - lib/kb/models/base_model.rb