igniter 0.4.3 → 0.5.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.
Files changed (162) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +217 -0
  3. data/docs/APPLICATION_V1.md +253 -0
  4. data/docs/CAPABILITIES_V1.md +207 -0
  5. data/docs/CONSENSUS_V1.md +477 -0
  6. data/docs/CONTENT_ADDRESSING_V1.md +221 -0
  7. data/docs/DATAFLOW_V1.md +274 -0
  8. data/docs/MESH_V1.md +732 -0
  9. data/docs/NODE_CACHE_V1.md +324 -0
  10. data/docs/PROACTIVE_AGENTS_V1.md +293 -0
  11. data/docs/SERVER_V1.md +200 -1
  12. data/docs/SKILLS_V1.md +213 -0
  13. data/docs/STORE_ADAPTERS.md +41 -13
  14. data/docs/TEMPORAL_V1.md +174 -0
  15. data/docs/TOOLS_V1.md +347 -0
  16. data/docs/TRANSCRIPTION_V1.md +403 -0
  17. data/examples/README.md +37 -0
  18. data/examples/consensus.rb +239 -0
  19. data/examples/dataflow.rb +308 -0
  20. data/examples/elocal_webhook.rb +1 -0
  21. data/examples/incremental.rb +142 -0
  22. data/examples/llm_tools.rb +237 -0
  23. data/examples/mesh.rb +239 -0
  24. data/examples/mesh_discovery.rb +267 -0
  25. data/examples/mesh_gossip.rb +162 -0
  26. data/examples/ringcentral_routing.rb +1 -1
  27. data/lib/igniter/agents/ai/alert_agent.rb +111 -0
  28. data/lib/igniter/agents/ai/chain_agent.rb +127 -0
  29. data/lib/igniter/agents/ai/critic_agent.rb +163 -0
  30. data/lib/igniter/agents/ai/evaluator_agent.rb +193 -0
  31. data/lib/igniter/agents/ai/evolution_agent.rb +286 -0
  32. data/lib/igniter/agents/ai/health_check_agent.rb +122 -0
  33. data/lib/igniter/agents/ai/observer_agent.rb +184 -0
  34. data/lib/igniter/agents/ai/planner_agent.rb +210 -0
  35. data/lib/igniter/agents/ai/router_agent.rb +131 -0
  36. data/lib/igniter/agents/ai/self_reflection_agent.rb +175 -0
  37. data/lib/igniter/agents/observability/metrics_agent.rb +130 -0
  38. data/lib/igniter/agents/pipeline/batch_processor_agent.rb +131 -0
  39. data/lib/igniter/agents/proactive_agent.rb +208 -0
  40. data/lib/igniter/agents/reliability/retry_agent.rb +99 -0
  41. data/lib/igniter/agents/scheduling/cron_agent.rb +110 -0
  42. data/lib/igniter/agents.rb +56 -0
  43. data/lib/igniter/application/app_config.rb +32 -0
  44. data/lib/igniter/application/autoloader.rb +18 -0
  45. data/lib/igniter/application/generator.rb +157 -0
  46. data/lib/igniter/application/scheduler.rb +109 -0
  47. data/lib/igniter/application/yml_loader.rb +39 -0
  48. data/lib/igniter/application.rb +174 -0
  49. data/lib/igniter/capabilities.rb +68 -0
  50. data/lib/igniter/compiler/validators/dependencies_validator.rb +50 -2
  51. data/lib/igniter/compiler/validators/remote_validator.rb +2 -0
  52. data/lib/igniter/consensus/cluster.rb +183 -0
  53. data/lib/igniter/consensus/errors.rb +14 -0
  54. data/lib/igniter/consensus/executors.rb +43 -0
  55. data/lib/igniter/consensus/node.rb +320 -0
  56. data/lib/igniter/consensus/read_query.rb +30 -0
  57. data/lib/igniter/consensus/state_machine.rb +58 -0
  58. data/lib/igniter/consensus.rb +58 -0
  59. data/lib/igniter/content_addressing.rb +133 -0
  60. data/lib/igniter/contract.rb +12 -0
  61. data/lib/igniter/dataflow/aggregate_operators.rb +147 -0
  62. data/lib/igniter/dataflow/aggregate_state.rb +77 -0
  63. data/lib/igniter/dataflow/diff.rb +37 -0
  64. data/lib/igniter/dataflow/diff_state.rb +81 -0
  65. data/lib/igniter/dataflow/incremental_collection_result.rb +39 -0
  66. data/lib/igniter/dataflow/window_filter.rb +48 -0
  67. data/lib/igniter/dataflow.rb +65 -0
  68. data/lib/igniter/dsl/contract_builder.rb +71 -7
  69. data/lib/igniter/executor.rb +60 -0
  70. data/lib/igniter/extensions/capabilities.rb +39 -0
  71. data/lib/igniter/extensions/content_addressing.rb +5 -0
  72. data/lib/igniter/extensions/dataflow.rb +117 -0
  73. data/lib/igniter/extensions/incremental.rb +50 -0
  74. data/lib/igniter/extensions/mesh.rb +31 -0
  75. data/lib/igniter/fingerprint.rb +43 -0
  76. data/lib/igniter/incremental/formatter.rb +81 -0
  77. data/lib/igniter/incremental/result.rb +69 -0
  78. data/lib/igniter/incremental/tracker.rb +108 -0
  79. data/lib/igniter/incremental.rb +50 -0
  80. data/lib/igniter/integrations/llm/config.rb +48 -4
  81. data/lib/igniter/integrations/llm/executor.rb +221 -28
  82. data/lib/igniter/integrations/llm/providers/anthropic.rb +37 -4
  83. data/lib/igniter/integrations/llm/providers/openai.rb +34 -5
  84. data/lib/igniter/integrations/llm/transcription/providers/assemblyai.rb +200 -0
  85. data/lib/igniter/integrations/llm/transcription/providers/base.rb +122 -0
  86. data/lib/igniter/integrations/llm/transcription/providers/deepgram.rb +162 -0
  87. data/lib/igniter/integrations/llm/transcription/providers/openai.rb +102 -0
  88. data/lib/igniter/integrations/llm/transcription/transcriber.rb +145 -0
  89. data/lib/igniter/integrations/llm/transcription/transcript_result.rb +29 -0
  90. data/lib/igniter/integrations/llm.rb +37 -1
  91. data/lib/igniter/memory/agent_memory.rb +104 -0
  92. data/lib/igniter/memory/episode.rb +29 -0
  93. data/lib/igniter/memory/fact.rb +27 -0
  94. data/lib/igniter/memory/memorable.rb +90 -0
  95. data/lib/igniter/memory/reflection_cycle.rb +96 -0
  96. data/lib/igniter/memory/reflection_record.rb +28 -0
  97. data/lib/igniter/memory/store.rb +115 -0
  98. data/lib/igniter/memory/stores/in_memory.rb +136 -0
  99. data/lib/igniter/memory/stores/sqlite.rb +284 -0
  100. data/lib/igniter/memory.rb +80 -0
  101. data/lib/igniter/mesh/announcer.rb +55 -0
  102. data/lib/igniter/mesh/config.rb +45 -0
  103. data/lib/igniter/mesh/discovery.rb +39 -0
  104. data/lib/igniter/mesh/errors.rb +31 -0
  105. data/lib/igniter/mesh/gossip.rb +47 -0
  106. data/lib/igniter/mesh/peer.rb +21 -0
  107. data/lib/igniter/mesh/peer_registry.rb +51 -0
  108. data/lib/igniter/mesh/poller.rb +77 -0
  109. data/lib/igniter/mesh/router.rb +109 -0
  110. data/lib/igniter/mesh.rb +85 -0
  111. data/lib/igniter/metrics/collector.rb +131 -0
  112. data/lib/igniter/metrics/prometheus_exporter.rb +104 -0
  113. data/lib/igniter/metrics/snapshot.rb +8 -0
  114. data/lib/igniter/metrics.rb +37 -0
  115. data/lib/igniter/model/aggregate_node.rb +34 -0
  116. data/lib/igniter/model/collection_node.rb +3 -2
  117. data/lib/igniter/model/compute_node.rb +13 -0
  118. data/lib/igniter/model/remote_node.rb +18 -2
  119. data/lib/igniter/node_cache.rb +231 -0
  120. data/lib/igniter/replication/bootstrapper.rb +61 -0
  121. data/lib/igniter/replication/bootstrappers/gem.rb +32 -0
  122. data/lib/igniter/replication/bootstrappers/git.rb +39 -0
  123. data/lib/igniter/replication/bootstrappers/tarball.rb +56 -0
  124. data/lib/igniter/replication/expansion_plan.rb +38 -0
  125. data/lib/igniter/replication/expansion_planner.rb +142 -0
  126. data/lib/igniter/replication/manifest.rb +45 -0
  127. data/lib/igniter/replication/network_topology.rb +123 -0
  128. data/lib/igniter/replication/node_role.rb +42 -0
  129. data/lib/igniter/replication/reflective_replication_agent.rb +238 -0
  130. data/lib/igniter/replication/replication_agent.rb +87 -0
  131. data/lib/igniter/replication/role_registry.rb +73 -0
  132. data/lib/igniter/replication/ssh_session.rb +77 -0
  133. data/lib/igniter/replication.rb +54 -0
  134. data/lib/igniter/runtime/cache.rb +35 -6
  135. data/lib/igniter/runtime/execution.rb +26 -2
  136. data/lib/igniter/runtime/input_validator.rb +6 -2
  137. data/lib/igniter/runtime/node_state.rb +7 -2
  138. data/lib/igniter/runtime/resolver.rb +323 -31
  139. data/lib/igniter/runtime/stores/redis_store.rb +41 -4
  140. data/lib/igniter/server/client.rb +44 -1
  141. data/lib/igniter/server/config.rb +13 -6
  142. data/lib/igniter/server/handlers/event_handler.rb +4 -0
  143. data/lib/igniter/server/handlers/execute_handler.rb +6 -0
  144. data/lib/igniter/server/handlers/liveness_handler.rb +20 -0
  145. data/lib/igniter/server/handlers/manifest_handler.rb +34 -0
  146. data/lib/igniter/server/handlers/metrics_handler.rb +51 -0
  147. data/lib/igniter/server/handlers/peers_handler.rb +115 -0
  148. data/lib/igniter/server/handlers/readiness_handler.rb +47 -0
  149. data/lib/igniter/server/http_server.rb +54 -17
  150. data/lib/igniter/server/router.rb +54 -21
  151. data/lib/igniter/server/server_logger.rb +52 -0
  152. data/lib/igniter/server.rb +6 -0
  153. data/lib/igniter/skill/feedback.rb +116 -0
  154. data/lib/igniter/skill/output_schema.rb +110 -0
  155. data/lib/igniter/skill.rb +218 -0
  156. data/lib/igniter/temporal.rb +84 -0
  157. data/lib/igniter/tool/discoverable.rb +151 -0
  158. data/lib/igniter/tool.rb +52 -0
  159. data/lib/igniter/tool_registry.rb +144 -0
  160. data/lib/igniter/version.rb +1 -1
  161. data/lib/igniter.rb +17 -0
  162. metadata +128 -1
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ module Handlers
6
+ # GET /v1/live — Kubernetes liveness probe.
7
+ #
8
+ # Returns 200 as long as the process is running. This endpoint should
9
+ # NEVER return a non-200 status unless the process is truly broken (e.g.
10
+ # deadlocked). A failing liveness probe causes K8s to restart the pod.
11
+ class LivenessHandler < Base
12
+ private
13
+
14
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument
15
+ json_ok({ status: "alive", pid: Process.pid })
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ module Handlers
6
+ # Returns a JSON manifest describing this peer: its name, advertised
7
+ # capabilities, registered contracts, and its base URL.
8
+ # Used by Igniter::Mesh::Router health-probing and peer discovery.
9
+ class ManifestHandler < Base
10
+ def initialize(registry, store, config: nil)
11
+ super(registry, store)
12
+ @config = config
13
+ end
14
+
15
+ private
16
+
17
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument
18
+ json_ok({
19
+ peer_name: @config&.peer_name,
20
+ capabilities: (@config&.peer_capabilities || []).map(&:to_s),
21
+ contracts: @registry.names,
22
+ url: node_url
23
+ })
24
+ end
25
+
26
+ def node_url
27
+ return nil unless @config
28
+
29
+ "http://#{@config.host}:#{@config.port}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "igniter/metrics"
4
+
5
+ module Igniter
6
+ module Server
7
+ module Handlers
8
+ # GET /v1/metrics — Prometheus text format metrics endpoint.
9
+ #
10
+ # Requires a Collector to be configured on the server:
11
+ # Igniter::Server.configure { |c| c.metrics_collector = Igniter::Metrics::Collector.new }
12
+ #
13
+ # When no collector is configured, returns a 501 Not Implemented response.
14
+ class MetricsHandler < Base
15
+ def initialize(registry, store, collector:)
16
+ super(registry, store)
17
+ @collector = collector
18
+ end
19
+
20
+ # Override Base#call to return Prometheus text/plain instead of JSON.
21
+ def call(params:, body:)
22
+ handle(params: params, body: body)
23
+ rescue StandardError => e
24
+ { status: 500,
25
+ body: "# ERROR: #{e.message}\n",
26
+ headers: { "Content-Type" => Igniter::Metrics::PrometheusExporter::CONTENT_TYPE } }
27
+ end
28
+
29
+ private
30
+
31
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument
32
+ unless @collector
33
+ return { status: 501,
34
+ body: JSON.generate({ error: "metrics_collector not configured" }),
35
+ headers: { "Content-Type" => "application/json" } }
36
+ end
37
+
38
+ exporter = Igniter::Metrics::PrometheusExporter.new(
39
+ @collector,
40
+ store: @store,
41
+ registry: @registry
42
+ )
43
+
44
+ { status: 200,
45
+ body: exporter.export,
46
+ headers: { "Content-Type" => exporter.content_type } }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Igniter
6
+ module Server
7
+ module Handlers
8
+ # Shared helper — merges static (add_peer) and dynamic (PeerRegistry) peer pools.
9
+ # Static entries take precedence when the same name appears in both pools.
10
+ module MeshPeersMerger
11
+ private
12
+
13
+ def merged_peers
14
+ return [] unless defined?(Igniter::Mesh)
15
+
16
+ static = Igniter::Mesh.config.peers
17
+ dynamic = Igniter::Mesh.config.peer_registry.all
18
+ seen = static.each_with_object({}) { |p, h| h[p.name] = true }
19
+ static + dynamic.reject { |p| seen[p.name] }
20
+ end
21
+ end
22
+
23
+ # GET /v1/mesh/peers
24
+ # Returns the merged list of static + dynamically discovered peers.
25
+ class MeshPeersListHandler < Base
26
+ include MeshPeersMerger
27
+
28
+ private
29
+
30
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument
31
+ json_ok(merged_peers.map do |p|
32
+ { "name" => p.name, "url" => p.url, "capabilities" => p.capabilities.map(&:to_s) }
33
+ end)
34
+ end
35
+ end
36
+
37
+ # GET /v1/mesh/sd
38
+ # Returns the peer list in Prometheus HTTP SD format so that Prometheus can
39
+ # dynamically discover all igniter-server scrape targets without a static target list.
40
+ #
41
+ # Response shape (one object per peer):
42
+ # [{ "targets" => ["host:port"], "labels" => { "__meta_igniter_peer_name" => ..., ... } }]
43
+ #
44
+ # Usage in prometheus.yml:
45
+ # scrape_configs:
46
+ # - job_name: igniter
47
+ # http_sd_configs:
48
+ # - url: http://any-seed:4567/v1/mesh/sd
49
+ # refresh_interval: 30s
50
+ # metrics_path: /v1/metrics
51
+ class MeshSdHandler < Base
52
+ include MeshPeersMerger
53
+
54
+ private
55
+
56
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument
57
+ json_ok(merged_peers.map { |p| sd_entry(p) })
58
+ end
59
+
60
+ def sd_entry(peer)
61
+ {
62
+ "targets" => [host_port(peer.url)],
63
+ "labels" => {
64
+ "__meta_igniter_peer_name" => peer.name,
65
+ "__meta_igniter_capabilities" => peer.capabilities.map(&:to_s).join(",")
66
+ }
67
+ }
68
+ end
69
+
70
+ def host_port(url)
71
+ uri = URI.parse(url)
72
+ "#{uri.host}:#{uri.port}"
73
+ rescue URI::InvalidURIError
74
+ url
75
+ end
76
+ end
77
+
78
+ # POST /v1/mesh/peers
79
+ # Body: { "name": "peer-name", "url": "http://host:port", "capabilities": ["a", "b"] }
80
+ class MeshPeersRegisterHandler < Base
81
+ private
82
+
83
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument, Metrics/AbcSize
84
+ return json_error("Igniter::Mesh is not loaded", status: 422) unless defined?(Igniter::Mesh)
85
+
86
+ name = body["name"].to_s.strip
87
+ url = body["url"].to_s.strip
88
+ return json_error("name is required", status: 400) if name.empty?
89
+ return json_error("url is required", status: 400) if url.empty?
90
+
91
+ caps = Array(body["capabilities"]).map(&:to_sym)
92
+ peer = Igniter::Mesh::Peer.new(name: name, url: url, capabilities: caps)
93
+ Igniter::Mesh.config.peer_registry.register(peer)
94
+
95
+ json_ok({ "registered" => true, "name" => name })
96
+ end
97
+ end
98
+
99
+ # DELETE /v1/mesh/peers/:name
100
+ # Idempotent — no error if the peer was not registered.
101
+ class MeshPeersDeleteHandler < Base
102
+ private
103
+
104
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument
105
+ return json_error("Igniter::Mesh is not loaded", status: 422) unless defined?(Igniter::Mesh)
106
+
107
+ name = params[:name].to_s
108
+ Igniter::Mesh.config.peer_registry.unregister(name)
109
+
110
+ json_ok({ "unregistered" => true, "name" => name })
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ module Handlers
6
+ # GET /v1/ready — Kubernetes readiness probe.
7
+ #
8
+ # Returns 200 when the server is ready to accept traffic:
9
+ # - Store is reachable (list_pending does not raise)
10
+ # - At least one contract is registered
11
+ #
12
+ # Returns 503 when the server should be removed from the load balancer
13
+ # rotation but NOT restarted. A failing readiness probe causes K8s to
14
+ # stop routing traffic to the pod without killing it.
15
+ class ReadinessHandler < Base
16
+ private
17
+
18
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument,Metrics/MethodLength
19
+ checks = {}
20
+
21
+ # Store connectivity check
22
+ checks[:store] = check_store
23
+
24
+ # Contract registration check
25
+ checks[:contracts] = @registry.names.any? ? "ok" : "no_contracts_registered"
26
+
27
+ if checks.values.all? { |v| v == "ok" }
28
+ json_ok({ status: "ready", checks: checks })
29
+ else
30
+ service_unavailable({ status: "not_ready", checks: checks })
31
+ end
32
+ end
33
+
34
+ def check_store
35
+ @store.list_pending(graph: nil)
36
+ "ok"
37
+ rescue StandardError => e
38
+ "error: #{e.message}"
39
+ end
40
+
41
+ def service_unavailable(data)
42
+ { status: 503, body: JSON.generate(data), headers: { "Content-Type" => "application/json" } }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -7,29 +7,35 @@ module Igniter
7
7
  # Pure-Ruby HTTP/1.1 server built on TCPServer (stdlib, zero external deps).
