Packages
snakepit
0.7.0
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.1
0.9.0
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
High-performance pooler and session manager for external language integrations. Supports Python, Node.js, Ruby, and more with gRPC streaming, session management, and production-ready process cleanup.
Current section
Files
Jump to
Current section
Files
lib/snakepit/telemetry/grpc_stream.ex
defmodule Snakepit.Telemetry.GrpcStream do
@moduledoc """
Manages gRPC telemetry streams from Python workers.
This GenServer maintains bidirectional telemetry streams with Python workers,
translating Python telemetry events into Elixir `:telemetry` events.
Features:
- Automatic stream registration when workers connect
- Dynamic sampling rate adjustments
- Event filtering
- Graceful handling of worker disconnections
"""
use GenServer
require Logger
alias GRPC.Channel
alias Snakepit.Bridge.{BridgeService, TelemetryEvent}
alias Snakepit.Telemetry.{Control, Naming, SafeMetadata}
@type worker_ctx :: %{
worker_id: String.t(),
pool_name: atom(),
python_pid: integer() | nil
}
## Client API
@doc """
Starts the telemetry stream manager.
"""
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Registers a worker for telemetry streaming.
Automatically initiates a telemetry stream with the worker and starts
consuming events.
## Examples
iex> channel = connect_to_worker()
iex> Snakepit.Telemetry.GrpcStream.register_worker(channel, %{
...> worker_id: "worker_1",
...> pool_name: :default,
...> python_pid: 12345
...> })
:ok
"""
def register_worker(channel, worker_ctx) do
if stream_capable_channel?(channel) do
GenServer.cast(__MODULE__, {:register_worker, channel, worker_ctx})
else
Logger.debug(
"Skipping telemetry stream registration; channel unsupported",
worker_id: worker_ctx.worker_id,
pool_name: worker_ctx.pool_name,
channel_type: describe_channel(channel)
)
:ok
end
end
@doc """
Removes a worker from telemetry streaming.
Called when a worker disconnects or terminates.
"""
def unregister_worker(worker_id) do
GenServer.cast(__MODULE__, {:unregister_worker, worker_id})
end
@doc """
Updates the sampling rate for a specific worker.
## Examples
iex> Snakepit.Telemetry.GrpcStream.update_sampling("worker_1", 0.1)
:ok
iex> Snakepit.Telemetry.GrpcStream.update_sampling("worker_1", 0.5, ["python.call.*"])
:ok
"""
def update_sampling(worker_id, rate, patterns \\ []) do
GenServer.cast(__MODULE__, {:update_sampling, worker_id, rate, patterns})
end
@doc """
Enables or disables telemetry for a specific worker.
"""
def toggle(worker_id, enabled) do
GenServer.cast(__MODULE__, {:toggle, worker_id, enabled})
end
@doc """
Updates event filters for a specific worker.
"""
def update_filter(worker_id, opts) do
GenServer.cast(__MODULE__, {:update_filter, worker_id, opts})
end
@doc """
Gets the current state of all registered streams.
"""
def list_streams do
case Process.whereis(__MODULE__) do
nil -> []
pid -> GenServer.call(pid, :list_streams)
end
end
## Server Callbacks
@impl true
def init(_opts) do
{:ok, %{streams: %{}}}
end
@impl true
def handle_cast({:register_worker, channel, worker_ctx}, state) do
case initiate_stream(channel, worker_ctx) do
{:ok, stream_info} ->
new_state = put_in(state, [:streams, worker_ctx.worker_id], stream_info)
Logger.info(
"Telemetry stream registered for worker #{worker_ctx.worker_id}",
worker_id: worker_ctx.worker_id,
pool_name: worker_ctx.pool_name
)
{:noreply, new_state}
{:error, reason} ->
Logger.warning(
"Failed to register telemetry stream for worker #{worker_ctx.worker_id}: #{inspect(reason)}",
worker_id: worker_ctx.worker_id,
reason: reason
)
{:noreply, state}
end
end
@impl true
def handle_cast({:unregister_worker, worker_id}, state) do
case Map.get(state.streams, worker_id) do
nil ->
{:noreply, state}
stream_info ->
# Cancel the consumer task
if stream_info.task && Process.alive?(stream_info.task.pid) do
Task.shutdown(stream_info.task, :brutal_kill)
end
new_state = update_in(state, [:streams], &Map.delete(&1, worker_id))
Logger.debug("Telemetry stream unregistered for worker #{worker_id}",
worker_id: worker_id
)
{:noreply, new_state}
end
end
@impl true
def handle_cast({:update_sampling, worker_id, rate, patterns}, state) do
case Map.get(state.streams, worker_id) do
nil ->
Logger.debug("Cannot update sampling for unknown worker #{worker_id}")
{:noreply, state}
%{stream: stream} ->
control_msg = Control.sampling(rate, patterns)
new_state =
case send_control_request(stream, control_msg) do
{:ok, updated_stream} ->
Logger.debug("Updated sampling for worker #{worker_id} to #{rate}",
worker_id: worker_id,
rate: rate,
patterns: patterns
)
put_in(state, [:streams, worker_id, :stream], updated_stream)
{:error, reason} ->
Logger.warning(
"Failed to update sampling for worker #{worker_id}: #{inspect(reason)}",
worker_id: worker_id,
reason: reason
)
state
end
{:noreply, new_state}
end
end
@impl true
def handle_cast({:toggle, worker_id, enabled}, state) do
case Map.get(state.streams, worker_id) do
nil ->
{:noreply, state}
%{stream: stream} ->
control_msg = Control.toggle(enabled)
new_state =
case send_control_request(stream, control_msg) do
{:ok, updated_stream} ->
Logger.debug("Toggled telemetry for worker #{worker_id} to #{enabled}",
worker_id: worker_id,
enabled: enabled
)
put_in(state, [:streams, worker_id, :stream], updated_stream)
{:error, reason} ->
Logger.warning(
"Failed to toggle telemetry for worker #{worker_id}: #{inspect(reason)}",
worker_id: worker_id,
reason: reason
)
state
end
{:noreply, new_state}
end
end
@impl true
def handle_cast({:update_filter, worker_id, opts}, state) do
case Map.get(state.streams, worker_id) do
nil ->
{:noreply, state}
%{stream: stream} ->
control_msg = Control.filter(opts)
new_state =
case send_control_request(stream, control_msg) do
{:ok, updated_stream} ->
Logger.debug("Updated filters for worker #{worker_id}", worker_id: worker_id)
put_in(state, [:streams, worker_id, :stream], updated_stream)
{:error, reason} ->
Logger.warning(
"Failed to update filters for worker #{worker_id}: #{inspect(reason)}",
worker_id: worker_id,
reason: reason
)
state
end
{:noreply, new_state}
end
end
@impl true
def handle_call(:list_streams, _from, state) do
stream_info =
Enum.map(state.streams, fn {worker_id, info} ->
%{
worker_id: worker_id,
pool_name: info.worker_ctx.pool_name,
task_alive: info.task && Process.alive?(info.task.pid)
}
end)
{:reply, stream_info, state}
end
@impl true
def handle_info({ref, :stream_completed}, state) when is_reference(ref) do
# Task completed successfully
Process.demonitor(ref, [:flush])
{:noreply, state}
end
@impl true
def handle_info({:DOWN, _ref, :process, _pid, reason}, state) do
# Task crashed or was killed
Logger.debug("Telemetry stream consumer task terminated: #{inspect(reason)}")
{:noreply, state}
end
@impl true
def handle_info({:gun_response, _pid, _stream_ref, _fin, status, headers}, state) do
Logger.debug("Telemetry stream HTTP response received", status: status, headers: headers)
{:noreply, state}
end
@impl true
def handle_info({:gun_data, _pid, _stream_ref, _is_fin, _data}, state) do
# gRPC data frames are consumed by GRPC.Stub.recv/2; ignore low-level messages.
{:noreply, state}
end
@impl true
def handle_info({:gun_down, _pid, _proto, _reason, _killed_streams, _}, state) do
Logger.debug("Telemetry stream HTTP connection closed by gun")
{:noreply, state}
end
@impl true
def handle_info({:gun_error, _pid, _stream_ref, reason}, state) do
Logger.debug("Telemetry stream HTTP error from gun", reason: reason)
{:noreply, state}
end
@impl true
def handle_info(_message, state) do
{:noreply, state}
end
## Private Helpers
defp send_control_request(stream, control_msg) do
try do
{:ok, GRPC.Stub.send_request(stream, control_msg)}
rescue
error ->
{:error, error}
catch
:exit, reason ->
{:error, reason}
end
end
defp initiate_stream(channel, worker_ctx) do
# Use longer timeout for stream operations
case channel |> open_telemetry_stream() |> normalize_stream_response() do
{:ok, stream} ->
# Send initial toggle message to enable telemetry
stream = GRPC.Stub.send_request(stream, Control.toggle(true))
# Start async task to consume events
task =
Task.Supervisor.async_nolink(
Snakepit.TaskSupervisor,
fn -> consume_stream(stream, worker_ctx) end
)
{:ok,
%{
stream: stream,
task: task,
worker_ctx: worker_ctx,
started_at: System.monotonic_time()
}}
{:error, reason} ->
{:error, reason}
end
end
defp open_telemetry_stream(channel) do
try do
BridgeService.Stub.stream_telemetry(channel, timeout: :infinity)
rescue
exception ->
{:error, {:invalid_channel, exception}}
end
end
defp normalize_stream_response({:ok, %GRPC.Client.Stream{} = stream}), do: {:ok, stream}
defp normalize_stream_response(%GRPC.Client.Stream{} = stream), do: {:ok, stream}
defp normalize_stream_response({:error, _reason} = error), do: error
defp normalize_stream_response(other), do: {:error, {:unexpected_stream_response, other}}
defp consume_stream(stream, worker_ctx) do
case GRPC.Stub.recv(stream, timeout: :infinity) do
{:ok, enum} ->
Enum.each(enum, fn
{:ok, %TelemetryEvent{} = event} ->
translate_and_emit(event, worker_ctx)
{:error, reason} ->
Logger.warning(
"Telemetry stream error for worker #{worker_ctx.worker_id}: #{inspect(reason)}",
worker_id: worker_ctx.worker_id,
reason: reason
)
{:trailers, trailers} ->
Logger.debug("Telemetry stream trailers: #{inspect(trailers)}",
worker_id: worker_ctx.worker_id
)
end)
Logger.debug("Telemetry stream completed for worker #{worker_ctx.worker_id}",
worker_id: worker_ctx.worker_id
)
{:error, reason} ->
log_stream_closed(worker_ctx, reason)
end
end
defp stream_capable_channel?(%Channel{}), do: true
defp stream_capable_channel?(%{__struct__: module}) when is_atom(module) do
String.starts_with?(Atom.to_string(module), "GRPC.")
end
defp stream_capable_channel?(_), do: false
defp describe_channel(%Channel{}), do: "GRPC.Channel"
defp describe_channel(%{__struct__: module}) when is_atom(module), do: Atom.to_string(module)
defp describe_channel(channel) when is_reference(channel), do: "reference"
defp describe_channel(channel) when is_pid(channel), do: "pid"
defp describe_channel(channel) when is_map(channel), do: "map"
defp describe_channel(channel) when is_binary(channel), do: "binary"
defp describe_channel(channel) when is_list(channel), do: "list"
defp describe_channel(channel), do: inspect(channel)
defp log_stream_closed(worker_ctx, reason) do
log_fun =
if shutdown_reason?(reason), do: &Logger.debug/2, else: &Logger.warning/2
log_fun.(
"Telemetry stream closed for worker #{worker_ctx.worker_id}: #{inspect(reason)}",
worker_id: worker_ctx.worker_id,
reason: reason
)
end
defp shutdown_reason?(%GRPC.RPCError{message: message}) when is_binary(message) do
message
|> String.downcase()
|> String.contains?("shutdown")
end
defp shutdown_reason?(%GRPC.RPCError{}), do: false
defp shutdown_reason?(:shutdown), do: true
defp shutdown_reason?({:shutdown, _}), do: true
defp shutdown_reason?({:down, :shutdown}), do: true
defp shutdown_reason?({:error, reason}), do: shutdown_reason?(reason)
defp shutdown_reason?(_), do: false
defp translate_and_emit(event, worker_ctx) do
with {:ok, event_name} <- Naming.from_parts(event.event_parts),
{:ok, measurements} <- cast_measurements(event.measurements),
{:ok, metadata} <-
SafeMetadata.enrich(event.metadata,
node: node(),
worker_id: worker_ctx.worker_id,
pool_name: worker_ctx.pool_name,
python_pid: worker_ctx.python_pid,
correlation_id: blank_to_nil(event.correlation_id),
timestamp_ns: event.timestamp_ns
) do
:telemetry.execute(event_name, measurements, metadata)
else
{:error, reason} ->
Logger.debug(
"Skipping telemetry event #{inspect(event.event_parts)}: #{inspect(reason)}",
worker_id: worker_ctx.worker_id,
event_parts: event.event_parts,
reason: reason
)
end
end
defp cast_measurements(measurements) do
Enum.reduce_while(measurements, {:ok, %{}}, fn {key, value}, {:ok, acc} ->
case Naming.measurement_key(key) do
{:ok, atom_key} ->
val =
case value.value do
{:int_value, v} -> v
{:float_value, v} -> v
{:string_value, v} -> v
nil -> nil
end
{:cont, {:ok, Map.put(acc, atom_key, val)}}
{:error, reason} ->
{:halt, {:error, {:invalid_measurement_key, key, reason}}}
end
end)
end
defp blank_to_nil(""), do: nil
defp blank_to_nil(value), do: value
end