legionio 1.5.11 → 1.5.12

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: 2e6a0551564a834f76d460cb9ffed6cbc5e48e6dfe15a377c64e3a2c0540ac99
4
- data.tar.gz: 0daf3944544ddcc630fdba40b35b8430b968db898c5dbfcfb9b348cee752ba48
3
+ metadata.gz: 2b5d012d4df4a245648fe489a4c998c34fa209c119b74301d209f29cdeb44977
4
+ data.tar.gz: 2f818144217a965f4353201773843cbfef1e151406b27996f6d493f144fa7de9
5
5
  SHA512:
6
- metadata.gz: 7bfab92636665b30da8655966c8d88a6bc112da34f96eae8968118917e0c7ca525ba8bf59ac98b62ede1cc41e704cb1b57707e9b4ff90e625ffe2bb6237111a5
7
- data.tar.gz: 210b3a406307fed54591b2bc3bda9e0a35f8af8cad45788d1d1137d40db8b61637c2450b74b18a772d6065f3451031ca9d07ad3722ea7d209e1c22907afe120f
6
+ metadata.gz: e436068fa3c9159a904fd9a57434916ad49a1e332b6220c2b89c290725094da4173aadea83c0656cccb564a05b0033521ad5ee37b6e0dc054eb83fa7facf2114
7
+ data.tar.gz: d83cacb7a7afddfb35892ef4d1183c32cce83c64bfa53fed51d599b8eb04a99589af9fc1202a778a7389b257ea2c9fdd9a71010f2ebfb2c8b2b193b54fbe31fe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.5.12] - 2026-03-25
4
+
5
+ ### Added
6
+ - `GET /api/stats` endpoint — comprehensive daemon runtime stats: extensions (loaded/actor counts), gaia (status/channels/phases), transport (session/channels), cache/cache_local (pool stats), llm (provider health/routing), data/data_local (pool/tuning via legion-data stats), api (puma threads/routes)
7
+
8
+ ### Changed
9
+ - Bumped gemspec dependency: legion-data >= 1.6.0 (required for `Legion::Data.stats`)
10
+
3
11
  ## [1.5.11] - 2026-03-25
4
12
 
5
13
  ### Added
data/CLAUDE.md CHANGED
@@ -633,6 +633,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
633
633
  | `lib/legion/telemetry.rb` | Opt-in OpenTelemetry tracing: `with_span` wrapper, `sanitize_attributes`, `record_exception` |
634
634
  | `lib/legion/metrics.rb` | Opt-in Prometheus metrics: event-driven counters, pull-based gauges, `prometheus-client` guarded |
635
635
  | `lib/legion/api/metrics.rb` | `GET /metrics` Prometheus text-format endpoint with gauge refresh |
636
+ | `lib/legion/api/stats.rb` | `GET /api/stats` comprehensive daemon runtime stats (extensions, gaia, transport, cache, llm, data, api) |
636
637
  | `lib/legion/chat/notification_queue.rb` | Thread-safe priority queue for background notifications (critical/info/debug) |
637
638
  | `lib/legion/chat/notification_bridge.rb` | Event-driven bridge: matches Legion events to chat notifications via fnmatch patterns |
638
639
  | `lib/legion/api/middleware/auth.rb` | Auth: JWT Bearer auth middleware (real token validation, skip paths for health/ready) |
data/legionio.gemspec CHANGED
@@ -54,7 +54,7 @@ Gem::Specification.new do |spec|
54
54
 
55
55
  spec.add_dependency 'legion-cache', '>= 1.3.16'
56
56
  spec.add_dependency 'legion-crypt', '>= 1.4.12'
57
- spec.add_dependency 'legion-data', '>= 1.5.3'
57
+ spec.add_dependency 'legion-data', '>= 1.6.0'
58
58
  spec.add_dependency 'legion-json', '>= 1.2.1'
59
59
  spec.add_dependency 'legion-logging', '>= 1.3.2'
60
60
  spec.add_dependency 'legion-settings', '>= 1.3.19'
@@ -155,6 +155,7 @@ module Legion
155
155
  .merge(gaia_paths)
156
156
  .merge(apollo_paths)
157
157
  .merge(openapi_paths)
158
+ .merge(stats_paths)
158
159
  end
159
160
  private_class_method :paths
160
161
 