8
8
  # Spawns one thread per connection. Intended for development and orchestration use.
9
9
  # For production, use RackApp with Puma via `Igniter::Server.rack_app`.
10
- class HttpServer
10
+ class HttpServer # rubocop:disable Metrics/ClassLength
11
11
  CRLF = "\r\n"
12
12
  STATUS_MESSAGES = {
13
13
  200 => "OK",
14
14
  400 => "Bad Request",
15
15
  404 => "Not Found",
16
16
  422 => "Unprocessable Entity",
17
- 500 => "Internal Server Error"
17
+ 500 => "Internal Server Error",
18
+ 501 => "Not Implemented",
19
+ 503 => "Service Unavailable"
18
20
  }.freeze
19
21
 
20
22
  def initialize(config)
21
- @config = config
22
- @router = Router.new(config)
23
+ @config = config
24
+ @router = Router.new(config)
25
+ @logger = ServerLogger.new(format: config.log_format)
26
+ @in_flight = 0
27
+ @in_flight_mu = Mutex.new
23
28
  end
24
29
 
25
- def start # rubocop:disable Metrics/MethodLength
30
+ def start # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
26
31
  @tcp_server = TCPServer.new(@config.host, @config.port)
27
32
  @running = true
28
33
 
29
34
  trap("INT") { stop }
