Add OpenTelemetry with Rack instrumentation
Gated on OTEL_EXPORTER_OTLP_ENDPOINT — no gems loaded or SDK
initialized when unset. The SDK reads endpoint, headers, and
protocol directly from standard OTEL_EXPORTER_OTLP_* env vars.
Moved CONFIG initialization from db.rb to config.rb so it is
available at require-time without depending on load order.
Closes #29
Assisted-by: Claude Opus 4.6 via pi
diff --git a/Gemfile b/Gemfile
index 9469cef..bc8d3e5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -12,6 +12,10 @@ gem "sentry-ruby"
gem "sequel"
gem "sqlite3"
+gem "opentelemetry-sdk"
+gem "opentelemetry-exporter-otlp"
+gem "opentelemetry-instrumentation-rack"
+
group :development do
gem "ferrum"
gem "minitest"
diff --git a/Gemfile.lock b/Gemfile.lock
index e1eb94a..58247b4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -40,6 +40,11 @@ GEM
ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.3-x86_64-linux-musl)
fileutils (1.8.0)
+ google-protobuf (4.33.5)
+ bigdecimal
+ rake (>= 13)
+ googleapis-common-protos-types (1.22.0)
+ google-protobuf (~> 4.26)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
json (2.18.1)
@@ -53,6 +58,31 @@ GEM
prism (~> 1.5)
mutex_m (0.3.0)
nio4r (2.7.5)
+ opentelemetry-api (1.7.0)
+ opentelemetry-common (0.23.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-exporter-otlp (0.31.1)
+ google-protobuf (>= 3.18)
+ googleapis-common-protos-types (~> 1.3)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-sdk (~> 1.10)
+ opentelemetry-semantic_conventions
+ opentelemetry-instrumentation-base (0.25.0)
+ opentelemetry-api (~> 1.7)
+ opentelemetry-common (~> 0.21)
+ opentelemetry-registry (~> 0.1)
+ opentelemetry-instrumentation-rack (0.29.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-registry (0.4.0)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-sdk (1.10.0)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-registry (~> 0.2)
+ opentelemetry-semantic_conventions
+ opentelemetry-semantic_conventions (1.36.0)
+ opentelemetry-api (~> 1.0)
parser (3.3.10.1)
ast (~> 2.4.1)
racc
@@ -155,6 +185,9 @@ PLATFORMS
DEPENDENCIES
ferrum
minitest
+ opentelemetry-exporter-otlp
+ opentelemetry-instrumentation-rack
+ opentelemetry-sdk
phlex
puma
rack-test
@@ -190,6 +223,8 @@ CHECKSUMS
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02
+ google-protobuf (4.33.5) sha256=1b64fb774c101b23ac3f6923eca24be04fd971635d235c4cd4cfe0d752620da0
+ googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
@@ -198,6 +233,14 @@ CHECKSUMS
minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
+ opentelemetry-api (1.7.0) sha256=ccfd264ea6f2db5bf4185e3c07a1297977b44a944e2ce65457c4fe63a697214f
+ opentelemetry-common (0.23.0) sha256=da721190479d57bae0ad2207468f47f3e2c3b9a91024b5bc32c9d280183eb32c
+ opentelemetry-exporter-otlp (0.31.1) sha256=5358be17d7849cbcc4f49e1fc24105edc780a6f96c8e57b64192ab9a8e47474a
+ opentelemetry-instrumentation-base (0.25.0) sha256=642a3a7f08354e6e969423327a4fa67ed2cca7ac6fe5ee09e55b17d1c576da27
+ opentelemetry-instrumentation-rack (0.29.0) sha256=9e2cbb8336087064cbe33b502d917d85b174162bc717efda1cfdbd182342f377
+ opentelemetry-registry (0.4.0) sha256=903fa6bfaa29eac1c1d73a4fdd29b850977b5353b84b8cdff11222c00ad2968f
+ opentelemetry-sdk (1.10.0) sha256=43719949be8df24dcaeb86ebbf75636cda87d51a01af2729499b92a48b80521a
+ opentelemetry-semantic_conventions (1.36.0) sha256=c1b1607dbc7853aac7f9e23f6e8b76969c45b07f2b812a4aa4383c19a3b0f617
parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
phlex (2.4.1) sha256=e596717fbfe38b5271840266758779ebe75092e02629f0c170287e6290a70b12
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
diff --git a/config.ru b/config.ru
index 431becc..224b661 100644
--- a/config.ru
+++ b/config.ru
@@ -8,7 +8,20 @@ unless ENV["COMMIT_SHA"]
ENV["BUILD_DATE"] = Time.now.strftime("%Y-%m-%d")
end
-require_relative "lib/ketchup/web"
+require_relative "lib/ketchup/config"
+
+if CONFIG.otel
+ require "opentelemetry/sdk"
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/instrumentation/rack"
+
+ OpenTelemetry::SDK.configure do |c|
+ c.service_name = "ketchup"
+ c.use "OpenTelemetry::Instrumentation::Rack"
+ end
+
+ use(*OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.middleware_args)
+end
$stderr.puts CONFIG
@@ -29,4 +42,5 @@ if CONFIG.default_user
use Ketchup::DevAuth, CONFIG.default_user
end
+require_relative "lib/ketchup/web"
run Web.freeze.app
diff --git a/lib/ketchup/config.rb b/lib/ketchup/config.rb
index 28f964d..41fb9e5 100644
--- a/lib/ketchup/config.rb
+++ b/lib/ketchup/config.rb
@@ -7,6 +7,7 @@ Config = Data.define(
:database_url, #: String
:session_secret, #: String
:sentry, #: SentryConfig?
+ :otel, #: OtelConfig?
:default_user, #: DefaultUser?
:commit_sha, #: String?
:change_id, #: String?
@@ -19,6 +20,10 @@ class Config
:env, #: String?
)
+ OtelConfig = Data.define(
+ :endpoint, #: String
+ )
+
DefaultUser = Data.define(
:login, #: String
:name, #: String
@@ -37,6 +42,7 @@ class Config
def to_s
parts = ["database=#{database_url}"]
parts << "sentry=#{sentry.env || "on"}" if sentry
+ parts << "otel=on" if otel
parts << "default_user=#{default_user.login}" if default_user
parts << "commit=#{commit_sha}" if commit_sha
parts << "change=#{change_id}" if change_id
@@ -47,11 +53,13 @@ class Config
#: (?Hash[String, String] env) -> Config
def self.from_env(env = ENV)
sentry_dsn = env["SENTRY_DSN"]
+ otel_endpoint = env["OTEL_EXPORTER_OTLP_ENDPOINT"]
default_user = env["DEFAULT_USER"]
new(
database_url: env.fetch("DATABASE_URL") { "db/ketchup.db" },
session_secret: env.fetch("SESSION_SECRET") { SecureRandom.hex(64) },
sentry: sentry_dsn ? SentryConfig.new(dsn: sentry_dsn, env: env["SENTRY_ENV"]) : nil,
+ otel: otel_endpoint ? OtelConfig.new(endpoint: otel_endpoint) : nil,
default_user: default_user ? DefaultUser.parse(default_user) : nil,
commit_sha: env["COMMIT_SHA"],
change_id: env["CHANGE_ID"]&.slice(0, 8),
@@ -59,3 +67,5 @@ class Config
)
end
end
+
+CONFIG = Config.from_env
diff --git a/lib/ketchup/db.rb b/lib/ketchup/db.rb
index d21088a..7deb168 100644
--- a/lib/ketchup/db.rb
+++ b/lib/ketchup/db.rb
@@ -4,7 +4,6 @@ require "sequel"
require_relative "config"
-CONFIG = Config.from_env
DB = Sequel.sqlite(CONFIG.database_url)
Sequel.extension :migration
Sequel::Migrator.run(DB, File.expand_path("../../db/migrate", __dir__))