@@ -1661,6 +1662,42 @@ module Legion
1661
1662
  }
1662
1663
  end
1663
1664
  private_class_method :openapi_paths
1665
+
1666
+ def self.stats_paths
1667
+ {
1668
+ '/api/stats' => {
1669
+ get: {
1670
+ tags: ['Stats'],
1671
+ summary: 'Comprehensive daemon runtime stats',
1672
+ description: 'Returns runtime statistics for all subsystems: extensions, gaia, transport, cache, llm, data, and api. ' \
1673
+ 'Each section collects independently — one subsystem failure does not affect others.',
1674
+ operationId: 'getStats',
1675
+ responses: {
1676
+ '200' => ok_response('Stats', wrap_data('StatsObject').merge(
1677
+ properties: {
1678
+ data: {
1679
+ type: 'object',
1680
+ properties: {
1681
+ extensions: { type: 'object' },
1682
+ gaia: { type: 'object' },
1683
+ transport: { type: 'object' },
1684
+ cache: { type: 'object' },
1685
+ cache_local: { type: 'object' },
1686
+ llm: { type: 'object' },
1687
+ data: { type: 'object' },
1688
+ data_local: { type: 'object' },
1689
+ api: { type: 'object' }
1690
+ }
1691
+ },
1692
+ meta: { '$ref' => '#/components/schemas/Meta' }
1693
+ }
1694
+ ))
1695
+ }
1696
+ }
1697
+ }
1698
+ }
1699
+ end
1700
+ private_class_method :stats_paths
1664
1701
  end
1665
1702
  end
1666
1703
  end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Stats