30
- trap("TERM") { stop }
35
+ trap("TERM") { graceful_stop }
31
36
 
32
- log("igniter-server listening on http://#{@config.host}:#{@config.port}")
37
+ @logger.info("igniter-server started",
38
+ host: @config.host, port: @config.port, pid: Process.pid)
33
39
 
34
40
  loop do
35
41
  break unless @running
@@ -39,6 +45,9 @@ module Igniter
39
45
  end
40
46
  rescue IOError
41
47
  # Server socket closed via stop
48
+ ensure
49
+ drain_in_flight
50
+ @logger.info("igniter-server stopped", pid: Process.pid)
42
51
  end
43
52
 
44
53
  def stop
@@ -46,6 +55,13 @@ module Igniter
46
55
  @tcp_server&.close
47
56
  end
48
57
 
58
+ def graceful_stop
59
+ @logger.info("SIGTERM received — draining",
60
+ drain_timeout: @config.drain_timeout, pid: Process.pid)
61
+ @running = false
62
+ @tcp_server&.close
63
+ end
64
+
49
65
  private
50
66
 
51
67
  def accept_connection
@@ -57,7 +73,7 @@ module Igniter
57
73
  nil
58
74
  end
59
75
 
60
- def handle_connection(socket) # rubocop:disable Metrics/MethodLength
76
+ def handle_connection(socket) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
61
77
  request_line = socket.gets&.chomp
