Timeless

LiveDashboard Plugin for Timeless Traces

Hex.pmDocsLicense


"I found it ironic that the first thing you do to time series data is squash the timestamp. That's how the name Timeless was born." --Mark Cotner

Phoenix LiveDashboard page for browsing OpenTelemetry spans stored by TimelessTraces.

Provides four tabs:

Installation

Quick Start (Igniter)

mix igniter.install timeless_traces_dashboard

This automatically:

  1. Adds config :timeless_traces, data_dir: "priv/timeless_traces" to your config
  2. Adds config :opentelemetry, traces_exporter: {TimelessTraces.Exporter, []} to your config
  3. Adds import TimelessTracesDashboard.Router to your router
  4. Adds timeless_traces_dashboard "/dashboard" to your browser scope
  5. Updates your .formatter.exs

For in-memory storage (traces lost on restart):

mix igniter.install timeless_traces_dashboard --storage memory

Manual Setup

Add timeless_traces_dashboard to your dependencies:

def deps do
[
{:timeless_traces_dashboard, "~> 0.3"}
]
end

Configure TimelessTraces and OpenTelemetry in config/config.exs:

config :timeless_traces, data_dir: "priv/timeless_traces"
config :opentelemetry, traces_exporter: {TimelessTraces.Exporter, []}

Add the router macro:

# lib/my_app_web/router.ex
import TimelessTracesDashboard.Router
scope "/" do
pipe_through :browser
timeless_traces_dashboard "/dashboard"
end

Or add the page directly to an existing LiveDashboard:

live_dashboard "/dashboard",
additional_pages: [
traces: TimelessTracesDashboard.Page
]

Navigate to /dashboard/traces in your browser.

Experimental Rust traces data plane

The POC branch can route only historical Search and Traces-tab detail through the separately supervised timeless-traces-api process. Phoenix still owns dashboard sessions, state, rendering, stats, and live tail. The default remains the embedded TimelessTraces source.

Add the executable owner to your application's supervision tree:

{TimelessTracesDashboard.DataPlane.Process,
binary: "/opt/timeless/bin/timeless-traces-api",
extension: "/opt/timeless/lib/libtimeless_ext.so",
database: "/var/lib/timeless/traces.db",
listen: "127.0.0.1:19449"}

Then opt the two historical paths into the loopback client:

config :timeless_traces_dashboard, :historical_source,
{TimelessTracesDashboard.HistoricalSource.DataPlane,
client_opts: [process: TimelessTracesDashboard.DataPlane.Process]}

The Rust child is the exclusive owner of that database. The client owns no SQLite connection and validates a complete rich-span response before exposing it to LiveView. Invalid JSON, a truncated response, disconnect, or restart is one failed operation and never a partial waterfall. Normal OTP shutdown sends SIGTERM and waits for the child to drain, flush, and exit; abnormal exit is isolated for supervisor restart. Only IPv4 loopback is accepted in this POC.

bench/traces_data_plane_boundary.exs measures the incremental supervision lookup, process memory, and SIGKILL-to-ready recovery using sibling release artifacts.

Existing OpenTelemetry Exporter

OpenTelemetry's :traces_exporter config only supports a single exporter. If you already export traces to an external system (Jaeger, Tempo, Datadog, etc.), setting traces_exporter: {TimelessTraces.Exporter, []} will replace your existing exporter and traces will stop flowing to that system.

If you need traces sent to both TimelessTraces and an external collector, you'll need a thin fan-out exporter that calls both. For example:

defmodule MyApp.CompositeExporter do
@behaviour :otel_exporter_traces
@impl true
def init(_config) do
{:ok, otel_state} = :otel_exporter_otlp.init(%{})
{:ok, timeless_state} = TimelessTraces.Exporter.init(%{})
{:ok, %{otlp: otel_state, timeless: timeless_state}}
end
@impl true
def export(tab, resource, state) do
:otel_exporter_otlp.export(tab, resource, state.otlp)
TimelessTraces.Exporter.export(tab, resource, state.timeless)
{:ok, state}
end
@impl true
def shutdown(state) do
:otel_exporter_otlp.shutdown(state.otlp)
TimelessTraces.Exporter.shutdown(state.timeless)
:ok
end
end

Then configure: config :opentelemetry, traces_exporter: {MyApp.CompositeExporter, []}

This does not apply to metrics or logs -- the metrics Reporter uses :telemetry (which supports multiple handlers) and TimelessLogs registers as a Logger handler (which coexists with other handlers).

Requirements

License

MIT