rivulet-rb 0.2.2 → 0.3.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: b9136fe495990a216c80bd3d82b93e7677c8b32a57783aaf14ec8fe8e4030c41
4
- data.tar.gz: 394d3af940cd77e33316318cd47d5b640399f3de01f7c3f1b2420395f826d7dc
3
+ metadata.gz: 622289122f59a270997655c87af3287273f2f16f820f7713dd95092a32cb102b
4
+ data.tar.gz: 99896962459012b47744e0e8466a1b691bf5bc04f5337ef44ab3039b12938296
5
5
  SHA512:
6
- metadata.gz: 949131806a94fed7dd142974eae0ecf740e176c23521035ef7a16473bdb555740d4dfc1f6dd2eb2c58fd32395bef16b6ef2308b65cd52bf32f15079583b84aaf
7
- data.tar.gz: 18e342d91c24f28379df3511430adb63d6cf5f1e32d9134eea658f21bce791bfbe9a2a4fda38f8b03c234388785b9b6d86454c2a611eec3a19f3c7fca50f2771
6
+ metadata.gz: '081156052069871b24b394ef91db773c57a9c3c1225fb52b76499f944e7d834edb0319f87211e0c53bc5d57d24e43c6d744df0455f3206a850c427c72ef0db87'
7
+ data.tar.gz: 25b0d8cdb73570dd48c16d5e15d8bfb371f2721e47ffff66b12d9f87c9057957629c4fc2b005c85295fd7e1c4fa2d460b0522aea2b80da67aef5ddb8c6253038
data/docs/AGENTS.md CHANGED
@@ -84,7 +84,7 @@ app/
84
84
  utils/
85
85
  models/ # Sequel models
86
86
  config/
87
- application.rb # App configuration (DSN, logger, sendfile)
87
+ application.rb # App configuration (DSN, logger, sendfile, telemetry)
88
88
  routes.rb # Route definitions
89
89
  db/
90
90
  migrations/ # SQL migration files
@@ -739,3 +739,8 @@ must be self-contained SQL; there is no `drop`/`alter` safety net.
739
739
  them in the container instead.
740
740
  - Keep nesting in routes shallow and meaningful — express context, not the
741
741
  full domain hierarchy.
742
+ - Every request is automatically timed through `Rivulet::Telemetry` (one node
743
+ per operation and step, plus DB timing). The tree is logged at the end of
744
+ each request. If `config.telemetry.sink` is set (e.g. to
745
+ `Rivulet::OTel::Sink.new` via `rivulet-otel setup`), the sink receives
746
+ callbacks for each lifecycle event. The default sink is a no-op.
data/docs/telemetry.md ADDED
@@ -0,0 +1,54 @@
1
+ # Telemetry
2
+
3
+ Rivulet includes a built-in, fiber-local telemetry system that records the
4
+ execution flow of every request. Each `Rivulet::Operation` and `Rivulet::Step`
5
+ is automatically timed via `Rivulet::Telemetry::TimingWrapper`, which is
6
+ prepended into every subclass. Database queries are timed through
7
+ `Rivulet::Telemetry::SequelExtension`. The resulting tree of nodes is logged
8
+ at the end of each request:
9
+
10
+ ```
11
+ Completed total_ms=42.3 db_ms=8.1 flow:
12
+ Handlers::Posts::Operations::Show (2.1) =>
13
+ Services::Posts::Steps::LoadPost (35.4) =>
14
+ Services::Posts::Steps::Authorize (1.2)
15
+ Handlers::Posts::Steps::BuildResponse (1.1)
16
+ ```
17
+
18
+ ## Sink Protocol
19
+
20
+ The telemetry system is extensible through a sink protocol
21
+ (`Rivulet::Telemetry::Sink`). A sink receives callbacks for each lifecycle
22
+ event:
23
+
24
+ - `on_start(node, parent)` — called when an operation or step begins.
25
+ - `on_stop(node)` — called when it ends, with `duration_ms` and `self_ms`
26
+ already computed.
27
+ - `on_db(elapsed_ms)` — called after each database query.
28
+ - `on_root(node, total_ms)` — called at the end of the request, after the
29
+ full tree is built.
30
+
31
+ The default sink is `Rivulet::Telemetry::Sink::Null` (no-op). Apps configure
32
+ a custom sink via `config.telemetry.sink` in `config/application.rb`.
33
+
34
+ ## OpenTelemetry
35
+
36
+ Add the `rivulet-opentelemetry` gem to your application and run
37
+ `bundle exec rivulet-otel setup` to wire in OpenTelemetry. The gem provides:
38
+
39
+ - `Rivulet::OTel::Sink` — implements the sink protocol, emitting one span
40
+ per operation and per step, with correct parent-child nesting via a
41
+ fiber-local span stack. Also records metrics (request count/duration,
42
+ DB queries/duration, step duration) on each callback.
43
+ - `Rivulet::OTel::Logger` — replaces the default dry-logger. Each log
44
+ record is emitted as a real OTel log with the current span context
45
+ attached (trace_id, span_id), enabling trace-to-log correlation in
46
+ Grafana. Logs are also written to stdout for local visibility.
47
+ - `Rivulet::OTel.configure(service_name:)` — boots the OTel SDK with
48
+ traces, logs, and metrics exporters, gated on
49
+ `OTEL_EXPORTER_OTLP_ENDPOINT` (no-op when unset).
50
+
51
+ The setup command adds a single `grafana/otel-lgtm` service to
52
+ `docker-compose.yml` and wires in the initializer. Grafana is available at
53
+ `http://localhost:3000` (admin/admin) with Tempo, Loki, and Prometheus
54
+ pre-configured.
@@ -74,7 +74,7 @@ module Rivulet
74
74
  private