62
78
  return unless request_line&.include?(" ")
63
79
 
@@ -65,14 +81,38 @@ module Igniter
65
81
  headers = read_headers(socket)
66
82
  body = read_body(socket, headers["content-length"].to_i)
67
83
 
68
- result = @router.call(http_method, path, body)
69
- write_response(socket, result)
84
+ with_in_flight do
85
+ result = @router.call(http_method, path, body)
86
+ write_response(socket, result)
87
+ @logger.info("#{http_method} #{path}", status: result[:status])
88
+ end
70
89
  rescue StandardError => e
71
- log("Connection error: #{e.message}")
90
+ @logger.error("Connection error", error: e.message)
72
91
  ensure
73
92
  socket.close rescue nil # rubocop:disable Style/RescueModifier
74
93
  end
75
94
 
95
+ def with_in_flight
96
+ @in_flight_mu.synchronize { @in_flight += 1 }
97
+ yield
98
+ ensure
99
+ @in_flight_mu.synchronize { @in_flight -= 1 }
100
+ end
101
+
102
+ def drain_in_flight
103
+ timeout = @config.drain_timeout.to_i
104
+ deadline = Time.now + timeout
105
+
106
+ loop do
107
+ remaining = @in_flight_mu.synchronize { @in_flight }
108
+ break if remaining.zero?
109
+ break if Time.now > deadline
110
+
111
+ @logger.info("Draining in-flight connections", remaining: remaining)
112
+ sleep 0.1
113
+ end
114
+ end
115
+
76
116
  def read_headers(socket)
