Tails the SLURM log files of a B-Fabric workunit in the browser, live.
| URL | Purpose |
|---|---|
/?jwt=<launch token> |
entry point for a B-Fabric app launch, and the OAuth redirect URI |
/log/<instance>/<workunit id> |
redirects to that workunit's primary log |
/log/<instance>/<workunit id>/<file name> |
the viewer page — bookmarkable |
/ws/<instance>/<workunit id>/<file name> |
the file's content, then its live tail (WebSocket) |
/health, /logout |
status, session teardown |
<instance> is a key of the instances config section (prod, test, trace). A launch
does not carry it in the URL: the instance is read from the launch token's iss claim and
matched against that config section, which doubles as the allowlist. Log URLs do name it,
because workunit IDs are not unique across instances — the test instance is a restore of
production, so after the restore the same ID means different jobs with identically named
log files in different directories.
There is exactly one way into a session: an OAuth authorization-code login (PKCE) against the B-Fabric instance named in the URL. Opening a log URL without a grant for that workunit — a bookmark, an expired cookie, someone else's link, a fresh launch — starts that login and returns to the requested log. That is why log URLs are worth bookmarking and sharing: the recipient logs in as themselves.
A launch from B-Fabric is therefore not a credential. Its short-lived JWT is read for two
claims only — iss (which instance) and entityId (which workunit) — and redirected to
/log/<instance>/<workunit id>, which logs the user in like any other log URL. The claims
are read without verifying the signature, because nothing depends on them: the resulting
URL grants nothing by itself, so a forged launch link buys a login prompt.
Whether a user may see a workunit's logs is decided by reading that workunit as them. If
B-Fabric does not return it, the answer is 404 — no file is read. That check happens once,
at login, and its result is what the signed cookie carries: the instance and the workunit
IDs B-Fabric confirmed. No token and no B-Fabric client is kept server-side, so sessions
survive a restart, and serving a log costs no API call. The cookie is the whole session, so
LOG_VIEWER_SECRET_KEY is what its integrity rests on — rotating that key logs everyone
out at once. session_ttl bounds an idle session: starlette re-signs the cookie whenever
the session changes, so recording a new grant extends the window for the grants already in
it.
The file describes the deployment; the environment carries what must not be committed.
CONFIG_FILE (default /app/config.yml) points at a YAML file — config/config.local.yml
is a minimal one, and the deployment's own lives in web-apps/log-viewer/config/config.yml.
It holds public_base_url and the instances map, and nothing else:
every other setting (oauth_scope, session_ttl, max_connections, max_file_size) has a
default in log_viewer/config.py, so name one only to override it. An unknown key is an
error rather than a no-op, and cookie_secure is derived from the public_base_url scheme
rather than set.
Credentials come from the environment (in the deployment, from its config/secrets.env):
LOG_VIEWER_SECRET_KEY— signs the session cookieLOG_VIEWER_CLIENT_ID_<INSTANCE>andLOG_VIEWER_CLIENT_SECRET_<INSTANCE>— that instance's OAuth client, one pair per instance in theinstancesmap
A missing one fails at startup, listing everything that is missing at once.
LOG_FILE (default /app/logs/app.log) is the app's own log: rotated at 10 MB, kept
10 days, also written to stdout.
Each instance is registered separately, and both halves of the resulting client go in the
deployment's secrets.env — nothing about a registration is committed. Every one of those clients
registers the same
redirect_uri — exactly {public_base_url}/, the same URL as the B-Fabric application's
weburl — because the app tells the instances apart by the launch token's iss, not by
the URL.
The only grant type the app uses is authorization_code. It never exchanges a launch
token and never refreshes: the client it builds from the redeemed code makes one read and is
discarded. bfabric.oauth.register_webapp() cannot express that — it always requests
token-exchange, refresh_token and authorization_code — so register least-privilege in two
steps instead (both need an employee bearer token):
from bfabric.oauth import register_client
oauth = register_client(
base_url=base_url,
token=employee_token,
client_name="Log Viewer",
redirect_uri=f"{public_base_url}/",
scope="api:read openid profile email groups",
grant_types=["authorization_code"],
)
client.save(
"application",
{
"name": "Log Viewer",
"type": "WebApp",
"weburl": f"{public_base_url}/",
"oauthclientid": oauth["id"],
},
)make test runs the test suite. There is no way to run the app without B-Fabric: OAuth is
the only way to get a session, so a local run needs a real instance.
export LOG_VIEWER_SECRET_KEY=$(openssl rand -base64 32)
export LOG_VIEWER_CLIENT_ID_TEST=<client id>
export LOG_VIEWER_CLIENT_SECRET_TEST=<client secret>
mkdir -p logs
while true; do echo "$(date '+[%Y-%m-%d %H:%M:%S]') INFO demo line" >> logs/workunitid-1.log; sleep 1; done &
CONFIG_FILE=config/config.local.yml LOG_FILE=logs/app-local.log make dev
# then launch from a workunit, or open the log URL directly and log in:
# http://localhost:8000/log/test/1/workunitid-1.logThe OAuth client's redirect_uri must be http://localhost:8000/, matching
public_base_url in that file.
make lint runs ruff, and make docker-build builds the image locally. CI runs the tests
and ruff on every push and pull request.
bfabric is resolved from bfabricPy's main branch rather than PyPI, because the
authorization-code helpers this app uses are not in a release yet; uv.lock pins the
commit, and make lock is what moves it. Drop the [tool.uv.sources] entry once bfabric
1.23.0 is on PyPI.
The image is published to ghcr.io/fgcz/log-viewer by the publish workflow, which builds
linux/amd64 and linux/arm64 (the deployment host is arm64) after the test suite passes
on the tagged commit:
# bump `version` in pyproject.toml and add its section to CHANGELOG.md, then
uv lock
git commit -am "Release 0.7.0"
git tag v0.7.0
git push && git push --tagsThere is one deployment, on fgcz-r-039 host port 9402, serving every configured B-Fabric
instance. It lives in the web-apps
repository under log-viewer/, which holds the production Compose file, config/config.yml
and config/secrets.env — nothing of the application itself. Deploying a release is bumping
IMAGE_TAG in that directory's .env to the new vX.Y.Z and running make deploy; see its
README. For a local run from source, see Development above.