abilian_cdn.records¶
What happened: the counters and the log.
records.audit¶
abilian_cdn.records.audit ¶
Who did what, to what, from where.
Every write worth asking about later leaves an entry here: objects uploaded, replaced and deleted, caches purged, tokens minted and revoked, zones created and reconfigured, people invited and disabled. Reads are not recorded — that is what the statistics are for, and a log with a line per download is one nobody reads.
The actor is described as well as referenced. A token that has since been revoked still has to have a name against the entries it left, or the log stops answering the question it exists for.
Actor
dataclass
¶
Whoever did it and where from, in a form that outlives them.
Entry
dataclass
¶
One thing that happened, before it is written down.
by_user ¶
by_user(
user: User, request: Request | None = None
) -> Actor
A person, named by the address they sign in with.
by_token ¶
A machine, named by whatever the token was called when it was made.
by_the_system ¶
by_the_system(what: str = 'the command line') -> Actor
Something with no credential to name — provisioning, a worker.
record ¶
record(
session: AsyncSession,
organisation_id: UUID,
entry: Entry,
) -> AuditEntry
Add one entry. The caller commits, with whatever it was recording.
Not committed here on purpose: an entry saying an object was deleted, in a transaction that then rolls back, is a lie — and one saying nothing when the delete succeeded is a gap. They go together or not at all.
about_object ¶
about_object(
session: AsyncSession,
zone: Zone,
path: str,
*,
action: str,
actor: Actor,
detail: dict[str, str] | None = None,
) -> None
The common case: something happened to one object in one zone.
recent
async
¶
recent(
session: AsyncSession,
organisation_id: UUID,
limit: int = 200,
) -> list[AuditEntry]
The most recent entries for one organisation, newest first.
forget_old
async
¶
Drop entries past retention.
Append-only means nothing is edited, not that nothing is ever dropped: a log kept for ever is a growing store of who did what, which is a liability rather than an asset once nobody is going to ask.
records.stats.counters¶
abilian_cdn.records.stats.counters ¶
Counting on the request path, and getting the counts to the database.
The request path never writes to the database. A delivered response bumps an in-memory counter; a background task upserts the counters every few seconds. Ten thousand downloads of one file in one hour cost one row and one update, rather than ten thousand inserts in front of the client.
That trade is deliberate and it is lossy: a process killed between flushes forgets its last few seconds. Statistics are for deciding what to cache and what to bill against, not for accounting, and paying a round trip per delivered byte-range would be the wrong price for the difference.
Counters ¶
The in-memory tally, drained by the flush task.
One per process. Not thread-safe and not trying to be: an asyncio worker runs one event loop, and the increments below never await, so no request can observe a half-applied one.
CountingMiddleware ¶
Bases: ASGIMiddleware
Counts what actually reached the client, on the way out.
Bytes are measured here rather than taken from Content-Length because a download that stops half way costs what it transferred, and a range request never claimed the whole object to begin with. The status comes from the response too, so a 404 or a refused hotlink is counted as one — Litestar puts its exception handler between this and the handler exactly so that send wrappers see the error response.
The counters come from the application rather than from this instance: registering a Router deep-copies it, middleware included, so anything this object held would be a copy nobody else can read — an empty chart with no error anywhere to explain it.
outcome_of ¶
Which counter a response belongs to.
A delivered response is classified by where its bytes came from, and a refused one by why it was refused — those are the two questions the numbers get asked. 304 counts as delivered: the client got the answer it asked for, and it came from what we had.
flush
async
¶
Add drained counters to what the database already holds.
Counters for a zone that no longer exists are dropped rather than inserted: the slug in memory is whatever a client asked for, and most of what does not resolve was never a zone in the first place.
records.stats.reports¶
abilian_cdn.records.stats.reports ¶
Answering the questions: how much went out, to what, and how often.
What the dashboard renders and what the API returns, including the CSV a person opens in a spreadsheet.
Summary
dataclass
¶
What one zone did over a window, broken down by outcome.
hit_ratio
property
¶
Share of delivered responses that came off local disk.
None rather than zero when nothing was delivered: "no requests" and "every request missed" are different facts, and a chart that draws them the same way sends someone looking for a cache bug.
Point
dataclass
¶
One day of one zone's traffic.
PathCount
dataclass
¶
One path's share of a zone's traffic.
clamp_days ¶
A window anyone may ask for: at least a day, at most what is kept.
window ¶
The start of the days-day window ending today, in UTC.
summaries
async
¶
summaries(
session: AsyncSession,
zone_ids: Sequence[UUID],
since: datetime,
) -> dict[UUID, Summary]
Totals per zone since since, for every zone asked about.
daily
async
¶
One zone's traffic per day, oldest first, skipping days with none.
fill ¶
One point per day of the window, zeros included.
daily skips days nothing happened on, which is the right answer for a
table and the wrong one for a chart: a bar chart with the quiet days
missing draws a busy Monday next to a busy Friday and calls them
neighbours.
top_paths
async
¶
top_paths(
session: AsyncSession,
zone_id: UUID,
since: datetime,
limit: int = 50,
) -> list[PathCount]
The busiest paths of one zone, by request count.
downloads
async
¶
downloads(
session: AsyncSession,
zone_id: UUID,
paths: Sequence[str],
since: datetime,
) -> dict[str, int]
How many times each of these paths was delivered.
Hits and misses only: a HEAD asked about the object without taking it, and a 404 is not a download of anything.
records.stats.rollup¶
abilian_cdn.records.stats.rollup ¶
Folding hourly rows into daily ones, and dropping what has expired.
Hourly detail stops being interesting long before the traffic does, so a worker folds anything past the window into one row per day and deletes what is older than the retention.
compact
async
¶
Fold old hourly rows into daily ones and drop what is past retention.
Both halves in one transaction: a reader that saw the daily rows inserted but not the hourly ones deleted would count the same traffic twice.
records.stats.periods¶
abilian_cdn.records.stats.periods ¶
How a period is named, and how the rows inside one add up.
Shared by all three of counting, folding and reporting, because a day that started at a different hour in one of them would make the three disagree about the same traffic.