EndPointBlank (Ruby)
The Ruby client for EndPointBlank: API endpoint tracking, endpoint authorization, error/request/response/log reporting, and client-side data masking — with a framework-agnostic core that runs in plain Ruby or Sinatra, plus a Rails adapter that auto-loads (railtie + middleware) when Rails is present.
Capabilities
- Endpoint tracking — every request/response passing through the Rack middleware is reported.
- Authorization — outbound calls to other EndPointBlank-protected services are signed
(
Basicclient-credential or cachedBearertoken), and inbound requests can be authorized against the EndPointBlank service before your action runs. - Error, request, response, and log reporting — background, queued, non-blocking delivery to the EndPointBlank intake API.
- Client-side data masking (
EndPointBlank::Masking/masking_rules) — strip or redact sensitive fields from payloads before they leave your process, as defense in depth on top of server-side masking. - Framework-agnostic core —
EndPointBlank::Middleware::Rack::ReportInteractionand the writers work directly against Rack env/::Rack::Request, so the gem behaves correctly under plain Ruby, Sinatra, or any Rack app. When::Railsis defined, aRailtieauto-inserts the middleware and wires upRails.logger; nothing extra needs loading.
Installation
This gem is not yet published on RubyGems.org. Until it is, install it from git.
Add to your Gemfile:
gem "end_point_blank", github: "EndPointBlank/end_point_blank_rails"
(Once released to RubyGems, this collapses to gem "end_point_blank".)
Then:
bundle install
Quick start
EndPointBlank.configure do |config|
config.client_id = "your-client-id"
config.client_secret = "your-client-secret"
config.app_name = "my-service"
end
That's it for a Rails app — the railtie auto-inserts the reporting middleware, and every request processed by your app is tracked. For plain Ruby / Sinatra, see Framework integration below to wire up the Rack middleware yourself.
To send your first log line:
EndPointBlank::Writers::LogWriter.info("service started", { pid: Process.pid })
Configuration
EndPointBlank.configure { |c| ... } yields the EndPointBlank::Configuration singleton.
Every setting listed below can be set explicitly in that block, and most also fall back to an
ENDPOINTBLANK_* environment variable, then to a built-in default.
Precedence: explicit configure value > ENDPOINTBLANK_* environment variable > default.
configure setting |
Env var fallback | Default | Notes |
|---|---|---|---|
client_id |
ENDPOINTBLANK_CLIENT_ID |
nil |
Used to build the Basic authorization header. |
client_secret |
ENDPOINTBLANK_CLIENT_SECRET |
nil |
Paired with client_id. |
base_url |
ENDPOINTBLANK_BASE_URL |
https://in.endpointblank.com |
Base for access-token, authorize, and endpoint-update APIs. |
log_base_url |
ENDPOINTBLANK_LOG_BASE_URL |
https://log.endpointblank.com |
Base for error/request/response/log reporting APIs. |
app_name |
ENDPOINTBLANK_APP_NAME |
Rails.application.name.underscore if Rails is defined, else nil |
Identifies your app to EndPointBlank. |
env_name |
ENDPOINTBLANK_ENV |
RACK_ENV, then APP_ENV, then Rails.env if defined, else "production" (resolved per-request by SessionConfiguration.env_name, not read directly off Configuration) |
The environment name reported with each request/response payload. |
logger |
— | A ::Logger.new($stdout, level: ::Logger::INFO), or Rails.logger under Rails (set by the railtie) |
Any object with .debug/.info/.warn/.error/.fatal works. |
worker_count |
— | 4 |
Currently unused by the delayed writer (which always spins up 2 threads); reserved. |
token_ttl |
— | nil |
Optional TTL (seconds) requested when generating a Bearer access token. |
cache_ttl |
— | 300 |
TTL (seconds) for the authorization decision cache. |
trust_proxy_headers |
— | true |
Whether the per-request scheme/host/port report honors X-Forwarded-Proto/-Host/-Port. See Reported base URL. |
masking_rules |
— | [] |
Ordered list of masking rule hashes — see Data masking. |
mask_hook |
— | nil |
Optional ->(payload, record_type_string) { payload } run after masking_rules. |
version_finder |
— | nil |
Optional ->(request) { "1" } overriding EndPointBlank::Commands::VersionFinder's default header/param/path detection. |
application_version |
— | nil |
Reserved for reporting your app's own version. |
Note: there is also a bare environment accessor on Configuration, but it is not read by any
code path in this gem (the real per-request environment name is env_name, described above) — do
not rely on it.
Reported base URL
Every request payload carries the base URL the caller used, as three separate fields —
scheme, host and port. A field that cannot be resolved is omitted rather than sent as
null. EndPointBlank uses these to fill in an application environment's base URL for you,
instead of asking someone to type it.
By default the gem honors X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Port,
reading the last comma-separated hop. It does this on its own, without consulting Rails'
or Rack's trusted-proxy configuration, so that all five EndPointBlank clients answer
identically for the same request.
Turn this off if your application is reachable directly, with no proxy in front of it — or if you would simply rather report nothing than report something a caller could influence:
EndPointBlank.configure { |c| c.trust_proxy_headers = false }
With it off, the X-Forwarded-* headers are ignored entirely and scheme, host and port
come from the connection and the Host header only.
It defaults to true because the alternative is worse for almost everyone. Most production
deployments sit behind an ALB, nginx, Caddy or an Ingress, and a client that ignored the
forwarded headers there would not report nothing — it would confidently report an internal
hostname on an internal port. host is caller-controlled either way (it has always come from
the Host header), and none of these three values is ever used as an identity or
authorization key, so the worst case is a wrong suggestion that an admin has to approve.
configure block example
EndPointBlank.configure do |config|
config.client_id = "abc123"
config.client_secret = "s3cr3t"
config.base_url = "https://in.endpointblank.com"
config.log_base_url = "https://log.endpointblank.com"
config.app_name = "checkout-service"
config.env_name = "staging"
config.logger = Logger.new($stdout)
end
12-factor / env-var example
With no configure block at all (or a partial one), the same values can come entirely from the
environment:
export ENDPOINTBLANK_CLIENT_ID=abc123
export ENDPOINTBLANK_CLIENT_SECRET=s3cr3t
export ENDPOINTBLANK_BASE_URL=https://in.endpointblank.com
export ENDPOINTBLANK_LOG_BASE_URL=https://log.endpointblank.com
export ENDPOINTBLANK_APP_NAME=checkout-service
export ENDPOINTBLANK_ENV=staging
Usage
Authorization
EndPointBlank::Authorization.header(base_url = nil) builds the outbound Authorization header
used by the gem's own HTTP calls: a cached Bearer token covering base_url when one is
available (via EndPointBlank::AccessTokens), otherwise Basic credentials built from
client_id / client_secret -- which covers both giving no target and a token that could not
be obtained.
EndPointBlank::Authorization.header # => "Basic ..."
# Pass the URL you are about to call, NOT a hostname.
# Strip any query string or fragment first -- intake rejects both.
EndPointBlank::Authorization.header("https://api.example.com/orders") # => "Bearer ..." if a token is cached
The argument is the URL you are about to call. intake matches it against registered base URLs by
longest path prefix, so you need not know how the target registered itself -- header for
https://api.example.com/orders/42 reuses a token already cached for
https://api.example.com/orders. EndPointBlank::AccessTokens caches one token per base URL
intake resolves to, not one per process, so a service that calls several targets holds a token
for each. A URL that does not match character-for-character (a different case, a query string,
an unregistered path) simply misses and mints a new token -- it never guesses.
Under Rails, protect an inbound endpoint by including the Authorized concern in a controller —
it calls EndPointBlank::Commands::EndpointAuthorize.authorize(request) before the action, and
raises EndPointBlank::UnauthorizedError (which you can rescue with
rescue_from EndPointBlank::UnauthorizedError in ApplicationController) on a non-201 response:
class OrdersController < ApplicationController
include EndPointBlank::Rails::Authorized
end
EndPointBlank::Commands::EndpointAuthorize.authorize sends the request's path, HTTP method,
inbound Authorization header, app name, resolved endpoint version, and remote IP to
#{base_url}/api/authorize, authenticating itself to intake with Basic, and caches a positive
(201) result for cache_ttl seconds via EndPointBlank::Commands::AuthenticationCache. It never
mints or presents a Bearer token for this call: intake already holds this service's own
credential, so exchanging one to present it back would buy nothing.
Behavior change: target_hostname on the authorize call now comes from the Host header
only. It previously came from request.host, which reads the last X-Forwarded-Host hop. If
your app sits behind a proxy that rewrites Host (nginx's default; Caddy and most ALBs
preserve it) and you registered the external hostname in the portal, either update the
registered hostname to the internal one the app now reports, or configure the proxy to preserve
Host. Deployments where Host and X-Forwarded-Host agree are unaffected.
Error reporting
Exceptions raised while EndPointBlank::Middleware::Rack::ReportInteraction is on the stack are
reported automatically (see Framework integration). To report one
manually:
begin
risky_operation!
rescue => e
EndPointBlank::Writers::ExceptionWriter.write(e)
raise
end
Request/response/log reporting
Requests and responses are written automatically by the Rack middleware. Application logs are sent explicitly:
EndPointBlank::Writers::LogWriter.info("cache warmed", { keys: 42 })
EndPointBlank::Writers::LogWriter.warn("slow query", { duration_ms: 820 })
EndPointBlank::Writers::LogWriter.error("payment webhook rejected", { code: "sig_mismatch" })
EndPointBlank::Writers::LogWriter.fatal("out of workers")
All writers (RequestWriter, ResponseWriter, ExceptionWriter, LogWriter) enqueue their
payload onto a bounded, in-memory queue (DelayedWriter, capacity 1000, drop-oldest under
sustained backpressure) drained by two background threads that POST batches via excon. Delivery
is fire-and-forget and never raises into your request cycle.
Data masking
Mask sensitive data client-side, before it leaves your process. Configure an ordered list of rules; each rule targets one field and masks by a JSONPath, a regex, or both. (Server-side intake also masks independently, so this is defense in depth.)
EndPointBlank.configure do |config|
config.masking_rules = [
# Replace any "ssn" field at any depth in the request body.
{ target: "request_body", path: "$..ssn", replacement_value: "***" },
# Keep first/last 4 of a card number in error messages via backreferences.
{ target: "error_message", regex: "(\\d{4})-\\d{4}-\\d{4}-(\\d{4})", replacement_value: "$1-****-****-$2" }
]
# Optional: runs after the rules; last chance to transform the payload.
config.mask_hook = ->(payload, record_type) { payload }
end
Rules are hashes with symbol (or string) keys.
Rule fields
target— exactly one of"request_body","request_headers","path","response_body","error_message".path— an optional JSONPath (supported subset:$,.name,['name'],[n],.*/[*], and..namefor recursive descent). Keys are case-sensitive.regex— an optional regular expression source string.replacement_value— the replacement string (default"...").
Semantics — path scopes, regex matches within. With only a path, the selected node is
replaced entirely. With only a regex, every matching string leaf is replaced. With both, the
regex is applied only within the path-selected node(s). When a regex is present,
replacement_value supports backreferences: $1, $2, … insert capture groups ($0 the whole
match; $$ for a literal $). Stacktraces and log messages/data are never masked (there is no
log entry in the masking field map).
Framework integration
Rails
Nothing to wire up manually. When ::Rails is defined, lib/end_point_blank.rb requires
EndPointBlank::Rails::Railtie, which:
- inserts
EndPointBlank::Middleware::Rack::ReportInteractioninto the middleware stack right afterActionDispatch::DebugExceptions, so every request/response is reported and exceptions are captured before Rails' own exception rendering; and - sets
Configuration.instance.logger ||= Rails.logger, soEndPointBlank.loggerwrites throughRails.loggerunless you've already configured your own.
Optional concerns for controllers:
class ApplicationController < ActionController::Base
rescue_from EndPointBlank::UnauthorizedError do |e|
render json: { error: e. }, status: e.status
end
end
class OrdersController < ApplicationController
include EndPointBlank::Rails::Authorized # authorize inbound requests before each action
include EndPointBlank::Rails::Versioned
version ["v1", "v2"], only: [:index]
end
app_name falls back to Rails.application.name.underscore automatically, so Rails apps
typically only need to configure client_id / client_secret (and app_name only to override
the Rails-derived default).
Plain Ruby / Sinatra
There's no Rails to auto-load anything, so insert the Rack middleware yourself and set app_name
and env_name explicitly (via configure or ENDPOINTBLANK_APP_NAME / ENDPOINTBLANK_ENV,
since there's no Rails.application.name / Rails.env to infer them from):
require "sinatra"
require "end_point_blank"
EndPointBlank.configure do |config|
config.client_id = ENV.fetch("ENDPOINTBLANK_CLIENT_ID")
config.client_secret = ENV.fetch("ENDPOINTBLANK_CLIENT_SECRET")
config.app_name = "my-sinatra-app" # or set ENDPOINTBLANK_APP_NAME and omit this
config.env_name = "production" # or set ENDPOINTBLANK_ENV / RACK_ENV and omit this
config.logger = Logger.new($stdout)
end
use EndPointBlank::Middleware::Rack::ReportInteraction
get "/" do
"ok"
end
The middleware calls EndPointBlank::Rack::EnvStore.set(env), reports the request via
RequestWriter, invokes the app, and — in an ensure — reports the response via ResponseWriter
and clears the env store, reporting any raised exception via ExceptionWriter along the way. It
reads/writes plain Rack request objects (::Rack::Request), so it works identically under any
Rack-compatible server or framework, not only Sinatra.
Development
bundle install
bundle exec rspec
bundle exec rubocop
bundle exec rspec runs the full suite, including specs that assert the framework-agnostic core
behaves correctly with ::Rails undefined (spec/no_rails_spec.rb,
spec/generate_access_token_no_rails_spec.rb,
spec/route_pattern_finder_and_version_finder_no_rails_spec.rb).
License
No LICENSE file or spec.license is currently present in this repository. Treat usage as
proprietary/all-rights-reserved until a license is added, or confirm terms with the repository
owners.