A wrapper of Barkibu's Knowledge Base Endpoint to make those entities and their respective CRUD operations available to a ruby app.
Add this line to your application's Gemfile:
gem 'barkibu-kb'And then execute:
$ bundle
Or install it yourself as:
$ gem install kb
Specs and RuboCop run on every pull request (.github/workflows/ci.yml) on Ruby 3.4. Locally:
docker compose run --rm kb bundle exec rspec
docker compose run --rm kb bundle exec rubocop lib specThis gem wraps the Knowledge Base Api and exposes CRUD-able entities into the requiring application.
The configuration of the connection to the Knowledge Base is done using ENV variable:
-
KB_API_KEY: the Knowledge Base API Key
-
KB_PARTNER_KEY: the Partner KB Key
-
KB_API_URL_TEMPLATE: the template url of the Knowledge Base where will be extrapolated bounded_context, version and entity.
For instance:
https://dev.api.%{bounded_context}.barkkb.com/%{version}/%{entity}.
We add the ability to cache GET responses. To enable it, just set KB.config.cache properties:
# config/initializers/kb_ruby.rb
KB.config.cache.instance = Rails.cache # ActiveSupport::Cache::NullStore.new
KB.config.cache.expires_in = 1.second # 0 by default
KB.config.log_level = :debugger # :info by defaultTimeouts are per phase, and each phase fails with its own error class:
connect_timeout maps to Net::HTTP's open_timeout, bounding TCP connect plus
the TLS handshake (Net::OpenTimeout); read_timeout bounds the wait for the
response (Net::ReadTimeout); write_timeout bounds sending the request
(Net::WriteTimeout).
# config/initializers/kb_ruby.rb
KB.config.request.connect_timeout = 2 # 1 by default; TCP connect + TLS handshake
KB.config.request.write_timeout = 4 # 3 by default
KB.config.request.read_timeout = 10 # 5 by defaultThe read budget can be raised for a single call through KB::Client#request, for
the few endpoints whose server-side work legitimately runs for seconds. Connect
and write budgets stay global:
KB::Pet.kb_client.request('birthdays', filters: { month: 9, day: 22, size: 1000 }, read_timeout: 30)The client retries a failed call once when the failure is in the transport, never
when KB answered with an HTTP error. Which calls retry depends on whether the
request can have reached KB (KB::RetryPolicy):
| Failure | Retried for |
|---|---|
Never sent: connect/TLS timeout (Net::OpenTimeout), connection refused, host/network unreachable, DNS failure |
every verb, POST included |
| Maybe sent: read/write timeout, connection reset, EOF, TLS error mid-stream | GET and HEAD only |
| HTTP 4xx/5xx | never |
PUT and DELETE are not retried on "maybe sent" failures: upsert and
PetParent#merge! are PUTs whose second run is not a no-op on KB's side, and a
repeated DELETE would turn a success into a 404. A call that raised its own read
budget (read_timeout: on KB::Client#request) is not retried on "maybe sent"
failures either, so a 30s birthdays read can't become 60s; failures that never
reached KB are still retried.
# config/initializers/kb_ruby.rb
KB.config.request.retries = 1 # default; 0 disables retries
KB.config.request.retry_interval = 0.1 # default, seconds; each wait is 1x-2x thisLike the timeouts, these are read when the process's shared KB connection is
built, i.e. on its first KB call, so set them in an initializer (or call
KB::Connections.reset! after changing them at runtime).
Worst case, a call now takes two attempts' worth of phase budgets plus the interval, e.g. a GET that read-times-out twice takes about 2 x (1 + 3 + 5)s with the default timeouts.
KB calls reuse TCP/TLS connections instead of opening a new one per call, through
the stock net_http_persistent Faraday adapter (net-http-persistent). Every
model's client shares one Faraday connection (KB::Connections), so they all
draw on one connection pool per process; each
thread checks a connection out per call. A pooled connection idle for longer
than idle_timeout is closed and reopened on the next call.
# config/initializers/kb_ruby.rb
KB.config.request.keep_alive = true # default; false opens a connection per call (net_http)
KB.config.request.idle_timeout = 30 # default, secondsKeep idle_timeout below the Heroku router's own idle close: it drops an idle
client connection after about 55 seconds (measured against KB staging,
2026-09-23). A connection the server already closed is noticed before the next
request is written, and reopened. A close that races the request in the same
instant can't be noticed: a GET is retried on a fresh connection (see Retries),
a POST fails with Faraday::ConnectionFailed wrapping EOFError.
Net::HTTP's own retry of idempotent requests stays off (max_retries = 0), so a
PUT is never resent behind KB::RetryPolicy's back.
A call with its own read_timeout: goes through a separate plain connection,
opened for that call: the pooled adapter keeps one timeout setting for the whole
pool, where one call's override would reach other threads' calls.
Differences from keep_alive = false: a connect timeout surfaces as
Faraday::TimeoutError wrapping Net::OpenTimeout (retries classify it by that
underlying error, so it is still retried for every verb), and a refused
connection as Faraday::ConnectionFailed wrapping Net::HTTP::Persistent::Error.
Every KB call emits one request.kb_client event through
ActiveSupport::Notifications, wrapping the whole call: cache lookup, TCP
connect, TLS, write, read and JSON parsing. The payload carries verb, path,
base_url, cache_hit (GET calls only), status (when a response arrived),
retries and retry_errors (only when the call was retried: the count, and the
underlying error class of each failed attempt, e.g. ["Net::OpenTimeout"]) and
ActiveSupport's exception / exception_object when the call raised. The event
covers the whole call including retries, so exception is set only when every
attempt failed. Subscribe to it for logging, metrics or anything else:
ActiveSupport::Notifications.subscribe(KB::Client::REQUEST_EVENT) do |event|
Rails.logger.info("KB #{event.payload[:verb]} #{event.payload[:path]} #{event.duration.round}ms")
endA ready-made subscriber turns each event into a kb.client.request APM span.
Opt in from the app's Datadog initializer, after Datadog.configure. Works with
both ddtrace 1.x and datadog 2.x; the tracer gem is the app's dependency.
# config/initializers/datadog_tracer.rb
Datadog.configure { |c| ... }
require 'kb/instrumentation/datadog'
KB::Instrumentation::Datadog.subscribe!The span opens when the event starts and closes when it finishes, so the
tracer's own Net::HTTP spans nest under it. It inherits the app's service
(c.service), so nothing new appears in the APM service list, and it carries no
span.kind:client or peer.service, so Datadog does not attribute it to the
knowledge-base service either: it measures the client's whole call, not a KB
operation. Resources are low-cardinality (GET /v1/pets/birthdays,
GET /v1/pets/?/contracts). Tags: peer.hostname (the KB host used),
kb.method, kb.cache_hit (GET calls only), http.status_code, kb.retries
and kb.retry_errors (retried calls only), plus the standard
error.type/error.message when the call raises. A span with kb.retries and
no error is a failure the retry absorbed.
Why not rely on the Net::HTTP tracer alone: faraday-net_http opens the socket
before Net::HTTP#request, the method that tracer patches, so a connect timeout
produces no http span at all. This span sees every phase.
KB::PetParent acts almost like an ActiveRecord implementing ActiveModel::Model exposing:
find- arg:
keystring - returns: a PetParent instance when provided an existing key or raise
ActiveRecord::RecordNotFound
- arg:
create- arg:
attributesto initialize the entity - returns: the raw attributes of the created PetParent instance
- throws an
KB::Errorexception if something went wrong
- arg:
find_or_create_by- arg:
attributes,additional_attributesto look for or initialize the entity - returns: look for a PetParent matching the passed attributes or initialize and persist one with the given attributes and launching the block provided
- throws an
KB::Errorexception if something went wrong
- arg:
all- arg:
filtershash of filters - returns: an array of PetParent instances matching the filters
- arg:
save!- persists (create or update) the entity to the Knowledge Base
- throws an
KB::Errorexception if something went wrong
destroy!- deletes the entity in the Knowledge Base
- throws a
KB::Errorexception if something went wrong
contracts- returns all the KB::PetContract associated with this pet parent
referrals- returns all the KB::Referral associated with this pet parent
referrers- returns all the KB::Referral associated with any of the pet parent's pets
iban- returns the IBAN of the pet parent
update_iban- arg:
ibanstring - updates the IBAN of the pet parent and reloads the entity
- arg:
KB::Assessment represents a read-only resource exposing:
find- arg:
keystring - returns: an Assessment instance when provided an existing key or raise
ActiveRecord::RecordNotFound
- arg:
all- arg:
filtershash of filters - returns: an array of Assessment instances matching the filters
- arg:
KB::Condition represents a read-only resource.
KB::Symptom represents a read-only resource.
KB::Pet represents a resource exposing:
all- arg:
filtershash of filters - returns: an array of Pet instances matching the filters
- arg:
create- arg:
attributesto initialize the entity - returns: the raw attributes of the Pet instance
- throws an
KB::Errorexception if something went wrong
- arg:
find_or_create_by- arg:
attributes,additional_attributesto look for or initialize the entity - returns: look for a Pet matching the passed attributes or initialize and persist one with the given attributes and launching the block provided
- throws an
KB::Errorexception if something went wrong
- arg:
save!- persists (create or update) the entity to the Knowledge Base
- throws an
KB::Errorexception if something went wrong
destroy!- deletes the entity in the Knowledge Base
- throws a
KB::Errorexception if something went wrong
contracts- returns all the KB::PetContract associated with this pet
upsert- updates KB:Pet if exists a Pet with same name for its PetParent
- creates a new KB:Pet if not exists a Pet with same name for its PetParent
KB::PetContract represents a resource exposing:
find- arg:
keythe key of the contract in the Knowledge Base - returns the contract with the matching key
- throws an
KB::ErrororKB::ResourceNotFoundexception if something went wrong
- arg:
find_by_contract_number- arg:
contract_numberthe contract number to find - returns the identified KB::PetContract
- throws an
KB::ErrororKB::ResourceNotFoundexception if something went wrong
- arg:
create- arg:
attributesto initialize the entity - returns: the raw attributes of the PetContract instance
- throws an
KB::Errorexception if something went wrong
- arg:
save!- persists (create or update) the entity to the Knowledge Base
- throws an
KB::Errorexception if something went wrong
search- arg:
filtershash. Currently, KB API supports these keys:page: results page number. Default 0.size: amount of results per page. Default 10.chip: searches contracts by insured pet chip
- returns: a hash including:
total: total amount of contracts foundpage: current page numberelements: array of PetContract instances matching the filters
- arg:
KB::Plan represents a resource exposing:
all- returns: the array of available plans
> KB Breed endpoint requires `locale` as param. By default is set to 'es-es' but can be override setting **KB_BREEDS_DEFAULT_LOCALE** ENV var
KB::Breed represents a resource exposing:
all- arg:
filtershash of filters - returns: and array of Breed instances matching the filters
- arg:
dogs(alias for all(species: 'dog'))- arg:
filtershash of filters - returns: and array of Dog Breed instances matching the filters
- arg:
cats(alias for all(species: 'cat'))- arg:
filtershash of filters - returns: and array of Cat Breed instances matching the filters
- arg:
KB::Referral represets a referral resource
create- arg:
pet_parent_key,attributesto initialize the entity - returns: the raw attributes of the Referral instance
- throws an
KB::Errorexception if something went wrong
- arg:
The KB::Concerns::AsKBWrapper concern has been created in order to easily make an ActiveRecord model wrap a KB model.
To use it:
- include it into your wrapping model, define an attribute
kb_keyon your wrapping model - call
wrap_kbwith the wrapped KB model class (available option:skip_callback)
You have then access to the wrapped model under kb_model and can delegate attributes to it, for instance:
class User < ActiveRecord::Base
include KB::Concerns::AsKBWrapper
wrap_kb model: KB::PetParent
KB_DELEGATED_ATTRIBUTES = %i[email first_name].freeze
KB_DELEGATED_ATTRIBUTES.each do |attribute|
delegate attribute, to: :kb_model, prefix: false
delegate "#{attribute}=", to: :kb_model, prefix: false
end
end
user = User.create(first_name: 'Léo', email: 'leo@barkibu.com')
p user.kb_model
# => #<KB::PetParent: 0x000055fd72d32c30 key: "373ad90e-c2ce-46cb-9749-deb2b03be995", first_name: "Léo", ..., email: "leo@barkibu.com">For testing purposes, you may want to fake Knowledge Base’s API calls. We provide a FakeApi app that you can use including the kb-fake gem in your dev/test dependencies.
Add gem webmock and gem sinatra to your Gemfile to use the following configuration:
# ...
RSpec.configure do |config|
# ...
config.before(:all) do
stub_request(:any, /test_api_barkkb.com/).to_rack(KB::Fake::Api)
end
config.around(:each) do |example|
snapshot = KB::Fake::Api.snapshot()
stub_request(:any, /test_api_barkkb.com/).to_rack(KB::Fake::Api)
example.run
KB::Fake::Api.restore snapshot
end
# ...
endMake sure to set the KB_API_URL_TEMPLATE to something that will match above the request interceptor, for instance: http://test_api_barkkb.com/%{version}/%{entity}
You should be able to use the API seemlessly and the calls to the API will be intercepted and a local one used instead in a similar fashion to how ActiveRecord operations are wrapped into a transaction in a rails app with use_transactional_fixtures activated.
docker compose run --rm kb bash
> bundle install
> rspecYou can also start an interactive console if needed:
KB_API_URL_TEMPLATE=https://example.com/%{version}/%{entity} \
KB_API_KEY=YourKbApiKey \
KB_PARTNER_KEY=YourKbPartnerKey \
bin/consoleBug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/kb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the KB project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.