Current section

Files

Jump to
open_feature_provider_flagd lib openfeature provider flagd grpc event_stream.ex
Raw

lib/openfeature/provider/flagd/grpc/event_stream.ex

defmodule OpenFeature.Provider.Flagd.GRPC.EventStream do
@moduledoc """
Handles gRPC event streaming for the Flagd provider.
This module listens for lifecycle events from `flagd` (such as `provider_ready` and
`configuration_change`) and emits them via `OpenFeature.EventEmitter`.
This module is a `GenServer` and should be added to your supervision tree.
## Static Supervision Example
# In your Application module
client = OpenFeature.get_client("gRPC")
children = [
{OpenFeature.Provider.Flagd.GRPC.EventStream, client}
]
Supervisor.start_link(children, strategy: :one_for_one)
## Dynamic Supervision Example
# In your Application module
children = [
{DynamicSupervisor, name: MyApp.EventStreamSupervisor, strategy: :one_for_one}
]
Supervisor.start_link(children, strategy: :one_for_one)
# Later at runtime
client = OpenFeature.get_client("gRPC")
DynamicSupervisor.start_child(MyApp.EventStreamSupervisor, {OpenFeature.Provider.Flagd.GRPC.EventStream, client})
"""
@moduledoc since: "0.1.0"
use GenServer
require Logger
alias Flagd.Evaluation.V1.EventStreamRequest
alias Flagd.Evaluation.V1.Service.Stub
alias OpenFeature.Client
alias OpenFeature.EventEmitter
alias OpenFeature.Provider.Flagd.GRPC, as: FlagdGRPC
alias Protobuf.JSON.Encode
@spec start_link(Client.t()) :: GenServer.on_start()
def start_link(%Client{provider: %FlagdGRPC{channel: channel, domain: domain}}) do
GenServer.start_link(__MODULE__, %{channel: channel, domain: domain})
end
def start_link(%Client{provider: provider}) do
Logger.error("EventStream.start_link/1 called with client that doesn't use the gRPC provider: #{inspect(provider)}")
:error
end
@spec child_spec(Client.t()) :: Supervisor.child_spec()
def child_spec(%Client{provider: %FlagdGRPC{domain: domain}} = client) do
%{
id: {__MODULE__, domain},
start: {__MODULE__, :start_link, [client]},
restart: :permanent,
shutdown: 5000,
type: :worker
}
end
@impl true
def init(state) do
Logger.debug("Starting gRPC event stream for domain: #{state.domain}")
{:ok, state, {:continue, :start_stream}}
end
@impl true
def handle_continue(:start_stream, %{channel: channel, domain: domain} = state) do
listen(channel, domain)
{:noreply, state}
end
defp listen(channel, domain) do
Logger.debug("Listening to flagd event stream on domain: #{domain}")
case Stub.event_stream(channel, %EventStreamRequest{}) do
{:ok, stream} ->
for event <- stream do
handle_event(event, domain)
end
{:error, err} ->
Logger.warning("Event stream failed to start: #{inspect(err)}")
end
end
defp handle_event({:ok, msg}, domain) do
case Encode.encodable(msg, nil) do
%{"type" => "keep_alive"} ->
:ok
%{"type" => "provider_ready"} ->
EventEmitter.emit(domain, :ready, %{})
%{"type" => "configuration_change", "data" => %{"flags" => flags}} ->
Enum.each(flags, fn {flag, attrs} ->
EventEmitter.emit(domain, :configuration_changed, %{
flag_key: flag,
type: attrs["type"],
source: attrs["source"]
})
end)
%{"type" => "provider_shutdown"} ->
Logger.info("Received provider_shutdown event from flagd")
other ->
Logger.debug("Unknown or unsupported event: #{inspect(other)}")
end
end
defp handle_event({:error, error}, _domain) do
Logger.warning("Event stream error: #{inspect(error)}")
end
end