77
117
  headers = {}
78
118
  while (line = socket.gets&.chomp) && !line.empty?
@@ -86,13 +126,14 @@ module Igniter
86
126
  length.positive? ? socket.read(length).to_s : ""
87
127
  end
88
128
 
89
- def write_response(socket, result)
129
+ def write_response(socket, result) # rubocop:disable Metrics/MethodLength
90
130
  body = result[:body].to_s
91
131
  code = result[:status].to_i
92
132
  phrase = STATUS_MESSAGES.fetch(code, "Unknown")
133
+ ct = result.dig(:headers, "Content-Type") || "application/json"
93
134
 
94
135
  response = "HTTP/1.1 #{code} #{phrase}#{CRLF}"
95
- response += "Content-Type: application/json#{CRLF}"
136
+ response += "Content-Type: #{ct}#{CRLF}"
96
137
  response += "Content-Length: #{body.bytesize}#{CRLF}"
97
138
  response += "Connection: close#{CRLF}"
98
139
  response += CRLF
@@ -100,10 +141,6 @@ module Igniter
100
141
 
101
142
  socket.write(response)
102
143
  end
103
-
104
- def log(message)
105
- @config.logger&.puts(message) || $stdout.puts(message)
106
- end
107
144
  end
108
145
  end
109
146
  end
