telescope

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: Apache-2.0

README

telescope Go Reference alpha x

A terminal log viewer for the oteldb project.

It streams logs from journalctl, kubectl, docker or any command, locally or through ssh, and from a VictoriaLogs or Loki database, directly or through a Grafana datasource. Structured lines are pretty printed with go-faster/pl; anything else passes through with timestamps, levels, numbers and paths highlighted.

telescope

Installation

A release carries a binary for Linux, macOS and Windows on amd64 and arm64. It is one static file and depends on nothing — unpack it and put it on $PATH:

$ tar -xzf telescope_0.1.0_linux_amd64.tar.gz telescope
$ install -m755 telescope ~/.local/bin/

The same releases carry .deb, .rpm, .apk and Arch packages, and a checksums.txt signed with cosign:

$ cosign verify-blob --bundle checksums.txt.sigstore.json \
    --certificate-identity-regexp 'https://github.com/oteldb/telescope/.*' \
    --certificate-oidc-issuer https://token.actions.githubusercontent.com \
    checksums.txt

With a Go toolchain, no release is needed:

$ go install github.com/oteldb/telescope/cmd/telescope@latest

And from a checkout, which is also how it is developed:

$ go run ./cmd/telescope

Nothing has to be configured: the start screen lists the units, pods and containers it can find. Every screen writes its keys along the bottom, so this file does not. Declaring the places you read daily saves picking them again — see Configuration.

Sources

type reads
journalctl a systemd unit, or the whole journal
kubectl a pod, a container, or a label selector
docker a container
command anything writing to stdout
victorialogs a LogsQL query, over HTTP
loki a Loki endpoint, over HTTP; the filter selects the stream

