Documentation
Everything you need to go from zero to searchable logs. Ten minutes, tops.
Getting started
From zero to searchable logs in four steps:
- Create your free account and sign in (details below).
- Create an ingestion API key under Settings → API keys.
- Add a ZipLogger package to your app, or point your OTel exporter at it.
- Open Search and watch your logs arrive live.
Your workspace
ZipLogger is a hosted service, so there is no backend to install, run, patch, or scale. Sign up,
and your workspace is ready in seconds at app.ziplogger.ai. Your applications keep
running wherever they already run (any cloud, container platform, or your own network) and simply
send their telemetry to ZipLogger over HTTPS. Two optional features run a small component on
your side: Kubernetes monitoring (a read-only Helm release per cluster)
and private browser-automation agents.
Once you are in, invite your team under Team, create an ingestion key under Settings → API keys, and pick a plan under Billing (the Free plan is active by default and needs no credit card; a card is asked for only when you start a paid plan's trial or upgrade, and the Free plan is never charged).
.NET integration
Two NuGet packages cover the standard .NET logging abstractions. Both share the same transport: a bounded in-memory queue, NDJSON batching, retry with exponential backoff (429-aware), and drop-on-backpressure: a logging call never blocks or throws in your application.
Both packages also enrich every event automatically with the release (assembly informational
version), commit SHA (SourceLink suffix or GIT_COMMIT), environment, and
machine name. The commit SHA is what powers git regression detection.
Serilog
dotnet add package ZipLogger.Serilog
Log.Logger = new LoggerConfiguration()
.WriteTo.ZipLogger("https://app.ziplogger.ai", "zk_...")
.CreateLogger();
Log.Information("Order {OrderId} created for {Customer}", 83112, "acme");
// → structured fields OrderId + Customer, searchable instantly
Destructured objects ({@Cart}), the message template, and exceptions are preserved as
structured fields; exceptions map to the stack-trace field that feeds regression detection.
Microsoft ILogger
dotnet add package ZipLogger.Extensions.Logging
// Program.cs
builder.Logging.AddZipLogger(options =>
{
options.Endpoint = "https://app.ziplogger.ai";
options.ApiKey = "zk_...";
});
Or configure from appsettings.json under Logging:ZipLogger, including
standard per-category level filtering. Scopes are captured as fields, and
logger.LogError(ex, ...) ships the full exception.
Python
pip install ziplogger
import logging
from ziplogger import ZipLoggerHandler
logging.getLogger().addHandler(ZipLoggerHandler(
endpoint="https://app.ziplogger.ai",
api_key="zk_...",
))
log = logging.getLogger("app.orders")
log.info("Order %s created", 83112, extra={"orderId": 83112})
log.exception("Payment failed") # traceback → stackTrace
Standard library only, no dependencies. extra= values become searchable fields,
the logger name becomes category, and tracebacks feed git regression detection.
Same delivery semantics as the .NET client: bounded queue, batching, 429-aware retries, and a
handler that never blocks or raises.
Node.js
npm install ziplogger
Pino
const pino = require('pino')
const logger = pino(pino.transport({
target: 'ziplogger/pino',
options: { endpoint: 'https://app.ziplogger.ai', apiKey: 'zk_...' },
}))
logger.info({ orderId: 83112 }, 'Order created')
Winston
const { ZipLoggerTransport } = require('ziplogger/winston')
const logger = winston.createLogger({
transports: [new ZipLoggerTransport({ endpoint: 'https://...', apiKey: 'zk_...' })],
})
A zero-dependency core client (ZipLoggerClient) is also exported for custom
setups. Error objects map to stackTrace; timers are unrefed so the
SDK never keeps your process alive.
Go
go get github.com/ziploggerhq/ZipLogger_Client/sdk_go
client, err := ziplogger.New(ziplogger.Options{
Endpoint: "https://app.ziplogger.ai",
APIKey: "zk_...",
})
defer client.Close(5 * time.Second)
logger := slog.New(ziplogger.NewSlogHandler(client, slog.LevelInfo))
logger.Info("order created", "orderId", 83112)
logger.Error("payment failed", "err", err) // → stackTrace
Standard library only. slog attributes become searchable fields (groups are
dot-prefixed); an err attribute maps to the exception fields that feed regression
detection. The direct client.Log(ziplogger.Entry{...}) API is there for
everything else.
Java
<dependency>
<groupId>dev.ziplogger</groupId>
<artifactId>ziplogger</artifactId>
<version>0.1.0</version>
</dependency>
var client = new ZipLoggerClient(new ZipLoggerClient.Options(
"https://app.ziplogger.ai", "zk_..."));
Logger.getLogger("").addHandler(new ZipLoggerJulHandler(client, true));
log.info("order created");
log.log(Level.SEVERE, "payment failed", exception); // → stackTrace
JDK-only (Java 17+), zero dependencies. A java.util.logging handler ships today;
log through the client directly from SLF4J/Logback setups; native appenders are on the
roadmap.
Ruby
Use the official Sentry SDK, sentry-ruby, with a ZipLogger DSN. It sends errors (grouped into
Issues, with five lines of source per frame), tracing, structured logs
(Sentry.logger), cron check-ins and, with the stackprof gem, profiles. Tested end to end
with sentry-ruby 7.1.0 on Ruby 3.4.
Sentry.init do |config|
config.dsn = "https://[email protected]/1"
config.release = "orders@#{ENV["GIT_SHA"]}" # service@commit
config.traces_sample_rate = 0.2
end
Unhandled errors are reported by the SDK’s integrations (Rack, and so Rails and Sinatra; Rake; Sidekiq); a
plain script rescues, calls Sentry.capture_exception and re-raises.
Setup and what arrives →
A ZipLogger gem (ziplogger, logs only) was announced here earlier and is not published:
the Sentry SDK covers logs and everything else. To send plain log lines without an SDK, use the
REST API.
PHP
Use the official Sentry SDK, sentry/sentry (Laravel and Symfony use sentry/sentry-laravel and
sentry/sentry-symfony, which wrap it), with a ZipLogger DSN. It sends errors (grouped into
Issues, with five lines of source per frame), tracing, structured logs
(\Sentry\logger()), cron check-ins and, with the excimer extension, profiles. Tested end to end with
sentry/sentry 4.34.0 on PHP 8.4.
\Sentry\init([
'dsn' => 'https://[email protected]/1',
'release' => 'checkout@' . getenv('GIT_SHA'), // service@commit
'traces_sample_rate' => 0.2,
'enable_logs' => true,
]);
A ZipLogger Composer package (ziplogger/ziplogger, logs only, with a Monolog handler) was
announced here earlier and is not published: the Sentry SDK covers logs and everything else. To send plain log
lines without an SDK, use the REST API.
Browser / React
npm install @ziplogger/browser
import { ZipLoggerBrowser } from '@ziplogger/browser'
import { createErrorBoundary } from '@ziplogger/browser/react'
const zl = new ZipLoggerBrowser({ endpoint: 'https://app.ziplogger.ai', apiKey: 'zk_...', release: '[email protected]' })
zl.captureGlobalErrors() // window.onerror + unhandledrejection
const Boundary = createErrorBoundary(React, zl)
// <Boundary fallback={<p>Something went wrong.</p>}><App /></Boundary>
Every event carries url and userAgent; render errors include the React
component stack; a keepalive flush on pagehide means events survive tab
closes. Use a dedicated API key for browser traffic so it can be revoked independently. Errors with
an exception are grouped into Issues; set release to the
same string you upload source maps under.
Frontend-to-backend tracing
zl.instrumentFetch() // same-origin by default; propagateTo: ['https://api.you.com'] for more
This wraps fetch: every call to your backend gets a W3C traceparent
header (your OpenTelemetry-instrumented server continues the same trace), a browser-side root
span is exported so the waterfall starts in the user's browser, and failed requests are logged
with the trace id. Result: a user hits an error → one click from that log to the full
browser → backend → database waterfall on the Traces page.
Fluent Bit / Vector
For apps you can't modify, ship files and container output with a log shipper; both talk to the plain NDJSON endpoint.
Fluent Bit
[INPUT]
Name tail
Path /var/log/app/*.log
Tag app
# the tail input emits "log"; ZipLogger expects "message"
[FILTER]
Name modify
Match *
Rename log message
[OUTPUT]
Name http
Match *
Host app.ziplogger.ai
Port 443
TLS On
URI /ingest/v1/logs
Format json_lines
Json_date_key timestamp
Json_date_format iso8601
Header X-Api-Key zk_...
Vector
[sources.app]
type = "file"
include = ["/var/log/app/*.log"]
[transforms.shape]
type = "remap"
inputs = ["app"]
source = '''
.severity = "info"
.source = "legacy-app"
'''
[sinks.ziplogger]
type = "http"
inputs = ["shape"]
uri = "https://app.ziplogger.ai/ingest/v1/logs"
encoding.codec = "json"
framing.method = "newline_delimited"
request.headers.X-Api-Key = "zk_..."
.message already;
Fluent Bit's tail input uses log, hence the rename filter. Unknown JSON keys are
ignored by the ingestion endpoint, so extra metadata is harmless.OpenTelemetry
ZipLogger exposes native OTLP/HTTP receivers for all three signals: /v1/logs,
/v1/traces and /v1/metrics. Protobuf and JSON, gzip, brotli or deflate,
partial success per the OTLP spec. Any OTel SDK or Collector can export to them:
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=https://app.ziplogger.ai
OTEL_EXPORTER_OTLP_HEADERS=X-Api-Key=zk_...
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta
Resource attributes map to first-class fields: service.name → source,
service.version → release, host.name, and
exception.stacktrace → the regression engine and Issues.
The ZipLogger environment is decided by the ingest key; deployment.environment is kept
as an attribute only. Trace and span IDs are preserved as searchable hex fields. On Kubernetes,
the ZipLogger chart can run an in-cluster OTLP endpoint for your applications; see
Kubernetes monitoring.
Metrics. Gauges and counters are stored as one value per interval; cumulative counters are
converted to per-interval increases, but delta temporality (the last variable above) is exact
across restarts and recommended. Histograms and summaries become .count,
.sum, .avg, .p95, .p99 and .max
series, so http.server.request.duration is charted as
http.server.request.duration.p95. Labels are the data point’s attributes plus
environment and host.name; other resource attributes are left out so they do not multiply your
series count. Metric points count against the same per-minute, per-day and series limits as
/ingest/v1/metrics. Chart them on the Metrics page and
alert on them with a metric monitor.
Sentry SDKs
ZipLogger accepts traffic from Sentry SDKs as they are, so an app instrumented with Sentry moves by changing its DSN. Create an ingest key under Settings → API keys with Can read data unchecked; the Sentry DSN for it is shown once, under the key.
Sentry.init({
dsn: "https://[email protected]/1",
release: "checkout@3f9a2c1", // service@commit
tracesSampleRate: 0.2,
})
Errors become error log lines, and captured messages become log lines at their own level (info
unless you pass one), with their stack trace, trace id, release and commit. Error events that
carry an exception are also grouped into Issues; a
fingerprint set with scope.setFingerprint is used for that grouping. Transactions and spans become traces. Sentry.logger records become log
lines, and cron check-ins become log lines too, with failed runs at error level; a check-in
whose monitor_slug matches a heartbeat also reports
that heartbeat’s run. Sessions
feed release health (crash-free sessions and users per release).
Profiles (profile and profile_chunk) feed continuous
profiling as wall-time samples, and a transaction’s profile links to its trace. Client reports,
attachments, replay recordings, user feedback and metrics (statsd) are acknowledged but not
stored or billed: changing the DSN does not import Sentry replays, attachments or feedback, and past
events stay in Sentry. Breadcrumbs, allow-listed request headers and the event’s other contexts are shown on the
Issue page (debugging context).
Each official server SDK below was run end to end against ZipLogger on 2026-10-21 (errors and Issues, unhandled errors, breadcrumbs, tags, user and contexts, release, tracing and APM, structured logs, cron check-ins, profiles), and every request it sent is replayed by ZipLogger’s test suite.
| Language | SDK tested | Source lines on Issues | Profiles | Setup |
|---|---|---|---|---|
| Python | sentry-sdk 2.72.0 | Yes | Yes, linked to the trace | Python |
| Node.js | @sentry/node 11.8.0 | Yes | Continuous (flame graph, not linked to a trace) | Node.js |
| Go | sentry-go 0.50.0 | No (the SDK sends none) | No profiler in the SDK | Go |
| Java | sentry 8.61.0 | No (source bundles only) | Continuous (flame graph, not linked to a trace) | Java |
| Ruby | sentry-ruby 7.1.0 | Yes | Yes, linked to the trace | Ruby |
| PHP | sentry/sentry 4.34.0 | Yes | Yes, linked to the trace | PHP |
| .NET | Sentry 6.12.1 | No (the SDK sends none) | Yes, linked to the trace | .NET |
ZipLogger has no project per service, so set release to
<service>@<git sha>: the part before @ becomes the service
and a SHA after it becomes the commit the regression engine blames from. A service
tag overrides the name. Tags are searchable as tag.<name>.
1.
To read minified browser frames, upload your source maps to ZipLogger under the same
release; see Source maps. sentry-cli uploads
are not accepted.
What carries over from Sentry, and what does not →REST API
Ingestion
curl -X POST https://app.ziplogger.ai/ingest/v1/logs \
-H "X-Api-Key: zk_..." \
-H "Content-Type: application/json" \
-d '{"message":"deploy finished","severity":"info","source":"ci"}'
Accepts a single object, a JSON array, or NDJSON (one object per line). Fields:
timestamp, source, severity, message,
release, commitSha, stackTrace, fields{},
tags[]; everything optional except message.
Search
GET /api/v1/logs/search?q=payment+failed&severity=error&from=2026-07-01T00:00:00Z
Authorization: Bearer <jwt>
Additional endpoint groups: /api/v1/logs/histogram, /api/v1/templates
(patterns), /api/v1/dashboards, /api/v1/alerts,
/api/v1/metrics, /api/v1/regressions, and /api/v1/billing.
Quotas
Every plan includes a monthly allowance for each metered resource — log and trace ingest (the uncompressed size of what you send, before indexing), product events, session replays, browser-check minutes, programmatic API requests, downloads, storage and notifications — pooled across the month; the pricing table has the figures. A daily ceiling on each counted resource stops a runaway spike from using the whole month in a day; it is a safety limit and is never charged. Overage is off by default: at an allowance, usage stops. A workspace admin on a paid plan can opt into overage for every resource at once, under a single monthly spend cap; the Free plan has no card on file and never bills.
A refused request answers 429 with a Retry-After header (the next UTC
midnight for the daily ceiling, the start of the next monthly window for an allowance), plus a
JSON body naming the resource and why it was refused. The official SDKs honor it automatically.
GET /api/v1/billing/usage/resources returns every resource's allowance, use,
projection and behaviour for your workspace.
Search & live tail
The Search page queries the logs of every service in the environment you are viewing, with a histogram, the patterns view and the field explorer. Four tools sit on top of it:
- Live tail. In the events view, ▶ Live tail shows new lines as they arrive, with the current query, severity and filters, newest on top (the latest 2,000 stay on screen). It asks for new lines every 3 seconds, so a line that arrives late, with a timestamp older than the newest one on screen, shows up only in a normal search.
- Saved views. Saved views ▾ names the current query, severity, filters, time range and view (patterns or events) and shares it with the workspace. Up to 100 per workspace; Editors and Admins save and delete them.
- Export. Export CSV or NDJSON downloads the lines that match, newest first, up
to 50,000. CSV columns are
timestamp, severity, source, message, release, commitSha, id, fields(fields as JSON), and a cell that starts with=,+,-or@gets a leading apostrophe so a spreadsheet never runs a log line as a formula. An export counts toward your plan’s downloads and exports allowance. API:GET /api/v1/logs/export?format=csv|ndjson&max=with the search parameters. - Surrounding lines. Expand a line and click ☰ Surrounding lines to see the 25 lines
logged just before and after it, from the same source unless you untick only from ….
API:
GET /api/v1/logs/{id}/context?before=25&after=25&sameSource=true(up to 200 each side).
Log pipelines All plans
Log pipelines run on your logs as they arrive. Parse rules pull fields out of the message
text, so a plain line such as GET /api/orders 200 12.5ms becomes searchable
http.method, url.path, http.status and
duration_ms fields. Metric rules turn matching logs into
metric points you can chart and alert on. Scrub rules replace personal
data before anything is stored (below). Open them from
Search → Log processing. Rules belong to the environment
you are viewing; Editors and Admins can change them.
Every rule can be narrowed with Only service (optional) (the log's source) and
Only messages containing (optional) (case-insensitive text). Rules apply to every log path:
SDKs, OpenTelemetry, Sentry SDKs, Fluent Bit and Vector, and the REST API.
Parse rules
Click + Parse rule and pick what to parse:
| Parse | What it extracts |
|---|---|
| Pattern | %{TYPE:field} tokens, regex named groups (?<field>…) and literal regex around them. Field names may contain dots. |
| JSON in the message | The JSON object in the message: all of it, or from its first { to its last }, so order failed {"order": {"id": "A-1"}} works. Nested keys join with dots (order.id), up to 3 levels and 50 fields; values over 1,000 characters are cut. |
| key=value pairs | Pairs anywhere in the message, separated by spaces, commas or semicolons. A value may be "double quoted". Up to 50 fields. |
# Pattern
%{IP:client.ip} %{HTTPMETHOD:http.method} %{URIPATH:url.path} %{INT:http.status} %{NUMBER:duration_ms}ms
# matches
203.0.113.7 GET /api/orders 200 12.5ms
Pattern types: WORD, NOTSPACE, DATA,
GREEDYDATA, INT, NUMBER, IP, UUID,
LOGLEVEL, QUOTEDSTRING, HTTPMETHOD and URIPATH.
INT and NUMBER fields are stored as numbers, the rest as text. In JSON,
numbers stay numbers and true/false are stored as text; in key=value,
plain decimals such as 42 or -1.5 become numbers. Test with a log
line in the editor shows what a rule extracts before you save it.
Parse rules run in Order, and a field the log already has is never overwritten. A field keeps
the type it first had that day: if duration_ms was a number this morning and a log now
sends "abc", the log is still stored, but that value cannot be searched as a field for the
rest of the day. Keep each field to one type.
Metric rules
Click + Metric rule, give it a Metric name (for example
logs.checkout.failures) and choose:
- A count: leave Value field (optional) empty. Each batch of incoming logs adds one point per minute (and per group) holding the number of matching logs, so add them up: chart it with sum, and alert on it with a metric monitor using sum.
- A value: name a numeric field, such as
duration_msfrom a parse rule. Each matching log becomes one point, so avg, max and p95 work. - Group by field (optional) puts the field's value in a label, such as
http.status; a log without the field is grouped as(none).
Metric rules run after parse rules, so they can use the fields those extracted. The points land in the normal metric store: open them on the Metrics page, put them on a dashboard with the Metric widget, or alert on them.
Limits
- New logs only. Logs already stored are not reparsed and do not produce metric points.
- Up to 50 rules per environment. An edit applies everywhere within 30 seconds.
- A pattern stops after 50 ms on one log; a pattern that is too slow extracts nothing rather than slowing ingestion.
- A value rule writes at most 1,000 points per rule per batch of logs. If the metric store cannot keep up, points are dropped and the Log processing page shows how many.
- Parse rules add fields; they do not drop, sample or rename logs. Masking is what scrub rules do.
Scrub personal data
+ Scrub personal data creates a rule that replaces personal data before anything is stored: in logs and errors (HTTP ingest, OpenTelemetry logs, Sentry events), the message, stack trace, user, tags and every field; in spans (OpenTelemetry traces, Sentry transactions), the name, status message and every text attribute; in product events, the name, URL, page and every property; and in metric label values. A rule combines any of:
| Option | What it does |
|---|---|
| Detectors | Email addresses; card numbers (13 to 19 digits, Luhn-checked); IPv4 and IPv6 addresses; phone numbers; US social security numbers; JWTs; and secrets (the value after password=, token:, api_key, Bearer and similar, plus AWS access key ids). |
| Custom pattern | A regular expression, up to 500 characters. |
| Fields | Field or attribute names replaced whole, comma-separated; user.* covers every field starting with user.. |
| Replace with | redact: [REDACTED:email]. hash: [email:3f2a9c0d1b7e], a short hash of the value, so the same value still groups and correlates. A hash is pseudonymous, not anonymous: anyone who can guess a value can confirm it. |
Scrub rules run after every parse rule, so extracted fields are scrubbed too, and before metric rules.
A match is limited to 50 ms; text the scrubber cannot finish in time is stored as
[REDACTED:timeout], never unscrubbed. The editor previews a rule on a sample line.
Not covered: session replays and profiles (replays mask inputs in the browser), and the user, anonymous and session ids of product events, which link a person’s events, so send a pseudonymous id rather than an email. Data already stored is not rewritten. Detectors find what they are shaped to find; they reduce exposure, they do not certify that a log holds no personal data. With redact, errors that differ only in an email or a card number fall into the same Issue.
Session replay All plans
Watch what a user actually did before an error: the pages they saw, where the cursor went, what they clicked, which field they struggled with. ZipLogger records the DOM and rebuilds it — not video — so a session is a few hundred kilobytes and the text in it is real text. Replays attach to the session id your product events carry, so a replay is one click from the session journey, and from an error whose record carries that session id.
npm install @ziplogger/browser @rrweb/record
import { attachSessionReplay } from '@ziplogger/browser/replay'
const zl = new ZipLoggerBrowser({
endpoint: 'https://app.ziplogger.ai', apiKey: 'zk_...',
sessionReplay: { enabled: true, sampleRate: 0.1 }, // 10% of sessions
})
attachSessionReplay(zl)
Which sessions are recorded
- Off until you turn it on. Nothing is recorded unless the page sets
sessionReplay.enabledand callsattachSessionReplay. A workspace admin can also switch recording off for every app, or change the sample rate, without a redeploy. - Sampling.
sampleRateis the share of visits recorded, from 0 to 1 (1 if you leave it out). The decision is made once per visit and kept for that visit, so a visit outside the sample has no replay at all. There is no "record only when an error happens" mode. - Consent. The replay recorder does not ask for consent itself. If your site needs it, leave
enabledoff, callzl.sessionReplay.start()once your consent manager allows it, andzl.sessionReplay.stop({ discard: true })if the visitor withdraws it: buffered data is dropped instead of uploaded. - Allowances. All plans (Free: 100 sessions and 50 MB a month). Each plan includes a monthly number of recorded sessions and amount of replay data, with a daily ceiling; the pricing table has every figure. When one runs out, new visits are not recorded (visits already recording finish) until the next month, unless a paid workspace opted into overage. On Free it is a hard stop.
What is never recorded
Passwords, one-time codes and payment fields are always masked, whatever the
configuration says. Every other input is masked by default, so a replay shows that someone
typed sixteen characters into the card field without ever showing the number. Add
data-ziplogger-mask to mask an element's text, or
data-ziplogger-ignore to leave a whole area out of the recording. Cookies,
local storage, headers, tokens and URL query strings are never captured at all.
All of it happens in the visitor's browser, inside the serialiser, before anything is sent — the masked values never exist in the uploaded data, so there is nothing on our side to delete. A workspace admin can also force full-text masking for every app, set the sample rate centrally, or switch recording off entirely without your redeploying anything.
What it costs the page
Measured, not estimated: +2.8 KB gzipped in your bundle, 0.019 ms of extra main-thread time per DOM mutation, no long tasks, and about 9 MB an hour of gzipped upload for a continuously busy recorded visitor. The recorder itself (22.7 KB gzipped) is downloaded only by visitors the sampling decision actually records, and a page that does not enable replay ships exactly the bytes it shipped before.
Watching one
Play, pause, seek, 0.5×–8× speed and skip-idle, with markers under the timeline for the errors, failed requests and events ZipLogger recorded in the same session — click one to jump to that moment. Long sessions load in parts as you watch. Recordings are kept for 30 days by default, on a retention window separate from logs and traces that an admin can set up to 90 days, and admins can delete any one of them outright.
Every replay is rendered in a sandboxed frame with scripting disabled, and scripts, event handlers and unsafe URLs are stripped from the recorded DOM before it is played: a recording is treated as untrusted data, never as a page.
Not the same thing as a browser automation. Session replay is passive: it records real visitors as they use your app, on whatever sample rate you set, and only after something worth watching has already happened. A browser automation is active: it drives a real browser through a journey you recorded, on a schedule, whether or not a real visitor shows up — which is what catches a broken sign-in button at 3am with zero traffic. Use replay to understand what a real user did; use automations to find out before one does.
Security at a glance
Which control protects what, and where each one is explained. Use it to answer a security questionnaire, then follow the links for the detail.
| Area | What ZipLogger does | Details |
|---|---|---|
| Signing in | Email and password, with two-step sign-in (an authenticator app) per person, which an Admin can require for every password member. | Two-step sign-in |
| Enterprise single sign-on | A separate Enterprise feature: the workspace signs in through its own OpenID Connect provider (Microsoft Entra ID supported; Google Workspace and other providers on request) and can refuse passwords. The provider applies its own second factor. SAML and SCIM are not available. | Single sign-on |
| Sending data | Ingest-only API keys, safe in browser code: they can send telemetry and never read it back. Each key belongs to one environment. | API keys |
| Reading data from tools | Separate keys with Can read data for MCP and Grafana, and personal access tokens (read-only, or read and write) that act as their owner, with an expiry and an audit trail. | Personal access tokens |
| Separation | Every query is scoped to your workspace; environments keep Production, Staging and others apart, decided by the key, not by the request. | Environments |
| Personal data | Optional scrub rules replace emails, card numbers, IP addresses, phone numbers, tokens and your own patterns before anything is stored. Session replay masks inputs in the browser. Scrubbing reduces exposure; it does not certify that a record holds no personal data. | Scrub personal data, Session replay |
| Retention and deletion | Each plan sets the longest retention; a workspace can choose shorter windows per signal and a storage cap that removes the oldest days first. Editors and Admins can delete matching logs, and recordings can be deleted one by one. | Pricing, Privacy policy |
| Secrets you give us | Git tokens, AI keys and integration secrets are encrypted (AES-GCM) and never shown again; API keys and tokens are stored only as hashes. | Configuration |
| Audit | Configuration changes, sign-in policy changes and token creation are recorded in the workspace audit log (Team and above). | Pricing |
ZipLogger holds no security certification yet, and the terms do not promise an uptime level.
Authentication
Two credential types, used for different things:
| Credential | Used for | How |
|---|---|---|
API key (zk_…) | Ingestion (logs, metrics, OTLP, Sentry SDKs), and with the matching permission: reading data, uploading source maps, reporting Kubernetes cluster state | X-Api-Key header |
| JWT | Everything else (search, dashboards, billing…), in the app | Authorization: Bearer |
Personal access token (zlp_…) | Scripts and CI calling the same management API as a person (details) | Authorization: Bearer |
Sign in via POST /api/v1/auth/login to receive a short-lived access token and a
rotating refresh token; refresh with POST /api/v1/auth/refresh. Roles (Admin, Editor,
Viewer) gate destructive and administrative operations.
If your workspace requires single sign-on, password sign-in with a correct
password answers 403 with the code sso_required; a wrong password still
answers 401. API keys are not affected.
API keys
Create keys under Settings → API keys (or POST /api/v1/admin/api-keys). The
full key is shown exactly once at creation; only a SHA-256 hash is stored. Each key belongs to one
tenant and one environment (it decides where ingested data lands and what a read key can
read), counts toward your plan's key limit, and can be revoked instantly. Rotate by creating a
new key, deploying it, then revoking the old one: zero downtime.
A key’s permissions are chosen when it is made. A plain key only sends telemetry and is safe in a browser. Can read data lets it read logs and traces (MCP, Grafana). Upload source maps makes a key for CI that uploads JavaScript source maps and sends no telemetry. A Kubernetes collector key is made on the Kubernetes Install page. Keep read, source-map and collector keys secret: never put them in code a browser loads.
Two-step sign-in
Anyone who signs in with a password can require a code from an authenticator app as well. Open your Profile, find Two-step sign-in and click Set up two-step sign-in: scan the QR code (or type the key) into any authenticator app, enter the 6-digit code it shows, and save the ten recovery codes. From then on a password sign-in asks for a code, and a recovery code works in its place, once.
- New recovery codes (needs a current code) replaces all the old ones.
- Turn off needs your password and a code, so a session left open is not enough to remove it.
- Lost the phone and the recovery codes? A workspace Admin can reset it for you; you then sign in with your password and set it up again. Turning it on or off and resets are in the audit log.
- Accounts that sign in through single sign-on are protected by the identity provider and are not affected. API keys and personal access tokens are separate credentials.
- Require it for everyone. An Admin can turn on Settings → Security → Two-step
sign-in → Require two-step sign-in for every member. A password member without a second factor
can then sign in only to set one up (other requests answer
403mfa_enrollment_required, and their personal access tokens are held the same way). It does not apply to single sign-on sessions or API keys. The Admin turning it on must use it themselves.
For API clients: POST /api/v1/auth/login with the right password answers 401
with { "code": "mfa_required", "mfaToken": "…" }, and no session. Send
POST /api/v1/auth/mfa/verify with { "mfaToken", "code" } within 5 minutes to get
the session. A code is accepted once; a wrong one answers 401 mfa_invalid.
Personal access tokens & the OpenAPI document
Scripts and CI call the management API (/api/v1/…) with a personal access token. Create
one under Profile → API tokens; it is shown once, and only a hash is stored.
curl -H "Authorization: Bearer zlp_..." https://app.ziplogger.ai/api/v1/uptime/monitors
- It acts as you, with your current role, in your workspace. Change your role or leave the workspace and the token changes or stops with you.
- Read only tokens can only read (anything else answers
403token_read_only); Read and write tokens can do what you can. A Viewer can only create read tokens. - A token can never create tokens, change two-step sign-in, change your password or profile, or start
a browser session (
403token_not_allowed). - Expiry is 1 to 366 days, or never. Revoking is immediate on the server that handled it and reaches every server within 30 seconds. At most 20 live tokens per person; creating and revoking them is in the audit log.
- If your workspace requires single sign-on, a token created from an SSO session keeps working; one created from a password session is refused, like the password session.
Sending telemetry is different: SDKs, OpenTelemetry and Sentry use API keys, which belong to the workspace and an environment, not to a person.
OpenAPI. GET https://app.ziplogger.ai/ (no sign-in needed) is the
OpenAPI 3 description of every /api/v1/ route, generated from the running code so it
cannot drift from it. Load it into Postman, Insomnia or an OpenAPI client generator. To read it, open the
API reference: every endpoint grouped by area, searchable,
with its parameters, request body and a curl example.
Environments
A workspace can hold several environments, such as Production, Staging and Development. Each one has its own logs, traces, metrics, events and replays, its own alert rules, incidents and monitors, and its own API keys. You sign in once and switch between them with the selector at the top of the sidebar. Data never crosses from one environment to another.
| Each environment has its own | Shared by the whole workspace |
|---|---|
| Logs, traces, metrics, events, session replays | Members, roles, single sign-on, branding |
| Ingest and read API keys | Plan, billing and usage |
| Alert rules, incidents, heartbeats, browser monitors | On-call teams, schedules, escalation and notification policies |
| Inbound alert sources, log pipeline rules, SLOs | Slack, Teams and other outbound destinations |
| Issues, releases, regressions | Dashboard definitions, retention settings, maintenance windows |
Admins add environments under Settings → Environments: pick a type (Production, Staging,
Development, QA, Sandbox or Custom), a name, and optionally a key. Leave Create an ingest API key for
it ticked to get the environment's first key, shown once. The key (such as staging)
appears in addresses and on every stored record, so it can never be changed or reused.
The API key decides where data goes. Every key belongs to one environment for life, and whatever
arrives with it is stored there. A body field, a header, a query string or the OpenTelemetry
deployment.environment attribute cannot change that. So give each deployment the key of
its environment; no SDK change is needed. The SDKs' own environment field is kept as
ordinary, searchable data.
- In the app, the environment is part of the address (
/env/staging/traces), so a pasted link opens where it was copied. Any environment that is not of type Production shows a band naming it on every page. - From scripts, send
X-ZipLogger-Environment: staging(or?env=staging); without it, requests read the workspace's default environment. A read key (Grafana, MCP) only ever reads its own environment. - Notifications from an environment other than the primary one start with its name, such as
[Staging], and their links open in that environment. - Archive refuses data sent with the environment's keys, pauses its rules and monitors and hides it, but keeps its data until retention removes it. Restore brings it back. The primary environment, which holds everything sent before environments existed, and the default cannot be archived.
Free, Pro and Team include one environment, Business three, and Enterprise has no limit. Usage from every environment adds up on one bill. Every member can use every environment: there are no per-environment permissions yet. A new environment starts empty; nothing is copied from another. Walkthrough: separate staging and production logs →
Single sign-on Enterprise
A workspace can connect to its organization’s own OpenID Connect identity provider and then require it, so members sign in with their work account and password sign-in is refused. Microsoft Entra ID is supported. Google Workspace and other OpenID Connect providers are available on request. SAML and SCIM are not available. Enterprise SSO is available on Enterprise plans: contact us to have it enabled for your workspace.
- Where. Single sign-on belongs to the workspace and happens at its own address,
https://<workspace>.ziplogger.ai. An Admin configures it under Settings → Security. - Enabled versus enforced. Test the connection, turn the sign-in button on (the sign-in page then offers the provider’s button next to the password form), and optionally require single sign-on for everyone, which refuses password sign-in for everyone in the workspace. In Settings these appear as Sign-in button (enabled) and Required for everyone (enforced).
- Recovery. Requiring single sign-on shows eight one-time recovery codes, once. With an
Admin’s email and an unused code at
/sso/recover, enforcement can be turned off. - API keys (ingestion, read keys, Grafana) and webhooks are not affected by enforcement. AI assistants connected to the MCP server through OAuth are disconnected when you enforce; connect them with a read API key instead.
- ZipLogger staff support access is not subject to enforcement. It is a separate, time-limited, audited grant given only to ZipLogger staff, not a sign-in by a member, and it is recorded in our platform audit log.
- Sessions are not tied to the provider after sign-in: disabling someone in your provider does not end an existing ZipLogger session, so also unlink them and remove them under Team. Access already issued can still last up to about 30 minutes (up to about 60 for an AI-assistant connection), and API keys belong to the workspace, so unlinking and removing do not revoke them: revoke any key the person created or knew.
Read the Enterprise SSO overview, or follow the step-by-step Microsoft Entra ID setup guide.
Branding & white label Team and above
A workspace can carry your own, or your client's, name, logo and colours. The look applies on the
workspace's own address, https://<workspace>.ziplogger.ai, to everyone, on the
sign-in page and inside the app. app.ziplogger.ai always shows the ZipLogger look. An Admin
sets it under Settings → Branding, with a live preview in light and dark.
| Setting | Rules |
|---|---|
| Display name | Up to 80 characters; blank uses the workspace name. |
| Logo, dark-mode logo | PNG, JPEG or WebP, up to 1 MB and 4096 px a side. |
| Favicon | PNG or ICO, up to 256 KB and 512 px a side. |
| Primary, accent and navigation colours | Six-digit hex. Text colours are adjusted for a 4.5:1 contrast ratio. |
| Default theme | Follow the device, Light or Dark. A person's own choice always wins. |
SVG and GIF images are refused, and there is no custom CSS, HTML, script or font. Images are public on the workspace address, because the sign-in page shows them before anyone signs in. Every change is in the workspace audit log.
Custom branding comes with Team and above. White label (Business and Enterprise) adds the
option to hide Powered by ZipLogger; with it hidden, the sign-in page also drops ZipLogger's
product tour, site links and "Other workspaces" link. Some places still say ZipLogger: the
ziplogger.ai address (custom domains are not available), invitation and password-reset
emails, buttons in Slack and Teams messages, the SDK package names, and the Community,
Billing and Platform status links in the sidebar. Moving to a plan without branding keeps the settings
but shows the ZipLogger look until you move back.
White label for agencies and MSPs →
Configuration
Day-to-day configuration happens in the app, per workspace:
| Setting | Where |
|---|---|
| Ingestion API keys (create / rotate / revoke) | Settings → API keys |
| Team members and roles | Team |
| Plan, usage, invoices, payment method | Billing |
| Alert rules, channels (email / webhook / SMS / call / script) | Alerts |
| Twilio account for SMS & voice alerts | Settings → Alert notifications |
| Connected GitHub repositories for regression detection | Settings → Repositories |
| Your AI provider key (Claude / OpenAI / Gemini) | Settings → AI analysis |
| JavaScript source maps (upload / delete) | Issues → Source maps |
| Kubernetes collector key, clusters, problem monitors | Kubernetes → Install, Kubernetes → Clusters, Kubernetes → Monitors |
| Per-service health-check URLs | Dashboards → Services → ⚙ |
| Uptime monitors and public status pages | Alerts → Detection → Uptime, Alerts → Detection → Status pages |
| Heartbeats for scheduled jobs | Alerts → Detection → Heartbeats |
| Saved search views | Search → Saved views ▾ |
| Scrub rules for personal data | Search → Log processing |
| Your own two-step sign-in and personal access tokens | Profile |
| Environments and their first ingest key | Settings → Environments |
| Logo, colours, white label | Settings → Branding |
| Single sign-on | Settings → Security |
| Log parse and metric rules | Search → Log processing |
| Service-level objectives | APM → SLOs |
| On-call teams, schedules, escalation and notification policies, maintenance windows | Alerts → On-call, Alerts → Delivery |
Infrastructure (TLS, backups, capacity, upgrades) is our job, not yours: there are no server-level settings to manage.
Custom dashboards All plans
One dashboard for the whole picture. A ZipLogger dashboard is not a logs dashboard with charts bolted on: every part of the product supplies widgets to the same board, so a single screen can hold your error rate, your checkout funnel, active alerts, service health, the last deployment and what AI makes of it all.
Build one under Dashboards → + New, then + Widget. The catalog is grouped by where the data comes from:
- Events — event trend (grouped by service, country, device or version), event and user counts, sessions, top events, funnels, users by country/device/browser, numeric property totals, and event → errors: how often the requests carrying an event fail.
- Errors & logs — error count, error rate, error trend, top error patterns, log volume, and breakdowns by severity or service.
- Alerts — what is firing right now, and recent firings across every rule.
- Services & health — service status with health-check results, p95 response time, request volume, trace error rate.
- Metrics — the Metric widget charts any metric you send (OpenTelemetry, log pipeline or request metrics) with an aggregation, label filters and a group-by. Add to dashboard… on the Metrics page creates one from the chart on screen; the dashboard's service filter becomes its default service.
- Deployments & Git — recent releases, suspected regressions with their suspect commit, and release impact: error rate before and after the latest deploy.
- AI — a written production summary, using your own AI key.
- Text: Text (Markdown) for notes, runbook steps and links (details below).
The breakdown widgets (Logs by severity, Logs by service, Events by country, Events by device, Events by browser) offer bars, table or pie under Show as. A pie shows eight slices and folds the rest into “other”. Log activity heatmap and Event activity heatmap count by weekday and hour of day, in the viewer’s time zone.
Widgets can be dragged to reorder, resized between one and four columns wide, duplicated and configured. The layout is saved on the server, not in your browser, so it is the same on every machine you sign in from.
A time range and filters at the top apply to every widget that understands the dimension — environment, service, country, version, or one specific end user. A widget can narrow further in its own settings, and where both set the same dimension the widget wins. Useful filter combinations can be saved by name and reused across dashboards.
Dashboards are shared with your team by default or can be made private, which is enforced on the server: a private dashboard is not reachable by another member even with its URL. Each dashboard has a stable link you can paste into an incident channel, and you can keep as many as you like — Production overview, Checkout, API health, Customer support.
Template variables
A variable is a dropdown above the grid. Add them under the dashboard’s ⋯ menu → Variables…, up to 12 per dashboard. Each has a Name, a Label, where its values come from, an optional Default and an optional All choice (Offer “All”):
| Values from | The choices are |
|---|---|
| Fixed list | The options you type, comma-separated. |
| Free text | Whatever the viewer types. |
| Services | Services seen in your telemetry. |
| Environments | The environment field of events and logs in the range. |
| Metric label values | The values of one label of one metric, e.g. host.name of system.cpu.utilization. |
| Log field values | The most common values of a log field, such as source, severity or fields.region. |
| Event property values | The most common values of an event property. |
Write $name or ${name} in any widget setting: a search query, a service filter, a
metric label filter, a title or a text widget. It is replaced with the value picked before the widget runs
its query.
# Query (optional) on a Log count widget
source:$service AND severity:error
# Label filters on a Metric widget
host.name:$host
# Widget title
Errors in $service
With All picked: a setting that is only the variable (a service filter of $service) is
dropped, so it means no filter; inside a search query the variable becomes *; in a metric label
filter the pair is dropped; in titles and text it reads “All”. A $ that names no
variable is left as written, so costs $5 survives. The value picked is kept in the URL
(?var-service=checkout-api), so a link opens the same view. Picking a value does not change
the saved dashboard; the default does.
Text (Markdown)
The Text (Markdown) widget, under the Text category, holds headings, lists, bold, italic,
code, links and code blocks. HTML is shown as text, and only http(s) and in-app links become
links. Variables are filled in, so “Owner of **$service**” follows the dropdown.
Export and import
⋯ → Export JSON downloads the dashboard as <name>-<date>.dashboard.json:
its widgets, layout, time range, filters, variables and visibility, without ids or owner. Import, next
to + New, creates a new dashboard from such a file in the workspace you are in. The file carries
"schema": "ziplogger.dashboard/v1", and a file with another schema is refused. Over the API:
GET /api/v1/dashboards/{id}/export for anyone who can see the dashboard, and
POST /api/v1/dashboards/import for Editors. An import counts against your plan’s
dashboard limit like any new dashboard.
Alerts & notifications
An alert rule counts matching events over a sliding window and fires on a condition: count > threshold, count < threshold, no data (nothing matched at all, a service went quiet), service down (a health check is failing, see below), or API latency increase, or API traffic anomalies. The latency condition is powered by traces: it fires when an operation's p95 latency rises a chosen percentage above its own 24-hour baseline (for example +50%), per service or per route. Traffic rules catch retry storms, runaway loops, and abuse: an absolute cap ("more than 500 requests to /api/orders in 1 minute") or a relative one ("3x normal traffic"), optionally narrowed by service, endpoint, or any span attribute (method, path, status code, user agent), each with a triage severity. Alerts fire once when the threshold is crossed and once on recovery, so there are no notification storms while it stays exceeded. Log-count rules can filter by severity and by the same query syntax as Search.
Three conditions come from error tracking: new issue, issue regressed and issue occurrences > threshold (details). Two come from APM: APM: P95 latency above N ms and APM: error rate above N %, which watch a service or one endpoint against a fixed threshold and need two evaluations in a row to fire and to resolve (details). SLO error budget burn rate (APM SLOs) watches a service-level objective, and crash-free sessions below N % (release health) watches release health. Metric above / below threshold and the three anomaly conditions watch any metric (below), and event count > / < threshold (analytics) counts a product event.
What "no data" is, and isn't. It fires when a query matches nothing at all inside the window — a reasonable stand-in for "this service went quiet." It is not job monitoring: it has no idea what a "run" is, so it cannot tell you a specific execution started, how long it took, or that it failed while still logging normally. A backup job that hangs forever without logging anything will eventually trip a no-data rule once the window passes; a backup job that fails loudly and logs an error will not, unless you also alert on that error. There is no cron calendar behind it and no per-run history. For a scheduled job specifically, see Heartbeats below.
Metric monitors
A metric monitor alerts on any metric ZipLogger stores: OpenTelemetry metrics, a log pipeline metric, or request metrics. Create one under Alerts → Rules → + New alert with metric above threshold or metric below threshold:
| Field | Meaning |
|---|---|
| Metric | The name exactly as reported, as listed on the Metrics page, e.g. system.cpu.utilization. |
| Aggregation | How the points in the window become one number: avg, max, min, sum, count or p95. |
| Above / Below, Window (min) | The threshold, in the metric's own unit (90% CPU is 0.9), over the last N minutes. |
| Group by (optional) | A text label such as host.name. Each value is watched on its own and opens and resolves its own incident, titled rule: value. Up to 25 groups are open at once, worst first. |
| Service (optional), Label filter (optional) | Only points from one service, and only points whose label matches one key=value. |
No data is not zero. With any aggregation except count, a window with no points leaves the rule as it was: silence never fires or resolves it. Count of no points is 0, so an ungrouped count below 1 means "stopped reporting". In a grouped rule, a group that stops reporting while others still report has its incident closed.
# CPU per host, from the OpenTelemetry Collector's hostmetrics receiver
Metric system.cpu.utilization
Aggregation max
Label filter state=user
Group by host.name
Above 0.9 Window (min) 5
Anomaly monitors
metric unusually high (anomaly), metric unusually low (anomaly) and metric unusual either way (anomaly) fire when a metric is unusual for this time of day, instead of crossing a fixed line. They take the same metric, aggregation, service and label filter (no group by), a Window (min) of 5 to 360, and:
- Normal is: same time on previous days (the same window on each of the last 7 days, needs 5 with data) or same time and weekday on previous weeks (the last 4 weeks, needs 3, and enough raw metric retention). Use weekly when weekends or Mondays look different. Normal is the median of those windows, so one past incident does not move it.
- Sensitivity: how far from normal counts as unusual, in robust standard deviations. 3 is a good start; higher fires less.
Without enough history, or with no points in the window, the rule keeps its state. A metric that writes no points when nothing happens (a log pipeline count with no matching logs, for example) is then "no data", not "unusually low", so add a count below 1 metric monitor to catch it stopping.
When a rule fires (and when it recovers) it notifies every channel you configured:
| Channel | Notes |
|---|---|
| One or more comma-separated addresses. Works out of the box. | |
| Webhook | HTTP POST with a Slack-compatible body ({"text": "…"}): Slack, Discord, Mattermost, or your own endpoint. |
| SMS / voice call | Works with no setup through ZipLogger's built-in sender, or with your own Twilio account (connect the Account SID, Auth Token, and a From number under Settings → Alert notifications; verified on save, token encrypted at rest). Calls ring only when the alert fires and read it aloud. Message text is capped at 150 characters. |
| Custom script | Sandboxed JavaScript run on every state change: page PagerDuty, open a ticket, call any API. The script sees alert, secrets.NAME (encrypted, never displayed again), http.post/get to public URLs, and log(); a built-in test runner dry-runs it against a fake alert. Limits: 5s, five HTTP requests. |
A custom message text on the rule (runbook link, who to wake) is included in every channel.
What notifications cost
Phone and email alerts work immediately, with a monthly allowance of emails, SMS and voice calls per workspace (Free includes emails; the pricing table has each plan's figures). Past the allowance, a send is billed at the plan's per-message rate if the workspace opted into overage, or drawn from prepaid credits at the same rate. Top up in $5, $10, or $25 packs under Settings → Alert notifications, where you can also see the month's usage and your balance. A send that fails is not charged. Webhooks and custom scripts are not metered.
Connect your own Twilio account and none of this applies: SMS, calls, and emails all become unmetered, billed by Twilio at your own rates, with no ZipLogger notification charges. If the allowance runs out and no credits remain, the alert is still recorded in ZipLogger and its other channels still fire; only the metered send is skipped.
On-call, notification policies & maintenance windows
Alert rules decide that something is wrong. The rest of the Alerts section decides who is woken up, how, and when nobody should be. On-call holds teams, schedules and escalation policies; Delivery holds notification policies, destinations and maintenance windows. These belong to the workspace and are shared by every environment.
| Piece | Where | What it decides |
|---|---|---|
| On-call team | Alerts → On-call → Teams | The people who can be paged. Each member is a member or manager and can be marked as not pageable. Who is on call? shows who would be paged now, and why. |
| Schedule | Alerts → On-call → Schedules | Which team member is on call: a rotation of up to 50 people with a time zone, a first shift and a shift length (hours, days or weeks). Day and week shifts hand over at the same local time across daylight saving changes. + Override covers a period with someone else from the team, up to 60 days. The schedule’s Layer (primary, secondary, backup and so on) lets several rotations of one team run side by side. |
| Escalation policy | Alerts → On-call → Escalation policies | Up to 20 steps, each an on-call team (optionally one layer of it) or a user. Step 1 is paged when the incident opens; each later step waits 1 to 1440 minutes after the previous one. Repeat the whole policy runs it again, up to 9 times, until someone acknowledges. |
| Notification policy | Alerts → Delivery → Notification policies | Which channels a paged person gets, per severity, and when. |
| Maintenance window | Alerts → Delivery → Maintenance windows | When nobody should be paged at all. |
Attach an escalation policy to an alert rule, a heartbeat, an uptime monitor or an inbound source. When a schedule gives no answer, the team's managers are paged, then every pageable member. Acknowledge, Resolve, a recovery from the source, or assigning the incident by hand stops the escalation. When the last step has been paged and nobody acknowledged, escalation ends, unless the policy repeats (below). Every page is on the incident timeline with its channel and a masked address.
Repeating an escalation
On an escalation policy, Repeat the whole policy (0 to 9 times, 0 by default) starts again from step 1 when the last step has been paged and nobody acknowledged. The next round starts 1 to 1440 minutes after the last step (10 by default). Each round pages again; it is never dropped as a duplicate of the round before. Acknowledging or resolving the incident stops it at once. The timeline says which round each step belongs to and when the policy will start again.
Rotation layers
Each schedule has a Layer: primary (the default), secondary, backup, up to five. One team can have a schedule per layer, and each rotates on its own, with its own people, shift length and time zone. An escalation step that targets a team can name a layer: step 1 pages the primary, step 2 the secondary, and so on. A step without a layer pages the first layer that has someone on call, primary first, so a team with one schedule works as before.
Calendar subscription
On the Schedules page, Subscribe to my shifts gives you a private calendar (ICS) address with your shifts across every schedule. Subscribe on a schedule gives one for that schedule’s whole rotation. Add it in Google Calendar, Outlook or Apple Calendar as a subscription from a URL. The address is shown once. Calendar apps re-read it, so rotation and override changes appear without exporting again. It covers 14 days back to 90 days ahead and carries display names, the layer and the schedule; no email addresses or phone numbers. Anyone with the address can read it, so revoke it if it leaks. Each person manages only their own, up to 10.
Postmortems
Once an incident is resolved, its page has a Postmortem section. It holds a title, a summary, the impact and its start and end (the incident’s own times by default), the root cause, contributing factors, the resolution, preventive actions, an owner and up to 50 action items (title, owner, due date, open or done, an optional link to Jira, Linear, GitHub or another tracker). Its status is draft, in review or published. Every save keeps the previous text as a revision and adds a line to the incident’s timeline. If someone saved a newer version while you were editing, your save is refused instead of overwriting theirs. Viewers can read it; Editors can write it.
Notification policies
By default, everyone who is paged gets every channel they switched on under Profile → On-call paging (email, and SMS and voice calls once they add a phone number), all at once. A notification policy changes that per severity, for example "critical: email now, SMS after 5 minutes, a call after 10; anything else: email only". Attach it to an on-call team, or make one the workspace default for teams without their own.
- A delayed channel is dropped if someone acknowledges or resolves the incident first, which is the point of the delay. Each scheduled step and its outcome is on the timeline.
- A policy can hold a channel back or leave it out, but never pages a channel a person has not switched on. Email is never throttled; SMS and calls still obey the per-number flood limit.
- A severity with no lines uses the policy's "any severity" lines, and a policy with neither falls back to the default, so a half-written policy cannot turn into silence.
Maintenance windows
Under Alerts → Delivery → Maintenance windows → + New maintenance window, set a name, a start and an end (at most 30 days), and optionally a service, environment label, alert rule or inbound source; with every filter empty it covers the whole workspace. Incidents that open during the window are still opened and recorded in full, with a SUPPRESSED line naming the window, but nobody is paged, nothing escalates and no destination is told. Alert rules keep evaluating and ingestion is not affected.
A window only affects incidents that open while it is active. To quiet an incident that is already open, use Suppress… on the incident: escalation stops and queued pages are dropped, events keep being recorded, and acknowledging or resolving it ends the suppression. The Environment filter matches the incident's free-text environment label, not the environment selected in the app.
Acknowledging a page. In the app, from a Slack or Microsoft Teams card, from PagerDuty or Opsgenie through an inbound key, or from the page itself, without signing in:
| Page | How |
|---|---|
| The Acknowledge button opens a page in the app that shows the incident and acknowledges when you click. Opening the link changes nothing, because mail scanners open links. | |
| SMS | The text ends with Reply 4821 to ack. (a 4-digit code). Reply 4821 or ack 4821. When only one page to that number is open, ok or 1 also works. |
| Voice call | The call says “Press 1 to acknowledge.” |
Each works once, for 24 hours, only for that incident, and acts as the person who was paged, who must still be an Editor or Admin. The timeline records it as “Email link: name”, “SMS reply: name” or “Phone: name”, and the escalation stops as with any acknowledgement. Recovery messages carry no acknowledge offer. Discord and Telegram messages have no buttons: use their link, or acknowledge from the email, text or call.
SMS replies need Twilio. Replies reach ZipLogger through Twilio’s
incoming-message webhook. If you send through your own Twilio account, point each sending number’s
webhook at
https://app.ziplogger.ai/api/v1/notifications/twilio/sms (Twilio → Phone Numbers →
Messaging → “A message comes in” → Webhook, HTTP POST). Every request must carry a
valid Twilio signature; anything else is refused. The same webhook handles STOP for
status page SMS subscribers. Telnyx SMS replies are not supported;
on Telnyx, the call’s keypress and the email button still work.
Not available yet: follow-the-sun rotations and layers limited to certain hours or weekdays (cover a gap with an override), postmortem templates and export.
Walkthroughs: on-call schedules and escalation and SMS and voice paging.
Uptime monitors All plans
An uptime monitor checks a URL or a port on a schedule, from one or more locations. It keeps the result of every check and a daily uptime figure, and opens an incident when the target stops answering. Create one under Alerts → Detection → Uptime with + New uptime monitor. Monitors belong to the environment you are viewing; Editors and Admins can create and change them.
An uptime monitor answers “is it reachable?”. A browser automation answers “does the sign-in journey still work?”, and a heartbeat answers “did the nightly job run?”.
Check kinds
| Check | Passes when |
|---|---|
| HTTP status | The response status is in Expected status: ranges and codes, comma-separated, such as 200-299 or 200-399,401. Method GET, HEAD, POST, PUT, PATCH, DELETE or OPTIONS. |
| Keyword in page | As HTTP status (any method but HEAD), and the first 1 MB of the body contains the keyword, ignoring case. Or, with Match: page must not contain it, does not contain it. |
| TCP port | A TCP connection to host:port opens within the timeout, for example db.example.com:5432. With Greeting contains, the first line the server sends must also contain that text: 220 for SMTP, * OK for IMAP, +OK for POP3, SSH-2.0 for SSH. |
| DNS record | The host name resolves to at least one A or AAAA address (Record). With Must include, the answer must contain that IP address. |
| Ping (ICMP) | The host name or IP answers an ICMP echo within the timeout. The location running the check must be allowed to send ICMP; a private agent whose container is not allowed to fails every ping check. |
- Redirects are not followed unless you tick Follow redirects (up to 5). Without it, a
301is a status like any other: add it to Expected status if that is what the target should answer. With it, the checker follows up to 5 hops and checks every hop against the same address rules as the first URL, so a redirect to a private address is a failed check, never a request. - Check every 30 seconds to an hour (the API accepts up to 24 hours), no faster than your plan allows (below). Timeout (s) is 2 to 60 seconds and shorter than the interval. A timeout is a failed check, and its error says so.
- Certificates. On
httpstargets every check also reads the TLS certificate’s expiry date. Certificate warning (days) (14 by default, up to 90, 0 turns it off) opens a separate incident when the certificate expires within that many days: medium severity, high within 3 days. It resolves when a later check sees the renewed certificate. A certificate that is invalid now fails the check itself.
Advanced settings
The form’s Advanced section holds settings most monitors never need.
- Request (HTTP and keyword checks): up to 20 headers, a Body and Content
type for
POST,PUT,PATCHandDELETE(up to 10,000 characters), and a Basic auth user and password. Header values and the password are stored encrypted and never shown again. When you edit a monitor, leave a value empty to keep the stored one; a header you remove is deleted.Hostcannot be set. - Slower than (ms) fails: a response slower than this is a failed check, even with the right status. From 50 ms up to the timeout; empty means no limit.
- Passes before up (1 to 10, default 1): once a location is failing, how many passing checks in a row it needs before it counts as up again, so an incident does not close on one lucky check.
- Domain expiry warning (days) (0 to 120, 0 is off): once a day ZipLogger looks up the registration of the target’s domain through RDAP, and opens a separate incident when it expires within that many days. A later lookup that sees the renewal resolves it.
Locations and confirmation
- ZipLogger (Europe) checks from ZipLogger’s own servers in Europe. It is the only public location, by decision. Its targets must be public: a URL or host that resolves to a private, loopback or cloud-metadata address is refused when you save, and again at every check, so a later DNS change cannot point a monitor inside our network.
- A private agent (the container that runs browser automations
inside your network) can run uptime checks from there, so it can reach intranet addresses and
internal ports. A monitor whose locations are all private agents may target private addresses.
The agent pulls checks outbound over HTTPS, like automations. Note: the agent image
published today (
0.1.0, also:latest) predates uptime checks; private locations run uptime checks once a newer agent image is published. - Failures before down (1 to 10, default 2) is how many failed checks in a row make one location failing. 2 absorbs a single blip.
- Confirm from (1 up to the number of locations) is how many failing locations make the monitor down. With three locations and a confirmation of 2, one flaky location never pages anyone.
ZipLogger runs public checks from Europe only. This is a decision, not a gap: there is no choice of public regions. To check from more than one place, add private agents in the networks you care about and use Confirm from across them.
Incidents and notifications
When a monitor goes down it opens one incident, with the monitor’s Severity (critical by default) and Service. The first round of checks that is no longer down resolves it. The incident links to the monitor and names the target, the check kind and the failing locations. Switching a monitor off, or deleting it, while it is down resolves its incident and any automatic status page incident it opened, since no later check would.
Uptime monitors have no channel list of their own. People are reached through the escalation policy you choose in Page through (see on-call), and tools through destinations (PagerDuty, Opsgenie, Slack and others) that match the incident’s severity and service. With neither, the incident is still recorded and its timeline says nobody was notified.
History and uptime figures
Every check is kept for 30 days with its location, result, status code, latency, error and certificate expiry. A failed HTTP or keyword check also keeps a snippet of the response (the status line, a few headers and the first 1,500 characters of the body), shown under response in the latest checks. A monitor’s page draws response time per location over a period you choose. Daily figures are kept for 400 days, and a monitor’s page shows them as 90 day bars. Daily uptime is up checks / (up + down checks). A day with no checks is no data, never 100%. Failed checks during a published status page maintenance window on a component the monitor feeds count as maintenance and are left out of the figure.
Plan limits
| Plan | Uptime monitors | Most frequent check |
|---|---|---|
| Free | 3 | Every 5 minutes |
| Pro | 10 | Every minute |
| Team | 50 | Every minute |
| Business | 200 | Every 30 seconds |
| Enterprise | Custom | Every 30 seconds |
Checks are not counted or billed one by one: the limits are the number of monitors and the shortest
interval. Creating a monitor past the limit, or asking for a faster interval, answers 403
with the code plan_limit. A monitor keeps its interval through later edits even if a plan
change raised the floor.
API
GET/POST /api/v1/uptime/monitors, GET/PUT/DELETE
/api/v1/uptime/monitors/{id}, GET /api/v1/uptime/monitors/{id}/checks?limit=,
GET /api/v1/uptime/monitors/{id}/days?days= (up to 400),
GET /api/v1/uptime/monitors/{id}/latency?hours= (up to 720), GET /api/v1/uptime/regions
and GET /api/v1/uptime/limits. Call them with a personal access
token. Changes need the Editor role and are in the audit log; a validation error answers
400 with the field it is about. Header values and the basic auth password are
never returned: a monitor answers with requestHeaderNames and
hasBasicAuthPassword.
Not available yet: UDP checks and multi-step API checks (use a browser automation). Public checks run from Europe only, by decision; more locations means private agents.
Status pages All plans
A status page tells your customers whether your service is working:
https://app.ziplogger.ai/status/your-page, with components whose state follows your
uptime monitors or heartbeats, incidents and maintenance (written by you, or opened
automatically by monitors), 90 day uptime bars, and updates by email, Slack, webhook and RSS. A page can
be public, password-protected, limited to IP ranges, or both. Create one under Alerts → Detection → Status pages
with + New status page. (status.ziplogger.ai is a different page: it reports on ZipLogger
itself.)
Setting one up
- Give it a Title and an Address: 3 to 64 lowercase letters, digits and dashes,
unique across ZipLogger (a few, such as
status,apiandadmin, are reserved). A new page is a draft, and the public gets a 404 until it is published. - On the Components tab, + Add component for each part of your service customers care about (“API”, “Dashboard”, “Webhooks”), with an optional description and group. Link each to an uptime monitor or a heartbeat (listed under Heartbeats), or choose Set by hand and pick its state. Up to 50 components per page.
- On the Settings tab, add a Support link (a URL or
mailto:), a description, and choose Show 90-day uptime bars and Let visitors subscribe (email, Slack, webhook). Choose Who can see it (below), tick Published and save.
Who can see it
- Anyone with the address is the default. The page needs no sign-in.
- Only people with the password: a password of 8 to 200 characters, stored hashed. A visitor enters it once, and that browser stays unlocked for 30 days. Changing the password signs out everyone who unlocked the page.
- IP allowlist (optional): IPv4 or IPv6 addresses or CIDR ranges, one per line or comma-separated. Visitors from other addresses see nothing, even with the password.
The limits apply to every public address of the page: the page, its incident history, its RSS feed, its badge and its subscribe box. A restricted page has no badge.
Component state
| When | The component shows |
|---|---|
| Linked uptime monitor is up | Operational |
| Linked uptime monitor is down | Major outage |
| Linked monitor is paused or has never run | Unknown |
| Linked heartbeat is healthy | Operational |
| Linked heartbeat missed or failed a run | Major outage |
| Linked heartbeat is paused or has never been pinged | Unknown |
| No monitor linked | The state you set by hand: operational, degraded performance, partial outage or major outage |
| An open incident names the component | At least degraded (minor), partial outage (major) or major outage (critical) |
| A maintenance window covers the component (between its start and end, and not completed) | Under maintenance, whatever else is true |
The banner at the top shows the worst component’s state. An incident or maintenance that names no components applies to all of them. Without automatic incidents, a failing monitor changes a component’s state but never writes anything: the words your customers read are written by a person.
Incidents and maintenance
- + Report an incident: a title, an Impact (Minor, Major or Critical), a status (investigating, identified, monitoring, resolved), the affected components and a first message. Show on the page publishes it, and Notify subscribers sends it. Each later update appends to the incident’s timeline and can move its status; updates are never edited away.
- + Schedule maintenance: the same, with Starts and Ends (at most 14 days apart), moving through scheduled, in progress and completed. Visitors see it under Scheduled maintenance before it starts. While a published window covers a component, failed checks of that component’s monitor count as maintenance, not downtime, in the uptime bars.
- Maintenance on a status page does not stop paging. To silence your own on-call during planned work, add a maintenance window under Alerts → Delivery → Maintenance windows as well (see on-call).
Automatic incidents
Under Automatic incidents on the Settings tab, tick Open an incident when an uptime monitor behind a component goes down, and resolve it when it recovers. When a linked monitor goes down, the page gets one incident naming the monitor’s components (“API unavailable”), with Major impact, posted as automatic. When the monitor recovers, a resolved update is posted to it. With Notify subscribers about automatic incidents also ticked, subscribers get those updates; otherwise they appear on the page only. You can edit and update an automatic incident like any other.
The same applies to a linked heartbeat: a missed or failed run opens the incident, and the next successful run resolves it. Switching off or deleting an uptime monitor, or pausing or deleting a heartbeat, resolves the automatic incidents it opened. Incidents a person posted are never touched.
What visitors see
The overall state, the components (grouped, with 90 day uptime bars when enabled; a day without
checks is grey “no data”, never 100%), open incidents with every update, upcoming
maintenance, and incidents resolved in the last 14 days, with Older incidents paging back
through the full history. The page refreshes every minute. It carries an RSS link
(/api/v1/status-pages/public/your-page/feed.rss, the latest 50 published incidents; on a
password-protected page the link carries the visitor’s access token, so feed readers can use it),
a subscribe box and your support link. On plans with
custom branding (Team and above) it shows your workspace’s name, logo
and primary colour; the “Powered by ZipLogger” line is removed only with white label
(Business and Enterprise).
Subscribers
| Channel | Address | Updates arrive as |
|---|---|---|
| An email address | One email per posted update, each with its own unsubscribe link | |
| Slack | A Slack incoming webhook (https://hooks.slack.com/… only) | A Slack message |
| Webhook | Any public https URL | A JSON incident.update POST |
| SMS (when switched on) | A phone number in international format, e.g. +14155550123 | A text: the page, the incident, its phase, a link and “Reply STOP to opt out.”, at most two SMS segments |
SMS subscribers are off by default on every page. Tick Also by text message (SMS) on the
editor’s Settings tab, under Let visitors subscribe, and the page’s Get updates
box offers SMS. Every text, the confirmation included, is paid from the workspace’s SMS allowance and
credits on ZipLogger’s sender, or sent through your own Twilio or Telnyx, and the plan must include
phone notifications. Replying STOP removes the number. To limit SMS pumping, confirmation texts
are capped at 20 per page per hour and 200 per workspace per day.
Subscribing is double opt-in on every channel: the first message carries a confirmation link, and
nothing else is sent until someone opens it. For a webhook, that first message is a JSON
subscription.confirm POST that also carries the signing secret. The page answers the same
whether or not the address was already subscribed. A visitor can subscribe to all components or only
to chosen ones; they then hear about incidents naming one of those components, or naming none (which
means all).
Webhook bodies are signed: X-ZipLogger-Signature: sha256=<hex> is the HMAC-SHA256 of
the raw body with the signing secret. Slack and webhook addresses are checked against private
addresses when someone subscribes and again at every send, and redirects are never followed.
Subscribers get updates for incidents posted with Notify subscribers ticked. Editors see and remove subscribers on the Subscribers tab, which shows only the host of Slack and webhook addresses; the list is never public. Up to 5,000 subscribers per page.
Status badge
/api/v1/status-pages/public/your-page/badge.svg is a small SVG badge (“status |
operational”, coloured by state) for a README or your website. The Status badge card on the
Settings tab has Markdown and HTML snippets. Badges are served only for pages anyone can see.
Limits
| Plan | Status pages |
|---|---|
| Free | 1 |
| Pro | 1 |
| Team | 3 |
| Business | 10 |
| Enterprise | Custom |
Public reads are rate-limited per address, and subscribing more tightly. Every change to a page, its
components and its incidents is in the workspace audit log. The API is /api/v1/status-pages
(see the OpenAPI document).
Custom domains (status.example.com) are available on request. A domain goes live only
after it is activated on our side, your DNS record points to ZipLogger and its certificate is issued, so a
domain field in Settings is not by itself a live domain. Contact us to set one
up.
Not available yet: sign-in-only (SSO) pages, Microsoft Teams
subscribers, SMS STOP through Telnyx (Twilio only), and translations.
Service health checks
The Dashboards page lists every service seen in your telemetry over the last 24 hours with a derived status: healthy, errors (error-level events in the last 15 minutes), or silent (no telemetry for 15+ minutes). A quiet service isn't necessarily a dead one, so any card's ⚙ lets you add an active health check: a public URL ZipLogger requests on your chosen interval (1–60 min). Any response below HTTP 400 counts as up (shown with latency); an error status or timeout marks the service down on the dashboard. Tick “Alert when this service goes down” to create a linked service down alert rule with any of the notification channels above.
A health check keeps only its latest result. For a check that keeps history and uptime figures, runs from several locations, looks for a keyword, checks a TCP port or a certificate, and can feed a public status page, use an uptime monitor.
Heartbeats
A heartbeat watches a scheduled job: a backup, a nightly report, a cron task. The job calls ZipLogger when it runs. ZipLogger opens an incident when a run is missed, fails or runs too long, and resolves it when the job runs again. Unlike a health check (ZipLogger polling a URL you expose) or a no-data alert (watching whether a log query stopped matching), a heartbeat knows what a run is. It can tell “never ran” from “started and never finished”.
Create one under Alerts → Detection → Heartbeats with + New heartbeat. Heartbeats
used to be called job monitors: the page address (/alerts/job-monitors) and the API
(/api/v1/job-monitors) keep that name.
Schedule
- Interval: a run is expected that long after the last successful run, from one minute to 30 days.
- Cron: a run is expected at each time a cron expression names (five fields, or six with seconds), in the Time zone you pick. Daylight saving is handled: a 02:30 run on the night the clocks skip from 02:00 to 03:00 is expected at 03:00, and on the night an hour repeats it is expected once. The form shows the next runs as you type. At most one run a minute.
- Grace period (minutes): how long after the expected time a run may still arrive. In that time the heartbeat is late, with no incident. After it, the run is missed and an incident opens.
- Max runtime (minutes), optional: for jobs that report a start, a run open longer than this is a failure.
A heartbeat is awaiting until its first ping, and nothing is judged before then. For a cron schedule, a ping that arrives shortly before the expected time counts for that run, so a job whose clock runs a little fast is not missed. The tolerance is the grace period, at most 10 minutes. A successful run after a missed or failed one resolves the incident. Pause resolves the open incident and stops judging; Resume, or changing the schedule, starts a fresh expectation from that moment. The heartbeat’s page lists every ping under Run history, with where it came from (ping URL, API or Sentry).
Ping URL
Every heartbeat has a ping URL, shown to Editors on its page. Call it when the job succeeds. No body, no header and no SDK are needed:
curl -fsS -m 10 --retry 3 https://app.ziplogger.ai/api/v1/heartbeat/<token>
| Path (GET, HEAD or POST) | Reports |
|---|---|
/api/v1/heartbeat/<token> | A successful run |
/api/v1/heartbeat/<token>/start | A run started (needed for Max runtime) |
/api/v1/heartbeat/<token>/fail | A failed run. A POST body (plain text, first 2,000 characters) is kept as its message. |
/api/v1/heartbeat/<token>/<exit code> | 0 is a success; anything else is a failure |
The exit code form reports the outcome of the command before it, for example in a crontab:
0 2 * * * /opt/backup/run.sh; curl -fsS -m 10 --retry 3 https://app.ziplogger.ai/api/v1/heartbeat/<token>/$?
Add ?rid=<run id> to name a run; without it, a start begins a new run and a success or
failure closes the open one. The URL is a secret: anyone with it can report runs. Rotate token
issues a new one and the old URL stops working at once. Pings are limited to 120 a minute per heartbeat.
A heartbeat created before ping URLs existed asks you to rotate its token to get one.
Sentry cron check-ins
A Sentry SDK pointed at ZipLogger (see Sentry SDKs) can report cron runs with
capture_checkin, @monitor or Sentry.withMonitor. Set the
heartbeat’s Sentry monitor slug to the SDK’s monitor_slug, in the same
environment as the DSN key. in_progress starts a run, ok ends it as a success
and error as a failure. A missed run is found from the schedule, as for any heartbeat.
A check-in that carries a monitor_config creates the heartbeat the first time, named after
the slug, and keeps its schedule (crontab or interval, margin, maximum runtime and time zone) in step
afterwards. A heartbeat you created yourself keeps its own schedule; the SDK only reports its runs. A
check-in for an unknown slug without a config only becomes a log line. Up to 200 heartbeats per
workspace can be created this way.
JSON check-ins
For full control over run ids, use the JSON endpoints with the heartbeat’s token as a bearer token:
# Give each run its own id; your job's own execution id works too
RUN_ID="$(hostname)-$$-$(date +%s)"
TOKEN="jmk_..."
curl -sf -X POST https://app.ziplogger.ai/api/v1/job-monitors/checkins/start \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"runId\": \"$RUN_ID\"}"
if ./run-nightly-backup.sh; then
curl -sf -X POST https://app.ziplogger.ai/api/v1/job-monitors/checkins/success \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"runId\": \"$RUN_ID\"}"
else
curl -sf -X POST https://app.ziplogger.ai/api/v1/job-monitors/checkins/failure \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"runId\": \"$RUN_ID\", \"message\": \"backup script exited non-zero\"}"
fi
Duplicate or out-of-order check-ins are handled safely: a retried call is a no-op, and an old run can
never overwrite the outcome of a newer one. Only one run is open at a time: a second
start while the first has not finished is refused with 409, not merged or
queued.
Incidents and status pages
A missed, failed or too-long run opens an incident with the heartbeat’s Severity, through its Escalation policy or On-call team (see on-call), like every other alert. Pausing or deleting a heartbeat resolves its open incident. A status page component can follow a heartbeat: healthy is operational, missed or failed is a major outage, paused or never pinged is unknown.
Not available yet: overlapping runs of one heartbeat (give each
copy of the job its own heartbeat), Sentry’s failure_issue_threshold and
recovery_threshold (ZipLogger opens an incident on the first missed or failed run), and
calendar exclusions such as holidays.
Error tracking
ZipLogger groups the errors your services report into Issues. One Issue is one bug: every time it happens is an occurrence. You see how often it happens, how many users it hits and in which releases, then assign it, resolve it, and get told if it comes back. Open it from Issues in the app.
What becomes an Issue
A log record is an occurrence when all of these hold:
- its severity is
errororfatal; - it carries an exception: an exception type, a stack trace, or an explicit fingerprint (see below);
- it is not opted out with the field
errorTracking: false.
So logger.LogError(ex, "Payment failed") forms an Issue, and
logger.LogError("Payment failed") without an exception stays a log line. Warnings
never form Issues, even with an exception.
Errors reach Issues through every log path: the ZipLogger SDKs (they send the exception type,
message and stack trace when you pass an exception), OpenTelemetry logs with the
exception.type and exception.stacktrace attributes, the
REST API with stackTrace or fields.exceptionType,
and Sentry SDKs. An exception recorded only on a span, with no log record,
does not form an Issue. Errors logged before error tracking was available are not grouped into
Issues.
How errors are grouped
Each Issue belongs to one service (the source your logs carry) and one
ZipLogger environment (the one the API key belongs to). The same exception in two services is two
Issues. Within a service, two occurrences are the same Issue when:
- The stack names your functions: they have the same exception types and the same application frames. Line numbers, column numbers, build paths and bundle hashes are ignored, so a redeploy does not split an Issue. The message is not used.
- The stack has no readable function names (a minified browser bundle without source maps, or no stack): they have the same exception type, the same message with ids, timestamps, e-mail addresses and long numbers removed, and the same crash-site file.
Minified JavaScript with source maps is grouped on your original code: the source file and function each frame maps to. The same bug therefore stays one Issue across builds, however the bundle is named. When a Sentry SDK marks frames as in-app or not, ZipLogger follows it.
A source map uploaded after an error was first seen groups that error's later occurrences on the original code, which makes a new Issue. Use Merge on the Issue page to join the two: the merged Issue's occurrences, past and future, then count on the other one, and Unmerge undoes it. Merge also joins any two Issues you know are the same bug.
For minified code without maps, keep variable values out of the message (put them in fields), or two values of the same error become two Issues.
To decide the grouping yourself, send errorFingerprint as a field: a string, or a
list of up to 8 strings of at most 200 characters. With a Sentry SDK, use
scope.setFingerprint. Every Issue has a Why these are grouped panel showing
exactly what was compared.
zl.captureError(err, { errorFingerprint: ['checkout', 'coupon-rejected'] })
The Issues page
The list shows the Issues of the current environment seen in the selected time range (last hour to last 90 days; 14 days by default). Filter by Unresolved, Resolved, Ignored or All, by service, by release and by assignee, and search by title, exception type or location. Sort by last seen, newest, most occurrences (total or in range) or most users (in range). Filters live in the URL, so a filtered view is a link you can share.
| Column | What it shows |
|---|---|
| Events | Occurrences in the range, and the total since the Issue was first seen. |
| Users | Distinct users in the range. When no occurrence named a user it shows unknown, not 0. |
| Last seen | The latest occurrence, with when it was first seen underneath. |
| Release | The latest release the Issue was seen in. |
A badge marks each open Issue New (first seen in the last 72 hours), Regressed (came back after a resolution in the last 7 days) or Ongoing. The location under the title is the crash site as received: for a minified bundle it stays minified in the list.
Users are counted from the userId field (also enduser.id,
user.id, userEmail or userName). The browser SDK does not
add it to errors on its own, so pass it:
zl.captureError(err, { userId: currentUser.id })
Only a one-way hash of the user is used for counting. An Issue has no session total to compare with, so it shows no crash-free rate of its own; crash-free sessions and users per release are under Release health.
An Issue
- Trend and impact: a histogram, occurrences in range, users affected, occurrences without a user, and the releases it appeared in.
- Occurrence: the stack trace with the exception chain, your frames first and library frames folded. JavaScript frames with a matching source map show the original file, line and code, with the minified frame underneath. Raw shows the text as received.
- Debugging context: under the stack, the occurrence’s breadcrumbs, tags, request, user, environment and other contexts (details).
- Tags: for release, environment, browser, OS, runtime, server and the issue’s most common tags, the five busiest values and their share of the issue’s occurrences over 14 days.
- Occurrences: each one with its time, release, user and message.
- Investigate: links built from the occurrence itself: this log in Search, the trace of the request and its logs, the session replay and session journey when the occurrence carries a session id, and the release and commit.
- Analysis: Find suspect commit runs git regression detection on the stack trace, and AI suggestion asks your AI provider for a likely cause. Commit blame reads frames as received, so it does not work on minified code even with source maps.
- Linked issues: issues in GitHub, Jira, Linear or another tracker (below).
- Comments: notes from the team, in Markdown (below).
- History: every change and who made it.
Debugging context
| Section | What it shows |
|---|---|
| Breadcrumbs | What happened before the error, from Sentry SDK events: the newest 100 (the page shows the last 15, with Show all). URLs lose their query string, and long messages and data are clipped. |
| Tags | The tags the SDK set, as sent. |
| Request | Method, URL (without its query string), transaction and request headers from an allow-list only: user-agent, referer, the accept headers, content type and length, host, origin, cache-control, x-forwarded-proto, x-request-id and traceparent. Cookies, authorization and API-key headers are never kept. |
| User | Id and name; the email only when the SDK sends it (Sentry’s send_default_pii). |
| Environment and other contexts | Browser, OS, runtime, device, server and SDK, plus the simple values of any other context the SDK attached (app, culture, cloud…). |
Breadcrumbs come only from Sentry SDK events today; ZipLogger’s own SDKs do not send them.
Assign, resolve and ignore
Everyone in the workspace can read Issues. Editors and Admins can change them. Assign an Issue to any member of the workspace with the Assignee list.
| Action | Reopens as Regressed when |
|---|---|
| Resolve | It happens again after you resolved it. |
| Resolve in release… | It happens in that release, or in a release ZipLogger first saw after it, in this environment and service. |
| Resolve in next release | It happens in a release ZipLogger first saw after the newest release at the time you resolved it. |
| Ignore… | Never. For 1 hour, 24 hours or 7 days, the Issue opens again with the first occurrence after the snooze ends. Until I reopen it keeps it ignored. |
Releases are compared by when ZipLogger first saw them, never as version numbers, so
web@3f9a2c1 works as well as 1.4.2. An occurrence that arrives late but
happened before the resolution does not reopen it. An occurrence with no release cannot reopen a
release-based resolution; the Issue shows how many there were. Ignored Issues keep counting but
never notify. Reopen opens an Issue by hand. If someone changed the Issue while you were
looking at it, your change is refused and the page reloads.
Issues are deleted 90 days after they were last seen.
Bulk actions
On the Issues list, Editors and Admins get a checkbox on each row. Tick several and a bar offers
Resolve, Ignore… (with no end date, for 24 hours, for 7 days or for 30 days),
Reopen, Assign to…, Merge (the others into the first one selected) and
Clear. Each Issue is changed as if you had pressed its own button, so one that changed in the
meantime, or is already in that state, is reported rather than overwritten. Over the API,
POST /api/v1/issues/bulk takes resolve, ignore, reopen
or assign and up to 100 ids, and answers per Issue with ok,
conflict, not_found or skipped:
{ "action": "resolve", "ids": ["…", "…"] }
Comments
The Comments card on an Issue takes Markdown. Everyone in the workspace can read comments; Editors and Admins can write them. The author can edit a comment, and the author or an Admin can delete it, which leaves “Comment deleted.” in its place so the thread still reads in order. HTML in a comment shows as text.
GitHub, Jira and Linear issues
The Linked issues card on an Issue has Create issue… and Link existing… (Editors and Admins).
| Tracker | Setup |
|---|---|
| GitHub | Uses a repository already connected under Settings → Integrations → Repositories (the one git regression detection uses). The personal access token, or the ZipLogger GitHub App, needs Issues: write. |
| Jira Cloud | An Admin connects it under Settings → Integrations → Issue trackers: the site (https://<site>.atlassian.net), the Atlassian account e-mail and an API token, then a default project. New issues have the type Bug unless you set another. |
| Linear | An Admin connects it in the same place with a personal API key, then picks a default team. |
Tokens are checked against the tracker before they are saved, stored encrypted and never shown again. A created issue carries the title, the service, where it happened (the culprit), how many times it was seen, first and last seen, the first and latest release, and a link back to the Issue. It does not carry the error message, the stack trace, request data or user fields.
Link existing… takes any https issue URL. GitHub (acme/shop#42), Jira
(SHOP-123) and Linear (ENG-7) links are recognised and shown by their key; a link
to any other tracker is kept as a link. An Issue holds up to 20 links, and creating, linking and unlinking
are written to its history. Removing a link does not change the outside issue.
Not available yet: two-way status sync (closing the GitHub, Jira or Linear issue does not resolve the ZipLogger Issue, or the reverse), Jira Server and Data Center, and creating GitLab issues (a GitLab issue can be linked).
Issue alerts
Create them under Alerts → Rules (or Issue alerts on the Issues page) with one of three conditions:
| Condition | Fires when |
|---|---|
| new issue | An Issue is seen for the first time. |
| issue regressed | A resolved Issue reopens as Regressed. |
| issue occurrences > threshold | Any one Issue occurs more than Occurrences of one issue times within Window (min). |
Each can be narrowed to one service. Notifications go through the rule’s channels and your destinations, like any alert. New and regressed Issues notify once per Issue, never once per occurrence and never for ignored Issues, and at most 30 times per environment per hour. Resolving or ignoring an Issue resolves its open incident. Notifications carry the Issue title with ids and long numbers removed, the service, counts, release and a link, not the stack trace or user data.
Source maps
A minified browser bundle reports frames like index-4f2a91.js:1:48213. Upload the
source maps of each build and the Issue page shows the original file, line and code instead.
ZipLogger stores the stack exactly as received. It applies the map when you view an occurrence,
so a map uploaded after the error still works, and also when the error arrives, to group it on
your original code (see How errors are grouped). Upload the maps as
part of the deploy, before the new build takes traffic. Sentry SDKs that send debug ids find their
map by id, whatever the release. Source maps apply to JavaScript only: .NET, Java and mobile debug
symbols are not supported.
1. Make an upload key
Under Settings → API keys, an Admin creates a key with Upload source maps
ticked. That key can upload maps and nothing else: it cannot send or read telemetry. Store it as
a secret in your CI (for example ZIPLOGGER_SOURCEMAPS_KEY). Never put it in code that
ships to a browser: your app keeps using its normal ingest key.
2. Send a release from the app
Maps are chosen by release, so the app must report one, and the upload must use exactly the same string.
const zl = new ZipLoggerBrowser({
endpoint: 'https://app.ziplogger.ai',
apiKey: 'zk_...', // ingest-only key
source: 'web',
release: import.meta.env.VITE_RELEASE, // e.g. web@3f9a2c1
})
zl.captureGlobalErrors()
With a Sentry SDK, upload under the release you pass to
Sentry.init, and pass dist too if you set one.
3. Build with source maps
Generate maps without a sourceMappingURL comment, so browsers never ask for
them:
// vite.config.js
export default { build: { sourcemap: 'hidden' } }
// webpack.config.js
module.exports = { devtool: 'hidden-source-map' }
Each bundle then gets a map next to it named after it, such as
dist/assets/index-4f2a91.js.map. That name is how ZipLogger pairs a map with its
script, so uploading the .map files alone is enough.
4. Upload from CI
After the build and before the deploy, send the maps to POST /api/v1/sourcemaps as
multipart/form-data, then remove them from what you deploy:
RELEASE="web@$(git rev-parse --short HEAD)"
VITE_RELEASE="$RELEASE" npm run build
for f in dist/assets/*.js.map; do
curl --fail-with-body -sS -X POST https://app.ziplogger.ai/api/v1/sourcemaps \
-H "X-Api-Key: $ZIPLOGGER_SOURCEMAPS_KEY" \
-F release="$RELEASE" \
-F urlPrefix="~/assets/" \
-F "files=@$f"
done
rm dist/assets/*.map # do not deploy the maps
| Field | Meaning |
|---|---|
release | Required. Exactly the release the app reports. At most 200 characters. |
urlPrefix | The URL path the scripts are served from. ~/ means any host: ~/assets/ matches https://shop.example.com/assets/index-4f2a91.js. Defaults to ~/. |
dist | Optional. Only needed when your Sentry SDK sets dist. |
| files | One or more .map files, with or without their .js files, or one .zip of a build folder. Paths inside a zip are kept, so zip from the web root and use urlPrefix=~/. |
The answer lists what was stored, replaced, skipped and why. Uploading the same release and file again replaces it. Signed-in Editors and Admins can also upload from Issues → Source maps, which lists every map with its release, size and when it was last used.
When a frame is not mapped
A frame is matched to a map of the same release whose urlPrefix plus file path equals
the frame’s URL path (host and query string ignored). If there is no exact match, a file name
that only one map of that release has is used. Each frame says what happened:
| Shown | What to do |
|---|---|
| no release | The error carries no release. Set release in the SDK. |
| no artifact | No map of this release matches the file. Check that the upload’s release is identical and that urlPrefix matches the path the script is served from. |
| ambiguous | Two maps of the release have that file name and neither path matches exactly. Fix urlPrefix. |
| out of range | The map does not cover that position: it is probably from another build. Upload the maps of the build you deployed. |
| malformed artifact | The stored map cannot be read. Upload it again. |
| no column | The browser reported no column, which minified code needs. |
Function names come from the map where it records them; the outermost frame can keep its minified name. The Issue list and Find suspect commit keep using the frames as received.
Limits, privacy and retention
- Size. At most 20 MB per upload request. Send one request per map, as above, or a zip under 20 MB. A single map may be up to 50 MB uncompressed (zip it). Up to 1,000 files per upload, and 1 GB of stored maps per workspace.
- Formats. Source map version 3, including index maps with
sections. Maps inlined asdata:URLs are not read, and nothing is ever fetched from your site. - Private. Maps belong to your workspace and are never served back as files. Members see the list; the code inside is shown only as context lines on the Issue page, never in notifications or AI prompts.
- Retention. A map not used in 90 days, and uploaded more than 90 days ago, is deleted. Editors and Admins can delete maps at any time on the Source maps page.
- Sentry tooling.
sentry-cliuploads and debug IDs are not supported; use the upload above with your Sentryrelease.
Releases and deploys
A release is created the first time your telemetry carries it, or from CI before a single event arrives. From CI it also gets its commits and deploys, and its page under Issues → Releases compares it with the release before it: new and regressed issues, crash-free sessions, sessions with errors and Web Vitals.
From CI
Create a key with CI: releases and source maps under Settings → API keys, in the environment you
deploy to, and store it as a CI secret. The ziplogger CLI (Node 18 or later) does the rest:
export ZIPLOGGER_API_KEY=zk_...
VERSION="web@$(git rev-parse --short HEAD)"
npx ziplogger releases new "$VERSION" --service web
npx ziplogger sourcemaps upload ./dist --release "$VERSION" --url-prefix "~/assets/"
npx ziplogger releases finalize "$VERSION" --service web
# after the deploy
npx ziplogger releases deploy "$VERSION" --service web --name "deploy #$RUN"
Use the same version your SDKs send as release. releases new records the checked-out commit
and the commits since the previous release, read from git log. In a shallow clone it sends only the
commit, and ZipLogger reads the range from the repository connected under Settings → Repositories
(GitHub or GitLab). With GitHub Actions, check out with fetch-depth: 0 to send them from CI.
Without the CLI, the same calls are POST /api/v1/releases (version, service, commit, commits, ref,
url, finalize) and POST /api/v1/releases/deploys, with the key in X-Api-Key; see the
API reference.
A release's page
- Compared with the previous release, over the same period: issues first seen in it, issues that were resolved and came back in it, crash-free sessions, sessions with errors, and LCP, INP and CLS at p75.
- Commits, linked to the repository, and deploys with their name, link and duration.
- An issue can be resolved in this release or in the next release: it reopens only when it happens in a later one.
Releases belong to an environment: a key records releases and deploys only in the environment it was created in.
Alert on a bad release
In Alerts → Rules → + New alert, pick newest release less stable than the previous one. Set
the service, the drop in crash-free sessions that fires in percentage points (for example 1) and a
window. It needs release-health sessions on both releases (20 or more each); with fewer it keeps its state.
Release health
Release health answers "is the new version crashing more than the old one?" with crash-free sessions, crash-free users, errored sessions and adoption for every release. Open it under Issues → Releases. Like everything else, it shows the environment you are viewing.
Sending sessions
Sessions are only counted for a release, so set one. Any of these sends them:
- Browser SDK (
@ziplogger/browser0.7.0 or later): on by default whenreleaseis set. One session per page load. The first error-level log marks it errored, and withcaptureGlobalErrors()an uncaught error or unhandled rejection marks it crashed. The session's user is the user id you set, else the anonymous id. Turn it off withtrackSessions: false. - .NET (
ZipLogger.Client0.7.0 or later): setReleasein the client options (a release taken only from the environment or the assembly version is not enough). Then there is one session per process, reported at start, at the first error-level log, on exit, and on an unhandled exception (crashed).SessionDistinctIdnames the user; turn it off withTrackSessions = false. - Sentry SDKs: the session data they send when a
releaseis configured is stored as it is (see Sentry SDKs).
const zl = new ZipLoggerBrowser({ endpoint: 'https://app.ziplogger.ai', apiKey: 'zk_...', release: '[email protected]' })
zl.captureGlobalErrors() // uncaught errors close the session as crashed
Other senders can POST the same session shape to /ingest/v1/sessions with an ingest key.
The environment is always the ingest key's, whatever the SDK's environment says.
What the numbers mean
Each session ends as exactly one of: crashed (an unhandled error ended it), abnormal (the SDK reported it killed or hung), errored (it had errors but did not crash) or healthy.
| Column | Definition |
|---|---|
| Crash-free sessions | 1 − crashed sessions / sessions. |
| Crash-free users | 1 − users with at least one crashed session / users. Sessions without a user id count toward sessions, not users. |
| Adoption | The release's share of all sessions in the window. |
| Errored | Sessions that had an error. |
The window (the last 24 hours, 7 days or 30 days) selects sessions by when they started. A chart shows crash-free sessions over time for the five busiest releases; an hour or day with no sessions is a gap, not 100%.
Alert on it
In Alerts → Rules → + New alert, pick crash-free sessions below N % (release
health). Set the percentage (for example 99.5), a Window (min) of at least 15,
and optionally one release; empty watches all releases together. With fewer than 20 sessions in the
window the rule keeps its state rather than firing or resolving on noise.
- Sessions are kept for 90 days and are not metered.
- An Issue page shows no crash-free rate of its own; the rates are per release, here.
- Native crash reports (minidumps) and symbolication are not supported.
Browser RUM
Real-user monitoring shows how fast your site is for the people using it: Core Web Vitals, how each page loads, how long its API calls take and which assets slow it down. Open it under Browser RUM. Every number is a percentile computed on the server over all the page views collected in the window, for the environment you are viewing.
Turn it on
Install @ziplogger/browser 0.8.0 or later with its web-vitals peer, and attach RUM
once your consent manager allows it. Nothing is collected before consent.
import { ZipLoggerBrowser } from '@ziplogger/browser'
import { attachRum } from '@ziplogger/browser/rum'
const zl = new ZipLoggerBrowser({ endpoint: 'https://app.ziplogger.ai', apiKey: 'zk_...', release: '[email protected]' })
zl.instrumentFetch() // links browser calls to backend traces
const rum = attachRum(zl, { enabled: true, hasConsent: false, sampleRate: 0.25,
getRoute: () => router.currentRouteTemplate }) // /orders/:id, never a real URL
rum.setConsent(true) // from your consent manager
What is collected
- Web Vitals: LCP, INP and CLS, plus FCP and TTFB, each rated good / needs improvement / poor on web.dev's boundaries.
- Page loads: redirect, DNS, connect, TLS, request and response times, then first byte, DOM interactive, DOM content loaded and load, for the waterfall.
- Fetch and XHR calls from the browser's own timings (nothing is wrapped): endpoint, status and
duration. Ids in paths become
:idand queries are dropped. Calls made throughinstrumentFetch()carry the trace id, so a slow call opens its backend trace. - The slowest assets of each page load (scripts, styles, images, fonts) and main-thread blocking from long tasks (Chromium browsers).
Requests and assets have budgets of their own per page (maxRequests, default 30;
maxResources, default 10), and each can be switched off with requests: false,
resources: false or longTasks: false.
The Browser RUM page
Overview has a card per vital with its p75 and the share of good, needs-improvement and poor views, the p75 over time against the good and poor lines, and a breakdown by route, browser, device, OS, country or release; click a row to narrow the whole page to it. Pages lists routes with their load times and vitals and draws the typical load of the one you pick, with its slowest assets. Requests ranks endpoints by total time spent, p75 or failure rate and lists the slowest recent calls with links to their traces and sessions. Resources covers assets by type and by file.
Alert on it
In Alerts → Rules → + New alert, pick Web Vital above N (browser RUM): the vital, the
percentile (p75 is what Google rates pages by), the threshold in milliseconds (a score for CLS), a window and
optionally one route, for example /checkout. With fewer than 20 page views measured in the window
the rule keeps its state.
- Only sampled, consented page views are measured; the numbers describe them, not every visitor. ZipLogger does not scale a sample up, so do not read page-view counts as your total traffic, or use them as an availability SLO. Route changes inside a single-page app are not measured as page loads.
- INP needs an interaction; a page nobody clicked has none.
- RUM events are ordinary events: they count toward your event usage and follow event retention.
APM
APM shows how fast your services answer, how often they fail, and what they spend their time on. It is built from the OpenTelemetry traces you send, plus ZipLogger request metrics when a service has them. Open it from APM in the app's navigation. It has four tabs: Services, Service map, Regressions and SLOs (service-level objectives).
1. Instrument a service
Any OpenTelemetry SDK or Collector works. Point the trace exporter at ZipLogger with an ingest key:
OTEL_SERVICE_NAME=checkout-api
OTEL_RESOURCE_ATTRIBUTES=service.version=1.5.0
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=https://app.ziplogger.ai
OTEL_EXPORTER_OTLP_HEADERS=X-Api-Key=zk_...
Use your framework's HTTP server, HTTP client and database instrumentation. Server spans become requests. Client spans become dependencies. Without client instrumentation, APM cannot see the database or the APIs a service calls.
Optional, for ASP.NET Core: add the request metrics middleware too. It
measures every request, so APM's request counts, error rates and percentiles stay complete even when you
sample traces. Give it the same service name as OTEL_SERVICE_NAME:
builder.Services.AddZipLoggerMetrics(builder.Configuration, service: "checkout-api");
var app = builder.Build();
app.UseZipLoggerMetrics(); // early, so timings cover the whole pipeline
OpenTelemetry metrics sent to /v1/metrics also help: the request count of
http.server.request.duration gives APM complete traffic and error counts. It gives no
latency, so percentiles then come from traces.
2. Send service, environment and release
| What | Where it comes from |
|---|---|
| Service | service.name. Add service.namespace to keep two services with the same name apart. Spans without a name are shown separately as an unidentified service, never merged into a named one. |
| Environment | The ingest key. Each key belongs to one ZipLogger environment, and everything sent with it lands there. deployment.environment is stored as an attribute only. |
| Release | service.version on each span. APM assigns every request to the release on its own span, so a canary and the old version are counted apart even in the same minute. Request metrics carry no release. |
Use one ingest key per environment (Settings → Environments). APM pages always show the environment you have selected in the app.
3. Open the service and endpoint views
APM → Services lists every service that sent traces or request metrics in the time range, with Traffic, Error rate, P50, P95, P99, Source, Last seen and Status (open regressions and firing APM alerts). The range runs from Last 15 minutes to Last 30 days; the default is the last hour. Every filter is in the URL, so a link reproduces the view.
Click a service to open its page: totals, charts of traffic, error rate and latency, and the
Endpoints, Called by, Calls, Dependencies, Releases,
Regressions & incidents, Issues and Kubernetes sections. Click an endpoint, such
as POST /checkout/{cartId}, to open the endpoint page.
4. Inspect a slowdown
On the service or endpoint page, the Latency chart shows P50, P95 and P99 over the range. A dashed line marks the first time each release was seen. Narrow the range to the slow period.
On the endpoint page, Where the time goes splits the mean request duration between the dependencies that were in flight, Parallel calls, and Unattributed time. The parts add up to the mean duration. To compare versions, use the vs link in Releases (service page) or compare with in By release (endpoint page).
5. Open the traces behind it
The endpoint page's Traces section offers three sets from the same environment, endpoint and window: Slow (at or above the window's P95), Typical (between P40 and P60) and Errors. Each trace links to its Waterfall, Investigate, Logs, a Replay when the request carried a session, and a Breakdown of where that one request's time went. The Traces page shows the same breakdown under every waterfall, as Where each request's time went (APM).
6. Check related errors and dependencies
The service page's Issues section lists that service's busiest issues in the window. A trace's
breakdown lists Issues in this trace: issues with an occurrence that carries the trace's id.
Dependencies lists each database, HTTP host, queue or RPC service the service called, with calls,
errors and P50/P95/P99 as the caller measured them; Traces on a row opens slow, typical and failed
calls to it. When spans carry Kubernetes attributes (k8s.pod.uid,
k8s.namespace.name, k8s.deployment.name and similar), the service page lists
its workloads. If ZipLogger also monitors that cluster, a trace's breakdown shows the Pod it ran on and
the Pod's CPU and memory around that time. A service is never linked to a workload by name alone.
7. Configure an alert
Go to Alerts → Rules → + New alert and pick the condition APM: P95 latency above N ms or APM: error rate above N %. Fill in:
- Service, as shown on APM → Services (
namespace/namewhen it has a namespace). - Endpoint (optional), exactly as listed on the service page. Empty watches the whole service.
- The threshold, in milliseconds or percent, and a Window (min) of 5 to 180 minutes.
- At least one channel under Notify via, or an escalation policy. With none, the alert is only recorded in ZipLogger.
The rule fires after two evaluations in a row above the threshold, and resolves after two in a row below 90% of it. With fewer than 20 requests in the window, or if APM data is behind, it shows not enough data and keeps its state. Silence never resolves an alert. The newest two minutes are left out because they are not aggregated yet.
Measurements
| Measurement | Definition |
|---|---|
| Traffic | Requests from request metrics or OTLP request counts when the service sends them, otherwise Observed spans. The rate is per minute, over the minutes that had data. |
| Request | A server or consumer span, or a root span of a job. Client spans are calls, not requests. |
| Error rate | Errors divided by the count from the same source. An error is a span with error status, or HTTP 500 or above on a server span. A 4xx response is not an error. |
| P50, P95, P99 | Percentiles read from merged histograms, accurate to about 2.4%. They are never averaged across time or endpoints. |
| Sampling | Complete: traces cover every request that request metrics counted. Sampled · N% of requests traced: counts come from request metrics, and dependency and breakdown figures from the sampled traces. Observed spans · sampling unknown: only spans exist, so error rates and percentiles can be biased if you sample by outcome. |
| No data | A minute with nothing received is a gap in the chart, not a zero. |
Ranges up to 6 hours within the last two days use one-minute points. Longer or older ranges use hourly points. A range can be at most 31 days.
Service map
APM → Service map draws the services and dependencies that called each other in the time range. Every edge comes from a parent and child span, or from a span link: a call is counted once, from the callee's span. Nothing is inferred from timing or host names, and nothing is joined across environments. Edge labels show calls and P95, with (C) when the caller measured it and (S) when only the callee's server span exists. Dashed edges are messaging, with delivery lag; dotted edges are span links; red edges have 5% errors or more. A call whose parent span never arrived comes from (unknown caller). Focus on one service with 1 or 2 hops. Very large maps fold low-traffic dependencies into one node.
Comparisons
Release comparison puts two releases of a service, or of one endpoint, side by side: sample size, P50/P95/P99, error rate, traffic, the change in each, dependency latency, endpoints present on both sides, traces from each, and for one endpoint, where the extra time went. Each side counts only requests whose span carried that release, so overlapping canary or rolling deploys are handled. Time-window comparison opens from a regression's Compare windows link and shows the release mix of each window. Both views list their limitations, such as small samples or a change in sampling.
Regression detection
Every 5 minutes, APM compares each endpoint's P95 over the last 30 minutes with its own P95 over the 24 hours ending an hour before that. It checks endpoints, not whole services, so a shift in traffic toward slower endpoints does not trigger it. A regression needs:
- P95 up by at least 100 ms and at least 25%;
- at least 200 observations over 120 minutes in the baseline, and 50 over 15 minutes in the recent window;
- the same source (request metrics or spans) in both windows;
- three breaching checks in a row. Until then it is Pending; after that it is Open.
It resolves after three checks in a row with P95 back within 10% of the baseline it was detected against. That baseline stays fixed while the regression is open, so a slow period never becomes the new normal. Missing or late data never opens or resolves one. The evidence panel shows both windows, the release mix, dependency changes and the breakdown changes. Editors can change every threshold under Detector settings (this environment), and turn on Open incidents for regressions, which is off by default.
Service-level objectives (SLOs)
An SLO is a target for one service, or one endpoint, over a rolling window: "99.9% of checkout requests succeed over 30 days" or "99% of search requests are faster than 300 ms over 28 days". Create one under APM → SLOs → + New SLO:
| Field | Meaning |
|---|---|
| Service, Endpoint (optional) | As shown on the Services tab. Empty endpoint = the whole service. |
| Measure | Availability: requests without error (an error as defined under Measurements above), or Latency: requests faster than a threshold with Faster than (ms). A latency SLO counts a fast failed request as good; pair it with an availability SLO. |
| Target (% good) | For example 99.9. |
| Window | 7, 28 or 30 days, rolling. |
The SLOs table shows each objective's Status (OK, Breached or No data), Current (the share of good requests over the window), Error budget left and Burn (1 h). The error budget is the share of requests allowed to be bad: 0.1% for a 99.9% target. Error budget left is how much of it the window has not used yet. Burn rate is how fast it is being used: 1 means the budget runs out exactly at the end of the window, 14.4 means a 30-day budget is gone in about two days. History on a row charts the budget left at the end of each day, with each day's share of good requests against the target.
Counts come from the same requests as the rest of APM: request metrics when the service sends them, otherwise spans, in which case the ratio is an estimate from your sampled traces. Latency is read from histograms, so a request within about 2.4% of the threshold can fall on either side.
Burn-rate alerts. In Alerts → Rules → + New alert, pick SLO error budget burn rate (APM SLOs), choose the SLO, a Burn rate at least value and a Window (min). The rule fires only when both your window and a short window of 1/12 of it (at least 5 minutes) burn at that rate, so a spike that has already ended does not page anyone, and it needs at least 20 requests in the window. The usual pair is 14.4 over 60 minutes (page now) and 6 over 360 minutes (gone in five days).
- Up to 100 SLOs per environment. Editors and Admins create and delete them; an SLO used by a burn alert cannot be deleted until the alert is removed.
- SLOs measure APM requests only, not log queries or custom metrics. Windows are rolling, not calendar months.
- An SLO cannot look back further than your workspace keeps traces: on 7-day trace retention, a 30-day SLO counts 7 days.
- An SLO is a monitoring target, not an SLA. It tells your team how reliable a service has been against a goal you chose. It is not a contractual commitment to your customers, and ZipLogger does not enforce or report one. When the counts come from sampled traces, the SLO describes the sampled requests, not every request: send request metrics for complete counts.
Limits
- Sampling. APM does not re-weight sampled spans. Send request metrics or OTLP request counts if you need true request totals and unbiased error rates.
- Missing data. Late spans correct their minute for up to 6 hours. Incomplete traces are shown as incomplete, with the missing caller named (unknown caller).
- The map is what was observed in the time range. A service that made no traced call in the range has no edge.
- Size. Up to 300 services per environment, and 300 endpoints, 200 dependencies and 100 releases per service. Beyond that, new ones are still counted, in one row such as Other endpoints (limit reached).
- Unattributed is not CPU time. Unattributed time is time with no instrumented call in flight: your own code running, uninstrumented I/O, waiting on a lock, or missing instrumentation. APM cannot tell these apart. To see which functions the time went to, send profiles to continuous profiling, a separate feature; a profile taken during a span links back to its trace.
- Retention. One-minute data is kept for 48 hours. Hourly data is kept as long as your workspace keeps traces, up to 90 days. Once raw traces expire, the numbers remain but their example traces do not.
- Delivery. See what a 200 from
/v1/tracesmeans: a span is acknowledged only once it is written to disk, and a temporary failure answers a status exporters retry. The request metrics package retries three times, then drops the batch, and drops new timings when its queue is full.
Continuous profiling
Profiling shows which functions a service spends its CPU, wall time and memory in, as a flame graph over any time range. It answers the question APM leaves open: APM tells you an endpoint got slower and how much of its time was unattributed; a profile from the same period shows the code that ran. Open it under Profiling, or from Profiles on an APM service page.
Who it helps. Backend developers chasing a slow endpoint or a CPU or memory increase after a
deploy. Example: P95 of POST /checkout doubled with release 1.5.0 and APM shows most of
the new time as unattributed. Compare the CPU profile of 1.5.0 with 1.4.0: the red bars are the functions
that now take a larger share, such as a JSON serializer called in a loop.
Send profiles
No ZipLogger SDK is involved. ZipLogger accepts the upload protocol of the open-source Pyroscope profilers, and the profiles Sentry SDKs send.
| Runtime | Profiler | Point it at ZipLogger with |
|---|---|---|
| Go | github.com/grafana/pyroscope-go | Server address https://app.ziplogger.ai, basic auth user ziplogger, password = an ingest key |
| Python | pyroscope-io | server_address, basic_auth_username, basic_auth_password |
| Node.js | @pyroscope/nodejs | serverAddress, basicAuthUser, basicAuthPassword |
| .NET | Pyroscope .NET profiler | PYROSCOPE_SERVER_ADDRESS, PYROSCOPE_BASIC_AUTH_USER, PYROSCOPE_BASIC_AUTH_PASSWORD |
| Ruby, Rust | pyroscope gem or crate | Server address, and the ingest key as the auth token |
| Any Sentry SDK | profiles_sample_rate or Sentry’s continuous profiling | The ZipLogger DSN you already use (Sentry SDKs) |
// Go
pyroscope.Start(pyroscope.Config{
ApplicationName: "checkout-api",
ServerAddress: "https://app.ziplogger.ai",
BasicAuthUser: "ziplogger",
BasicAuthPassword: "zk_...", // an ingest key
Tags: map[string]string{"version": "1.5.0"},
})
The key is an ordinary ingest key, and the profile lands in its environment.
The service is the application name (or a service_name tag); the release is the
version tag. Uploads arrive as pprof (gzip or plain) or as folded stack lines
from older agents.
What you can look at
- Profile types are whatever your profiler sends: typically CPU and wall time, allocations and memory in use, and for Go also mutex and block contention and goroutines. Sentry profiles are wall time. Memory and goroutine profiles show the average held over the range; the others are summed.
- Flame graph for one service and type over a time range, optionally one release, with the heaviest functions in a table beside it.
- Compare with a baseline: another release over the same range, or the same length of time just before. Red bars took a larger share of the time than in the baseline, green a smaller one.
- Trace links. A trace that was profiled shows Profile above its waterfall. Sentry transaction profiles match by trace id; Pyroscope profiles match when your profiler labels samples with the span id (Pyroscope’s OpenTelemetry integration does).
Availability and limits
- Profiling runs on ZipLogger’s ClickHouse store and is switched on for the hosted service. A server
without it answers
503profiling_unavailable, and Sentry profiles are then ignored. - Profiles are kept for 14 days, and one view covers at most 14 days. Retention is the same on every plan.
- Profiles are measured but not billed today, and there is no plan cap; an upload is limited to 20 MB and 200,000 samples.
- Frames are function names: no line numbers or source view. Tags other than service and release are not stored, so there is no per-host filter yet.
- Not supported: Java JFR uploads (answered
415), Grafana Alloy’spyroscope.write, and OpenTelemetry profiles.
Metrics
ZipLogger stores metric points from three places: OpenTelemetry metrics exported to
/v1/metrics (for example host metrics from the OpenTelemetry Collector), the
metric rules of a log pipeline, and the native
/ingest/v1/metrics endpoint, which the ASP.NET Core request metrics package below uses.
OTEL_EXPORTER_OTLP_ENDPOINT=https://app.ziplogger.ai
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS=X-Api-Key=zk_...
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta # recommended
OTLP gauges are stored as they are, counters as the increase in each interval, and histograms as one
series per statistic (<name>.count, .sum, .avg,
.p95, .p99, .max). Data point attributes become labels, plus the
resource's host.name and deployment.environment. Units are the instrument's
own: OpenTelemetry durations are usually seconds and utilization is a ratio (90% is 0.9).
The Metrics page
The Metrics page charts any metric you send. Pick the Metric, an Aggregation (avg, min, max, sum, count, p50, p90, p95, p99, or rate in points per second), an optional Service, label filters, and Group by a label or the service to get one line per value (the busiest ten). The time range runs from the last 15 minutes to the last 7 days, and the interval is chosen for you unless you set it. A table under the chart gives each line's value over the whole range.
The whole query is in the address, so a chart is a link you can share. Add to dashboard… saves what is on screen as a Metric widget, and the same metric name and aggregation work in a metric monitor. A bucket with no points is a gap in the chart, not a zero. The page reads raw points, so a range older than your plan's raw metric retention shows gaps.
ASP.NET Core request metrics
Install ZipLogger.Metrics.AspNetCore (NuGet) and add the middleware; every request's
duration is batched and shipped with your API key as request.duration, with its method,
route and status as labels. APM uses the same points as complete request counts, per
endpoint. Other stacks can POST the same shape to /ingest/v1/metrics.
builder.Services.AddZipLoggerMetrics(builder.Configuration, service: "checkout-api");
var app = builder.Build();
app.UseZipLoggerMetrics();
The package reads ZipLogger:Endpoint and ZipLogger:ApiKey, and does nothing
when they are unset. /health and /favicon.ico are not measured. If the service
also sends traces, use the same service name as OTEL_SERVICE_NAME so APM pairs them.
Distributed traces
ZipLogger accepts OpenTelemetry traces on the standard OTLP/HTTP path (/v1/traces,
protobuf or JSON, gzip supported), authenticated with the same API key as logs. You do not need a
collector for traces: point your service's exporter straight at ZipLogger. On Kubernetes, you can
instead send to the in-cluster endpoint the ZipLogger chart installs.
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=https://app.ziplogger.ai
OTEL_EXPORTER_OTLP_HEADERS=X-Api-Key=zk_...
The Traces page lists recent requests (filter by service, time, or errors only). Opening one shows the waterfall: every operation the request triggered, nested under its parent, colored by service, with durations, error spans in red, and each span's attributes one click away. Error logs that carry a trace id show a View trace link, so you can jump from a log line to the full request flow that produced it. The same spans feed APM, and under each waterfall Where each request's time went (APM) splits the request's duration between the calls that were in flight. Trace volume is covered by your plan's log and trace ingest allowance (see pricing). Retention: traces that contain an error are kept for your plan's full retention; error-free traces are kept for up to 30 days, and never longer than your plan's retention. A workspace can shorten that under Settings → Data retention.
Delivery and retries
A 200 from /v1/traces means every span it accepted has been written to disk on
the server that answered, before it is indexed. If the search store is slow or down, accepted spans wait
there and are indexed when it recovers; a resent span replaces the same stored span instead of being
counted twice.
| Answer | When | What your exporter should do |
|---|---|---|
200 | Accepted. | Nothing. |
200 with partialSuccess.rejectedSpans | Your plan allowance covered only part of the batch. The rest is refused for good. | Nothing; a partial success must not be retried. |
429 + Retry-After | The allowance or daily ceiling is used up. | Retry after the time given. |
503 + Retry-After | ZipLogger cannot take the request right now: its buffer is full, its disk journal or a database it needs is unavailable. Nothing in the request was accepted. A 503 whose body says "outcome": "unknown" means a dependency failed mid-request and the result could not be confirmed. | Retry. A resend of spans that were in fact stored overwrites them. |
400, 401, 413, 415 | The payload, key, size or content type is wrong. | Fix the request; retrying will not help. |
Whether a retry actually happens is up to your exporter. The OpenTelemetry Collector and most SDKs retry
429 and 503. The .NET exporter does not retry at all unless you set
OTEL_DOTNET_, and even then it gives up at its export
timeout. Spans waiting on the server's disk survive a restart, but not the loss of that server, and this
is not a promise of zero loss or of exactly-once delivery.
Kubernetes monitoring
Install one Helm chart in a cluster and ZipLogger shows you every node, namespace, workload and pod in it. It reads Kubernetes events and object state, detects failures such as crash loops, image pull errors, missing Secrets and pods that cannot be scheduled, and explains each one with the evidence Kubernetes reported. Problems can open incidents through the same on-call routing as the rest of ZipLogger.
Unlike the rest of ZipLogger, Kubernetes monitoring runs software in your environment. The chart installs two
collectors in your cluster. Both are the upstream OpenTelemetry Collector
(otel/opentelemetry-collector-k8s, pinned by digest) with a configuration written by
ZipLogger. They only read from Kubernetes, and they only connect outbound, over HTTPS, to
ZipLogger. Nothing in your cluster has to accept inbound connections.
| Collector | Runs as | What it does |
|---|---|---|
Cluster collector (ziplogger-cluster) | A Deployment with one replica | Watches Kubernetes objects and events across the cluster, re-lists everything every 5 minutes to catch anything a watch missed, and sends workload and node state metrics. |
Node agent (ziplogger-agent) | A DaemonSet, one pod per node | Reads CPU, memory and disk usage from the node's kubelet, ships container logs from the node, and offers an in-cluster OTLP endpoint for your applications. |
Prerequisites
- Kubernetes 1.24 or newer. The chart refuses older clusters.
- Helm 3 or Helm 4. We test the chart with Helm 3.22 and 4.3.
kubectlaccess with rights to create a namespace, cluster roles and bindings, a Deployment and a DaemonSet. The install also reads thekube-systemnamespace once, to identify the cluster.- Outbound HTTPS from your nodes to
app.ziplogger.ai. A proxy is supported (see Configuration). - Your nodes must be able to pull
otel/opentelemetry-collector-k8sandbusybox(used byhelm test) from Docker Hub, or from a mirror you set withimage.repositoryandimagePullSecrets. - An Admin in your ZipLogger workspace, to create the collector key.
Where it has been tested. The chart uses only standard Kubernetes APIs and the kubelet's standard stats endpoint. We have tested it on kind clusters running Kubernetes 1.33, with Linux nodes. We have not yet tested it on EKS, GKE, AKS, DigitalOcean, OpenShift, GKE Autopilot or EKS Fargate. Two things are already known:
- EKS Fargate: DaemonSets do not run on Fargate, so Fargate pods get no usage metrics or container logs. Their state, events and problems still come from the cluster collector.
- Pod Security admission: container log collection mounts
/var/log/podsread-only from each node. The Baseline and Restricted levels do not allow that, so if your cluster enforces Pod Security, the release namespace must allow Privileged, or you can turn container logs off (collection.logs=false). Without container logs, both collectors meet the Restricted level.
# Only if your cluster enforces Pod Security admission and you keep container logs on
kubectl label namespace ziplogger pod-security.kubernetes.io/enforce=privileged
Create a collector key
The chart needs a Kubernetes collector key. It is a different kind of key from the ingestion key in a browser app: it can report cluster state, which can open incidents, so treat it as a secret and keep it only in the cluster.
- In ZipLogger, open Kubernetes → Install.
- Check the environment named on the page. The cluster reports into the environment the key belongs to, and a key cannot be moved to another environment later.
- Click Create collector key (Admins only). Copy the key now: it is shown once. The page also puts it into the install command for you.
The key is named Kubernetes collector and carries two scopes: kubernetes, to
report cluster state, and ingest, for container logs and application telemetry. It
counts toward your plan's API key limit. You cannot create this kind of key under
Settings → API keys; use the Install page.
Install with Helm
helm repo add ziplogger https://ziplogger.ai/charts
helm repo update
helm install ziplogger ziplogger/ziplogger \
--namespace ziplogger --create-namespace \
--set apiKey=zk_... \
--set cluster.name=YOUR_CLUSTER_NAME
That is the whole configuration. cluster.name is optional when you install with
helm install: without it, the cluster is identified by the UID of its
kube-system namespace and shows up as cluster- plus the first eight
characters of that UID. You can rename it in ZipLogger at any time.
helm template need cluster.name.
They render the chart without access to the cluster, so the chart cannot read the
kube-system UID and stops with "Could not read the kube-system namespace". Set
cluster.name and the name becomes the cluster's identity. Pick one way of installing
a given cluster and keep it: switching between helm install and an offline renderer
changes the identity, and the cluster appears in ZipLogger as a new one.There is no environment setting in the chart. The key decides the workspace and the environment.
The chart refuses plain http:// endpoints, so the key always travels over TLS.
Install with an existing Secret
With --set apiKey, the chart creates a Secret for the key, and the key is also
stored in the Helm release (helm get values shows it). To keep the key out of Helm
entirely, for example with GitOps or an external secrets operator, create the Secret yourself in
the release namespace and point the chart at it:
kubectl create namespace ziplogger
kubectl -n ziplogger create secret generic ziplogger-key \
--from-literal=api-key=zk_...
helm install ziplogger ziplogger/ziplogger -n ziplogger \
--set ziplogger.existingSecret.name=ziplogger-key \
--set cluster.name=YOUR_CLUSTER_NAME
The chart reads the data key api-key by default. If your Secret stores the key
under another name, set ziplogger.existingSecret.key.
Configuration
Every other value has a working default. The chart validates its values, so a misspelled setting fails the install instead of being ignored. The most useful ones:
| Value | Default | Use it to |
|---|---|---|
cluster.name | kube-system UID | Name the cluster. Required for Argo CD, Flux and helm template. |
ziplogger.existingSecret.name, .key | none, api-key | Read the key from your own Secret. |
collection.events | true | Collect Kubernetes events (Warning and Normal). |
collection.metrics | true | Collect node, pod and container CPU, memory and disk usage, and workload replica metrics. |
collection.logs | true | Ship container stdout and stderr. Set false if your logs already reach ZipLogger another way. |
collection.traces | true | Run the in-cluster OTLP endpoint (traces, metrics and logs from your applications). |
namespaces.include | all | Monitor only these namespaces. Nodes, namespaces and node metrics are always collected. |
namespaces.exclude | none | Never collect these namespaces. Ignored for object state when include is set. |
proxy.httpsProxy, proxy.noProxy | none | Send through an outbound proxy. The kubelet and the Kubernetes API are always reached directly. |
ziplogger.caSecret.name, .key | none, ca.crt | Trust an extra CA, for a TLS-inspecting proxy. The Secret holds a PEM file. |
clusterCollector.resources | requests 50m CPU, 128Mi; limit 500m, 512Mi | Change the cluster collector's requests and limits. |
nodeAgent.resources | requests 50m CPU, 96Mi; limit 500m, 384Mi | Change each node agent's requests and limits. |
nodeAgent.tolerations | tolerate everything | The agent runs on every node, including tainted ones, by default. Narrow it here. |
clusterCollector.tolerations, *.nodeSelector, *.affinity, *.priorityClassName | none | Place the collectors. |
clusterCollector.queue.maxBytes | 256 MiB | Hold more data on disk during a long network or ZipLogger outage. |
nodeAgent.checkpoints | emptyDir | hostPath keeps container log positions across agent upgrades, so no lines are skipped. It adds a one-second init step that runs as root. |
image.repository, imagePullSecrets | Docker Hub | Pull the collector from your own registry mirror. |
commonLabels, *.podLabels, *.podAnnotations | none | Add your own labels and annotations. |
The chart has no setting for extra environment variables. A values file for a shared cluster might look like this:
# values.yaml
cluster:
name: YOUR_CLUSTER_NAME
ziplogger:
existingSecret:
name: ziplogger-key
namespaces:
exclude: [kube-system]
proxy:
httpsProxy: http://proxy.internal:3128
nodeAgent:
nodeSelector:
kubernetes.io/os: linux
helm install ziplogger ziplogger/ziplogger -n ziplogger --create-namespace -f values.yaml
Send your applications' telemetry through the cluster. With collection.traces
on, each node runs an OTLP endpoint on ports 4317 (gRPC) and 4318 (HTTP). It adds the pod,
workload and node names to whatever your applications send, then forwards it to ZipLogger.
Point your OpenTelemetry SDKs at it:
OTEL_EXPORTER_OTLP_ENDPOINT=http://ziplogger-otlp.ziplogger:4318
The Service keeps traffic on the sender's own node. Your applications do not need an API key for
this endpoint; the agent adds the collector key when it forwards. That also means any pod that
can reach the Service can send telemetry into your workspace, so turn the endpoint off
(collection.traces=false) if you do not use it.
Verify the install
helm test ziplogger -n ziplogger
kubectl -n ziplogger get pods
kubectl -n ziplogger logs deploy/ziplogger-cluster --tail=50
helm testchecks that the cluster collector is running and healthy, from inside the cluster. It does not check that data reaches ZipLogger.get podsshould show oneziplogger-clusterpod and oneziplogger-agentpod per node, all Running.- The log should have no
401,403orExporting failedlines.
Then open Kubernetes in ZipLogger. For your first cluster, the Install page shows Waiting for the first data from your cluster until it reports, then Connected. The cluster usually appears under Kubernetes → Clusters within a minute. Problem detection starts once the first full listing of the cluster has arrived.
| You see | What to check |
|---|---|
| Install fails: "An ingest API key is required" | Pass --set apiKey=... or ziplogger.existingSecret.name. |
| Install fails: "Could not read the kube-system namespace" | The chart was rendered without cluster access. Set cluster.name. |
401 in the collector log | The key is wrong, revoked, or is not a Kubernetes collector key. |
403 with plan_feature | Your workspace's plan does not include Kubernetes monitoring. |
403 with cluster_limit | The environment already has 50 clusters. Remove clusters you no longer use. |
| No container logs | collection.logs, Pod Security admission (see Prerequisites), and kubectl -n ziplogger logs ds/ziplogger-agent. |
| Cluster shows not reporting | Outbound HTTPS from the nodes (firewall, proxy), and the state of the collector pod. |
Upgrade
helm repo update
helm upgrade ziplogger ziplogger/ziplogger -n ziplogger --reuse-values
# If something looks wrong, go back to the previous revision
helm rollback ziplogger -n ziplogger
Node agents are replaced a quarter of the nodes at a time. The cluster collector is replaced in place: it runs as a single replica on purpose, so it never sends the same change twice. The chart version and the collector version are shown on the cluster's page in ZipLogger.
Rotate the key
- Create a new key on Kubernetes → Install, in the same environment as the old one.
- Give the cluster the new key.
With# If the chart manages the Secret (you installed with --set apiKey) helm upgrade ziplogger ziplogger/ziplogger -n ziplogger --reuse-values \ --set apiKey=zk_NEW... # If you use your own Secret: update it, then restart both collectors kubectl -n ziplogger create secret generic ziplogger-key \ --from-literal=api-key=zk_NEW... --dry-run=client -o yaml | kubectl apply -f - kubectl -n ziplogger rollout restart deploy/ziplogger-cluster ds/ziplogger-agenthelm upgrade, both collectors restart on their own, because their pods track a checksum of the chart's Secret. A Secret you manage is outside Helm's view, so restart them yourself. - Check that the collectors are reporting (the Last report column on the cluster's page), then revoke the old key under Settings → API keys.
The cluster keeps its identity and its history: the key only decides where the data goes.
Uninstall and remove data
helm uninstall ziplogger -n ziplogger
kubectl delete namespace ziplogger
helm uninstall removes only what the chart created: the two collectors, their
ServiceAccounts, cluster roles and bindings, ConfigMaps, Services and (unless you used your own)
the key Secret. None of your workloads are changed. The namespace and a Secret you created
yourself stay until you delete them.
ZipLogger keeps the cluster's history after you uninstall. It cannot tell an uninstall from an outage, so about 10 minutes after the collectors stop, the cluster shows not reporting and the problem Cluster stopped reporting opens an incident.
To remove the cluster from ZipLogger, uninstall the chart first, then:
- Open Kubernetes → Clusters and the cluster.
- Click rename next to the cluster's name, then Remove cluster (Admins only), and confirm.
Removing a cluster resolves every open incident it raised, including Cluster stopped reporting, and deletes what ZipLogger stored about it: its objects, events, problems and the monitors you scoped to it. Container logs and application telemetry it sent are ordinary logs, traces and metrics, and age out with your normal retention. If the chart is still installed, the cluster reappears on the next export.
A cluster that sends nothing for 30 days is removed the same way automatically.
What it collects, and its permissions
Read-only access. Both collectors have their own ServiceAccount and ClusterRole, and every
permission is get, list or watch. They cannot create,
change, delete, scale, restart or exec into anything. They cannot read Secrets or ConfigMaps.
| Collector | API group | Resources | Verbs |
|---|---|---|---|
| Cluster collector | core | nodes, namespaces, pods, services, persistentvolumeclaims, events | get, list, watch |
| apps | deployments, replicasets, statefulsets, daemonsets | get, list, watch | |
| batch | jobs, cronjobs | get, list, watch | |
| core | replicationcontrollers, resourcequotas (with metrics on) | get, list, watch | |
| autoscaling | horizontalpodautoscalers (with metrics on) | get, list, watch | |
| Node agent | core | nodes/stats (with metrics on) | get |
| core | pods, namespaces, nodes (to name the pod behind a log file or OTLP sender) | get, list, watch | |
| apps | replicasets | get, list, watch |
Not granted: write verbs, wildcards, Secrets, ConfigMaps, pods/exec,
pods/attach, pods/portforward and nodes/proxy.
What leaves the cluster. Kubernetes objects are filtered against an allow-list inside the cluster before anything is sent. Fields that are not on the list never leave.
- Sent: names, namespaces, UIDs, labels, owners and timestamps; workload settings such as replicas, rollout strategy, CronJob schedule and Job limits; Service type and ports; volume claim size and class; node taints; for each container its name, image, resource requests and limits, probe targets, volume mounts, the names of its environment variables, and the names of Secrets and ConfigMaps it refers to; every status field (phase, conditions, container states, exit codes, restart counts, replica counts, node conditions and capacity); Kubernetes events.
- Never sent: environment variable values, annotations (except a Deployment's revision number), container commands and arguments, exec probe commands, and the contents of any Secret or ConfigMap.
- Free text: event messages, container termination messages and container logs are sent as your cluster and applications wrote them. They can contain anything an application printed.
This is how ZipLogger can tell you that Secret "database-password" was not found
without being able to read any Secret: the name comes from Kubernetes' own error message and the
pod's reference to it.
The key lives in a Kubernetes Secret and reaches the collectors as an environment variable.
It is sent only as an X-Api-Key header over HTTPS.
The containers run as a non-root user with a read-only root filesystem, all Linux
capabilities dropped and the RuntimeDefault seccomp profile. The node agent mounts
/var/log/pods read-only when container logs are on. No container uses the host
network or runs privileged.
What you see in ZipLogger
- Kubernetes → Clusters: one card per cluster with its state (healthy, degraded, failing or not reporting), nodes ready, pods running, pods failing and open problems.
- A cluster's page: tabs for Overview, Workloads, Pods, Nodes and Events. The overview shows active problems, a namespace summary, the pods with the most restarts, node usage, and a Collectors table.
- A workload, pod or node: its state, containers, CPU and memory usage against limits, its pods, recent events, past problems, recent container logs, and a timeline of the last two hours that puts events, rollouts, problems, incidents, error logs and error traces in one order.
- Kubernetes → Problems: every detected problem, open first.
Collector health. Each collector reports on itself every minute, separately from its data. The Collectors table shows, per collector, what it sent, failed exports, data dropped because its queue was full, data refused to stay inside its memory limit, its queue, its memory and its Last report. If a collector drops data, the problem Collector dropping monitoring data opens. If ZipLogger receives nothing from a cluster for 10 minutes, the cluster shows not reporting, its last known state stays on screen, and Cluster stopped reporting opens. While data is late, other problems are neither opened nor resolved on stale state.
Problems and incidents
ZipLogger reads failures from Kubernetes' own structured state and events. A problem names the workload a person would fix (the Deployment, not each of its pods), how many pods are affected, the reason Kubernetes gave, the cause when Kubernetes stated one, and the evidence. When Kubernetes did not state a cause, the problem says so and shows the evidence instead of guessing.
| Problem | Opens an incident by default |
|---|---|
| Container crash-looping | Yes |
| Image cannot be pulled | Yes |
| Missing Secret / ConfigMap (config error) | Yes |
| Container cannot be created | Yes |
| Container OOMKilled | Yes |
| Pod cannot be scheduled | Yes |
| Pod stuck in Pending | Yes, after 15 minutes |
| Volume cannot be mounted | Yes |
| PersistentVolumeClaim not bound | Yes |
| Workload has unavailable replicas | Yes |
| Rollout stuck | Yes |
| Node not ready | Yes |
| Node under resource pressure | Yes |
| Job failed | Yes |
| CronJob failing | Yes |
| Cluster stopped reporting | Yes |
| Collector dropping monitoring data | Yes |
| Excessive container restarts | No (ready at 10 restarts in 60 minutes) |
| Health probe failing | No |
| Pod evicted | No |
| High CPU usage | No (ready at 90% of the limit for 10 minutes) |
| High memory usage | No (ready at 90% of the limit for 10 minutes) |
Problems are always detected and listed. A monitor decides which of them open an incident. Each environment gets the built-in monitors above the first time a cluster reports. Under Kubernetes → Monitors, Editors and Admins can switch them on or off, change their numbers and severity, and add monitors for one cluster or one namespace.
- One problem is one incident, however many pods and repeated events it involves.
- An incident resolves on its own once its problem has not been seen for 3 minutes.
- Nothing is judged until the first full listing of a cluster has arrived.
- Deleting things is not a failure: nothing is raised in a namespace that is being deleted, or for a workload, pod or Job that is being deleted.
- Severity auto uses the problem's own: critical when nothing of the workload is available, high otherwise.
Limits and retention
- Usage. Container logs and the telemetry your applications send through the in-cluster endpoint are ordinary log, trace and metric ingestion and count toward your plan like any other. The collector key uses one of your plan's API keys.
- Retention. Kubernetes events are kept for 7 days, deleted objects for 3 days and resolved problems for 30 days. A cluster that sends nothing for 30 days is removed. Container logs follow your log retention.
- Size. Up to 50 clusters per environment and 100,000 live objects per cluster.
- Container logs are collected from the moment the agent starts on a node; older lines are not sent. Lines over 256 KiB are cut. Any single field in an object is cut at 2,048 characters.
- Usage history. Node and pod usage is kept as history for charts. Per-container usage is kept as the latest value only. Network usage is not collected.
- Not monitored: the control plane itself (API server, etcd, scheduler metrics), and anything the Kubernetes API and kubelet do not report.
- Outages. The cluster collector holds up to 256 MiB of data on disk and retries a batch for up to 30 minutes. The node agents retry for up to 15 minutes. While ZipLogger cannot take more container logs, the agent waits and resumes from where it stopped, limited by your kubelet's log rotation. Anything dropped is counted and shown in the Collectors table. Object state heals at the next full listing after an outage.
Infrastructure monitoring
Hosts, the processes on them and the containers they run, from one OpenTelemetry Collector per host. Like
Kubernetes monitoring, it runs a small component on your side: the upstream
otel/opentelemetry-collector-contrib (0.163.1, pinned by digest) with a configuration written by
ZipLogger. It reads /proc, /sys and the Docker socket, read-only, and connects
outbound over HTTPS. Nothing listens on the host.
Install
- In ZipLogger, Infrastructure → Set up → Create host collector key (Admins). The key carries the
infrascope (hosts, processes, containers) and theingestscope (for the host's system logs). One key can serve every host. Keep it incollector.envonly. - Download the Collector bundle on the same page:
collector.yaml,compose.yaml, a systemd unit andinstall.sh, a Windows config,logs.yamland INSTALL.md. - Copy
collector.env.exampletocollector.env, paste the key, setZIPLOGGER_HOST_NAME(required in Docker) and optionalZIPLOGGER_HOST_TAGS(team:payments,role:web). - Run it. Docker:
docker compose up -d(the host's root is mounted read-only at/hostfs, the Docker socket read-only). Bare metal:sudo ./install.shdownloads the pinned Collector from the official release, verifies its checksum and enablesziplogger-host-collector.service. Windows: unpack the Collector, set the variables machine-wide and registercollector-windows.yamlas a service (INSTALL.md has the commands). - The host appears under Infrastructure → Hosts within a minute. Kubernetes nodes need no bundle:
the Helm chart's node agent reports them (
collection.hostMetrics, on by default since chart 0.2.0).
What leaves the host
Utilization and counters of CPU, memory, load, disk I/O, filesystems (device, mountpoint, usage), network
interfaces (bytes, packets, errors, drops, connection counts), paging and process counts; the busiest
processes (name, command line with password-like arguments masked by ZipLogger, owner, CPU, memory, threads,
descriptors, I/O rates; at most 100 per sample); containers (id, name, image, CPU, memory, network, block
I/O, PIDs, restarts); the host's OS, architecture, machine id and, when a cloud metadata service answers,
provider, region, zone, instance id and type. No file contents, no environment variables, no credentials.
System logs only if you enable logs.yaml, billed as logs.
Alerts
Seven monitors come built in and on: a host that stops reporting for 5 minutes, a filesystem at or above 90%, memory at or above 90% for 10 minutes, CPU at or above 95% for 15 minutes, load above twice the core count for 15 minutes, a container restarting 3 times in 15 minutes, and a container that restarted at its memory limit (an inference: Docker reports no kill reason; Kubernetes containers get the exact OOMKilled from the kubelet). Each one can be tuned, scoped to a tag or switched off, and a custom monitor can give some hosts other numbers. A match opens one incident through your escalation policies and channels, with the measured numbers as evidence, and resolves three minutes after the condition clears (a host that reports again resolves at once). Any infrastructure metric also works in the generic metric and anomaly monitors.
Limits
Your plan includes a number of monitored hosts (the Set up page says how many). A host beyond the limit is
refused with 403 plan_limit and the Collector logs it; existing hosts are never refused. A host
keeps its place after it stops reporting until an editor removes it on its page or it has been silent for 30
days. Containers count under their host; Kubernetes nodes count as hosts. Process snapshots are kept for 7 days,
container history for 30 days (never beyond your plan's retention), host metrics for your plan's metric retention.
Database monitoring On request
Database monitoring watches a PostgreSQL, MySQL or MariaDB server from the inside: its connections, the queries that take the most time, sessions stuck waiting on a lock, and queries that suddenly got slower. APM already shows how long your service waited on the database; this shows what the database itself was doing.
Who it helps. Backend developers and whoever owns the database. Example: an APM alert says
checkout got slower. The Databases page shows one normalized query, SELECT … FROM orders
WHERE customer_id = ?, whose mean time doubled in the last 15 minutes, and its plan now reads a whole
table instead of an index.
Setup
- On Databases, choose the engine and download the setup bundle. Create the API key it asks for: a database monitoring key, bound to one environment.
- Give the monitoring login read-only rights. PostgreSQL: the
pg_stat_statementsextension (shared_preload_librariesandcompute_query_id=on) and thepg_read_all_statsandpg_read_all_settingsroles;SELECTon your tables only if you want execution plans. MySQL / MariaDB:PROCESS,REPLICATION CLIENTandSELECTonperformance_schemaand the monitored database; MariaDB also needsperformance_schema=ON. - Run the OpenTelemetry Collector (contrib) with the bundle’s configuration, next to the database. That is the only thing you install; it sends outbound over HTTPS.
What you see
| View | PostgreSQL | MySQL / MariaDB |
|---|---|---|
| Connections against the server’s limit, over 24 hours | Yes | Yes |
| Normalized queries ranked by total time, calls or mean latency, with a trend per query | Yes. Literals are replaced, so one query shape is one row. | Yes, by statement digest |
| Blocking: sessions waiting on a lock | Yes: how many sessions are blocked and the longest wait | Counts and the longest wait only: who blocks whom is not collected |
| Execution plans | Yes, for the top queries (operators and estimates; table and column names are dropped) | MySQL 8 only, and a plan can take up to an hour to appear. Not MariaDB or MySQL 5.x. |
| Problems that can open an incident | No data for 5 minutes, connections at 80% of the limit, a session waiting on a lock for 30 seconds or more, and a query regression: mean latency at least twice the previous 15 minutes (with at least 50 calls and 10 ms). Off until an Editor turns alerts on for the instance; incidents then go through on-call routing. | |
Usernames, client addresses, application names and query parameters are never read.
Limits
- Engines: PostgreSQL, MySQL and MariaDB only. SQL Server, Oracle, MongoDB, DocumentDB and ClickHouse are not supported.
- History (charts, query trends, availability) is kept for 30 days and needs ZipLogger’s ClickHouse store; a view covers at most 14 days.
- The availability figure counts minutes in which the Collector reported. It cannot tell a stopped Collector from a stopped database, and it never pages.
- Not available: query samples and wait-event breakdowns, deadlock and replication views, index recommendations, and linking a database query to the APM traces that ran it.
Product analytics All plans
Events are things users did — checkout_started, product_viewed,
signup_completed — stored next to your logs and traces and joined on the same
correlation ids. That join is the feature: on any error pattern in Search, one click on
"What were users doing?" reconstructs the journey that led into the error
(product_viewed → add_to_cart → checkout_started → ❌), how many users were affected,
and where they are. No second analytics vendor, no id-stitching project.
From .NET it is one call on the SDK you already have
(ZipLogger.Extensions.Logging 0.5.0+ registers it for you — inject
IEventTracker; or use ZipLogger.Client directly):
analytics.Track("checkout_started", new { userId = cart.UserId, amount = cart.Total });
analytics.Identify(userId, anonymousId); // link pre-login events after sign-in
Tracking is buffered, batched, and retried in the background, and never blocks or throws in your
request path. Any other stack can POST /ingest/v1/events with the same API key as
logs — single object, array, or NDJSON.
Funnels and process reports
Three tools answer "how many people got through these steps?", and they count differently:
| Tool | What it counts | Use it for |
|---|---|---|
| Presence funnel (dashboard widget) | Each step on its own inside the time range (sessions by default, or users or events). Someone can count for step 3 without step 1. | A quick view of how many reached each step. It has no drop-off column, because it cannot say that anyone left. |
| Sequential funnel (dashboard widget) | Users who did the steps in order inside the query range (up to 31 days). Events without a user id are left out. | A rough conversion line next to your error and latency panels. |
| Process report (Product events → Processes) | A saved, versioned definition: 2 to 8 steps, counted per person or per session, each person with their own conversion window, from 1 second to 90 days after they entered. | A conversion rate you can stand behind, the exact people at each step, and a path to their sessions. |
Example. A "Checkout" process with the steps cart_viewed →
checkout_started → payment_submitted and a 1-hour window. Open the
checkout_started → payment_submitted step, look at the people who did not complete it,
split them by browser, and open one person’s session around that step: the replay (when that visit was
sampled), the browser measurements, and Investigate, which lines up the logs, traces and errors of that
session.
Every person who reached a step is in exactly one state for the next one:
- Progressed: did the next step before their window closed.
- Pending: their window is still open. This is not drop-off; they may still finish.
- Did not complete: the window closed, ZipLogger had the data for all of it, and the next step did not happen.
- Unknown: the window closed but part of it cannot be seen, for example because retention already removed those events. It is never counted as a confirmed non-completion.
The headline mature conversion rate counts only people whose window has closed and whose whole window ZipLogger could observe; everyone else’s progress is shown separately as provisional. A report over the last hour with a 7-day window therefore has no mature rate yet. Evidence is shown as facts, not causes: "no linked error recorded" or "no replay" (normal when replay is sampled) do not say why someone stopped.
Limits: only the first entry per person counts (no repeat attempts), the entry range is up to 92 days, a workspace keeps up to 100 saved reports, and Editors create and change them. Events that a storage cap deleted early are not visible to a report. Walkthrough: Funnel drop-off: who left and what they did next.
Retention analysis (Events → Retention) builds the classic cohort triangle: pick the
event that starts a cohort (signup, first_purchase), optionally the
event that counts as coming back, and daily or weekly buckets. Each row is everyone whose
first cohort event fell in that day or week; each cell, the share who returned N periods
later. Unfinished periods render as pending rather than as churn, and identity falls back
to the anonymous id, so device-level retention works before sign-in.
Cohorts are saved audiences — "users who purchased in the last 30 days",
optionally narrowed by property conditions (plan:eq:business). Pick one in the
Events toolbar and every view narrows to its members: overview, explorer and retention. A
process report takes a cohort as its audience; the dashboard funnel widget has no cohort
picker. Membership is a stored definition resolved live at query time, never a stale
list.
The Events page has the dashboard (volume over time with deployment markers, unique users,
sessions, countries, devices, top events) and an explorer with cursor paging. Every user gets a
profile assembled from their events; every session gets a journey view with the request's errors
interleaved; and each event name gets its own page with volume, failure-rate correlation, and
numeric property analysis (sum/avg/min/max of e.g. amount).
Privacy is default-on: raw IPs are never stored (location is an approximate in-process lookup,
and only if the operator installs a GeoIP database), User-Agent strings are classified then
discarded, URLs lose their query strings, and credential-shaped properties — keys like
password, values that look like API keys, JWTs, or card numbers — are redacted at
ingest. Event volume has a monthly allowance and a daily ceiling per plan, enforced with
429 + Retry-After; over-quota
batches are partially accepted, never dropped wholesale, and your app never sees an exception.
Already using Mixpanel? ZipLogger accepts Mixpanel-shaped payloads, so migrating is
usually two lines of configuration: point your SDK's API host at
/ingest/v1/mp and swap the project token for a ZipLogger ingest key. Your event
names, properties, identify calls and batching keep working as written, and
/import takes your historical export. See the
migration guide.
Browser automations All plans
Logs tell you a request failed. They do not tell you that your sign-in button stopped working. A browser automation does: under Automation → New automation, enter the page your journey starts on and a real Chromium opens in your browser window. Sign in, search, add to basket, check out. Every click, keystroke and selection is captured as a replayable step. Press Stop recording, name it, and ZipLogger replays the whole journey on a schedule, from every minute to once a day.
Nothing to install: the browser runs on our infrastructure and streams to your tab, so there is no extension, no download, and no test framework to learn. Passwords are captured as a reference, never as text. You supply the value once when you save the automation, and it is encrypted at rest and masked out of every screenshot, error message and log line afterwards.
When a step stops working you get the screenshot of the page at the moment it broke, the console errors, the failed network requests, page-load timing, and the step it stopped on. A failure only counts once every retry has failed, so a momentary blip does not wake anyone. Real failures open an alert on your Alerts page and notify by email, Slack, Microsoft Teams or webhook. With an AI key configured you also get a plain-English explanation of the likely cause, and if you have connected a repository, the commits that landed since that journey last passed, usually the fastest route from "checkout is broken" to "it was this commit".
Each automation records its success rate and response time over the last 30 days, so a journey that still passes but has doubled in duration is visible before it fails. Recorded steps carry several ways to find each element (test id, accessible role and name, label, text, then a CSS path), so an ordinary front-end change does not break the automation, and a step that has fallen back to its last-resort selector is flagged before it does. When that happens, the run also proposes a replacement derived from the element as it exists now, for you to apply in one click: suggested, never applied automatically, because a selector rewritten unattended is an automation that quietly starts checking the wrong button.
It knows what your server did. Every run carries a trace id, so if your application sends traces to ZipLogger the failing request is already filed under it: the run page shows the endpoint that threw and its message, and when a stack trace is present it opens a regression case with ranked suspect commits from blame. No configuration on your side, because the ids line up by construction.
Broken and flaky are not the same thing. A step that fails every run is broken; a step that fails one run in thirty is flaky: a slow page, an animation racing a click, an intermittent third-party widget. Paging for it is how teams learn to ignore the channel. Every automation shows per-step pass rate and timing across recent runs, and a failure notification says outright when the step it stopped on has been intermittent.
API steps. A journey can also call an endpoint directly, from inside the browser's own
session, so it goes out as the signed-in user, with the cookies the flow has already earned.
"The logged-in customer's cart endpoint returns their cart" becomes one step of the journey
instead of a second monitor with its own copy of the login. Set the method, headers, body and the
statuses you accept (201, 2xx, 200-204); tokens go in as
encrypted secret references, like passwords.
Run it from where it matters. An automation can be pinned to a region — the choices offered are whichever runners are actually online when you pick one, not a fixed list, so what you see is what's really available right now. And for an application that is not on the public internet (an intranet, a staging environment behind a VPN, an internal admin tool), you can run a private agent: one container inside your own network, enrolled from the Automation page with a single token shown once. It pulls work outbound over HTTPS, so you open no ports and change no inbound firewall rules, and recording works exactly as it does for a public site.
# The token above is shown once when you enrol the agent
docker run -d --restart unless-stopped --name ziplogger-agent \
-e Agent__ServerUrl=https://app.ziplogger.ai \
-e Agent__Token=zla_... \
-e Agent__MaxConcurrentRuns=2 \
ghcr.io/ziploggerhq/ziplogger-synthetics-agent:latest
Public image, nothing to build: docker pull
works with no login. Pin a specific version instead of :latest if you'd rather
upgrade on your own schedule — tags are published alongside each release.
Every plan includes automations: 1 on Free, 3 on Pro, 10 on Team, 25 on Business. Each run is given up to one minute before it is stopped and recorded as failed.
Git regression detection
Under Settings → Repositories, the fastest path is the GitHub App: click "Connect GitHub", install the app on your org or account, and pick repositories: no tokens to create, access is minted on demand and revocable from GitHub in one click. Alternatively, connect a GitHub repository with a fine-grained personal access token (Contents: Read-only), or a GitLab project (gitlab.com or self-managed) with an access token carrying read_repository. Nothing is cloned either way: when you run “Find the commit that caused this” on an error pattern, or Find suspect commit on an Issue, ZipLogger parses the stack trace and uses the GitHub API to blame only those files, ranking the most likely commits with author and diff context. The token is encrypted at rest; an optional AI pass writes a root-cause narrative and a suggested fix.
AI analysis: bring your own key
AI features run on your AI account: paste an Anthropic Claude, OpenAI, or Gemini API key under Settings → AI analysis (verified on save, encrypted at rest, never shown again; the model is optional). That unlocks plain-English search (“payment errors in the last 2 hours”), one-click error summaries, and regression root-cause analysis, metered by your plan's monthly AI requests. Only a bounded sample of the logs you explicitly analyze is sent (never anything in the background), and without a key every other feature works normally.
PagerDuty / Opsgenie All plans
If your rota already lives in PagerDuty or Opsgenie, ZipLogger pushes its incidents there and takes acknowledgements back. Add one under Alerts → Destinations:
- PagerDuty: paste an Events API v2 integration key from the service you want paged (PagerDuty → your service → Integrations). No PagerDuty login, OAuth app or REST token.
- Opsgenie: paste an API integration key and pick your region (US or EU).
- Choose which incidents go there: a minimum severity, and optionally one service or environment. Or switch off "send automatically" and name the destination in an escalation-policy step — "our on-call first, PagerDuty after ten minutes".
The credential is verified against the provider before anything is stored, encrypted at rest, and never shown again. Delivery goes through the same queue as an SMS page, so a provider outage is retried with backoff and every attempt is on the incident timeline. The external alert's dedupe key is the ZipLogger incident id, so no retry of ours can ever open a second alert over there.
Optional and separate: issue an inbound key and paste the URL into the tool's webhook settings. Someone acknowledging or closing the alert in PagerDuty or Opsgenie then marks the ZipLogger incident the same way (PagerDuty deliveries are signature-verified). Acknowledging in ZipLogger updates the tool too, without the two systems echoing each other.
Slack All plans
An installed Slack app, not a webhook: incidents post to the channel you choose as a message that updates in place as the incident moves through Acknowledged and Resolved, with Acknowledge, Assign to me and Resolve buttons that act on the real incident as the person who clicked. (Alert rules can still post to a plain Slack incoming webhook, see the channel table under Alerts; that is one-way and has no buttons.)
- A ZipLogger Admin opens Settings → Integrations → Slack → Connect Slack. Slack asks which workspace to add ZipLogger to and shows the two permissions it needs: post messages, and list public channels. Nothing else: it cannot read your messages, look up members' email addresses, or post to a channel it was not invited to. Whoever connects it is linked to their own ZipLogger account automatically.
- In Slack, invite the bot to each channel that should receive incidents:
/invite @ZipLogger. Public channels only; the app does not ask for access to private ones. - Under Alerts → Delivery → Destinations → Connect a destination → Slack, pick the channel from the list (fetched from Slack), choose a minimum severity and optionally one service or environment, and save.
- Click the buttons. The first time someone ZipLogger does not recognise clicks one, only they see a "Link your account" prompt; completing it once in a signed-in ZipLogger tab ties their Slack account to their ZipLogger account, and the buttons act as them from then on. Buttons follow the same roles as the app: Admins and Editors can act, Viewers cannot.
Acknowledging or resolving in ZipLogger, Slack or Teams updates the others, without the systems echoing each other, and every delivery and every click is on the incident timeline, named by who did it and where. Disconnect under Settings → Integrations revokes the app's access; Slack destinations pause until it is reconnected, and nobody has to link their account again afterwards.
Microsoft Teams All plans
Incident cards in the Teams channels you choose, with Acknowledge, Assign to me and Resolve buttons that act on the real incident as the person who clicked. The card updates in place, and so does the same incident everywhere else it was posted. The ZipLogger app is not in the Teams store yet: your Teams admin adds it from the package below.
- Download the Teams app package (a 9 KB zip holding the app manifest and two icons; it contains no secrets).
- In Teams: Apps → Manage your apps → Upload an app → Upload a custom app, choose the zip, then Add. To install it in a team, open See all your teams → ⋯ → Manage team → Apps → Upload an app and choose the zip there. This needs "Upload custom apps" allowed in your Teams admin center (Teams apps → Setup policies). Alternatively your admin uploads it once to your organisation's app catalog (Teams admin center → Manage apps → Upload new app), after which everyone finds it under Apps → Built for your org.
- The bot posts a welcome card with a one-time code, valid for 30 minutes. A ZipLogger Admin pastes it under Settings → Integrations → Microsoft Teams. Nothing about who installed the app decides which workspace it belongs to; only that code, used by a signed-in Admin, binds it.
- Add a channel under Alerts → Delivery → Destinations → Microsoft Teams. Each person then links their own Teams account once (Settings → Integrations → Link my account, send the code to the bot, confirm in the browser) so the buttons act as them.
Remove the app from the team to revoke its access at any time; ZipLogger notices and stops posting there. Acknowledging or resolving in ZipLogger, Slack or Teams updates the others, without the systems echoing each other.
Verified end to end in production in ZipLogger’s own Microsoft 365 tenant. An install in another organisation’s tenant uses the same steps but has not been verified yet, and your tenant’s app policies decide whether a custom app may be uploaded: contact us before you rely on it for paging.
Discord & Telegram
Incidents posted to a Discord channel or a Telegram chat. Each incident is one message, posted when it triggers and edited in place as it is acknowledged and resolved, so the channel shows where it stands rather than a stream of updates. Add one under Alerts → Destinations → + Connect a destination and pick the Tool:
| Discord | Telegram | |
|---|---|---|
| What you paste | Webhook URL: Discord → Server settings → Integrations → Webhooks → New webhook, pick the channel, then Copy webhook URL. | Bot token from @BotFather (/newbot), and Chat id or @channel: a chat id such as -1001234567890, or a public channel as @name. Add the bot to the chat first; in a channel it must be an admin that can post. |
| Checked before saving | ZipLogger reads the webhook. Nothing is posted. | ZipLogger calls getMe, then getChat, which fails unless the bot is in the chat. |
| The message | An embed coloured by state (red, amber, green) with status, severity, service, environment and a link to the incident. | Text with the title, status, severity, service and description, and an Open in ZipLogger button. |
Discord mentions are switched off on every message, so an incident title can never ping
@everyone. The webhook URL and the bot token are stored encrypted and never shown again. Like
other destinations, each has a minimum severity and an optional service and environment.
There are no acknowledge buttons in Discord or Telegram. Use the link to the incident, or acknowledge from the email, SMS or call.
Grafana data source All plans
No plugin to install. ZipLogger speaks the wire protocols Grafana's own Loki, Prometheus and Tempo data sources already use, so existing dashboards can read your ZipLogger logs, request metrics and traces. Point three built-in data sources at:
https://app.ziplogger.ai/grafana/loki # logs (Loki)
https://app.ziplogger.ai/grafana/prometheus # request metrics (Prometheus)
https://app.ziplogger.ai/grafana/tempo # traces (Tempo)
Authenticate each with an API key created with Can read data ticked, as a custom header
X-Api-Key or as the password of HTTP basic auth; a plain ingest key is refused. The key
decides the workspace and environment: a query cannot name another one, so a single Grafana can hold
several ZipLogger workspaces side by side.
# Logs
{service="checkout-api", severity="error"} |= "timeout"
count_over_time({service="checkout-api"}[5m])
# Metrics
ziplogger_request_p95_ms{service="checkout-api"}
ziplogger_request_error_count
Labels are ZipLogger's own: service, severity/level,
release, commit_sha, and any structured field as
field_<key>. The metric catalog is fixed (p95 and mean latency, request count,
error count, log count, log error count) because those are what the stores can compute honestly;
a query beyond the supported subset is refused with a message naming what works, rather than
answered with the wrong series.
MCP server: debug from your AI assistant
ZipLogger is an MCP (Model Context Protocol) server, so AI coding assistants like Claude Code and Cursor can query your workspace directly: ask "why is checkout failing in prod?" and the assistant searches your logs, clusters error patterns, walks trace waterfalls, and reads regression analyses by itself. Read-only. Two ways to authenticate: OAuth (sign in with your ZipLogger account when the assistant asks — no key to manage) or a read API key for headless setups.
Connect with OAuth (recommended)
Add https://app.ziplogger.ai/mcp as a connector or MCP server in your assistant —
in Claude, Settings → Connectors → Add custom connector; in Claude Code:
claude mcp add --scope user --transport http ziplogger https://app.ziplogger.ai/mcp
On first use the assistant opens a ZipLogger consent page; sign in with your account email and password to grant read-only access to your workspace. The connection is per-user, scoped to your workspace, and revocable at any time. A workspace that requires single sign-on cannot connect this way: use a read API key instead.
Connect with an API key
Use a key created with Can read data ticked (API keys). A plain ingest key is refused: it is meant to be shipped in browser code, where anyone can read it. A key reads only its own environment.
# Claude Code. --scope user registers the server for every project;
# without it it is added only to the directory you happen to run this from.
claude mcp add --scope user ziplogger --transport http https://app.ziplogger.ai/mcp \
--header "X-Api-Key: zk_..."
Confirm with claude mcp list. Servers connect at session start, so restart
Claude Code before the tools appear.
Cursor reads the same server from a JSON file instead: ~/.cursor/mcp.json
for every project, or .cursor/mcp.json inside a single repository.
// ~/.cursor/mcp.json
{
"mcpServers": {
"ziplogger": {
"type": "http",
"url": "https://app.ziplogger.ai/mcp",
"headers": { "X-Api-Key": "zk_..." }
}
}
}
Both files hold the key in plain text, so keep the per-project variants out
of version control: a committed .cursor/mcp.json hands your workspace key to
everyone with repository access. Prefer the home-directory file, and revoke a leaked key
under Settings → API keys.
Every tool is read-only; none can change or delete anything.
| Tool | What it returns |
|---|---|
list_environments | The workspace's environments. Over OAuth, the other tools take an optional environment argument and start in the default one; a read key always reads its own. |
search_logs | The newest log lines matching a query, with the same syntax as the Search page. |
list_error_patterns | Recent error logs grouped into patterns, with counts and an example. |
list_traces, get_trace | Recent traces, optionally only failing ones, and one trace's waterfall. |
get_services_status | Each service seen in the last 24 hours, its recent errors and health-check results. |
list_regressions | Errors traced to a suspect commit, with the AI root cause when one was run. |
get_incident_context, list_incidents | One incident with the figures around it (logs and requests against the window before, health check, browser checks) and the incidents near it; or the incidents of the last hours. |
list_issues, get_issue | Error Issues active in the last hours, new ones marked; one Issue with its latest stack trace and suspect commit. |
operation_latency | Per service and operation, database and outbound calls included: p95 and errors against the window before, only what changed. |
list_releases | Releases first seen and deploys finished in the last hours, with their commits. |
get_infrastructure, get_kubernetes, get_database_monitoring | Hosts and containers (CPU, memory, disk, restarts) and their problems; Kubernetes problems; database connections, blocking and statements that got slower. ZipLogger never queries your database. |
get_frontend | Page load figures per route against the window before, and replay session ids (never recordings). |
get_investigation | The latest AI investigation of an incident or Issue: status, ranked hypotheses, suspect changes and links to its evidence. |
FAQ
Logs aren't showing up. What should I check?
1) The API key is sent as X-Api-Key and hasn't been revoked. 2) The endpoint is
reachable from your app (the SDKs retry silently; check DroppedCount).
3) You haven't exhausted your daily quota; the Billing page shows today's usage. 4) The
ingestion response: 202 accepted, 429 quota, 401 bad key.
Can I import logs with historical timestamps?
Yes, set timestamp on each event. They land in the right time buckets, count
toward today's ingestion quota, and age out based on their own timestamp.
How do I connect a repository for regression detection?
Under Settings → Repositories, connect your GitHub repo with a fine-grained access token (Contents: Read-only, stored encrypted). Nothing is cloned; blame runs via the GitHub API on the files in the stack trace, on demand. See Git regressions.
Where does the AI send my data?
Only to the provider whose key you configured (Anthropic Claude, OpenAI, or Gemini), only when you invoke an AI feature, and only a bounded sample of the window you're analyzing. No key configured → the features hide themselves. See AI analysis.
Can my workspace require single sign-on?
Yes, on Enterprise plans. A workspace connects its own OpenID Connect identity provider (Microsoft Entra ID is supported; Google Workspace and other providers are available on request) and can then require it, which refuses password sign-in. SAML and SCIM are not available. See Single sign-on.
Ready to send your first log?
Free plan, two-line setup. Start free. No credit card required.
Start free