@@ -9,54 +9,87 @@ module Igniter
9
9
  # Used by both HttpServer (TCPServer) and RackApp.
10
10
  class Router
11
11
  ROUTES = [
12
- { method: "GET", pattern: %r{\A/v1/health\z}, handler: :health },
13
- { method: "GET", pattern: %r{\A/v1/contracts\z}, handler: :contracts },
14
- { method: "POST", pattern: %r{\A/v1/contracts/(?<name>[^/]+)/execute\z}, handler: :execute },
15
- { method: "POST", pattern: %r{\A/v1/contracts/(?<name>[^/]+)/events\z}, handler: :event },
16
- { method: "GET", pattern: %r{\A/v1/executions/(?<id>[^/]+)\z}, handler: :status }
12
+ { method: "GET", pattern: %r{\A/v1/live\z}, handler: :liveness },
13
+ { method: "GET", pattern: %r{\A/v1/ready\z}, handler: :readiness },
14
+ { method: "GET", pattern: %r{\A/v1/metrics\z}, handler: :metrics },
15
+ { method: "GET", pattern: %r{\A/v1/health\z}, handler: :health },
16
+ { method: "GET", pattern: %r{\A/v1/manifest\z}, handler: :manifest },
17
+ { method: "GET", pattern: %r{\A/v1/mesh/peers\z}, handler: :mesh_peers_list },
18
+ { method: "GET", pattern: %r{\A/v1/mesh/sd\z}, handler: :mesh_sd },
19
+ { method: "POST", pattern: %r{\A/v1/mesh/peers\z}, handler: :mesh_peers_register },
20
+ { method: "DELETE", pattern: %r{\A/v1/mesh/peers/(?<name>.+)\z}, handler: :mesh_peers_delete },
21
+ { method: "GET", pattern: %r{\A/v1/contracts\z}, handler: :contracts },
22
+ { method: "POST", pattern: %r{\A/v1/contracts/(?<name>[^/]+)/execute\z}, handler: :execute },
23
+ { method: "POST", pattern: %r{\A/v1/contracts/(?<name>[^/]+)/events\z}, handler: :event },
24
+ { method: "GET", pattern: %r{\A/v1/executions/(?<id>[^/]+)\z}, handler: :status }
17
25
  ].freeze