The first four run a command, on this machine unless you name an ssh host with ctrl+o (or via: ssh://host). The last two query a database over HTTP and need a URL and usually a token; see Endpoints.

Targets are written the same way in the prompt and in the config file:

syntax means
kubelet a system unit
user/syncthing a unit of the user manager (journalctl --user)
pod a pod in the default namespace
oteldb/oteldb-0 a pod in a namespace
oteldb/oteldb-0:clickhouse one container of that pod
oteldb/deploy/api a workload: deployment, statefulset, daemonset, job, …
oteldb/app=oteldb a label selector (kubectl logs -l)

Naming a workload rather than a pod survives a restart.

Merging

Several places can be read as one timeline, each line tagged with where it came from. Pick them on the start screen with ctrl+a, or declare a group:

places:
  - {name: api, type: docker, container: api}
  - {name: worker, type: docker, container: worker}
groups:
  - name: prod
    places: [api, worker]
    range: 1h
  api     10:29:09.660  POST /orders 201
  worker  10:29:09.662  job started id=91
  api     10:29:09.671  GET /health 200

The places need not be alike — kubectl on a cluster, journalctl over ssh, and a database in another region read as one stream. The window, tail and follow belong to the group. A place that fails to open, or stops reading, is reported where its lines would have been; the rest keep streaming.

Time range

ctrl+g bounds the window, and shows what it resolves to:

written means
1h, 30m, 7d a window ending now
6h..1h one that has already closed
today, yesterday since local midnight, and the day before
10:00..12:00 clock times today
2026-01-02 10:00..12:00 a date and time, or RFC 3339
all no bounds — the tail alone

A range with an end is not followed. kubectl has no end bound, and a free-form command has no range at all — bound it in the command itself.

Features

The filter (/) is a small query: reset or "connection reset" for a substring, /res[ei]t/ for a regular expression, pod=api-7, pod~api, level>=warn for fields, and and, or, not, - and parentheses over the lot. Terms next to each other are and-ed, so a query that is only words reads as the grep it replaces. ? writes the language out in full, and tab completes field names and values — including the ones only the database has seen yet. Over a log database as much of the filter as the server can answer is sent to it.

Reading backwards. Over a database the tail is where reading starts and not how far back it goes: scroll to the first line and the ones before it are fetched.

An entry (enter) is shown whole — the labels, the fields, the raw bytes. y copies a value as it arrived, f narrows the list by it, and o opens what it points at: a URL in the browser, a file in $EDITOR at the right line, or, where the value is a stacktrace, the innermost frame that is in the checkout. Go, zap, the JVM, CPython and V8 traces are understood.

Traces. T opens the trace a line was written inside and draws it as a gantt; f goes back the other way, narrowing the list by the whole trace or by the row the cursor is on in a span.

Repetition and silence. A line repeated straight after itself is drawn once with ×n, and a gap in the log is drawn as the gap it is.

Log volume is drawn above the list as bars over time, stacked by severity, with the bucket the cursor is reading marked underneath. It counts the lines that have been read and follows the filter in force, so it never disagrees with the list below it. v folds it away when the rows are wanted for the log.

Traces from the command line
$ telescope trace --from https://tempo.example.com 4bf92f3577b34da6a3ce929d0e0e4736
$ telescope trace --from prod 4bf92f3577b34da6a3ce929d0e0e4736
$ telescope trace --from prod
$ telescope trace ./saved.json
$ curl -s "$TEMPO/api/traces/$ID" | telescope trace -

--from names a trace store, either as a url or as the name of a place that declares one, and the argument is then the trace id. With no argument it opens a search of that store instead — a form over service, operation, tags and duration, which alt+t on the start screen opens too. Without --from the argument is a file holding a response already, or - to read one on standard input.

Two APIs are read, and a store says which it speaks: Tempo's — the one oteldb and Grafana's Tempo datasource speak — and Jaeger's query API, which Jaeger and VictoriaTraces serve. For a url, --api tempo or --api jaeger says so; a place says it in the config. A file says nothing and needs to: which format it holds is worked out from what comes out of it, and OTLP arrives as JSON or as protobuf with both understood.

Configuration

Places live in $XDG_CONFIG_HOME/telescope/config.yaml, by default ~/.config/telescope/config.yaml. When the file declares any, the start screen opens on a picker; tab leaves it for the manual flow.

places:
  # Named in full: opens straight into the logs.
  - name: navidrome
    type: docker
    container: navidrome
    tail: 50

  # A cluster reachable only as root on a node that refuses root logins.
  # No pod named, so picking it opens the prompt with the rest filled in.
  - name: k3s-ops
    type: kubectl
    via: ssh://node1
    kubeconfig: /root/.kube/ops.kubeconfig
    context: admin@ops
    sudo: true

  - name: syncthing
    type: journalctl
    unit: user/syncthing
    query: error

groups:
  - name: prod
    places: [k3s-ops, navidrome]

A place does not have to be complete: one that pins a host, a kubeconfig and sudo but no pod fills in what it knew and stops at the step still missing.

A group takes name, places, and the same range, tail, follow and query. Fields a place cannot use — a command with a token, a database reached via: ssh://… — are reported as mistakes in the file rather than ignored, as is a key that is not a key at all.

What a key accepts

Every key is declared once, in the code that reads it, and telescope publishes that declaration as a JSON Schema. Point the file at it and an editor completes the keys, says what each one means and marks what it does not accept:

# yaml-language-server: $schema=https://raw.githubusercontent.com/oteldb/telescope/main/config.schema.json
places:
  - name: navidrome

The $schema key works too, for an editor that reads it, and telescope accepts it as the annotation it is. telescope schema writes the same document to standard output, for a schema store or an editor that wants a local copy.

Traces

A place can say where its traces are read from, which telescope trace --from <name> then refers to by name:

places:
  - name: prod
    type: victorialogs
    url: https://logs.example.com
    token:
      env: LOGS_TOKEN
    traces: https://tempo.example.com

A url on its own is a Tempo. A store that speaks Jaeger's query API — Jaeger itself, or VictoriaTraces — says so:

places:
  - name: prod
    type: victorialogs
    url: https://logs.example.com
    traces:
      url: https://victoria.example.com/select/jaeger
      type: jaeger

Either way the store borrows the place's token, tenant, proxy and TLS settings, since a system's traces usually sit behind the same door as its logs.

Endpoints
places:
  # A Grafana datasource: the URL is the Grafana, and telescope resolves the
  # datasource proxy path against it.
  - name: prod
    type: victorialogs
    url: https://grafana.example.com
    datasource: adm5h5433d8hsa
    token:
      env: GRAFANA_TOKEN
    tenant: "1:1"

  # The token from a keyring, a password manager, anything with a CLI.
  - name: staging
    type: victorialogs
    url: https://logs.staging.example.com
    token:
      exec: secret-tool lookup service telescope account staging

  # Loki: no query of its own, the filter selects the stream.
  - name: prod api
    type: loki
    url: http://127.0.0.1:3100
    query: app=api

An endpoint that needs no credentials needs no declaration either: ctrl+e takes a URL, and the ones typed there are remembered. Anything needing a token belongs in the config file, since the prompt writes what it is given to the history in plain text.

tail becomes the query's limit, the time range its bounds, and follow keeps it open. VictoriaLogs takes LogsQL in target, sent as written; it has a match-all, so an empty one tails the whole database.

LogQL has no match-all — every query selects streams by label — so a Loki place names no query at all and reads nothing until the filter names a label. app=api pod!=api-7 error is sent as {app=~"(?i)api", pod!~"(?i)api-7"}, and error is applied here. Label names that are not Prometheus identifiers are sent quoted, so service.name=api works where the server understands it.

proxy is per place, so one database behind a corporate proxy does not push every other request through it. Unset, the environment applies (HTTPS_PROXY, ALL_PROXY, NO_PROXY). It is also how a database reachable only from a bastion is reached — ssh -D 1080 bastion and proxy: socks5h://127.0.0.1:1080.

The token is named, never written, so the config file stays shareable: env: an environment variable, file: a file with ~ accepted, or exec: a command whose first line of output is the token.

exec takes a command line, run through sh -c so a pipe works, or a list of arguments, which needs no quoting:

    token:
      exec: pass show grafana/prod | head -1
    token:
      exec: ["bw", "get", "password", "grafana-prod"]

That covers a keyring, pass, Bitwarden, 1Password — anything with a CLI. It runs once per run, before the screen is taken over, so a manager that needs a passphrase can still ask.

History

Hosts, kubeconfigs and targets you open are written to $XDG_STATE_HOME/telescope/history.yaml, by default ~/.local/state/telescope/history.yaml, twenty of each, and offered first next time. Targets are remembered per cluster and per host: a pod name means nothing on another kubeconfig. The config file you write is never rewritten.

Notes

ssh runs through ssh(1), so ~/.ssh/config, ProxyJump, the agent and known_hosts all apply. It runs with BatchMode=yes, so an unknown host key or a passphrase without an agent fails with a message instead of hanging.

sudo is sudo -n and needs NOPASSWD. It prefixes the collector directly, so a sudoers rule may name the tool itself:

you ALL=(ALL) NOPASSWD: /usr/bin/kubectl

The kubeconfig is passed as --kubeconfig= rather than through the environment so that such a rule keeps working. A free-form command still needs a shell.

journalctl is run with -o cat, which drops the journal's own timestamps. That suits services logging structured lines and loses time information for plain ones.

Listing user units over ssh needs a session bus, so it works when the account has an active session or lingering enabled. When it fails, system units still complete and user/name can be typed by hand.

A listing is given five seconds, and up to 200 000 lines are kept per stream; older ones are dropped and counted in the top bar.

Directories

Path Synopsis
cmd
telescope command
Command telescope is a terminal log viewer.
Command telescope is a terminal log viewer.
demo
tracestub command
Command tracestub serves made-up traces over both APIs telescope reads, so the trace search can be driven without a Tempo or a VictoriaTraces to hand.
Command tracestub serves made-up traces over both APIs telescope reads, so the trace search can be driven without a Tempo or a VictoriaTraces to hand.
internal
complete
Package complete suggests values for the source prompt.
Package complete suggests values for the source prompt.
config
Package config reads the places logs are read from and the groups they are read as, and remembers the ones the user reaches for.
Package config reads the places logs are read from and the groups they are read as, and remembers the ones the user reaches for.
logs
Package logs stores, parses and renders log lines.
Package logs stores, parses and renders log lines.
query
Package query is the filter typed at the log view.
Package query is the filter typed at the log view.
source
Package source builds and runs the commands that produce log lines.
Package source builds and runs the commands that produce log lines.
trace
Package trace holds a trace as it is read: spans, the tree they form, and the window of time a view of them covers.
Package trace holds a trace as it is read: spans, the tree they form, and the window of time a view of them covers.
ui
Package ui implements the telescope terminal interface.
Package ui implements the telescope terminal interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL