feat(journal): surface Fedify logs via LogTape console sink

Inbound Mastodon Follows are being rejected 401 (signature
verification) on staging and Fedify's diagnostics were invisible:
it logs through LogTape, which is silent until configured.

- Configure LogTape with a console sink for the 'fedify' category when
  the federation instance is built; level via FEDERATION_LOG_LEVEL
  (default info).
- Staging deploy sets FEDERATION_LOG_LEVEL=debug for the soak —
  signature-verification detail per request; dial down once proven.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-06-06 21:41:28 +02:00
parent b9b1541dc6
commit 9a1ce61604
5 changed files with 37 additions and 0 deletions

View file

@ -131,6 +131,9 @@ jobs:
# rollout 12.2). FEDERATION_KEY_ENCRYPTION_KEY comes from the
# SOPS env decrypted above; previews never set this flag.
echo "FEDERATION_ENABLED=true"
# Verbose Fedify logs during the soak (signature verification
# detail). Dial down to info once federation is proven out.
echo "FEDERATION_LOG_LEVEL=debug"
} >> infrastructure/staging.env
- name: Copy compose file + env to server

View file

@ -25,6 +25,7 @@
// 202 for activity types without a registered listener, which is
// exactly the "acknowledge and drop" the spec wants; the same applies
// to wrapped types we inspect and ignore (e.g. Undo(Like)).
import { configure, getConsoleSink } from "@logtape/logtape";
import { createFederation, type Federation } from "@fedify/fedify";
import { Accept, Follow, Person, Reject, Undo } from "@fedify/fedify/vocab";
import { eq } from "drizzle-orm";
@ -96,8 +97,36 @@ async function findLocalPublicUserByIri(iri: URL | null): Promise<UserRow | null
}
let _federation: Federation<void> | null = null;
let _logtapeConfigured = false;
/**
* Fedify logs through LogTape, which is silent until configured and
* an unconfigured LogTape means signature-verification failures (the
* single most debuggable federation problem) vanish without a trace.
* Routed to the console so docker logs / Loki pick them up alongside
* pino. Level via FEDERATION_LOG_LEVEL (default "info"; set "debug"
* to see per-request signature verification detail).
*/
function configureFederationLogging(): void {
if (_logtapeConfigured) return;
_logtapeConfigured = true;
const level = (process.env.FEDERATION_LOG_LEVEL ?? "info") as "debug" | "info" | "warning";
configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: "fedify", lowestLevel: level, sinks: ["console"] },
// LogTape's meta logger warns about itself; keep it quiet unless
// something is really wrong.
{ category: ["logtape", "meta"], lowestLevel: "warning", sinks: ["console"] },
],
}).catch(() => {
// configure() throws if something else configured LogTape first —
// fine, their sinks win.
});
}
function buildFederation(): Federation<void> {
configureFederationLogging();
const federation = createFederation<void>({
kv: new PostgresKvStore(),
// Canonical origin so generated IRIs are correct behind the Caddy

View file

@ -14,6 +14,7 @@
"dependencies": {
"@fedify/fedify": "2.1.16",
"@js-temporal/polyfill": "^0.5.1",
"@logtape/logtape": "^2.0.5",
"@react-router/node": "catalog:",
"@react-router/serve": "catalog:",
"@sentry/node": "catalog:",

View file

@ -66,6 +66,7 @@ services:
# empty (flag off) so preview journals never emit federation traffic.
FEDERATION_ENABLED: ${FEDERATION_ENABLED:-}
FEDERATION_KEY_ENCRYPTION_KEY: ${FEDERATION_KEY_ENCRYPTION_KEY:-}
FEDERATION_LOG_LEVEL: ${FEDERATION_LOG_LEVEL:-}
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:3000/api/health || exit 1"]
interval: 15s

3
pnpm-lock.yaml generated
View file

@ -209,6 +209,9 @@ importers:
'@js-temporal/polyfill':
specifier: ^0.5.1
version: 0.5.1
'@logtape/logtape':
specifier: ^2.0.5
version: 2.1.1
'@react-router/node':
specifier: 'catalog:'
version: 7.16.0(react-router@7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.3)