Skip to content

Commit d1ad0d7

Browse files
feat: add blue-green gradual traffic shifting, OTel tracing, and automated rollback
Blue-green deployments now use multi-step traffic shifting (deploy green → shift traffic through configurable steps → deploy blue) with operator controls for advancing traffic, swapping slots, and instant rollback. Adds validation period enforcement, health-gate-based auto-rollback for all strategies, and extends rollout templates to support all four strategies. Also integrates OpenTelemetry tracing for observability.
1 parent e9594a7 commit d1ad0d7

28 files changed

Lines changed: 1534 additions & 239 deletions

config/config.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ config :sentinel_cp, :github_webhook,
9494
# ACME / Let's Encrypt configuration
9595
config :sentinel_cp, :acme, directory_url: "https://acme-v02.api.letsencrypt.org/directory"
9696

97+
# OpenTelemetry (disabled by default, enable via OTEL_EXPORTER_OTLP_ENDPOINT in runtime.exs)
98+
config :opentelemetry, span_processor: :batch, traces_exporter: :none
99+
97100
# Import environment specific config. This must remain at the bottom
98101
# of this file so it overrides the configuration defined above.
99102
import_config "#{config_env()}.exs"

config/runtime.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ if s3_endpoint = System.get_env("S3_ENDPOINT") do
4040
]
4141
end
4242

43+
# OpenTelemetry exporter (enable by setting OTEL_EXPORTER_OTLP_ENDPOINT)
44+
if otlp_endpoint = System.get_env("OTEL_EXPORTER_OTLP_ENDPOINT") do
45+
config :opentelemetry_exporter, otlp_protocol: :http_protobuf, otlp_endpoint: otlp_endpoint
46+
config :opentelemetry, span_processor: :batch, traces_exporter: {:opentelemetry_exporter, %{}}
47+
end
48+
4349
# Bundle compiler
4450
if sentinel_binary = System.get_env("SENTINEL_BINARY") do
4551
config :sentinel_cp, SentinelCp.Bundles.Compiler, sentinel_binary: sentinel_binary

config/test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ config :sentinel_cp, Oban, testing: :inline
3131
# Mark test environment (used to skip async notifications)
3232
config :sentinel_cp, :env, :test
3333

34+
# Disable OpenTelemetry trace export in tests
35+
config :opentelemetry, traces_exporter: :none
36+
3437
# Initialize plugs at runtime for faster test compilation
3538
config :phoenix, :plug_init_mode, :runtime
3639

lib/sentinel_cp/application.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ defmodule SentinelCp.Application do
77

88
@impl true
99
def start(_type, _args) do
10+
# Set up OpenTelemetry auto-instrumentation
11+
OpentelemetryPhoenix.setup()
12+
OpentelemetryEcto.setup([:sentinel_cp, :repo])
13+
1014
children = [
1115
SentinelCpWeb.Telemetry,
1216
SentinelCp.Repo,

lib/sentinel_cp/bundles/compile_worker.ex

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ defmodule SentinelCp.Bundles.CompileWorker do
99

1010
require Logger
1111

12+
alias SentinelCp.{Audit, Plugins, Services}
1213
alias SentinelCp.Bundles
1314
alias SentinelCp.Bundles.{Compiler, Risk, Signing, Storage}
14-
alias SentinelCp.{Audit, Plugins, Services}
15+
alias SentinelCp.Observability.Tracer
1516

1617
@impl Oban.Worker
1718
def perform(%Oban.Job{args: %{"bundle_id" => bundle_id}}) do
19+
Tracer.trace_compilation(bundle_id, fn ->
20+
do_perform(bundle_id)
21+
end)
22+
end
23+
24+
defp do_perform(bundle_id) do
1825
bundle = Bundles.get_bundle!(bundle_id)
1926

2027
Logger.info("Starting compilation for bundle #{bundle_id}")
Lines changed: 16 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,29 @@
11
defmodule SentinelCp.Observability.Tracer do
22
@moduledoc """
3-
Telemetry-based tracing for key control plane operations.
3+
OpenTelemetry tracing for key control plane operations.
44
5-
Wraps `:telemetry` to emit span events for bundle compilation,
6-
rollout lifecycle, webhook processing, and node heartbeats.
7-
Compatible with OpenTelemetry exporters when OTEL SDK is configured.
5+
Wraps `OpenTelemetry.Tracer.with_span/3` to create spans for bundle
6+
compilation, rollout lifecycle, webhook processing, and node heartbeats.
87
"""
98

10-
require Logger
9+
require OpenTelemetry.Tracer, as: Tracer
1110

1211
@doc """
13-
Executes a function within a traced span.
12+
Executes a function within a traced OpenTelemetry span.
1413
15-
Emits `[:sentinel_cp, span_name, :start]` and `[:sentinel_cp, span_name, :stop]`
16-
telemetry events with timing metadata.
14+
Attributes from the metadata map are set on the span.
1715
"""
1816
def span(span_name, metadata, fun) when is_atom(span_name) and is_map(metadata) do
19-
start_time = System.monotonic_time()
20-
trace_id = generate_trace_id()
21-
22-
:telemetry.execute(
23-
[:sentinel_cp, span_name, :start],
24-
%{system_time: System.system_time()},
25-
Map.merge(metadata, %{trace_id: trace_id})
26-
)
27-
28-
try do
29-
result = fun.()
30-
31-
duration = System.monotonic_time() - start_time
32-
33-
:telemetry.execute(
34-
[:sentinel_cp, span_name, :stop],
35-
%{duration: duration},
36-
Map.merge(metadata, %{trace_id: trace_id, result: :ok})
37-
)
38-
39-
result
40-
rescue
41-
e ->
42-
duration = System.monotonic_time() - start_time
43-
44-
:telemetry.execute(
45-
[:sentinel_cp, span_name, :exception],
46-
%{duration: duration},
47-
Map.merge(metadata, %{
48-
trace_id: trace_id,
49-
kind: :error,
50-
reason: Exception.message(e),
51-
stacktrace: __STACKTRACE__
52-
})
53-
)
54-
55-
reraise e, __STACKTRACE__
17+
attributes = Enum.map(metadata, fn {k, v} -> {to_string(k), to_string(v)} end)
18+
19+
Tracer.with_span :"sentinel_cp.#{span_name}", %{attributes: attributes} do
20+
try do
21+
fun.()
22+
rescue
23+
e ->
24+
Tracer.set_status(:error, Exception.message(e))
25+
reraise e, __STACKTRACE__
26+
end
5627
end
5728
end
5829

@@ -83,20 +54,4 @@ defmodule SentinelCp.Observability.Tracer do
8354
def trace_heartbeat(node_id, fun) do
8455
span(:node_heartbeat, %{node_id: node_id}, fun)
8556
end
86-
87-
@doc """
88-
Returns all telemetry event prefixes emitted by the tracer.
89-
"""
90-
def event_prefixes do
91-
[
92-
[:sentinel_cp, :bundle_compilation],
93-
[:sentinel_cp, :rollout_tick],
94-
[:sentinel_cp, :webhook_processing],
95-
[:sentinel_cp, :node_heartbeat]
96-
]
97-
end
98-
99-
defp generate_trace_id do
100-
:crypto.strong_rand_bytes(16) |> Base.hex_encode32(case: :lower, padding: false)
101-
end
10257
end

0 commit comments

Comments
 (0)