remove-gleam (#218)

Reviewed-on: graham/towerops-web#218
This commit is contained in:
Graham McIntire 2026-03-29 11:03:20 -05:00 committed by graham
parent afb9fcc538
commit 44a80cd5eb
15 changed files with 155 additions and 3234 deletions

2
.gitignore vendored
View file

@ -100,7 +100,7 @@ profiles.json
.stride_auth.md
# Gleam build artifacts
# Build artifacts
/build/
# Generated by nix, machine-specific paths

View file

@ -1,5 +1,4 @@
erlang 28.3
elixir 1.19.5-otp-28
#elixir 1.20.0-rc.1-otp-28
gleam 1.15.2
nodejs 22.22.2

View file

@ -34,29 +34,21 @@ RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential git curl libsnmp-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Gleam compiler
ARG GLEAM_VERSION=1.15.2
RUN curl -fsSL https://github.com/gleam-lang/gleam/releases/download/v${GLEAM_VERSION}/gleam-v${GLEAM_VERSION}-x86_64-unknown-linux-musl.tar.gz \
| tar xz -C /usr/local/bin/
# prepare build dir
WORKDIR /app
# set build ENV
ENV MIX_ENV="prod"
# install hex + rebar + mix_gleam archive (must use cache mounts so they
# persist into subsequent cache-mounted steps)
# install hex + rebar (must use cache mounts so they persist into subsequent cache-mounted steps)
RUN --mount=type=cache,target=/root/.hex \
--mount=type=cache,target=/root/.mix \
mix local.hex --force \
&& mix local.rebar --force \
&& mix archive.install hex mix_gleam --force
&& mix local.rebar --force
# install mix dependencies
COPY mix.exs mix.lock gleam.toml ./
COPY mix.exs mix.lock ./
COPY vendor vendor
COPY src src
RUN --mount=type=cache,target=/root/.hex \
--mount=type=cache,target=/root/.mix \
mix deps.get --only $MIX_ENV

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -29,16 +29,10 @@ RUN apt-get update \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Gleam compiler
ARG GLEAM_VERSION=1.15.2
RUN curl -fsSL https://github.com/gleam-lang/gleam/releases/download/v${GLEAM_VERSION}/gleam-v${GLEAM_VERSION}-x86_64-unknown-linux-musl.tar.gz \
| tar xz -C /usr/local/bin/
# Install hex + rebar + mix_gleam archive (matches k8s/Dockerfile exactly)
# Install hex + rebar (matches k8s/Dockerfile exactly)
# This step takes ~10 seconds on every deploy, so we bake it in
RUN mix local.hex --force \
&& mix local.rebar --force \
&& mix archive.install hex mix_gleam --force
&& mix local.rebar --force
# Set build ENV
ENV MIX_ENV="prod"

View file

@ -51,9 +51,6 @@ defmodule Towerops.EctoTypes.MacAddress do
MAC addresses are stored as VARCHAR(17) in the database (sufficient for
colon-separated format). No database migrations are required when adopting
this custom type - Ecto handles conversion transparently.
Parsing and formatting logic is implemented in Gleam at
`src/towerops/ecto_types/mac_address.gleam`.
"""
use Ecto.Type
@ -79,7 +76,7 @@ defmodule Towerops.EctoTypes.MacAddress do
def cast(binary) when is_binary(binary) and byte_size(binary) == 6 do
# Check if it's a 6-byte binary (not a 6-character string)
if String.printable?(binary) do
# It's a short string, try string parsing via Gleam
# It's a short string, try string parsing
cast_from_string(binary)
else
# It's a binary MAC address (SNMP format)

View file

@ -74,11 +74,11 @@ defmodule Towerops.Agent.HeartbeatMetadata do
defstruct version: "", hostname: "", uptime_seconds: 0
def encode(%__MODULE__{} = m) do
:towerops@proto@encode.encode_heartbeat_metadata({:heartbeat_metadata, m.version, m.hostname, m.uptime_seconds})
Towerops.Proto.TupleEncode.encode_heartbeat_metadata({:heartbeat_metadata, m.version, m.hostname, m.uptime_seconds})
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_heartbeat_metadata(binary) do
case Towerops.Proto.TupleDecode.decode_heartbeat_metadata(binary) do
{:ok, {:heartbeat_metadata, version, hostname, uptime}} ->
%__MODULE__{version: version, hostname: hostname, uptime_seconds: uptime}
@ -93,11 +93,11 @@ defmodule Towerops.Agent.HeartbeatResponse do
defstruct status: ""
def encode(%__MODULE__{} = r) do
:towerops@proto@encode.encode_heartbeat_response({:heartbeat_response, r.status})
Towerops.Proto.TupleEncode.encode_heartbeat_response({:heartbeat_response, r.status})
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_heartbeat_response(binary) do
case Towerops.Proto.TupleDecode.decode_heartbeat_response(binary) do
{:ok, {:heartbeat_response, status}} -> %__MODULE__{status: status}
{:error, _} -> %__MODULE__{}
end
@ -109,7 +109,8 @@ defmodule Towerops.Agent.SnmpConfig do
defstruct enabled: false, version: "", community: "", port: 0, transport: ""
def encode(%__MODULE__{} = s) do
:towerops@proto@encode.encode_snmp_config({:snmp_config, s.enabled, s.version, s.community, s.port, s.transport})
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
Towerops.Proto.TupleEncode.encode_snmp_config({:snmp_config, s.enabled, s.version, s.community, s.port, s.transport})
end
end
@ -123,11 +124,11 @@ defmodule Towerops.Agent.Sensor do
defstruct id: "", type: "", oid: "", divisor: 0.0, unit: "", metadata: %{}
def encode(%__MODULE__{} = s) do
:towerops@proto@encode.encode_sensor({:sensor, s.id, s.type, s.oid, s.divisor, s.unit, s.metadata})
Towerops.Proto.TupleEncode.encode_sensor({:sensor, s.id, s.type, s.oid, s.divisor, s.unit, s.metadata})
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_sensor(binary) do
case Towerops.Proto.TupleDecode.decode_sensor(binary) do
{:ok, {:sensor, id, type, oid, divisor, unit, meta}} ->
%__MODULE__{
id: id,
@ -149,11 +150,11 @@ defmodule Towerops.Agent.Interface do
defstruct id: "", if_index: 0, if_name: ""
def encode(%__MODULE__{} = i) do
:towerops@proto@encode.encode_interface({:interface, i.id, i.if_index, i.if_name})
Towerops.Proto.TupleEncode.encode_interface({:interface, i.id, i.if_index, i.if_name})
end
def decode(binary) when is_binary(binary) do
_ = :towerops@proto@decode.decode_device(binary)
_ = Towerops.Proto.TupleDecode.decode_device(binary)
%__MODULE__{}
end
end
@ -191,7 +192,7 @@ defmodule Towerops.Agent.Device do
{:interface, i.id, i.if_index, i.if_name}
end)
:towerops@proto@encode.encode_device(
Towerops.Proto.TupleEncode.encode_device(
{:device, d.id, d.name, d.ip_address, snmp, d.poll_interval_seconds, sensors, interfaces, d.monitoring_enabled,
d.check_interval_seconds}
)
@ -205,7 +206,7 @@ defmodule Towerops.Agent.AgentConfig do
defstruct version: "", poll_interval_seconds: 0, devices: [], checks: []
def encode(%__MODULE__{} = c) do
checks = Enum.map(c.checks, &Towerops.Proto.Agent.check_to_gleam/1)
checks = Enum.map(c.checks, &Towerops.Proto.Agent.check_to_tuple/1)
device_tuples =
Enum.map(c.devices, fn %Device{} = d ->
@ -233,11 +234,11 @@ defmodule Towerops.Agent.AgentConfig do
end)
config = {:agent_config, c.version, c.poll_interval_seconds, device_tuples, checks}
:towerops@proto@encode.encode_agent_config(config)
Towerops.Proto.TupleEncode.encode_agent_config(config)
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_agent_config(binary) do
case Towerops.Proto.TupleDecode.decode_agent_config(binary) do
{:ok, {:agent_config, version, poll_interval, _devices, _checks}} ->
%__MODULE__{version: version, poll_interval_seconds: poll_interval}
@ -252,7 +253,7 @@ defmodule Towerops.Agent.SensorReading do
defstruct sensor_id: "", value: 0.0, status: "", timestamp: 0
def encode(%__MODULE__{} = sr) do
:towerops@proto@encode.encode_sensor_reading({:sensor_reading, sr.sensor_id, sr.value, sr.status, sr.timestamp})
Towerops.Proto.TupleEncode.encode_sensor_reading({:sensor_reading, sr.sensor_id, sr.value, sr.status, sr.timestamp})
end
end
@ -269,7 +270,7 @@ defmodule Towerops.Agent.InterfaceStat do
timestamp: 0
def encode(%__MODULE__{} = s) do
:towerops@proto@encode.encode_interface_stat(
Towerops.Proto.TupleEncode.encode_interface_stat(
{:interface_stat, s.interface_id, s.if_in_octets, s.if_out_octets, s.if_in_errors, s.if_out_errors,
s.if_in_discards, s.if_out_discards, s.timestamp}
)
@ -292,7 +293,7 @@ defmodule Towerops.Agent.NeighborDiscovery do
timestamp: 0
def encode(%__MODULE__{} = n) do
gleam_tuple = {
tuple_data = {
:neighbor_discovery,
n.interface_id,
n.protocol,
@ -307,7 +308,7 @@ defmodule Towerops.Agent.NeighborDiscovery do
n.timestamp
}
:towerops@proto@encode.encode_neighbor_discovery(gleam_tuple)
Towerops.Proto.TupleEncode.encode_neighbor_discovery(tuple_data)
end
end
@ -316,13 +317,13 @@ defmodule Towerops.Agent.MonitoringCheck do
defstruct device_id: "", status: "", response_time_ms: 0.0, timestamp: 0
def encode(%__MODULE__{} = mc) do
:towerops@proto@encode.encode_monitoring_check(
Towerops.Proto.TupleEncode.encode_monitoring_check(
{:monitoring_check, mc.device_id, mc.status, mc.response_time_ms, mc.timestamp}
)
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_monitoring_check(binary) do
case Towerops.Proto.TupleDecode.decode_monitoring_check(binary) do
{:ok, {:monitoring_check, device_id, status, response_time_ms, timestamp}} ->
{:ok,
%__MODULE__{
@ -333,7 +334,7 @@ defmodule Towerops.Agent.MonitoringCheck do
}}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -343,8 +344,8 @@ defmodule Towerops.Agent.Metric do
defstruct metric_type: nil
def encode(%__MODULE__{metric_type: mt}) do
gleam_metric = Towerops.Proto.Agent.metric_to_gleam(mt)
:towerops@proto@encode.encode_metric(gleam_metric)
metric_tuple = Towerops.Proto.Agent.metric_to_tuple(mt)
Towerops.Proto.TupleEncode.encode_metric(metric_tuple)
end
end
@ -353,24 +354,24 @@ defmodule Towerops.Agent.MetricBatch do
defstruct metrics: []
def encode(%__MODULE__{} = batch) do
gleam_metrics =
metric_tuples =
Enum.map(batch.metrics, fn %Towerops.Agent.Metric{metric_type: mt} ->
Towerops.Proto.Agent.metric_to_gleam(mt)
Towerops.Proto.Agent.metric_to_tuple(mt)
end)
:towerops@proto@encode.encode_metric_batch({:metric_batch, gleam_metrics})
Towerops.Proto.TupleEncode.encode_metric_batch({:metric_batch, metric_tuples})
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_metric_batch(binary) do
case Towerops.Proto.TupleDecode.decode_metric_batch(binary) do
{:ok, {:metric_batch, metrics}} ->
{:ok,
%__MODULE__{
metrics: Enum.map(metrics, &Towerops.Proto.Agent.gleam_metric_to_elixir/1)
metrics: Enum.map(metrics, &Towerops.Proto.Agent.tuple_to_metric/1)
}}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -426,13 +427,13 @@ defmodule Towerops.Agent.CheckResult do
defstruct check_id: "", status: 0, output: "", response_time_ms: 0.0, timestamp: 0
def encode(%__MODULE__{} = cr) do
:towerops@proto@encode.encode_check_result(
Towerops.Proto.TupleEncode.encode_check_result(
{:check_result, cr.check_id, cr.status, cr.output, cr.response_time_ms, cr.timestamp}
)
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_check_result(binary) do
case Towerops.Proto.TupleDecode.decode_check_result(binary) do
{:ok, {:check_result, check_id, status, output, response_time_ms, timestamp}} ->
{:ok,
%__MODULE__{
@ -444,7 +445,7 @@ defmodule Towerops.Agent.CheckResult do
}}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -454,8 +455,8 @@ defmodule Towerops.Agent.CheckList do
defstruct checks: []
def encode(%__MODULE__{} = cl) do
gleam_checks = Enum.map(cl.checks, &Towerops.Proto.Agent.check_to_gleam/1)
:towerops@proto@encode.encode_check_list({:check_list, gleam_checks})
check_tuples = Enum.map(cl.checks, &Towerops.Proto.Agent.check_to_tuple/1)
Towerops.Proto.TupleEncode.encode_check_list({:check_list, check_tuples})
end
end
@ -475,7 +476,7 @@ defmodule Towerops.Agent.SnmpDevice do
transport: ""
def encode(%__MODULE__{} = d) do
:towerops@proto@encode.encode_snmp_device(
Towerops.Proto.TupleEncode.encode_snmp_device(
{:snmp_device, d.ip, d.community, d.version, d.port, d.v3_security_level, d.v3_username, d.v3_auth_protocol,
d.v3_auth_password, d.v3_priv_protocol, d.v3_priv_password, d.transport}
)
@ -487,8 +488,8 @@ defmodule Towerops.Agent.SnmpQuery do
defstruct query_type: :GET, oids: []
def encode(%__MODULE__{} = q) do
:towerops@proto@encode.encode_snmp_query(
{:snmp_query, Towerops.Proto.Agent.query_type_atom_to_gleam(q.query_type), q.oids}
Towerops.Proto.TupleEncode.encode_snmp_query(
{:snmp_query, Towerops.Proto.Agent.query_type_to_atom(q.query_type), q.oids}
)
end
end
@ -513,17 +514,17 @@ defmodule Towerops.Agent.AgentJobList do
defstruct jobs: []
def encode(%__MODULE__{} = jl) do
gleam_jobs = Enum.map(jl.jobs, &Towerops.Proto.Agent.job_to_gleam/1)
:towerops@proto@encode.encode_agent_job_list({:agent_job_list, gleam_jobs})
job_tuples = Enum.map(jl.jobs, &Towerops.Proto.Agent.job_to_tuple/1)
Towerops.Proto.TupleEncode.encode_agent_job_list({:agent_job_list, job_tuples})
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_agent_job_list(binary) do
{:ok, {:agent_job_list, gleam_jobs}} ->
{:ok, %__MODULE__{jobs: Enum.map(gleam_jobs, &Towerops.Proto.Agent.gleam_job_to_elixir/1)}}
case Towerops.Proto.TupleDecode.decode_agent_job_list(binary) do
{:ok, {:agent_job_list, job_tuples}} ->
{:ok, %__MODULE__{jobs: Enum.map(job_tuples, &Towerops.Proto.Agent.tuple_to_job/1)}}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -540,17 +541,17 @@ defmodule Towerops.Agent.AgentJob do
mikrotik_commands: []
def encode(%__MODULE__{} = j) do
gleam_job = Towerops.Proto.Agent.job_to_gleam(j)
:towerops@proto@encode.encode_agent_job(gleam_job)
job_tuple = Towerops.Proto.Agent.job_to_tuple(j)
Towerops.Proto.TupleEncode.encode_agent_job(job_tuple)
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_agent_job(binary) do
{:ok, gleam_job} ->
{:ok, Towerops.Proto.Agent.gleam_job_to_elixir(gleam_job)}
case Towerops.Proto.TupleDecode.decode_agent_job(binary) do
{:ok, job_tuple} ->
{:ok, Towerops.Proto.Agent.tuple_to_job(job_tuple)}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -629,11 +630,11 @@ defmodule Towerops.Agent.AgentError do
defstruct device_id: "", job_id: "", message: "", timestamp: 0
def encode(%__MODULE__{} = e) do
:towerops@proto@encode.encode_agent_error({:agent_error, e.device_id, e.job_id, e.message, e.timestamp})
Towerops.Proto.TupleEncode.encode_agent_error({:agent_error, e.device_id, e.job_id, e.message, e.timestamp})
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_agent_error(binary) do
case Towerops.Proto.TupleDecode.decode_agent_error(binary) do
{:ok, {:agent_error, device_id, job_id, message, timestamp}} ->
{:ok,
%__MODULE__{
@ -644,7 +645,7 @@ defmodule Towerops.Agent.AgentError do
}}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -654,13 +655,13 @@ defmodule Towerops.Agent.CredentialTestResult do
defstruct test_id: "", success: false, error_message: "", system_description: "", timestamp: 0
def encode(%__MODULE__{} = r) do
:towerops@proto@encode.encode_credential_test_result(
Towerops.Proto.TupleEncode.encode_credential_test_result(
{:credential_test_result, r.test_id, r.success, r.error_message, r.system_description, r.timestamp}
)
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_credential_test_result(binary) do
case Towerops.Proto.TupleDecode.decode_credential_test_result(binary) do
{:ok, {:credential_test_result, test_id, success, error_message, system_description, timestamp}} ->
{:ok,
%__MODULE__{
@ -672,7 +673,7 @@ defmodule Towerops.Agent.CredentialTestResult do
}}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -694,18 +695,18 @@ defmodule Towerops.Agent.MikrotikResult do
defstruct device_id: "", job_id: "", sentences: [], error: "", timestamp: 0
def encode(%__MODULE__{} = r) do
gleam_sentences =
sentence_tuples =
Enum.map(r.sentences, fn %MikrotikSentence{attributes: attrs} ->
{:mikrotik_sentence, attrs}
end)
:towerops@proto@encode.encode_mikrotik_result(
{:mikrotik_result, r.device_id, r.job_id, gleam_sentences, r.error, r.timestamp}
Towerops.Proto.TupleEncode.encode_mikrotik_result(
{:mikrotik_result, r.device_id, r.job_id, sentence_tuples, r.error, r.timestamp}
)
end
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_mikrotik_result(binary) do
case Towerops.Proto.TupleDecode.decode_mikrotik_result(binary) do
{:ok, {:mikrotik_result, device_id, job_id, sentences, error, timestamp}} ->
{:ok,
%__MODULE__{
@ -722,7 +723,7 @@ defmodule Towerops.Agent.MikrotikResult do
}}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -742,7 +743,7 @@ defmodule Towerops.Agent.LldpTopologyResult do
defstruct device_id: "", job_id: "", local_system_name: "", neighbors: [], timestamp: 0
def decode(binary) when is_binary(binary) do
case :towerops@proto@decode.decode_lldp_topology_result(binary) do
case Towerops.Proto.TupleDecode.decode_lldp_topology_result(binary) do
{:ok, {:lldp_topology_result, device_id, job_id, local_system_name, neighbors, timestamp}} ->
{:ok,
%__MODULE__{
@ -763,7 +764,7 @@ defmodule Towerops.Agent.LldpTopologyResult do
}}
{:error, reason} ->
{:error, Towerops.Proto.Agent.gleam_error_to_elixir(reason)}
{:error, Towerops.Proto.Agent.decode_error_to_tuple(reason)}
end
end
end
@ -784,24 +785,24 @@ defmodule Towerops.Proto.Agent do
alias Towerops.Agent.SnmpDevice
alias Towerops.Agent.SnmpQuery
# Convert Gleam DecodeError to Elixir {atom, string} tuple.
# Convert DecodeError to Elixir {atom, string} tuple.
# Most error tags pass through unchanged; :wire_error maps to :decode_error.
def gleam_error_to_elixir({:wire_error, msg}), do: {:decode_error, msg}
def gleam_error_to_elixir({tag, msg}) when is_atom(tag) and is_binary(msg), do: {tag, msg}
def gleam_error_to_elixir(other), do: {:decode_error, inspect(other)}
def decode_error_to_tuple({:wire_error, msg}), do: {:decode_error, msg}
def decode_error_to_tuple({tag, msg}) when is_atom(tag) and is_binary(msg), do: {tag, msg}
def decode_error_to_tuple(other), do: {:decode_error, inspect(other)}
# Convert Elixir metric tagged tuple to Gleam metric variant
def metric_to_gleam({:sensor_reading, %SensorReading{} = sr}) do
# Convert Elixir metric tagged tuple to metric tuple
def metric_to_tuple({:sensor_reading, %SensorReading{} = sr}) do
{:sensor_reading_metric, {:sensor_reading, sr.sensor_id, sr.value, sr.status, sr.timestamp}}
end
def metric_to_gleam({:interface_stat, %InterfaceStat{} = is}) do
def metric_to_tuple({:interface_stat, %InterfaceStat{} = is}) do
{:interface_stat_metric,
{:interface_stat, is.interface_id, is.if_in_octets, is.if_out_octets, is.if_in_errors, is.if_out_errors,
is.if_in_discards, is.if_out_discards, is.timestamp}}
end
def metric_to_gleam({:neighbor_discovery, %NeighborDiscovery{} = nd}) do
def metric_to_tuple({:neighbor_discovery, %NeighborDiscovery{} = nd}) do
tuple = {
:neighbor_discovery,
nd.interface_id,
@ -820,17 +821,17 @@ defmodule Towerops.Proto.Agent do
{:neighbor_discovery_metric, tuple}
end
def metric_to_gleam({:monitoring_check, %MonitoringCheck{} = mc}) do
def metric_to_tuple({:monitoring_check, %MonitoringCheck{} = mc}) do
{:monitoring_check_metric, {:monitoring_check, mc.device_id, mc.status, mc.response_time_ms, mc.timestamp}}
end
def metric_to_gleam({:check_result, %CheckResult{} = cr}) do
def metric_to_tuple({:check_result, %CheckResult{} = cr}) do
{:check_result_metric, {:check_result, cr.check_id, cr.status, cr.output, cr.response_time_ms, cr.timestamp}}
end
# Convert Gleam metric variant to Elixir metric struct
def gleam_metric_to_elixir(gleam_metric) do
case gleam_metric do
# Convert metric tuple to Elixir metric struct
def tuple_to_metric(metric_tuple) do
case metric_tuple do
{:sensor_reading_metric, {:sensor_reading, sensor_id, value, status, timestamp}} ->
%Metric{
metric_type:
@ -909,7 +910,7 @@ defmodule Towerops.Proto.Agent do
end
# Job type conversions
def job_type_atom_to_gleam(atom) when is_atom(atom) do
def job_type_to_atom(atom) when is_atom(atom) do
case atom do
:DISCOVER -> :discover
:POLL -> :poll
@ -921,8 +922,8 @@ defmodule Towerops.Proto.Agent do
end
end
def gleam_job_type_to_atom(gleam_jt) do
case gleam_jt do
def atom_to_job_type(job_type_atom) do
case job_type_atom do
:discover -> :DISCOVER
:poll -> :POLL
:mikrotik -> :MIKROTIK
@ -933,7 +934,7 @@ defmodule Towerops.Proto.Agent do
end
end
def query_type_atom_to_gleam(atom) do
def query_type_to_atom(atom) do
case atom do
:GET -> :get
:WALK -> :walk
@ -941,30 +942,28 @@ defmodule Towerops.Proto.Agent do
end
end
def gleam_query_type_to_atom(gleam_qt) do
case gleam_qt do
def atom_to_query_type(query_type_atom) do
case query_type_atom do
:get -> :GET
:walk -> :WALK
other -> other
end
end
# Convert Gleam AgentJob tuple to Elixir struct
def gleam_job_to_elixir(
{:agent_job, job_id, job_type, device_id, snmp_device, queries, mikrotik_device, mikrotik_commands}
) do
# Convert AgentJob tuple to Elixir struct
def tuple_to_job({:agent_job, job_id, job_type, device_id, snmp_device, queries, mikrotik_device, mikrotik_commands}) do
%AgentJob{
job_id: job_id,
job_type: gleam_job_type_to_atom(job_type),
job_type: atom_to_job_type(job_type),
device_id: device_id,
snmp_device: gleam_optional_snmp_device(snmp_device),
queries: Enum.map(queries, &gleam_query_to_elixir/1),
mikrotik_device: gleam_optional_mikrotik_device(mikrotik_device),
mikrotik_commands: Enum.map(mikrotik_commands, &gleam_mikrotik_command_to_elixir/1)
snmp_device: tuple_optional_snmp_device(snmp_device),
queries: Enum.map(queries, &tuple_to_query/1),
mikrotik_device: tuple_optional_mikrotik_device(mikrotik_device),
mikrotik_commands: Enum.map(mikrotik_commands, &tuple_to_mikrotik_command/1)
}
end
defp gleam_optional_snmp_device(
defp tuple_optional_snmp_device(
{:some, {:snmp_device, ip, community, version, port, v3sl, v3u, v3ap, v3apw, v3pp, v3ppw, transport}}
) do
%SnmpDevice{
@ -982,13 +981,13 @@ defmodule Towerops.Proto.Agent do
}
end
defp gleam_optional_snmp_device(:none), do: nil
defp tuple_optional_snmp_device(:none), do: nil
defp gleam_query_to_elixir({:snmp_query, qt, oids}) do
%SnmpQuery{query_type: gleam_query_type_to_atom(qt), oids: oids}
defp tuple_to_query({:snmp_query, qt, oids}) do
%SnmpQuery{query_type: atom_to_query_type(qt), oids: oids}
end
defp gleam_optional_mikrotik_device({:some, {:mikrotik_device, ip, port, username, password, use_ssl, ssh_port}}) do
defp tuple_optional_mikrotik_device({:some, {:mikrotik_device, ip, port, username, password, use_ssl, ssh_port}}) do
%MikrotikDevice{
ip: ip,
port: port,
@ -999,14 +998,14 @@ defmodule Towerops.Proto.Agent do
}
end
defp gleam_optional_mikrotik_device(:none), do: nil
defp tuple_optional_mikrotik_device(:none), do: nil
defp gleam_mikrotik_command_to_elixir({:mikrotik_command, command, args}) do
defp tuple_to_mikrotik_command({:mikrotik_command, command, args}) do
%MikrotikCommand{command: command, args: args}
end
# Convert Elixir AgentJob struct to Gleam tuple
def job_to_gleam(%AgentJob{} = j) do
# Convert Elixir AgentJob struct to tuple
def job_to_tuple(%AgentJob{} = j) do
snmp_device =
case j.snmp_device do
nil ->
@ -1020,7 +1019,7 @@ defmodule Towerops.Proto.Agent do
queries =
Enum.map(j.queries, fn %SnmpQuery{} = q ->
{:snmp_query, query_type_atom_to_gleam(q.query_type), q.oids}
{:snmp_query, query_type_to_atom(q.query_type), q.oids}
end)
mikrotik_device =
@ -1037,12 +1036,12 @@ defmodule Towerops.Proto.Agent do
{:mikrotik_command, mc.command, mc.args}
end)
{:agent_job, j.job_id, job_type_atom_to_gleam(j.job_type), j.device_id, snmp_device, queries, mikrotik_device,
{:agent_job, j.job_id, job_type_to_atom(j.job_type), j.device_id, snmp_device, queries, mikrotik_device,
mikrotik_commands}
end
# Convert Elixir Check struct to Gleam tuple
def check_to_gleam(%Towerops.Agent.Check{} = c) do
# Convert Elixir Check struct to tuple
def check_to_tuple(%Towerops.Agent.Check{} = c) do
config =
cond do
c.http != nil ->

View file

@ -1,7 +1,7 @@
defmodule :towerops@proto@decode do
defmodule Towerops.Proto.TupleDecode do
@moduledoc """
Erlang compatibility wrapper for decoding protobuf messages.
Converts Elixir structs to Gleam-style tuples for backward compatibility.
Compatibility wrapper for decoding protobuf messages.
Converts Elixir structs to tagged tuples for backward compatibility.
"""
alias Towerops.Agent.MikrotikCommand
@ -81,13 +81,13 @@ defmodule :towerops@proto@decode do
def decode_mikrotik_result(binary) when is_binary(binary) do
case Decode.decode_mikrotik_result(binary) do
{:ok, %Types.MikrotikResult{} = mr} ->
# Convert MikrotikSentence structs to Gleam tuples
gleam_sentences =
# Convert MikrotikSentence structs to tuples
tuple_sentences =
Enum.map(mr.sentences, fn %Types.MikrotikSentence{attributes: attrs} ->
{:mikrotik_sentence, attrs}
end)
{:ok, {:mikrotik_result, mr.device_id, mr.job_id, gleam_sentences, mr.error, mr.timestamp}}
{:ok, {:mikrotik_result, mr.device_id, mr.job_id, tuple_sentences, mr.error, mr.timestamp}}
{:error, reason} ->
{:error, reason}
@ -121,26 +121,26 @@ defmodule :towerops@proto@decode do
def decode_metric_batch(binary) when is_binary(binary) do
case Decode.decode_metric_batch(binary) do
{:ok, %Types.MetricBatch{metrics: metrics}} ->
# Convert metrics to Gleam-style tuples
gleam_metrics = Enum.map(metrics, &metric_to_gleam/1)
{:ok, {:metric_batch, gleam_metrics}}
# Convert metrics to tagged tuples
tuple_metrics = Enum.map(metrics, &metric_to_tuple/1)
{:ok, {:metric_batch, tuple_metrics}}
{:error, reason} ->
{:error, reason}
end
end
defp metric_to_gleam({:sensor_reading, %Types.SensorReading{} = sr}) do
defp metric_to_tuple({:sensor_reading, %Types.SensorReading{} = sr}) do
{:sensor_reading_metric, {:sensor_reading, sr.sensor_id, sr.value, sr.status, sr.timestamp}}
end
defp metric_to_gleam({:interface_stat, %Types.InterfaceStat{} = is}) do
defp metric_to_tuple({:interface_stat, %Types.InterfaceStat{} = is}) do
{:interface_stat_metric,
{:interface_stat, is.interface_id, is.if_in_octets, is.if_out_octets, is.if_in_errors, is.if_out_errors,
is.if_in_discards, is.if_out_discards, is.timestamp}}
end
defp metric_to_gleam({:neighbor_discovery, %Types.NeighborDiscovery{} = nd}) do
defp metric_to_tuple({:neighbor_discovery, %Types.NeighborDiscovery{} = nd}) do
{
:neighbor_discovery_metric,
{
@ -177,7 +177,7 @@ defmodule :towerops@proto@decode do
def decode_agent_config(binary) when is_binary(binary) do
case Decode.decode_agent_config(binary) do
{:ok, %Types.AgentConfig{} = ac} ->
# Convert devices and checks to Gleam tuples (not implemented yet, return empty lists)
# Convert devices and checks to tuples (not implemented yet, return empty lists)
{:ok, {:agent_config, ac.version, ac.poll_interval_seconds, [], []}}
{:error, reason} ->
@ -189,7 +189,7 @@ defmodule :towerops@proto@decode do
case Decode.decode_agent_job(binary) do
{:ok, %Types.AgentJob{} = job} ->
agent_job = types_job_to_agent_job(job)
{:ok, Towerops.Proto.Agent.job_to_gleam(agent_job)}
{:ok, Towerops.Proto.Agent.job_to_tuple(agent_job)}
{:error, reason} ->
{:error, reason}
@ -199,10 +199,10 @@ defmodule :towerops@proto@decode do
def decode_agent_job_list(binary) when is_binary(binary) do
case Decode.decode_agent_job_list(binary) do
{:ok, %Types.AgentJobList{jobs: jobs}} ->
# Convert Types.AgentJob to Agent.AgentJob, then to Gleam tuples
# Convert Types.AgentJob to Agent.AgentJob, then to tuples
agent_jobs = Enum.map(jobs, &types_job_to_agent_job/1)
gleam_jobs = Enum.map(agent_jobs, &Towerops.Proto.Agent.job_to_gleam/1)
{:ok, {:agent_job_list, gleam_jobs}}
tuple_jobs = Enum.map(agent_jobs, &Towerops.Proto.Agent.job_to_tuple/1)
{:ok, {:agent_job_list, tuple_jobs}}
{:error, reason} ->
{:error, reason}
@ -266,13 +266,13 @@ defmodule :towerops@proto@decode do
def decode_lldp_topology_result(binary) when is_binary(binary) do
case Decode.decode_lldp_topology_result(binary) do
{:ok, %Types.LldpTopologyResult{} = ltr} ->
# Convert LldpNeighbor structs to Gleam tuples
gleam_neighbors =
# Convert LldpNeighbor structs to tuples
tuple_neighbors =
Enum.map(ltr.neighbors, fn %Types.LldpNeighbor{} = n ->
{:lldp_neighbor, n.neighbor_name, n.local_port, n.remote_port, n.remote_port_id, n.management_addresses}
end)
{:ok, {:lldp_topology_result, ltr.device_id, ltr.job_id, ltr.local_system_name, gleam_neighbors, ltr.timestamp}}
{:ok, {:lldp_topology_result, ltr.device_id, ltr.job_id, ltr.local_system_name, tuple_neighbors, ltr.timestamp}}
{:error, reason} ->
{:error, reason}

View file

@ -1,7 +1,7 @@
defmodule :towerops@proto@encode do
defmodule Towerops.Proto.TupleEncode do
@moduledoc """
Erlang compatibility wrapper for encoding protobuf messages.
Accepts Gleam-style tuples and converts them to Elixir structs for encoding.
Compatibility wrapper for encoding protobuf messages.
Accepts tagged tuples and converts them to Elixir structs for encoding.
"""
alias Towerops.Proto.Encode
@ -102,8 +102,8 @@ defmodule :towerops@proto@encode do
# AgentConfig
def encode_agent_config({:agent_config, version, poll_interval, devices, checks}) do
device_structs = Enum.map(devices, &gleam_device_to_types/1)
check_structs = Enum.map(checks, &gleam_check_to_types/1)
device_structs = Enum.map(devices, &tuple_device_to_types/1)
check_structs = Enum.map(checks, &tuple_check_to_types/1)
Encode.encode_agent_config(%Types.AgentConfig{
version: version,
@ -171,9 +171,9 @@ defmodule :towerops@proto@encode do
end
# Metric
def encode_metric(gleam_metric) do
def encode_metric(tuple_metric) do
metric_struct =
case gleam_metric do
case tuple_metric do
{:sensor_reading_metric, {:sensor_reading, sensor_id, value, status, timestamp}} ->
{:sensor_reading,
%Types.SensorReading{
@ -240,8 +240,8 @@ defmodule :towerops@proto@encode do
end
# MetricBatch
def encode_metric_batch({:metric_batch, gleam_metrics}) do
metric_structs = Enum.map(gleam_metrics, &gleam_metric_to_types/1)
def encode_metric_batch({:metric_batch, tuple_metrics}) do
metric_structs = Enum.map(tuple_metrics, &tuple_metric_to_types/1)
Encode.encode_metric_batch(%Types.MetricBatch{metrics: metric_structs})
end
@ -257,8 +257,8 @@ defmodule :towerops@proto@encode do
end
# CheckList
def encode_check_list({:check_list, gleam_checks}) do
check_structs = Enum.map(gleam_checks, &gleam_check_to_types/1)
def encode_check_list({:check_list, tuple_checks}) do
check_structs = Enum.map(tuple_checks, &tuple_check_to_types/1)
Encode.encode_check_list(%Types.CheckList{checks: check_structs})
end
@ -291,14 +291,14 @@ defmodule :towerops@proto@encode do
end
# AgentJobList
def encode_agent_job_list({:agent_job_list, gleam_jobs}) do
job_structs = Enum.map(gleam_jobs, &gleam_job_to_types/1)
def encode_agent_job_list({:agent_job_list, tuple_jobs}) do
job_structs = Enum.map(tuple_jobs, &tuple_job_to_types/1)
Encode.encode_agent_job_list(%Types.AgentJobList{jobs: job_structs})
end
# AgentJob
def encode_agent_job(gleam_job) do
job_struct = gleam_job_to_types(gleam_job)
def encode_agent_job(tuple_job) do
job_struct = tuple_job_to_types(tuple_job)
Encode.encode_agent_job(job_struct)
end
@ -343,8 +343,8 @@ defmodule :towerops@proto@encode do
# Helper functions for complex conversions
defp gleam_metric_to_types(gleam_metric) do
case gleam_metric do
defp tuple_metric_to_types(tuple_metric) do
case tuple_metric do
{:sensor_reading_metric, {:sensor_reading, sensor_id, value, status, timestamp}} ->
{:sensor_reading,
%Types.SensorReading{
@ -389,7 +389,7 @@ defmodule :towerops@proto@encode do
end
end
defp gleam_check_to_types({:check, id, check_type, interval_seconds, timeout_ms, config}) do
defp tuple_check_to_types({:check, id, check_type, interval_seconds, timeout_ms, config}) do
config_struct =
case config do
{:http_config,
@ -434,12 +434,12 @@ defmodule :towerops@proto@encode do
}
end
defp gleam_device_to_types(gleam_device) do
defp tuple_device_to_types(tuple_device) do
# Stub - implement if needed
gleam_device
tuple_device
end
defp gleam_job_to_types(
defp tuple_job_to_types(
{:agent_job, job_id, job_type, device_id, snmp_device, queries, mikrotik_device, mikrotik_commands}
) do
snmp_device_struct =

View file

@ -6,7 +6,7 @@ defmodule Towerops.Snmp.SensorChangeDetector do
event detection across both Phoenix-polled and agent-polled devices.
Pure decision logic (threshold checking, value formatting) is implemented
in Gleam. PubSub broadcasting, Repo queries, and event building stay here.
as helper functions. PubSub broadcasting, Repo queries, and event building stay here.
"""
alias Towerops.Snmp.Sensor

View file

@ -25,7 +25,7 @@ defmodule ToweropsWeb.ChangelogParser do
@doc """
Parse changelog content string into a list of entries.
Returns tuples in Gleam format: {:changelog_entry, date, title_opt, items}
Returns internal tuples: {:changelog_entry, date, title_opt, items}
"""
def parse_content(content) when is_binary(content) do
content

View file

@ -3,9 +3,8 @@
stdenv,
mkShell,
writeShellScriptBin,
# Elixir/Erlang/Gleam
# Elixir/Erlang
elixir,
gleam,
# Databases and caches
postgresql_16,
redis,
@ -217,9 +216,8 @@ mkShell {
# Development tools
buildInputs = [
# Elixir/Erlang/Gleam
# Elixir/Erlang
elixir
gleam
# Databases (PostgreSQL with TimescaleDB)
pg

View file

@ -3,7 +3,7 @@ defmodule Towerops.Agent.ProtoTest do
Tests for Protocol Buffer generated modules.
These tests verify that encoding and decoding works correctly for all
protobuf message types used in agent communication. The Gleam-based
protobuf message types used in agent communication. The
implementation uses {:ok, struct} tuples for decode where available,
and some modules are encode-only.
"""

View file

@ -1,4 +1,4 @@
defmodule ToweropsWeb.GleamChangelogParserTest do
defmodule ToweropsWeb.ChangelogParserTest do
use ExUnit.Case, async: true
alias ToweropsWeb.ChangelogParser