7
+ def self.registered(app)
8
+ app.get '/api/stats' do
9
+ result = {}
10
+ result[:extensions] = Routes::Stats.collect_extensions
11
+ result[:gaia] = Routes::Stats.collect_gaia
12
+ result[:transport] = Routes::Stats.collect_transport
13
+ result[:cache] = Routes::Stats.collect_cache
14
+ result[:cache_local] = Routes::Stats.collect_cache_local
15
+ result[:llm] = Routes::Stats.collect_llm
16
+ result[:data] = Routes::Stats.collect_data
17
+ result[:data_local] = Routes::Stats.collect_data_local
18
+ result[:api] = Routes::Stats.collect_api
19
+ json_response(result)
20
+ end
21
+ end
22
+
23
+ EXTENSION_IVARS = {
24
+ loaded: :@loaded_extensions,
25
+ discovered: :@extensions,
26
+ subscription: :@subscription_tasks,
27
+ every: :@timer_tasks,
28
+ poll: :@poll_tasks,
29
+ once: :@once_tasks,
30
+ loop: :@loop_tasks,
31
+ running: :@running_instances
32
+ }.freeze
33
+
34
+ class << self
35
+ def collect_extensions
36
+ ext = Legion::Extensions
37
+ EXTENSION_IVARS.transform_values { |ivar| ext.instance_variable_get(ivar)&.count || 0 }
38
+ rescue StandardError => e
39
+ { error: e.message }
40
+ end
41
+
42
+ def collect_gaia
43
+ return { started: false } unless defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:started?) && Legion::Gaia.started?
44
+
45
+ Legion::Gaia.status
46
+ rescue StandardError => e
47
+ { error: e.message }
48
+ end
49
+
50
+ def collect_transport
51
+ conn = Legion::Transport::Connection
52
+ connected = begin
53
+ Legion::Settings[:transport][:connected]
54
+ rescue StandardError
55
+ false
56
+ end
57
+ connector = defined?(Legion::Transport::TYPE) ? Legion::Transport::TYPE.to_s : 'unknown'
58
+
59
+ info = { connected: connected, connector: connector }
60
+
61
+ session = conn.session
62
+ if session.respond_to?(:open?) && session.open?
63
+ info[:session_open] = true
64
+ info[:channel_max] = session.channel_max if session.respond_to?(:channel_max)
65
+ # Bunny tracks open channels in @channels hash
66
+ channels = session.instance_variable_get(:@channels)
67
+ info[:channels_open] = channels.is_a?(Hash) ? channels.count : nil
68
+ else
69
+ info[:session_open] = false
70
+ end
71
+
72
+ info[:build_session_open] = conn.build_session_open?
73
+ info[:lite_mode] = conn.lite_mode?
74
+ info
75
+ rescue StandardError => e
76
+ { error: e.message }
77
+ end
78
+
79
+ def collect_cache
80
+ return { connected: false } unless defined?(Legion::Cache)
81
+
82
+ info = { connected: Legion::Cache.connected? }
83
+ info[:using_local] = Legion::Cache.using_local? if Legion::Cache.respond_to?(:using_local?)
84
+ info[:using_memory] = Legion::Cache.instance_variable_get(:@using_memory) == true
85
+ info[:driver] = begin
86
+ Legion::Settings[:cache][:driver]
87
+ rescue StandardError
88
+ nil
89
+ end
90
+
91
+ if Legion::Cache.connected? && Legion::Cache.respond_to?(:size)
92
+ info[:pool_size] = begin
93
+ Legion::Cache.size
94
+ rescue StandardError
95
+ nil
96
+ end
97
+ info[:pool_available] = begin
98
+ Legion::Cache.available
99
+ rescue StandardError
100
+ nil
101
+ end
102
+ end
103
+ info
104
+ rescue StandardError => e
105
+ { error: e.message }
106
+ end
107
+
108
+ def collect_cache_local
109
+ return { connected: false } unless defined?(Legion::Cache::Local)
110
+
111
+ info = { connected: Legion::Cache::Local.connected? }
112
+ if Legion::Cache::Local.connected?
113
+ info[:pool_size] = begin
114
+ Legion::Cache::Local.size
115
+ rescue StandardError
116
+ nil
117
+ end
118
+ info[:pool_available] = begin
119
+ Legion::Cache::Local.available
120
+ rescue StandardError
121
+ nil
122
+ end
123
+ end
124
+ info
125
+ rescue StandardError => e
126
+ { error: e.message }
127
+ end
128
+
129
+ def collect_llm
130
+ return { started: false } unless defined?(Legion::LLM) && Legion::LLM.started?
131
+
132
+ info = { started: true }
133
+ s = Legion::LLM.settings
134
+ info[:default_model] = s[:default_model]
135
+ info[:default_provider] = s[:default_provider]
136
+ info[:pipeline_enabled] = s[:pipeline_enabled] == true
137
+
138
+ if defined?(Legion::LLM::Router) && Legion::LLM::Router.routing_enabled?
139
+ info[:routing_enabled] = true
140
+ tracker = Legion::LLM::Router.health_tracker
141
+ if tracker
142
+ providers = s[:providers] || {}
143
+ info[:provider_health] = providers.each_with_object({}) do |(name, _cfg), h|
144
+ h[name] = { circuit: tracker.circuit_state(name)&.to_s }
145
+ rescue StandardError
146
+ nil
147
+ end
148
+ end
149
+ else
150
+ info[:routing_enabled] = false
151
+ end
152
+
153
+ if defined?(Legion::LLM::ConversationStore)
154
+ store = Legion::LLM::ConversationStore
155
+ info[:conversations] = store.respond_to?(:size) ? store.size : nil
156
+ end
157
+ info
158
+ rescue StandardError => e
159
+ { error: e.message }
160
+ end
161
+
162
+ def collect_data
163
+ return { connected: false } unless defined?(Legion::Data) && Legion::Settings[:data][:connected]
164
+
165
+ if Legion::Data.respond_to?(:stats)
166
+ stats = Legion::Data.stats
167
+ stats[:shared] || stats
168
+ else
169
+ { connected: true, adapter: begin
170
+ Legion::Data::Connection.adapter
171
+ rescue StandardError
172
+ nil
173
+ end }
174
+ end
175
+ rescue StandardError => e
176
+ { error: e.message }
177
+ end
178
+
179
+ def collect_data_local
180
+ return { connected: false } unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
181
+
182
+ if Legion::Data::Local.respond_to?(:stats)
183
+ Legion::Data::Local.stats
184
+ else
185
+ { connected: true }
186
+ end
187
+ rescue StandardError => e
188
+ { error: e.message }
189
+ end
190
+
191
+ def collect_api
192
+ port = Legion::Settings.dig(:api, :port) || Legion::Settings.dig(:http, :port) || 4567
193
+ info = { port: port }
194
+
195
+ # Puma thread pool stats if available
196
+ puma_server = Puma::Server.current if defined?(Puma::Server) && Puma::Server.respond_to?(:current)
197
+ if puma_server.respond_to?(:pool_capacity)
198
+ info[:puma] = {
199
+ pool_capacity: puma_server.pool_capacity,
200
+ max_threads: puma_server.max_threads,
201
+ running: puma_server.running,
202
+ backlog: puma_server.backlog
203
+ }
204
+ end
205
+
206
+ info[:routes] = begin
207
+ Legion::API.routes.values.flatten.count
208
+ rescue StandardError
209
+ nil
210
+ end
211
+ info
212
+ rescue StandardError => e
213
+ { error: e.message }
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
data/lib/legion/api.rb CHANGED
@@ -45,6 +45,7 @@ require_relative 'api/marketplace'
45
45
  require_relative 'api/apollo'
