NAME
Punk::Observe::Backend - where the configuration lives
SYNOPSIS
my $db = Punk::Observe::Backend->new(dsn => 'dbi:SQLite:dbname=cfg.db');
my $db = Punk::Observe::Backend->new(dsn => 'dbi:Pg:dbname=observe');
my $db = Punk::Observe::Backend->new(dbh => $existing_handle);
my $db = Punk::Observe::Backend->new(dsn => ..., backend => '+My::Backend');
$db->migrate; # safe from every process at boot
DESCRIPTION
Dashboards, alert rules, check targets and saved views are configuration: a handful of small rows, written by a person, read once per page render. They are not telemetry, and they do not go anywhere near the store - segments are immutable and the write-ahead log is append-only, which is the whole storage design.
So they live in a database, and this distribution ships one. Before 0.02 it shipped DDL and no database code, and every host had to write a seam to reach it; the result was that dashboards had a renderer, a validator, two tables and no reader, because nobody wrote one.
It is Perl, and the rest of this distribution is not
Deliberately. Everything that touches telemetry here is C because it runs per record or per row. This runs per page, over a handful of rows, and C would buy nothing but a second place for a memory bug.
The pool carries the pid
Every worker is a fork. A handle made before the fork and used after it is shared by two processes that both believe they own it, which corrupts the protocol under load rather than failing in a test. Keying the pool on the pid means a forked child misses the cache and connects for itself.
An explicit dbh is never pooled and never reconnected: it belongs to the caller.
THE SCHEMA
Version 1 is the nine tables sqitch/deploy/alerts.sql describes. The DDL is written once, with type tokens the dialect fills in, so a column cannot exist in one backend and not the other. t/0910-backend.t asserts the migration and the sqitch change describe the same tables and columns, so drift is a failing build.
Instants are nanoseconds in a BIGINT, never a timestamp type. That is the same rule the rest of the distribution follows and it is not a stylistic one: a nanosecond instant does not survive a double, and the two backends disagree about timestamp precision in ways that would make a dashboard mean something different depending on where it was stored.
METHODS
new
my $db = Punk::Observe::Backend->new(%opts);
On this class it is a factory: the backend is inferred from the dsn's driver, and backend overrides it - a leading + means a literal class name, anything else is relative to Punk::Observe::Backend::. On a subclass it is an ordinary constructor.
Options: dsn, user, password, attr (merged into the DBI connect attributes), dbh (an existing handle, used as-is), backend.
dbh
The handle for this process, connecting if it has to.
disconnect
Drop this process's pooled handle. Call it after migrating at boot and before forking a worker pool: keying the pool on the pid stops a child reusing the parent's handle and does nothing about it inheriting the descriptor, which SQLite does not allow. An explicit dbh is left alone, because it belongs to the caller.
migrate
my $version = $db->migrate;
my $version = $db->migrate($to);
Bring the schema up to date, or to a given version. Idempotent, forward-only, and safe to call from every process at boot: it takes the backend's own lock and re-reads the version under it, because the version checked before taking a lock is the one somebody else may have just changed.
schema_version
The name of the most recently deployed change, or undef for a database with nothing applied.
ddl
my $statements = $db->ddl;
The migration as this backend would run it. For tests and for generating the sqitch change; not part of the runtime path.
dialect
The type map for this backend.
dialect_name
Which engine this is, spelled as sqitch spells it. The dialect table is keyed by the same names, so a subclass does not have to say it twice.
LATEST
The newest schema version this release knows how to build. migrate with no argument goes here.
WRITING A BACKEND
Subclass this, or do not - backend => '+My::Class' takes any class answering to new, dbh, migrate and schema_version. Subclassing gets the migration runner and the pool; _lock and _on_connect are the two things a dialect actually has to say for itself.
SEE ALSO
Punk::Observe::Backend::SQLite, Punk::Observe::Backend::Pg, Punk::Plugin::Observe.
AUTHOR
LNATION, <email at lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under the Artistic License 2.0.