18
26
 
19
27
  def initialize(config)
20
28
  @config = config
21
29
  end
22
30
 
23
- # Main dispatch entry point — called by both WEBrick and Rack adapters.
31
+ # Main dispatch entry point — called by both HttpServer and RackApp.
32
+ # Records HTTP metrics when a collector is configured.
24
33
  def call(http_method, path, body_str) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
25
- method_uc = http_method.to_s.upcase
34
+ started_at = Time.now.utc
35
+ method_uc = http_method.to_s.upcase
36
+
26
37
  ROUTES.each do |route|
27
38
  next unless route[:method] == method_uc
28
39
 
29
40
  match = route[:pattern].match(path)
30
41
  next unless match
31
42
 
32
- params = match.named_captures.transform_keys(&:to_sym)
33
- body = parse_body(body_str)
34
-
43
+ params = match.named_captures.transform_keys(&:to_sym)
44
+ body = parse_body(body_str)
35
45
  handler = build_handler(route[:handler])
36
- return handler.call(params: params, body: body)
46
+ result = handler.call(params: params, body: body)
47
+
48
+ record_http_metric(method_uc, path, result[:status], started_at)
49
+ return result
37
50
  end
38
51
 
39
- not_found_response(path)
52
+ result = not_found_response(path)
53
+ record_http_metric(method_uc, path, 404, started_at)
54
+ result
40
55
  rescue JSON::ParserError => e
41
56
  { status: 400, body: JSON.generate({ error: "Invalid JSON: #{e.message}" }), headers: json_ct }
42
57
  end
43
58
 
44
59
  private
45
60
 
46
- def build_handler(key)
47
- registry = @config.registry
48
- store = @config.store
49
- node_url = "http://#{@config.host}:#{@config.port}"
61
+ def build_handler(key) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
62
+ registry = @config.registry
63
+ store = @config.store
64
+ node_url = "http://#{@config.host}:#{@config.port}"
65
+ collector = @config.metrics_collector
50
66
 
51
67
  case key