46
46
  require_relative 'api/costs'
47
47
  require_relative 'api/traces'
48
+ require_relative 'api/stats'
48
49
  require_relative 'api/graphql' if defined?(GraphQL)
49
50
 
50
51
  module Legion
@@ -135,6 +136,7 @@ module Legion
135
136
  register Routes::Apollo
136
137
  register Routes::Costs
137
138
  register Routes::Traces
139
+ register Routes::Stats
138
140
  register Routes::GraphQL if defined?(Routes::GraphQL)
139
141
 
140
142
  use Legion::API::Middleware::RequestLogger
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'net/http'
4
5
  require 'json'
5
6
  require 'socket'
@@ -23,35 +24,58 @@ module Legion
23
24
  desc 'dump', 'Full diagnostic dump (markdown, suitable for piping to LLM)'
24
25
  default_task :dump
25
26
  def dump
26
- sections = {}
27
-
28
- sections[:versions] = section_versions
29
- sections[:doctor] = section_doctor
30
- sections[:config] = section_config
31
- sections[:gems] = section_gems
32
- sections[:extensions] = section_extensions
33
- sections[:rbac] = section_rbac
34
- sections[:llm] = section_llm
35
- sections[:gaia] = section_gaia
36
- sections[:transport] = section_transport
37
- sections[:events] = section_events
38
- sections[:apollo] = section_apollo
39
- sections[:remote_redis] = section_remote_redis
40
- sections[:local_redis] = section_local_redis
41
- sections[:postgresql] = section_postgresql
42
- sections[:rabbitmq] = section_rabbitmq
43
- sections[:api_health] = section_api_health
44
-
45
- if options[:json]
46
- puts ::JSON.pretty_generate(sections)
47
- else
48
- render_markdown(sections)
49
- end
27
+ sections = collect_all_sections
28
+
29
+ output = if options[:json]
30
+ ::JSON.pretty_generate(sections)
31
+ else
32
+ build_markdown(sections)
33
+ end
34
+
35
+ puts output
36
+
37
+ path = write_dump_file(output)
38
+ warn "Saved to #{path}" if path
50
39
  end
51
40
 
41
+ DEBUG_DIR = File.expand_path('~/.legionio/debug')
42
+
52
43
  no_commands do # rubocop:disable Metrics/BlockLength
53
44
  private
54
45
 
46
+ def collect_all_sections
47
+ sections = {}
48
+ sections[:versions] = section_versions
49
+ sections[:doctor] = section_doctor
50
+ sections[:config] = section_config
51
+ sections[:gems] = section_gems
52
+ sections[:extensions] = section_extensions
53
+ sections[:rbac] = section_rbac
54
+ sections[:llm] = section_llm
55
+ sections[:gaia] = section_gaia
56
+ sections[:transport] = section_transport
57
+ sections[:events] = section_events
58
+ sections[:apollo] = section_apollo
59
+ sections[:remote_redis] = section_remote_redis
60
+ sections[:local_redis] = section_local_redis
61
+ sections[:postgresql] = section_postgresql
62
+ sections[:rabbitmq] = section_rabbitmq
63
+ sections[:api_health] = section_api_health
64
+ sections
65
+ end
66
+
67
+ def write_dump_file(output)
68
+ FileUtils.mkdir_p(DEBUG_DIR)
69
+ ext = options[:json] ? 'json' : 'md'
70
+ filename = "#{Time.now.utc.strftime('%Y-%m-%d_%H%M%S')}.#{ext}"
71
+ path = File.join(DEBUG_DIR, filename)
72
+ File.write(path, output)
73
+ path
74
+ rescue StandardError => e
75
+ warn "Warning: could not write debug file: #{e.message}"
76
+ nil
77
+ end
78
+
55
79
  def api_host