75
75
 
76
76
  def with_telemetry
77
- t = Telemetry.new
77
+ t = Telemetry.new(sink: config.telemetry.sink)
78
78
  Fiber[:rivulet_telemetry] = t
79
79
 
80
80
  result = yield
@@ -84,6 +84,7 @@ module Rivulet
84
84
  )
85
85
  result
86
86
  ensure
87
+ t&.finish
87
88
  Fiber[:rivulet_telemetry] = nil
88
89
  end
89
90
  end
@@ -19,6 +19,7 @@ module Rivulet
19
19
  app/services/shared/utils
20
20
  app/models
21
21
  config
22
+ config/initializers
22
23
  db/migrations
23
24
  ].freeze
24
25
 
@@ -100,13 +101,14 @@ module Rivulet
100
101
 
101
102
  <<~RUBY
102
103
  Rivulet.configure do |config|
104
+ config.app.name = name
105
+
103
106
  #{dsn_line}
104
107
 
105
108
  # config.sendfile.enabled = true
106
109
  # config.sendfile.variation = 'x-accel-redirect'
107
110
  # config.sendfile.mappings = [['/var/www/', '/files/']]
108
111
 
109
- config.logger.name = :#{name}
110
112
  config.logger.level = :info
111
113
  end
112
114
  RUBY
@@ -20,6 +20,7 @@ module Rivulet
20
20
  register('load_settings') { Rivulet::Steps::LoadSettings.new }
21
21
  register('load_db') { Rivulet::Steps::LoadDb.new }
22
22
  register('load_routes') { Rivulet::Steps::LoadRoutes.new }
23
+ register('load_initializers') { Rivulet::Steps::LoadInitializers.new }
23
24
  register('run_migrations') { Rivulet::Steps::RunMigrations.new }
24
25
  register('run_console') { Rivulet::Steps::RunConsole.new }
25
26
  register('print_routes') { Rivulet::Steps::PrintRoutes.new }
@@ -2,12 +2,13 @@ module Rivulet
2
2
  module Operations
3
3
  class RunConsole < Rivulet::Operation
4
4
  include Import[
5
- build_config: 'steps.build_config',
6
- load_settings: 'steps.load_settings',
7
- load_app: 'steps.load_app',
8
- load_db: 'steps.load_db',
9
- load_routes: 'steps.load_routes',
10
- run_console: 'steps.run_console'
5
+ build_config: 'steps.build_config',
6
+ load_settings: 'steps.load_settings',
7
+ load_app: 'steps.load_app',
8
+ load_db: 'steps.load_db',
9
+ load_routes: 'steps.load_routes',
10
+ load_initializers: 'steps.load_initializers',
11
+ run_console: 'steps.run_console'
11
12
  ]
12
13
 
13
14
  def call(input = {})
@@ -16,6 +17,7 @@ module Rivulet
16
17
  result = step load_db.(result)
17
18
  result = step load_app.(result)
18
19
  result = step load_routes.(result)
20
+ result = step load_initializers.(result)
19
21
  result = step run_console.(result)
20
22
 
21
23
  result
@@ -2,11 +2,12 @@ module Rivulet
2
2
  module Operations
3
3
  class Startup < Rivulet::Operation
