ReviewOS

loghqorg/loghq

A read-scoped project key, so a Stacks dashboard can read logs without a person's credentials

#14
Closed glennmichael123 opened this 9 days ago ·
9 days ago

A Stacks app's dashboard cannot read logs from loghq, because the only credential a machine can hold is an ingest key and ingest keys grant no read access.

Why now

The Stacks dashboard (buddy dev --dashboard) already has the seam for this. config.dashboard.providers.<domain>.driver accepts local or hq, resolution lives in storage/framework/core/defaults/app/Actions/Dashboard/dashboard-provider.ts, and a remote provider is wrapped so an unreachable backend renders an empty section with a reason instead of taking the page down.

Selecting hq today renders empty, with this reason, which is written into the framework as a constant:

No data. The hosted provider has no read API yet, so this section cannot be filled from it.

So the dashboard side is finished and waiting. This issue is the missing half.

The actual blocker, verified rather than assumed

It is not that there are no read endpoints. There are. The blocker is that every one of them authenticates a person.

app/Support/request-auth.ts:18 resolves a Bearer token through Auth.getUserFromToken(token), and routes/logs.ts:138 gates GET /api/projects/{projectId}/logs on the user it returns. A Bearer token therefore works, but the only issuable one belongs to a human: it carries that person's full scope, it dies when they leave, and putting it in an app's config is the wrong shape for a server-to-server read.

Meanwhile app/Models/Project.ts:29 has ingest_key, which is the credential an app already holds, and it is write-only by design.

So the gap is one missing credential type, not a missing API.

What would unblock it

A read-scoped project key, issued from project settings beside the ingest key:

  • scoped to one project, independent of any user account
  • read-only, and never accepted on POST /logs
  • presented as Authorization: Bearer <key>
  • revocable, and listed with a last-used timestamp

The endpoints it must reach, which already exist and need only to accept the new credential:

  • GET /api/projects/{projectId}/logs with the existing filters
  • GET /api/logs/{logId}

The shape the dashboard needs back

The Logs section renders declared types, so this is a contract rather than a preference. In storage/framework/core/defaults/app/Actions/Dashboard/Infrastructure/:

  • log-provider.ts declares DashboardLogQuery (page, perPage, search, type, source, project, range), DashboardLogPagination (page, perPage, total, totalPages) and DashboardLogOptions
  • log-dashboard.ts declares DashboardLogRecord and DashboardLogSummary, which three stx components import directly

Serving those field names as-is means the driver on the Stacks side is a fetch and a cast rather than a translation layer. Anything close but differently named costs a mapping that then has to be kept in step.

Not in scope

Writes. The seam is read-only and no provider writes.

Where this is tracked

Raised by Chris: HQ apps should be usable from the Stacks dashboard for HQ.training and other Stacks apps. The framework-side driver is tracked in stacksjs/stacks, blocked on this.

9 days ago

Correction to the "shape the dashboard needs back" section above. The declared types are real here, unlike the Errors section, but I overstated how closely they align. Checked against what loghq actually stores:

DashboardLogRecord : id, timestamp, type, source, message, project, stacktrace, file, createdAt, updatedAt
loghq STREAM_COLS  : id, level, message, channel, environment, release, framework, host, timestamp, created_at, trace_id, request_id

Renames are trivial and not worth discussing. Two things are not.

The levels do not map. DASHBOARD_LOG_TYPES is exactly ['error', 'warning', 'info', 'success'] (Dashboard/Infrastructure/log-dashboard.ts:3). loghq has debug, info, notice, warning, error, critical. So debug, notice and critical have nowhere to go, and nothing in loghq produces "success". DashboardLogSummary's per-type counts inherit the same problem, so a summary built from loghq data would be counting into four buckets that do not describe it.

This needs a decision rather than a mapping: either the dashboard's type list widens to cover real syslog levels, or loghq collapses on the way out and the section shows fewer levels than loghq holds. Collapsing silently is the bad option, because "critical" arriving as "error" is a loss a reader cannot see.

Three fields have no source. project, stacktrace and file are on DashboardLogRecord and loghq stores none of them. project is probably derivable from the project the key is scoped to. stacktrace and file may simply be empty for loghq-sourced rows, which is fine if the section renders that gracefully, and worth confirming before it is assumed.

What stands unchanged: the read-scoped project key, which is the actual blocker and is independent of the response shape.

My suggestion is to serve loghq's native field names with its native levels, and let the Stacks-side driver own the mapping, since that is where the decision about collapsing or widening belongs. That is a change from what this issue originally said, and it is the honest version.

Credit where due: the mismatch was caught by the session implementing the branch, not by me.

Sign in to comment on this issue.