Logs, metrics and traces
Every project's logs and metrics are collected from the moment it runs, with nothing to install, and kept apart from every other project. Traces and your own metrics take one OpenTelemetry SDK, which finds the platform by itself.
You read them in the editor (the Logs and
Metrics buttons of each environment), the AI assistant reads them while it
works on your project, and your own agent reads them over MCP with
the get_logs and query_metrics tools. Anyone in the project's
organisation can read them; a query only ever sees its own project.
Logs
Everything every container writes to stdout and stderr, kept for seven days, including
containers that have since restarted and an environment that is asleep. A log file
inside a container is not read, so log to stdout. One line per event; a JSON line with a
level field, or a level word such as ERROR, gives the line its level.
A line longer than 64 KiB is cut, and says so.
When a container's lines need parsing first, such as a stack trace over several lines, the pod template carries OpenTelemetry's logs annotation with parsers:
metadata:
annotations:
io.opentelemetry.discovery.logs.app/config: |
operators:
- type: recombine
combine_field: body
is_first_entry: body matches "^\\d{4}-\\d{2}-\\d{2}"
Only parsers and transformers are allowed. A configuration that is refused gives way to
the default parsing, and a warning line in that container's own logs says why.
io.opentelemetry.discovery.logs.<container>/enabled: "false" stops
collecting a container. A project that logs far more than its share is read more slowly
rather than dropped; if a container's log files rotate away before they are read, a
warning line says which time range was lost.
Metrics
Without code
Every container's CPU, memory, network and file system, its restarts and readiness, why
it is waiting (CrashLoopBackOff, ImagePullBackOff) and why it last ended (OOMKilled), and
each pod's phase, every 30 seconds, kept for fifteen days. Every series is labelled with
the environment, namespace, workload, pod and container, the names in your
k8s/ files.
Requests
Every request that reaches one of your HTTPRoutes through the platform's gateway is
counted, with no code: edge_requests by route and
code_class (2xx to 5xx), and
edge_request_duration, the seconds until your app answered. The editor's
Metrics panel shows each route's requests per second, its server errors and its 95th
percentile response time.
Your own metrics
A container that serves Prometheus's text format is scraped at its own address once its pod template opts the port in:
metadata:
annotations:
io.opentelemetry.discovery.metrics.8080/enabled: "true"
io.opentelemetry.discovery.metrics.8080/config: |
metrics_path: /metrics
collection_interval: 30s
Only the path and the interval (10 seconds at least) can be set; the endpoint needs no
credentials. Each opted-in port reports an up series, 1 or 0, with
scrape_error saying why when it is 0. Or push your metrics with an
OpenTelemetry SDK, as below. A project keeps up to 10,000 series.
Names starting with edge, k8s or container
are the platform's, and so are labels starting with k8s or
basable: a metric of your own with such a name is not kept, and such a
label is removed from it.
Traces, and pushing anything
Every container gets these variables, unless it sets
OTEL_EXPORTER_OTLP_ENDPOINT in its own env:
BASABLE_NODE_IP=<its node's address>
OTEL_EXPORTER_OTLP_ENDPOINT=http://$(BASABLE_NODE_IP):4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
Every request through the platform's gateway starts a trace: a span of the
edge service with the method, the address without its query string, the
status, the user agent and the route. It carries no visitor's IP address. The gateway
passes the trace on to your app in the traceparent header, so an app with an
SDK continues it and the trace shows the request from the gateway to your database.
Any OpenTelemetry SDK reads them, so traces, metrics and logs it sends reach your
project with no address of your own. Basable's Rust framework (basable-app 0.8.0 and
later) starts the SDK by itself and sends its spans. Traces are kept for seven days and
open by their trace id. Inside your environment's cluster the variables show in
kubectl exec <pod> -- env, not in the pod's own spec.
What a container sends is always filed under the project and pod that sent it, whatever the data says about itself.
Grafana
Every project's logs, metrics and traces are also in Grafana, at grafana.basable.com. You sign in with your Basable account; nothing to set up. Each of your organisations is an organisation in Grafana, with its projects in it and nobody else's.
Its Basable folder holds a project's overview (CPU and memory per
workload, restarts, volumes, and each route's requests, server errors and response time),
its logs and its traces; pick the project and the environment at the top. Save
dashboards of your own beside them: only your organisation's members see them. In a
query of your own, a project is picked by its id, basable_tenant_id (in
traces resource.basable.tenant_id), which the project page's address shows.
The Grafana button in the editor, and Open in Grafana in its metrics window, open the project's overview there with the project already chosen.
Grafana reads only what your Basable account may read, decided again by Basable on every query: leave an organisation and its projects disappear from Grafana within a minute. Grafana gets no email address of yours. Disconnect it under Account Settings like any other app.
Basable staff have no standing access to your projects' logs, metrics and traces. When they need to look, they open a read grant on one project, with a reason, for an hour. Every grant on your organisation's projects is listed on its Staff access page with the reason, and the project's page shows a notice while one is open.
When something goes wrong
Basable watches every live environment and mails your organisation's contact address when a container keeps crashing, when one runs out of memory and is restarted, when more than half of the requests to a route end in a server error, or when a volume is more than 90% full. One mail per environment lists what it found, at most once a day for each problem while it lasts, with a link to the project.
Mail is off until you turn it on, per environment, in .basable/config.yaml:
alerts: {email: {enabled: true}} under the environment. The same block
takes your own rules: PromQL over the project's metrics, each with a name, read in that
environment alone, alerting for every series the query returns, so the query carries its
own threshold (jobs_waiting > 1000). A rule the metrics store cannot
parse fails the deploy, naming it. Your own rules work on preview too.
Alerts can also go to your app: add webhook: {path: /basable/alerts} to the
environment's alerts. Basable then POSTs each alert there, on the
environment's own address on Basable and never another host, signed the Standard
Webhooks way with an Ed25519 key (webhook-signature: v1a,...). The public
keys are at basable.com/api/alerts/webhook-keys.
Check the signature, check that the body's project_id,
environment and target are your own, and drop a
webhook-id you have already seen: a delivery that got no answer is tried
again. Your app forwards it wherever you like, to a chat or a pager. Route the path to a
small service of its own, so an alert still arrives while the app itself is down.