4
4
  include Import[
5
- build_config: 'steps.build_config',
6
- load_settings: 'steps.load_settings',
7
- load_app: 'steps.load_app',
8
- load_db: 'steps.load_db',
9
- load_routes: 'steps.load_routes'
5
+ build_config: 'steps.build_config',
6
+ load_settings: 'steps.load_settings',
7
+ load_app: 'steps.load_app',
8
+ load_db: 'steps.load_db',
9
+ load_routes: 'steps.load_routes',
10
+ load_initializers: 'steps.load_initializers'
10
11
  ]
11
12
 
12
13
  def call(input = {})
@@ -15,6 +16,7 @@ module Rivulet
15
16
  result = step load_db.(result)
16
17
  result = step load_app.(result)
17
18
  result = step load_routes.(result)
19
+ result = step load_initializers.(result)
18
20
 
19
21
  result
20
22
  end
@@ -2,6 +2,10 @@ module Rivulet
2
2
  module Steps
3
3
  class BuildConfig < Rivulet::Step
4
4
  def call(input)
5
+ Rivulet::Application.setting :app do
6
+ setting :name
7
+ end
8
+
5
9
  Rivulet::Application.setting :database do
6
10
  setting :dsn
7
11
  setting :pool
@@ -9,7 +13,6 @@ module Rivulet
9
13
 
10
14
  Rivulet::Application.setting :logger, reader: true do
11
15
  setting :engine
12
- setting :name
13
16
  setting :level
14
17
  end
15
18
 
@@ -19,6 +22,10 @@ module Rivulet
19
22
  setting :mappings, default: []
20
23
  end
21
24
 
25
+ Rivulet::Application.setting :telemetry do
26
+ setting :sink, default: Rivulet::Telemetry::Sink::Null.new
27
+ end
28
+
22
29
  Success(input)
23
30
  end
24
31
  end
@@ -1,6 +1,11 @@
1
1
  require 'sequel'
2
2
  require_relative '../telemetry/sequel_extension'
3
3
 
4
+ # Load globally BEFORE connecting — fiber_concurrency is a Sequel-wide extension,
5
+ # not a per-database extension. It replaces the default connection pool with a
6
+ # fiber-aware one that gives each fiber its own connection.
7
+ Sequel.extension(:fiber_concurrency)
8
+
4
9
  module Rivulet
5
10
  module Steps
6
11
  class LoadDb < Rivulet::Step
@@ -0,0 +1,15 @@
1
+ module Rivulet
2
+ module Steps
3
+ class LoadInitializers < Rivulet::Step
4
+ def call(input)
5
+ init_dir = Dir[File.expand_path('config/initializers/**/*.rb')]
6
+
7
+ init_dir.each do |init_file|
8
+ require init_file
9
+ end
10
+
11
+ Success(input)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -17,7 +17,7 @@ module Rivulet
17
17
  private
18
18
 
19
19
  def default_logger(app)
20
- Dry.Logger(app.config.logger.name, level: app.config.logger.level) do |setup|
20
+ Dry.Logger(app.config.app.name, level: app.config.logger.level) do |setup|
21
21
  setup.add_backend(
22
22
  stream: $stdout,
23
23
  log_if: :debug?,
@@ -0,0 +1,19 @@
1
+ module Rivulet
2
+ class Telemetry
3
+ module Sink
4
+ class Null
5
+ def on_start(node, parent)
6
+ end
7
+
8
+ def on_stop(node)
9
+ end
10
+
11
+ def on_db(elapsed_ms)
12
+ end
13
+
14
+ def on_root(node, total_ms)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,11 +2,12 @@ module Rivulet
2
2
  class Telemetry
3
3
  attr_reader :db_ms
4
4
 
5
- def initialize
5
+ def initialize(sink:)
6
6
  @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7
7
  @root = nil
8
8
  @stack = []
9
9
  @db_ms = 0.0
10
+ @sink = sink
10
11
  end
11
12
 
12
13
  def start_recording(activity)
@@ -16,12 +17,15 @@ module Rivulet
16
17
  children: []
17
18
  )
18
19
 
20
+ parent = @stack.last
21
+
19
22
  if @stack.empty?
20
23
  @root = node
21
24
  else
22
25
  @stack.last[:children] << node
23
26
  end
24
27
 
28
+ @sink.on_start(node, parent)
25
29
  @stack << node
26
30
  end
27
31
 
