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.
- checksums.yaml +4 -4
- data/README.md +217 -0
- data/docs/APPLICATION_V1.md +253 -0
- data/docs/CAPABILITIES_V1.md +207 -0
- data/docs/CONSENSUS_V1.md +477 -0
- data/docs/CONTENT_ADDRESSING_V1.md +221 -0
- data/docs/DATAFLOW_V1.md +274 -0
- data/docs/MESH_V1.md +732 -0
- data/docs/NODE_CACHE_V1.md +324 -0
- data/docs/PROACTIVE_AGENTS_V1.md +293 -0
- data/docs/SERVER_V1.md +200 -1
- data/docs/SKILLS_V1.md +213 -0
- data/docs/STORE_ADAPTERS.md +41 -13
- data/docs/TEMPORAL_V1.md +174 -0
- data/docs/TOOLS_V1.md +347 -0
- data/docs/TRANSCRIPTION_V1.md +403 -0
- data/examples/README.md +37 -0
- data/examples/consensus.rb +239 -0
- data/examples/dataflow.rb +308 -0
- data/examples/elocal_webhook.rb +1 -0
- data/examples/incremental.rb +142 -0
- data/examples/llm_tools.rb +237 -0
- data/examples/mesh.rb +239 -0
- data/examples/mesh_discovery.rb +267 -0
- data/examples/mesh_gossip.rb +162 -0
- data/examples/ringcentral_routing.rb +1 -1
- data/lib/igniter/agents/ai/alert_agent.rb +111 -0
- data/lib/igniter/agents/ai/chain_agent.rb +127 -0
- data/lib/igniter/agents/ai/critic_agent.rb +163 -0
- data/lib/igniter/agents/ai/evaluator_agent.rb +193 -0
- data/lib/igniter/agents/ai/evolution_agent.rb +286 -0
- data/lib/igniter/agents/ai/health_check_agent.rb +122 -0
- data/lib/igniter/agents/ai/observer_agent.rb +184 -0
- data/lib/igniter/agents/ai/planner_agent.rb +210 -0
- data/lib/igniter/agents/ai/router_agent.rb +131 -0
- data/lib/igniter/agents/ai/self_reflection_agent.rb +175 -0
- data/lib/igniter/agents/observability/metrics_agent.rb +130 -0
- data/lib/igniter/agents/pipeline/batch_processor_agent.rb +131 -0
- data/lib/igniter/agents/proactive_agent.rb +208 -0
- data/lib/igniter/agents/reliability/retry_agent.rb +99 -0
- data/lib/igniter/agents/scheduling/cron_agent.rb +110 -0
- data/lib/igniter/agents.rb +56 -0
- data/lib/igniter/application/app_config.rb +32 -0
- data/lib/igniter/application/autoloader.rb +18 -0
- data/lib/igniter/application/generator.rb +157 -0
- data/lib/igniter/application/scheduler.rb +109 -0
- data/lib/igniter/application/yml_loader.rb +39 -0
- data/lib/igniter/application.rb +174 -0
- data/lib/igniter/capabilities.rb +68 -0
- data/lib/igniter/compiler/validators/dependencies_validator.rb +50 -2
- data/lib/igniter/compiler/validators/remote_validator.rb +2 -0
- data/lib/igniter/consensus/cluster.rb +183 -0
- data/lib/igniter/consensus/errors.rb +14 -0
- data/lib/igniter/consensus/executors.rb +43 -0
- data/lib/igniter/consensus/node.rb +320 -0
- data/lib/igniter/consensus/read_query.rb +30 -0
- data/lib/igniter/consensus/state_machine.rb +58 -0
- data/lib/igniter/consensus.rb +58 -0
- data/lib/igniter/content_addressing.rb +133 -0
- data/lib/igniter/contract.rb +12 -0
- data/lib/igniter/dataflow/aggregate_operators.rb +147 -0
- data/lib/igniter/dataflow/aggregate_state.rb +77 -0
- data/lib/igniter/dataflow/diff.rb +37 -0
- data/lib/igniter/dataflow/diff_state.rb +81 -0
- data/lib/igniter/dataflow/incremental_collection_result.rb +39 -0
- data/lib/igniter/dataflow/window_filter.rb +48 -0
- data/lib/igniter/dataflow.rb +65 -0
- data/lib/igniter/dsl/contract_builder.rb +71 -7
- data/lib/igniter/executor.rb +60 -0
- data/lib/igniter/extensions/capabilities.rb +39 -0
- data/lib/igniter/extensions/content_addressing.rb +5 -0
- data/lib/igniter/extensions/dataflow.rb +117 -0
- data/lib/igniter/extensions/incremental.rb +50 -0
- data/lib/igniter/extensions/mesh.rb +31 -0
- data/lib/igniter/fingerprint.rb +43 -0
- data/lib/igniter/incremental/formatter.rb +81 -0
- data/lib/igniter/incremental/result.rb +69 -0
- data/lib/igniter/incremental/tracker.rb +108 -0
- data/lib/igniter/incremental.rb +50 -0
- data/lib/igniter/integrations/llm/config.rb +48 -4
- data/lib/igniter/integrations/llm/executor.rb +221 -28
- data/lib/igniter/integrations/llm/providers/anthropic.rb +37 -4
- data/lib/igniter/integrations/llm/providers/openai.rb +34 -5
- data/lib/igniter/integrations/llm/transcription/providers/assemblyai.rb +200 -0
- data/lib/igniter/integrations/llm/transcription/providers/base.rb +122 -0
- data/lib/igniter/integrations/llm/transcription/providers/deepgram.rb +162 -0
- data/lib/igniter/integrations/llm/transcription/providers/openai.rb +102 -0
- data/lib/igniter/integrations/llm/transcription/transcriber.rb +145 -0
- data/lib/igniter/integrations/llm/transcription/transcript_result.rb +29 -0
- data/lib/igniter/integrations/llm.rb +37 -1
- data/lib/igniter/memory/agent_memory.rb +104 -0
- data/lib/igniter/memory/episode.rb +29 -0
- data/lib/igniter/memory/fact.rb +27 -0
- data/lib/igniter/memory/memorable.rb +90 -0
- data/lib/igniter/memory/reflection_cycle.rb +96 -0
- data/lib/igniter/memory/reflection_record.rb +28 -0
- data/lib/igniter/memory/store.rb +115 -0
- data/lib/igniter/memory/stores/in_memory.rb +136 -0
- data/lib/igniter/memory/stores/sqlite.rb +284 -0
- data/lib/igniter/memory.rb +80 -0
- data/lib/igniter/mesh/announcer.rb +55 -0
- data/lib/igniter/mesh/config.rb +45 -0
- data/lib/igniter/mesh/discovery.rb +39 -0
- data/lib/igniter/mesh/errors.rb +31 -0
- data/lib/igniter/mesh/gossip.rb +47 -0
- data/lib/igniter/mesh/peer.rb +21 -0
- data/lib/igniter/mesh/peer_registry.rb +51 -0
- data/lib/igniter/mesh/poller.rb +77 -0
- data/lib/igniter/mesh/router.rb +109 -0
- data/lib/igniter/mesh.rb +85 -0
- data/lib/igniter/metrics/collector.rb +131 -0
- data/lib/igniter/metrics/prometheus_exporter.rb +104 -0
- data/lib/igniter/metrics/snapshot.rb +8 -0
- data/lib/igniter/metrics.rb +37 -0
- data/lib/igniter/model/aggregate_node.rb +34 -0
- data/lib/igniter/model/collection_node.rb +3 -2
- data/lib/igniter/model/compute_node.rb +13 -0
- data/lib/igniter/model/remote_node.rb +18 -2
- data/lib/igniter/node_cache.rb +231 -0
- data/lib/igniter/replication/bootstrapper.rb +61 -0
- data/lib/igniter/replication/bootstrappers/gem.rb +32 -0
- data/lib/igniter/replication/bootstrappers/git.rb +39 -0
- data/lib/igniter/replication/bootstrappers/tarball.rb +56 -0
- data/lib/igniter/replication/expansion_plan.rb +38 -0
- data/lib/igniter/replication/expansion_planner.rb +142 -0
- data/lib/igniter/replication/manifest.rb +45 -0
- data/lib/igniter/replication/network_topology.rb +123 -0
- data/lib/igniter/replication/node_role.rb +42 -0
- data/lib/igniter/replication/reflective_replication_agent.rb +238 -0
- data/lib/igniter/replication/replication_agent.rb +87 -0
- data/lib/igniter/replication/role_registry.rb +73 -0
- data/lib/igniter/replication/ssh_session.rb +77 -0
- data/lib/igniter/replication.rb +54 -0
- data/lib/igniter/runtime/cache.rb +35 -6
- data/lib/igniter/runtime/execution.rb +26 -2
- data/lib/igniter/runtime/input_validator.rb +6 -2
- data/lib/igniter/runtime/node_state.rb +7 -2
- data/lib/igniter/runtime/resolver.rb +323 -31
- data/lib/igniter/runtime/stores/redis_store.rb +41 -4
- data/lib/igniter/server/client.rb +44 -1
- data/lib/igniter/server/config.rb +13 -6
- data/lib/igniter/server/handlers/event_handler.rb +4 -0
- data/lib/igniter/server/handlers/execute_handler.rb +6 -0
- data/lib/igniter/server/handlers/liveness_handler.rb +20 -0
- data/lib/igniter/server/handlers/manifest_handler.rb +34 -0
- data/lib/igniter/server/handlers/metrics_handler.rb +51 -0
- data/lib/igniter/server/handlers/peers_handler.rb +115 -0
- data/lib/igniter/server/handlers/readiness_handler.rb +47 -0
- data/lib/igniter/server/http_server.rb +54 -17
- data/lib/igniter/server/router.rb +54 -21
- data/lib/igniter/server/server_logger.rb +52 -0
- data/lib/igniter/server.rb +6 -0
- data/lib/igniter/skill/feedback.rb +116 -0
- data/lib/igniter/skill/output_schema.rb +110 -0
- data/lib/igniter/skill.rb +218 -0
- data/lib/igniter/temporal.rb +84 -0
- data/lib/igniter/tool/discoverable.rb +151 -0
- data/lib/igniter/tool.rb +52 -0
- data/lib/igniter/tool_registry.rb +144 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +17 -0
- 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
|
|
22
|
-
@router
|
|
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") {
|
|
35
|
+
trap("TERM") { graceful_stop }
|
|
31
36
|
|
|
32
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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:
|
|
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",
|
|
13
|
-
{ method: "GET",
|
|
14
|
-
{ method: "
|
|
15
|
-
{ method: "
|
|
16
|
-
{ method: "GET",
|
|
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
|
|
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
|
-
|
|
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
|
|
33
|
-
body
|
|
34
|
-
|
|
43
|
+
params = match.named_captures.transform_keys(&:to_sym)
|
|
44
|
+
body = parse_body(body_str)
|
|
35
45
|
handler = build_handler(route[:handler])
|
|
36
|
-
|
|
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
|
|
48
|
-
store
|
|
49
|
-
node_url
|
|
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 :
|
|
53
|
-
when :
|
|
54
|
-
when :
|
|
55
|
-
when :
|
|
56
|
-
when :
|
|
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
|
data/lib/igniter/server.rb
CHANGED
|
@@ -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
|