52
- when :health then Handlers::HealthHandler.new(registry, store, node_url: node_url)
53
- when :contracts then Handlers::ContractsHandler.new(registry, store)
54
- when :execute then Handlers::ExecuteHandler.new(registry, store)
55
- when :event then Handlers::EventHandler.new(registry, store)
56
- when :status then Handlers::StatusHandler.new(registry, store)
68
+ when :liveness then Handlers::LivenessHandler.new(registry, store)
69
+ when :readiness then Handlers::ReadinessHandler.new(registry, store)
70
+ when :metrics then Handlers::MetricsHandler.new(registry, store, collector: collector)
71
+ when :health then Handlers::HealthHandler.new(registry, store, node_url: node_url)
72
+ when :manifest then Handlers::ManifestHandler.new(registry, store, config: @config)
73
+ when :mesh_peers_list then Handlers::MeshPeersListHandler.new(registry, store)
74
+ when :mesh_sd then Handlers::MeshSdHandler.new(registry, store)
75
+ when :mesh_peers_register then Handlers::MeshPeersRegisterHandler.new(registry, store)
76
+ when :mesh_peers_delete then Handlers::MeshPeersDeleteHandler.new(registry, store)
77
+ when :contracts then Handlers::ContractsHandler.new(registry, store)
78
+ when :execute then Handlers::ExecuteHandler.new(registry, store, collector: collector)
79
+ when :event then Handlers::EventHandler.new(registry, store, collector: collector)
80
+ when :status then Handlers::StatusHandler.new(registry, store)
57
81
  end
58
82
  end
59
83
 
84
+ def record_http_metric(method, path, status, started_at)
85
+ return unless @config.metrics_collector
86
+
87
+ duration = Time.now.utc - started_at
88
+ @config.metrics_collector.record_http(
89
+ method: method, path: path, status: status, duration: duration
90
+ )
91
+ end
92
+
60
93
  def parse_body(str)
61
94
  return {} if str.nil? || str.strip.empty?
62
95
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Igniter
6
+ module Server
7
+ # Minimal structured logger for igniter-server.
8
+ #
9
+ # format: :json → each line is a JSON object (Loki/ELK/CloudWatch compatible)
10
+ # format: :text → human-readable single-line string
11
+ #
12
+ # Thread-safe via an internal Mutex.
13
+ class ServerLogger
14
+ def initialize(format: :text, out: $stdout)
15
+ @format = format
16
+ @out = out
17
+ @mutex = Mutex.new
18
+ end
19
+
20
+ def info(message, **context)
21
+ log("INFO", message, context)
22
+ end
23
+
24
+ def warn(message, **context)
25
+ log("WARN", message, context)
26
+ end
27
+
28
+ def error(message, **context)
29
+ log("ERROR", message, context)
30
+ end
31
+
32
+ private
33
+
34
+ def log(level, message, context)
35
+ line = @format == :json ? json_line(level, message, context) : text_line(level, message, context)
36
+ @mutex.synchronize { @out.puts(line) }
37
+ end
38
+
39
+ def json_line(level, message, context)
40
+ JSON.generate(
41
+ { time: Time.now.utc.iso8601(3), level: level, msg: message }.merge(context)
42
+ )
43
+ end
44
+
45
+ def text_line(level, message, context)
46
+ ts = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
47
+ extra = context.empty? ? "" : " #{context.map { |k, v| "#{k}=#{v}" }.join(" ")}"
48
+ "[#{ts}] #{level} #{message}#{extra}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -12,6 +12,7 @@ module Igniter
12
12
  end
13
13
 
14
14
  require_relative "server/registry"
15
+ require_relative "server/server_logger"
15
16
  require_relative "server/config"
16
17
  require_relative "server/router"
17
18
  require_relative "server/http_server"
@@ -23,6 +24,11 @@ require_relative "server/handlers/contracts_handler"
23
24
  require_relative "server/handlers/execute_handler"
24
25
  require_relative "server/handlers/event_handler"
25
26
  require_relative "server/handlers/status_handler"
27
+ require_relative "server/handlers/liveness_handler"
28
+ require_relative "server/handlers/readiness_handler"
29
+ require_relative "server/handlers/metrics_handler"
30
+ require_relative "server/handlers/manifest_handler"
31
+ require_relative "server/handlers/peers_handler"
26
32
 
27
33
  module Igniter
28
34
  module Server