Activity Events
Starting with chart version 0.3.0, the Tenon licensing server records Action Events: structured
log entries reporting what each tenon seat was used for (e.g. flexcli.flex.check with the flags
used, never their values). Events are stored durably by activityd in a SQLite database on a dedicated
PVC and are visible on the operator dashboard's Activity page.
In the commands below, <fullname> is <release>-licensing-self-host, or your fullnameOverride.
Architecture
tenon ─POST /v1/actions (lease)─▶ licensed ─XADD─▶ Valkey stream ─▶ activityd ─▶ SQLite (+ snapshots)
dashboard / admin ─GET /v1/activity (lease | admin token)─▶ licensed ─proxy─▶ activityd
Writes never depend on activityd. If activityd is down, licensed buffers events in the
Valkey stream (up to activity.bufferMaxLen, default 500,000 entries). When activityd comes back
up, it drains the buffer automatically.
Who can see what
| Caller | Data visible |
|---|---|
| Seat holder (lease) | Their own activity only |
| Admin (admin token) | All holders' activity, including holder names |
| Anonymous | Name-free aggregate totals only; no holder names, including in /v1/status |
Holder names are never exposed to anyone without the admin token.
What is never recorded
- File paths, flag values (except fixed choices like
--format svg), or free text - Anything that would identify file contents or user data
The server rejects any event containing fields outside the approved schema.
The admin token
The admin token is generated automatically on first install into Secret
<fullname>-admin-token (key token). It is preserved across upgrades and survives
helm uninstall. The dashboard uses it automatically for signed-in admin sessions.
export TENON_NS="tenon-licensing"
export TENON_RELEASE="flex-license"
export FULLNAME="${TENON_RELEASE}-licensing-self-host"
TOKEN=$(kubectl -n "$TENON_NS" get secret "${FULLNAME}-admin-token" \
-o jsonpath='{.data.token}' | base64 -d)Use the token with curl against the licensed API, or via the flexl admin CLI (a separate
binary provided by Tangram Flex as part of the deployment package):
# List recent activity for all holders — via curl
curl -H "Authorization: Bearer $TOKEN" \
"http://${FULLNAME}-licensed.${TENON_NS}.svc:8080/v1/activity?limit=5"
# Or via the flexl admin CLI (if provided by Tangram Flex)
FLEXL_ADMIN_TOKEN=$TOKEN flexl activity list --all --since 7dRotating the admin token
-
Delete the existing Secret and run a
helm upgradeto generate a new one:kubectl -n "$TENON_NS" delete secret "${FULLNAME}-admin-token" helm upgrade "$TENON_RELEASE" -n "$TENON_NS" flex-licensing/licensing-self-host \ --reuse-valuesAlternatively, point
adminToken.existingSecretat a Secret you manage. -
Restart the components that read the token at startup:
kubectl -n "$TENON_NS" rollout restart \ deploy/"${FULLNAME}-licensed" \ deploy/"${FULLNAME}-activityd" \ deploy/"${FULLNAME}-dashboard" -
Distribute the new token to the admins who manage activity data.
Retention
activity.retentionDays (default 365) controls how long events are kept. activityd enforces
this at startup and then daily. The same window also allows an expired lease to still read its own
activity history.
activity.lateAcceptDays (default 7) is how long after a lease expires its offline-queued events
are still accepted by the server.
Backup and restore
Automated snapshots
activityd takes an online SQLite snapshot (VACUUM INTO) every activity.backup.interval
(default 24h) to the backup PVC <fullname>-activityd-backup, keeping
activity.backup.keep (default 14) snapshots. Snapshot files are named
activity-<UTC timestamp>.db and are ordinary SQLite databases.
Check backup health:
kubectl -n "$TENON_NS" port-forward svc/"${FULLNAME}-activityd" 18091:8080 &
curl -s localhost:18091/v1/health \
-H "Authorization: Bearer $TOKEN" | jq '.backup, .db'
# Trigger an immediate snapshot
curl -s -XPOST localhost:18091/v1/backup/run \
-H "Authorization: Bearer $TOKEN"PVC snapshots on the same cluster do not survive losing the cluster. Keep a copy off-cluster using an NDJSON export (see below).
NDJSON portable export/import
Export and import go through the licensed service (it proxies to activityd). Port-forward to
licensed, not activityd:
kubectl -n "$TENON_NS" port-forward svc/"${FULLNAME}-licensed" 18080:8080 &
# Export (streams NDJSON to a file)
curl -s "localhost:18080/v1/activity/export" \
-H "Authorization: Bearer $TOKEN" \
-o activity.ndjsonImport into any running instance (safe to repeat — rows already present are skipped by event ID):
curl -s -XPOST "localhost:18080/v1/activity/import" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/x-ndjson" \
--data-binary @activity.ndjsonAlternatively, use the flexl admin CLI if provided by Tangram Flex:
FLEXL_ADMIN_TOKEN=$TOKEN flexl activity export --since 365d -o activity.ndjson
FLEXL_ADMIN_TOKEN=$TOKEN flexl activity import activity.ndjsonRestore from a PVC snapshot
-
Stop
activityd:kubectl -n "$TENON_NS" scale deploy/"${FULLNAME}-activityd" --replicas=0 # Wait for the pod to terminate kubectl -n "$TENON_NS" wait --for=delete pod -l app.kubernetes.io/component=activityd --timeout=60s -
Copy the snapshot over the database using a one-off pod. The pod must run as uid/gid 65532 and must delete the WAL files:
kubectl -n "$TENON_NS" run activity-restore --rm -i --restart=Never \ --image=busybox \ --overrides='{ "spec": { "securityContext": { "runAsUser": 65532, "runAsGroup": 65532, "fsGroup": 65532 }, "containers": [{ "name": "restore", "image": "busybox", "command": ["sh", "-c", "cp /backup/activity-<timestamp>.db /data/activity.db && rm -f /data/activity.db-wal /data/activity.db-shm"], "volumeMounts": [ {"name": "data", "mountPath": "/data"}, {"name": "backup", "mountPath": "/backup"} ] }], "volumes": [ {"name": "data", "persistentVolumeClaim": {"claimName": "<fullname>-activityd"}}, {"name": "backup", "persistentVolumeClaim": {"claimName": "<fullname>-activityd-backup"}} ] } }'Replace
<timestamp>with the snapshot filename and<fullname>with your release's full name. -
Start
activitydagain:kubectl -n "$TENON_NS" scale deploy/"${FULLNAME}-activityd" --replicas=1 -
Optionally catch up any events newer than the snapshot by importing an NDJSON export (idempotent — rows already present are skipped by event ID). Import goes through
licensed:kubectl -n "$TENON_NS" port-forward svc/"${FULLNAME}-licensed" 18080:8080 & curl -s -XPOST "localhost:18080/v1/activity/import" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/x-ndjson" \ --data-binary @activity.ndjson
SIEM integration
Every accepted event is also written as a structured JSON log line on licensed's stdout
("msg":"activity", with the event payload under event). Collect licensed's pod logs to get an
independent copy suitable for forwarding to your SIEM.
To disable stdout logging:
activity:
logToStdout: falseDisabling activity events
Set activity.enabled=false to skip deploying activityd entirely:
activity:
enabled: falseWith this setting, POST /v1/actions accepts and silently discards events (clients never break), and
activity read endpoints return 404. Holder names remain admin-only regardless of this setting.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Dashboard / reads report activity store unavailable (503) | activityd is down or not yet Ready. Writes are still buffered in Valkey. |
| Dashboard tile shows Behind | activityd is slow or its PVC is full. Check pod logs and PVC usage. |
401 admin token required | Missing or wrong admin token, or not signed in on the dashboard. |
No holder names in /v1/status | Expected — holder names require the admin token. |
A client's actions are rejected with 401 … too long ago | Events were queued offline longer than lateAcceptDays. |
activityd can't write after a restore | The snapshot was copied as root, making activity.db read-only to activityd (uid 65532). Redo the copy step with the correct uid/gid. |
NetworkPolicy note: the chart permits only licensed to reach activityd:8080. If you run with
networkPolicy.enabled=false under a service mesh, express the same rule in the mesh's policy.
activityd requires the admin token on every API call.