Skip to main content

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​

CallerData visible
Seat holder (lease)Their own activity only
Admin (admin token)All holders' activity, including holder names
AnonymousName-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 7d

Rotating the admin token​

  1. Delete the existing Secret and run a helm upgrade to 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-values

    Alternatively, point adminToken.existingSecret at a Secret you manage.

  2. Restart the components that read the token at startup:

    kubectl -n "$TENON_NS" rollout restart \
      deploy/"${FULLNAME}-licensed" \
      deploy/"${FULLNAME}-activityd" \
      deploy/"${FULLNAME}-dashboard"
  3. 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"
Off-cluster copies

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.ndjson

Import 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.ndjson

Alternatively, 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.ndjson

Restore from a PVC snapshot​

  1. 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
  2. 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.

  3. Start activityd again:

    kubectl -n "$TENON_NS" scale deploy/"${FULLNAME}-activityd" --replicas=1
  4. 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: false

Disabling activity events​

Set activity.enabled=false to skip deploying activityd entirely:

activity:
  enabled: false

With 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​

SymptomLikely cause
Dashboard / reads report activity store unavailable (503)activityd is down or not yet Ready. Writes are still buffered in Valkey.
Dashboard tile shows Behindactivityd is slow or its PVC is full. Check pod logs and PVC usage.
401 admin token requiredMissing or wrong admin token, or not signed in on the dashboard.
No holder names in /v1/statusExpected — holder names require the admin token.
A client's actions are rejected with 401 … too long agoEvents were queued offline longer than lateAcceptDays.
activityd can't write after a restoreThe 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.