@@ -32,10 +36,13 @@ module Rivulet
32
36
  ((node.ended_at - node.started_at) * 1000.0).round(3)
33
37
  node.self_ms =
34
38
  (node.duration_ms - node.children.sum(&:duration_ms)).round(3)
39
+
40
+ @sink.on_stop(node)
35
41
  end
36
42
 
37
43
  def record_db(ms)
38
44
  @db_ms = (@db_ms + ms).round(3)
45
+ @sink.on_db(ms)
39
46
  end
40
47
 
41
48
  def total_ms
@@ -58,5 +65,9 @@ module Rivulet
58
65
 
59
66
  entry
60
67
  end
68
+
69
+ def finish
70
+ @sink.on_root(@root, total_ms)
71
+ end
61
72
  end
62
73
  end
@@ -1,3 +1,3 @@
1
1
  module Rivulet
2
- VERSION = '0.2.2'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/rivulet.rb CHANGED
@@ -17,6 +17,7 @@ require 'sequel'
17
17
 
18
18
  require_relative 'rivulet/version'
19
19
  require_relative 'rivulet/telemetry'
20
+ require_relative 'rivulet/telemetry/sink'
20
21
  require_relative 'rivulet/telemetry/node'
21
22
  require_relative 'rivulet/telemetry/sequel_extension'
22
23
  require_relative 'rivulet/telemetry/timing_wrapper'
@@ -37,6 +38,7 @@ require_relative 'rivulet/steps/load_app'
37
38
  require_relative 'rivulet/steps/load_db'
38
39
  require_relative 'rivulet/steps/load_routes'
39
40
  require_relative 'rivulet/steps/load_settings'
41
+ require_relative 'rivulet/steps/load_initializers'
40
42
  require_relative 'rivulet/steps/print_routes'
41
43
  require_relative 'rivulet/steps/run_migrations'
42
44
  require_relative 'rivulet/steps/run_console'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rivulet-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dombrovskiy <vold@fastmail.com>
@@ -219,6 +219,20 @@ dependencies:
219
219
  - - ">="
220
220
  - !ruby/object:Gem::Version
221
221
  version: '0'
222
+ - !ruby/object:Gem::Dependency
223
+ name: pry
224
+ requirement: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ type: :development
230
+ prerelease: false
231
+ version_requirements: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
222
236
  - !ruby/object:Gem::Dependency
223
237
  name: rake
224
238
  requirement: !ruby/object:Gem::Requirement
@@ -269,6 +283,7 @@ files:
269
283
  - bin/rivulet
270
284
  - docs/AGENTS.md
271
285
  - docs/architecture.md
286
+ - docs/telemetry.md
272
287
  - lib/rivulet.rb
273
288
  - lib/rivulet/application.rb
274
289
  - lib/rivulet/cli.rb
@@ -305,6 +320,7 @@ files:
305
320
  - lib/rivulet/steps/dispatch.rb
306
321
  - lib/rivulet/steps/load_app.rb
307
322
  - lib/rivulet/steps/load_db.rb
323
+ - lib/rivulet/steps/load_initializers.rb
308
324
  - lib/rivulet/steps/load_routes.rb
309
325
  - lib/rivulet/steps/load_settings.rb
310
326
  - lib/rivulet/steps/print_routes.rb
@@ -314,9 +330,11 @@ files:
314
330
  - lib/rivulet/telemetry.rb
315
331
  - lib/rivulet/telemetry/node.rb
316
332
  - lib/rivulet/telemetry/sequel_extension.rb
333
+ - lib/rivulet/telemetry/sink.rb
317
334
  - lib/rivulet/telemetry/timing_wrapper.rb
318
335
  - lib/rivulet/version.rb
319
- licenses: []
336
+ licenses:
337
+ - Apache-2.0
320
338
  metadata: {}
321
339
  rdoc_options: []
322
340
  require_paths:
@@ -325,14 +343,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
325
343
  requirements:
326
344
  - - ">="
327
345
  - !ruby/object:Gem::Version
328
- version: '3.1'
346
+ version: '3.3'
329
347
  required_rubygems_version: !ruby/object:Gem::Requirement
330
348
  requirements:
331
349
  - - ">="
332
350
  - !ruby/object:Gem::Version
333
351
  version: '0'
334
352
  requirements: []
335
- rubygems_version: 3.6.9
353
+ rubygems_version: 4.0.19
336
354
  specification_version: 4
337
355
  summary: A small Rack web framework built on dry-rb, falcon and forced layering architecture
338
356
  test_files: []