56
80
  options[:host] || '127.0.0.1'
57
81
  end
@@ -390,37 +414,38 @@ module Legion
390
414
  end
391
415
  end
392
416
 
393
- def render_markdown(sections)
394
- puts '# LegionIO Diagnostic Dump'
395
- puts
396
- puts "Generated: #{Time.now.utc.iso8601}"
397
- puts
398
-
399
- md_section('Versions', sections[:versions])
400
- md_section('Doctor Checks', sections[:doctor])
401
- md_section('Configuration (redacted)', sections[:config])
402
- md_section('Installed Gems', sections[:gems])
403
- md_section('Loaded Extensions', sections[:extensions])
404
- md_section('RBAC Roles', sections[:rbac])
405
- md_section('LLM Status', sections[:llm])
406
- md_section('GAIA Status', sections[:gaia])
407
- md_section('Transport Status', sections[:transport])
408
- md_section('Recent Events (last 20)', sections[:events])
409
- md_section('Apollo Stats', sections[:apollo])
410
- md_section('Remote Redis', sections[:remote_redis])
411
- md_section('Local Redis', sections[:local_redis])
412
- md_section('PostgreSQL', sections[:postgresql])
413
- md_section('RabbitMQ', sections[:rabbitmq])
414
- md_section('API Health', sections[:api_health])
415
- end
417
+ def build_markdown(sections)
418
+ lines = []
419
+ lines << '# LegionIO Diagnostic Dump'
420
+ lines << ''
421
+ lines << "Generated: #{Time.now.utc.iso8601}"
422
+ lines << ''
423
+
424
+ { 'Versions' => :versions,
425
+ 'Doctor Checks' => :doctor,
426
+ 'Configuration (redacted)' => :config,
427
+ 'Installed Gems' => :gems,
428
+ 'Loaded Extensions' => :extensions,
429
+ 'RBAC Roles' => :rbac,
430
+ 'LLM Status' => :llm,
431
+ 'GAIA Status' => :gaia,
432
+ 'Transport Status' => :transport,
433
+ 'Recent Events (last 20)' => :events,
434
+ 'Apollo Stats' => :apollo,
435
+ 'Remote Redis' => :remote_redis,
436
+ 'Local Redis' => :local_redis,
437
+ 'PostgreSQL' => :postgresql,
438
+ 'RabbitMQ' => :rabbitmq,
439
+ 'API Health' => :api_health }.each do |title, key|
440
+ lines << "## #{title}"
441
+ lines << ''
442
+ lines << '```json'
443
+ lines << ::JSON.pretty_generate(sections[key])
444
+ lines << '```'
445
+ lines << ''
446
+ end
416
447
 
417
- def md_section(title, data)
418
- puts "## #{title}"
419
- puts
420
- puts '```json'
421
- puts ::JSON.pretty_generate(data)
422
- puts '```'
423
- puts
448
+ lines.join("\n")
424
449
  end
425
450
  end
426
451
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.5.11'
4
+ VERSION = '1.5.12'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.11
4
+ version: 1.5.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -253,14 +253,14 @@ dependencies:
253
253
  requirements:
254
254
  - - ">="
255
255
  - !ruby/object:Gem::Version
256
- version: 1.5.3
256
+ version: 1.6.0
257
257
  type: :runtime
258
258
  prerelease: false
259
259
  version_requirements: !ruby/object:Gem::Requirement
260
260
  requirements:
261
261
  - - ">="
262
262
  - !ruby/object:Gem::Version
263
- version: 1.5.3
263
+ version: 1.6.0
264
264
  - !ruby/object:Gem::Dependency
265
265
  name: legion-json
266
266
  requirement: !ruby/object:Gem::Requirement
@@ -479,6 +479,7 @@ files:
479
479
  - lib/legion/api/relationships.rb
480
480
  - lib/legion/api/schedules.rb
481
481
  - lib/legion/api/settings.rb
482
+ - lib/legion/api/stats.rb
482
483
  - lib/legion/api/tasks.rb
483
484
  - lib/legion/api/tenants.rb
484
485
  - lib/legion/api/token.rb