Skip 503s in error reports, stamp a build-time release, list every outbound connection
503 is what a draining relay answers during a planned restart, so it no longer becomes an event. Every other 5xx still does, and an error logged while answering a 503 is still reported. FN_RELEASE, baked into the image by `--build-arg FN_RELEASE=<sha>` (Dockerfile, and passed through by Scripts/staging-gate.sh), is sent as the event's release. Unset, empty or not a plain version string, and events carry none. The configuration, hosting and self-hosting pages, and the Spanish self-hosting page, said SMTP was the relay's only outbound code. The relay also dials Apple's push service, peer relays, Stripe, an S3 store and now the error collector. Each is listed with the setting that turns it on, ending on what a relay with all of them unset opens: nothing. self-hosting.leaf is generated from flamenet-e2e SELF-HOSTING.md, so .e2e-ref moves to 310c315, that change made on the pinned commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
98dcc210ad
commit
1f8cf1ef82
@@ -1 +1 @@
|
||||
50fcc9992e81b294091e23ee1a72923899d4db1c
|
||||
310c315155833ef964962d55d74b6bb9faa3ab49
|
||||
|
||||
@@ -41,6 +41,12 @@ ENV FN_DB=/data/relay.sqlite \
|
||||
FN_ATTACHMENTS_DIR=/data/attachments
|
||||
VOLUME ["/data"]
|
||||
|
||||
# The commit this image was built from, for error reports
|
||||
# (`--build-arg FN_RELEASE=<sha>`). Last, so a new sha does not invalidate
|
||||
# the layers above. Unset leaves it empty and reports carry no release.
|
||||
ARG FN_RELEASE=
|
||||
ENV FN_RELEASE=${FN_RELEASE}
|
||||
|
||||
USER relay
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=5 \
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED by Scripts/build-doc-pages.sh from docs/CONFIGURATION.md.
|
||||
Edits made HERE are deleted the next time that script runs.
|
||||
Change the markdown source and re-run it.
|
||||
source-sha256: 9d20067215874430 -->
|
||||
source-sha256: 6d54d991e7466090 -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="configuring-a-relay">Configuring a relay</h1>
|
||||
@@ -264,17 +264,28 @@ it logs request URLs, and a protocol URL contains a device id.</td>
|
||||
<td>Error reporting to a Sentry-protocol collector you run (GlitchTip),
|
||||
as
|
||||
<code>http://<key>@<host>:<port>/<project></code>.
|
||||
Unset sends nothing. Set, errors, criticals and 5xx responses each
|
||||
become one event: the message with account ids, device ids, addresses,
|
||||
tokens, emails and phone numbers removed, the error type, the route
|
||||
template (<code>/e2e/devices/:deviceID</code>, never the path asked
|
||||
for), method, status and the source line. No headers, bodies, query
|
||||
strings, addresses or logger metadata. Warnings stay on stdout; info and
|
||||
debug are never sent. At most 30 events a minute, one per fault per 60
|
||||
seconds, 2 second timeout, no retries. The collector keeps what it
|
||||
receives for as long as it is configured to, so point it at one whose
|
||||
retention you have chosen. A value that is not a DSN stops the relay at
|
||||
start-up.</td>
|
||||
Unset sends nothing. Set, errors, criticals and 5xx responses other than
|
||||
503 (what a draining relay answers during a restart) each become one
|
||||
event: the message with account ids, device ids, addresses, tokens,
|
||||
emails and phone numbers removed, the error type, the route template
|
||||
(<code>/e2e/devices/:deviceID</code>, never the path asked for), method,
|
||||
status and the source line. No headers, bodies, query strings, addresses
|
||||
or logger metadata. Warnings stay on stdout; info and debug are never
|
||||
sent. At most 30 events a minute, one per fault per 60 seconds, 2 second
|
||||
timeout, no retries. The collector keeps what it receives for as long as
|
||||
it is configured to, so point it at one whose retention you have chosen.
|
||||
A value that is not a DSN stops the relay at start-up.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_RELEASE</code></td>
|
||||
<td>unset</td>
|
||||
<td>The release error reports are tagged with. Baked into the image at
|
||||
build time, normally the commit sha:
|
||||
<code>docker build --build-arg FN_RELEASE=$(git rev-parse --short HEAD)</code>,
|
||||
or <code>FN_RELEASE=<sha> Scripts/staging-gate.sh</code>, which
|
||||
passes it on. Letters, digits and <code>._-+</code>, at most 64
|
||||
characters; anything else, or unset, and reports carry no release. Read
|
||||
only when <code>FN_SENTRY_DSN</code> is set.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -334,10 +345,30 @@ graph is spread across operators you do not run. That is the honest
|
||||
reason this is off by default. If your relay exists so one group can
|
||||
talk without anyone else involved, <code>closed</code> is the correct
|
||||
answer, not the cautious one.</p>
|
||||
<p>A closed relay opens no outbound connections at all. The only
|
||||
outbound code in the relay is the SMTP client below, itself disabled
|
||||
unless configured. No telemetry, no update check, no directory to
|
||||
register with.</p>
|
||||
<p>The relay opens an outbound connection only for a feature you have
|
||||
switched on. Each one, and the setting that turns it on:</p>
|
||||
<ul>
|
||||
<li>Password-reset mail, over SMTP to <code>FN_SMTP_HOST</code>.</li>
|
||||
<li>Error reports, to the collector named in <code>FN_SENTRY_DSN</code>.
|
||||
Off unless set. It sends only to that address, and only scrubbed errors
|
||||
and 5xx responses other than 503.</li>
|
||||
<li>iOS wake-ups, to Apple's push service, when
|
||||
<code>FN_APNS_KEY_P8</code>, <code>FN_APNS_KEY_ID</code> and
|
||||
<code>FN_APNS_TEAM_ID</code> are all set.</li>
|
||||
<li>Federation, to peer relays, when <code>FN_FEDERATION</code> is
|
||||
<code>allowlist</code> or <code>open</code>. Sending to a peer also
|
||||
needs <code>FN_FEDERATION_KEY</code>.</li>
|
||||
<li>Donations, to <code>api.stripe.com</code>, when
|
||||
<code>FN_DONATE_SECRET</code> is set.</li>
|
||||
<li>Attachments, to an S3-compatible store, when
|
||||
<code>FN_S3_ENDPOINT</code> is set.</li>
|
||||
</ul>
|
||||
<p>It also connects to the databases you point it at: Postgres at
|
||||
<code>FN_POSTGRES_URL</code> and Redis at <code>FN_REDIS_URL</code>.
|
||||
Nothing reports to this project: there is no telemetry, no update check
|
||||
and no directory to register with. With every setting above unset, the
|
||||
relay keeps its data in a local SQLite file and opens no outbound
|
||||
connections.</p>
|
||||
<h2 id="shared-flamenet-accounts">Shared FlameNet accounts</h2>
|
||||
<p>Only for a relay that accepts identities from a FlameNet account
|
||||
service. Leaving all of these unset gives you a relay with its own user
|
||||
|
||||
@@ -289,13 +289,33 @@ no requiere ninguna configuración.</strong> <code>FN_FEDERATION</code> vale
|
||||
se registran, intercambian claves, se escriben y verifican tu registro de
|
||||
transparencia exactamente igual que en cualquier otro sitio. Un servidor
|
||||
cerrado es un producto completo, no uno recortado.</p>
|
||||
<p>Y está cerrado de verdad, no de nombre. El servidor tiene una sola pieza
|
||||
de código de red saliente, el cliente SMTP para el correo de restablecimiento
|
||||
de contraseña, y está desactivada salvo que configures un servidor de correo.
|
||||
Con <code>FN_SMTP_HOST</code> sin definir y la federación cerrada, el proceso
|
||||
<strong>no hace ninguna conexión saliente</strong>. Habla con su base de datos
|
||||
y con quien lo llama, y con nadie más. No hay telemetría, ni comprobación de
|
||||
actualizaciones, ni directorio en el que registrarse.</p>
|
||||
<p>Y está cerrado de verdad, no de nombre. El servidor solo abre una
|
||||
conexión hacia fuera para una función que hayas activado. Estas son todas, con
|
||||
el ajuste que enciende cada una:</p>
|
||||
<ul>
|
||||
<li>El correo para restablecer la contraseña, por SMTP hacia
|
||||
<code>FN_SMTP_HOST</code>.</li>
|
||||
<li>Los informes de errores, hacia el recolector indicado en
|
||||
<code>FN_SENTRY_DSN</code>. Están apagados mientras no lo definas. Solo se
|
||||
envían a esa dirección, y solo contienen errores ya limpios de identificadores
|
||||
y respuestas 5xx que no sean 503.</li>
|
||||
<li>Los avisos que despiertan a un iPhone, hacia el servicio de notificaciones
|
||||
de Apple, cuando están definidos <code>FN_APNS_KEY_P8</code>,
|
||||
<code>FN_APNS_KEY_ID</code> y <code>FN_APNS_TEAM_ID</code>.</li>
|
||||
<li>La federación, hacia otros servidores, cuando <code>FN_FEDERATION</code>
|
||||
vale <code>allowlist</code> u <code>open</code>. Para enviar a otro servidor
|
||||
hace falta además <code>FN_FEDERATION_KEY</code>.</li>
|
||||
<li>Las donaciones, hacia <code>api.stripe.com</code>, cuando está definido
|
||||
<code>FN_DONATE_SECRET</code>.</li>
|
||||
<li>Los adjuntos, hacia un almacenamiento compatible con S3, cuando está
|
||||
definido <code>FN_S3_ENDPOINT</code>.</li>
|
||||
</ul>
|
||||
<p>También se conecta a las bases de datos que le indiques: Postgres en
|
||||
<code>FN_POSTGRES_URL</code> y Redis en <code>FN_REDIS_URL</code>. Nada
|
||||
informa a este proyecto: no hay telemetría, ni comprobación de
|
||||
actualizaciones, ni directorio en el que registrarse. Con todos esos ajustes
|
||||
sin definir, el servidor guarda sus datos en un archivo SQLite local y no abre
|
||||
ninguna conexión hacia fuera.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED by Scripts/build-doc-pages.sh from docs/HOSTING.md.
|
||||
Edits made HERE are deleted the next time that script runs.
|
||||
Change the markdown source and re-run it.
|
||||
source-sha256: da3efb24c74a2759 -->
|
||||
source-sha256: daf5dd4c607184fc -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="running-your-own-relay">Running your own relay</h1>
|
||||
@@ -133,9 +133,30 @@ forever. Migrate the account service first, then the relays.</p>
|
||||
<pre><code>FN_FEDERATION=allowlist
|
||||
FN_FEDERATION_PEERS=relay.friend.example
|
||||
FN_FEDERATION_KEY=<base64 Ed25519 seed></code></pre>
|
||||
<p>A closed relay opens no outbound connections at all. No telemetry, no
|
||||
update check, no directory to register with. The only outbound code is
|
||||
the SMTP client, itself disabled unless configured.</p>
|
||||
<p>The relay opens an outbound connection only for a feature you have
|
||||
switched on. Each one, and the setting that turns it on:</p>
|
||||
<ul>
|
||||
<li>Password-reset mail, over SMTP to <code>FN_SMTP_HOST</code>.</li>
|
||||
<li>Error reports, to the collector named in <code>FN_SENTRY_DSN</code>.
|
||||
Off unless set. It sends only to that address, and only scrubbed errors
|
||||
and 5xx responses other than 503.</li>
|
||||
<li>iOS wake-ups, to Apple's push service, when
|
||||
<code>FN_APNS_KEY_P8</code>, <code>FN_APNS_KEY_ID</code> and
|
||||
<code>FN_APNS_TEAM_ID</code> are all set.</li>
|
||||
<li>Federation, to peer relays, when <code>FN_FEDERATION</code> is
|
||||
<code>allowlist</code> or <code>open</code>. Sending to a peer also
|
||||
needs <code>FN_FEDERATION_KEY</code>.</li>
|
||||
<li>Donations, to <code>api.stripe.com</code>, when
|
||||
<code>FN_DONATE_SECRET</code> is set.</li>
|
||||
<li>Attachments, to an S3-compatible store, when
|
||||
<code>FN_S3_ENDPOINT</code> is set.</li>
|
||||
</ul>
|
||||
<p>It also connects to the databases you point it at: Postgres at
|
||||
<code>FN_POSTGRES_URL</code> and Redis at <code>FN_REDIS_URL</code>.
|
||||
Nothing reports to this project: there is no telemetry, no update check
|
||||
and no directory to register with. With every setting above unset, the
|
||||
relay keeps its data in a local SQLite file and opens no outbound
|
||||
connections.</p>
|
||||
<p><strong>Federation costs metadata.</strong> Each relay in an exchange
|
||||
learns the other had traffic for it, so a slice of your users' social
|
||||
graph is spread across operators you do not run. If your relay exists so
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED by Scripts/build-doc-pages.sh from flamenet-e2e/SELF-HOSTING.md.
|
||||
Edits made HERE are deleted the next time that script runs.
|
||||
Change the markdown source and re-run it.
|
||||
source-sha256: 38f049224fdd009e -->
|
||||
source-sha256: 5f3dd761a60d7f48 -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="self-hosting">Self-hosting</h1>
|
||||
@@ -330,13 +330,31 @@ disabled by that choice: your users register, exchange keys, message
|
||||
each other, and verify your transparency log exactly as they would
|
||||
anywhere else. A closed relay is a complete product, not a crippled
|
||||
one.</p>
|
||||
<p>It is also closed rather than nominally closed. The relay has one
|
||||
piece of outbound network code in it, the SMTP client for password reset
|
||||
mail, and that is disabled unless you configure a mail server. With
|
||||
<code>FN_SMTP_HOST</code> unset and federation closed, the process makes
|
||||
<strong>no outbound connections at all</strong>. It talks to its
|
||||
database and to whoever dials it, and to nothing else. There is no
|
||||
telemetry, no update check, and no directory to register with.</p>
|
||||
<p>It is also closed rather than nominally closed. The relay opens an
|
||||
outbound connection only for a feature you have switched on. Each one,
|
||||
and the setting that turns it on:</p>
|
||||
<ul>
|
||||
<li>Password-reset mail, over SMTP to <code>FN_SMTP_HOST</code>.</li>
|
||||
<li>Error reports, to the collector named in <code>FN_SENTRY_DSN</code>.
|
||||
Off unless set. It sends only to that address, and only scrubbed errors
|
||||
and 5xx responses other than 503.</li>
|
||||
<li>iOS wake-ups, to Apple's push service, when
|
||||
<code>FN_APNS_KEY_P8</code>, <code>FN_APNS_KEY_ID</code> and
|
||||
<code>FN_APNS_TEAM_ID</code> are all set.</li>
|
||||
<li>Federation, to peer relays, when <code>FN_FEDERATION</code> is
|
||||
<code>allowlist</code> or <code>open</code>. Sending to a peer also
|
||||
needs <code>FN_FEDERATION_KEY</code>.</li>
|
||||
<li>Donations, to <code>api.stripe.com</code>, when
|
||||
<code>FN_DONATE_SECRET</code> is set.</li>
|
||||
<li>Attachments, to an S3-compatible store, when
|
||||
<code>FN_S3_ENDPOINT</code> is set.</li>
|
||||
</ul>
|
||||
<p>It also connects to the databases you point it at: Postgres at
|
||||
<code>FN_POSTGRES_URL</code> and Redis at <code>FN_REDIS_URL</code>.
|
||||
Nothing reports to this project: there is no telemetry, no update check
|
||||
and no directory to register with. With every setting above unset, the
|
||||
relay keeps its data in a local SQLite file and opens no outbound
|
||||
connections.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -46,7 +46,10 @@ trap cleanup EXIT
|
||||
fail() { echo "GATE FAILED: $1"; echo "--- candidate log tail ---"; docker logs --tail 40 ${STACK}-relay 2>&1 || true; exit 1; }
|
||||
|
||||
echo "GATE: building candidate image"
|
||||
docker build -t flamenet-relay:candidate ./src >/dev/null
|
||||
# FN_RELEASE (the commit sha being shipped) is baked into the image for error
|
||||
# reports: `sudo FN_RELEASE=<sha> ./src/Scripts/staging-gate.sh`. ./src is a
|
||||
# copy, not a checkout, so the sha cannot be read from it here.
|
||||
docker build --build-arg FN_RELEASE="${FN_RELEASE:-}" -t flamenet-relay:candidate ./src >/dev/null
|
||||
|
||||
echo "GATE: booting staging stack (throwaway db + redis, candidate relay)"
|
||||
cleanup
|
||||
|
||||
@@ -1643,6 +1643,7 @@ func errorReporter(for app: Application) throws -> ErrorReporter? {
|
||||
guard let dsn else { return nil }
|
||||
let reporter = ErrorReporter(
|
||||
dsn: dsn, environment: app.environment.name,
|
||||
release: ErrorReporting.releaseFromEnvironment(),
|
||||
transport: VaporClientTransport(
|
||||
client: app.client, logger: Logger(label: "flamenet.error-reporter")))
|
||||
ErrorReporting.install(reporter)
|
||||
|
||||
@@ -8,7 +8,9 @@ import Vapor
|
||||
// Error reporting to an operator-run Sentry-protocol collector (GlitchTip).
|
||||
//
|
||||
// Off unless FN_SENTRY_DSN is set. When it is, errors and criticals, and every
|
||||
// response with a 5xx status, become one event each. Warnings stay on stdout.
|
||||
// response with a 5xx status other than 503, become one event each. 503 is
|
||||
// what a draining relay answers during a planned restart, so it is not a
|
||||
// fault. Warnings stay on stdout.
|
||||
// Info and debug are never sent, because the `debug` level is the one that
|
||||
// writes request URLs and a protocol URL names a device.
|
||||
//
|
||||
@@ -288,6 +290,7 @@ enum SentryEnvelope {
|
||||
let level: String
|
||||
let logger: String?
|
||||
let environment: String
|
||||
let release: String?
|
||||
let message: Message
|
||||
let exception: Exception?
|
||||
let tags: [String: String]
|
||||
@@ -305,8 +308,8 @@ enum SentryEnvelope {
|
||||
}
|
||||
|
||||
static func encode(
|
||||
_ event: ReportEvent, environment: String, eventID: String = UUID().uuidString,
|
||||
now: Date = Date()
|
||||
_ event: ReportEvent, environment: String, release: String? = nil,
|
||||
eventID: String = UUID().uuidString, now: Date = Date()
|
||||
) throws -> Data {
|
||||
let id = eventID.replacingOccurrences(of: "-", with: "").lowercased()
|
||||
let iso = ISO8601DateFormatter()
|
||||
@@ -328,7 +331,7 @@ enum SentryEnvelope {
|
||||
])
|
||||
let payload = Payload(
|
||||
event_id: id, timestamp: stamp, level: sentryLevel(event.level),
|
||||
logger: event.logger, environment: environment,
|
||||
logger: event.logger, environment: environment, release: release,
|
||||
message: Message(formatted: event.message), exception: exception,
|
||||
tags: tags, fingerprint: [event.fingerprint])
|
||||
|
||||
@@ -386,6 +389,7 @@ struct VaporClientTransport: ErrorTransport {
|
||||
final class ErrorReporter: @unchecked Sendable {
|
||||
let dsn: SentryDSN
|
||||
let environment: String
|
||||
let release: String?
|
||||
private let transport: any ErrorTransport
|
||||
private let now: @Sendable () -> Date
|
||||
|
||||
@@ -400,12 +404,13 @@ final class ErrorReporter: @unchecked Sendable {
|
||||
private var recent: [String: Date] = [:]
|
||||
|
||||
init(
|
||||
dsn: SentryDSN, environment: String, transport: any ErrorTransport,
|
||||
perMinute: Int = 30, dedupeWindow: TimeInterval = 60,
|
||||
dsn: SentryDSN, environment: String, release: String? = nil,
|
||||
transport: any ErrorTransport, perMinute: Int = 30, dedupeWindow: TimeInterval = 60,
|
||||
now: @escaping @Sendable () -> Date = { Date() }
|
||||
) {
|
||||
self.dsn = dsn
|
||||
self.environment = environment
|
||||
self.release = release
|
||||
self.transport = transport
|
||||
self.now = now
|
||||
self.capacity = Double(perMinute)
|
||||
@@ -423,7 +428,8 @@ final class ErrorReporter: @unchecked Sendable {
|
||||
guard admit(event.fingerprint) else { return false }
|
||||
let body: Data
|
||||
do {
|
||||
body = try SentryEnvelope.encode(event, environment: environment, now: now())
|
||||
body = try SentryEnvelope.encode(
|
||||
event, environment: environment, release: release, now: now())
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
@@ -493,6 +499,19 @@ enum ErrorReporting {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
/// FN_RELEASE, baked into the image at build time (the commit sha, as a
|
||||
/// Docker build argument). Unset, empty, or not a plain version string:
|
||||
/// nil, and events carry no release.
|
||||
static func releaseFromEnvironment(
|
||||
_ env: [String: String] = ProcessInfo.processInfo.environment
|
||||
) -> String? {
|
||||
guard let raw = env["FN_RELEASE"]?.trimmingCharacters(in: .whitespaces),
|
||||
!raw.isEmpty, raw.count <= 64,
|
||||
raw.allSatisfy({ $0.isASCII && ($0.isLetter || $0.isNumber || "._-+".contains($0)) })
|
||||
else { return nil }
|
||||
return raw
|
||||
}
|
||||
|
||||
/// Reads FN_SENTRY_DSN. Unset or empty: nil, nothing is sent. Set but
|
||||
/// unparseable: refuse to start, like every other malformed setting.
|
||||
static func dsnFromEnvironment(
|
||||
@@ -667,7 +686,7 @@ struct ErrorReportingMiddleware: AsyncMiddleware {
|
||||
request.logger[metadataKey: RequestReportContext.metadataKey] = nil
|
||||
request.storage[RequestReportContext.StorageKey.self] = nil
|
||||
let seen = context.snapshot
|
||||
guard status >= 500 || seen.level != nil else { return }
|
||||
guard Self.statusIsReported(status) || seen.level != nil else { return }
|
||||
let values = Self.identifyingValues(request)
|
||||
let message =
|
||||
seen.message.map { ReportScrubber.scrub($0, replacing: values) }
|
||||
@@ -680,6 +699,13 @@ struct ErrorReportingMiddleware: AsyncMiddleware {
|
||||
function: seen.function, line: seen.line, logger: "codes.vapor.request"))
|
||||
}
|
||||
|
||||
/// Every 5xx but 503, which a draining relay answers on purpose. An
|
||||
/// error logged while answering a 503 is still reported: the log line
|
||||
/// is the fault, not the status.
|
||||
static func statusIsReported(_ status: UInt) -> Bool {
|
||||
status >= 500 && status != 503
|
||||
}
|
||||
|
||||
/// Route parameters and query values of this request: removed from the
|
||||
/// message by value, so a username is caught even though nothing about
|
||||
/// its shape gives it away.
|
||||
|
||||
@@ -289,8 +289,8 @@ struct ErrorReportingTests {
|
||||
func statusOnlyReporting() async throws {
|
||||
let transport = RecordingTransport()
|
||||
try await withReportingApp(transport) { app in
|
||||
try await app.testing().test(.GET, "/unavailable") { res in
|
||||
#expect(res.status == .serviceUnavailable)
|
||||
try await app.testing().test(.GET, "/bad-gateway") { res in
|
||||
#expect(res.status == .badGateway)
|
||||
}
|
||||
try await app.testing().test(.GET, "/no/such/\(Self.accountID)") { res in
|
||||
#expect(res.status == .notFound)
|
||||
@@ -298,11 +298,76 @@ struct ErrorReportingTests {
|
||||
try await transport.waitFor(1)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
#expect(transport.count == 1)
|
||||
#expect(transport.bodies[0].contains(#""status":"503""#))
|
||||
#expect(transport.bodies[0].contains(#""status":"502""#))
|
||||
#expect(!Self.content(transport.bodies[0]).contains(Self.accountID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("a 503, what a draining relay answers during a deploy, is not reported")
|
||||
func serviceUnavailableIsNotReported() async throws {
|
||||
#expect(!ErrorReportingMiddleware.statusIsReported(503))
|
||||
for status: UInt in [500, 501, 502, 504, 507, 599] {
|
||||
#expect(ErrorReportingMiddleware.statusIsReported(status))
|
||||
}
|
||||
for status: UInt in [200, 404, 429, 499] {
|
||||
#expect(!ErrorReportingMiddleware.statusIsReported(status))
|
||||
}
|
||||
let transport = RecordingTransport()
|
||||
try await withReportingApp(transport) { app in
|
||||
for _ in 0..<3 {
|
||||
try await app.testing().test(.GET, "/unavailable") { res in
|
||||
#expect(res.status == .serviceUnavailable)
|
||||
}
|
||||
}
|
||||
try await Task.sleep(for: .milliseconds(200))
|
||||
#expect(transport.count == 0)
|
||||
// An error logged while answering 503 is a fault of its own.
|
||||
try await app.testing().test(.GET, "/unavailable-with-error") { res in
|
||||
#expect(res.status == .serviceUnavailable)
|
||||
}
|
||||
try await transport.waitFor(1)
|
||||
#expect(transport.count == 1)
|
||||
#expect(transport.bodies[0].contains("redis went away"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Release
|
||||
|
||||
@Test("FN_RELEASE becomes the release; unset, empty or malformed sends none")
|
||||
func releaseFromEnvironment() throws {
|
||||
#expect(ErrorReporting.releaseFromEnvironment([:]) == nil)
|
||||
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": ""]) == nil)
|
||||
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": " 98dcc21 "]) == "98dcc21")
|
||||
#expect(
|
||||
ErrorReporting.releaseFromEnvironment(["FN_RELEASE": "flamenet-relay@1.2.3+98dcc21"])
|
||||
== nil)
|
||||
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": "1.2.3+98dcc21"]) == "1.2.3+98dcc21")
|
||||
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": "a b"]) == nil)
|
||||
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": "x\"}"]) == nil)
|
||||
#expect(
|
||||
ErrorReporting.releaseFromEnvironment(["FN_RELEASE": String(repeating: "a", count: 65)])
|
||||
== nil)
|
||||
|
||||
let event = ReportEvent(level: .error, message: "m")
|
||||
let with = String(
|
||||
decoding: try SentryEnvelope.encode(event, environment: "p", release: "98dcc21"),
|
||||
as: UTF8.self)
|
||||
#expect(with.contains(#""release":"98dcc21""#))
|
||||
let without = String(
|
||||
decoding: try SentryEnvelope.encode(event, environment: "p"), as: UTF8.self)
|
||||
#expect(!without.contains("release"))
|
||||
}
|
||||
|
||||
@Test("a reporter built with a release stamps it on what it sends")
|
||||
func reporterSendsRelease() async throws {
|
||||
let transport = RecordingTransport()
|
||||
let reporter = ErrorReporter(
|
||||
dsn: Self.dsn, environment: "test", release: "98dcc21", transport: transport)
|
||||
#expect(reporter.capture(ReportEvent(level: .error, message: "boom")))
|
||||
try await transport.waitFor(1)
|
||||
#expect(transport.bodies.first?.contains(#""release":"98dcc21""#) == true)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// The event without its random id, timestamp and hash, which could
|
||||
@@ -367,6 +432,13 @@ struct ErrorReportingTests {
|
||||
app.get("unavailable") { _ -> String in
|
||||
throw Abort(.serviceUnavailable)
|
||||
}
|
||||
app.get("unavailable-with-error") { req -> String in
|
||||
req.logger.error("redis went away")
|
||||
throw Abort(.serviceUnavailable)
|
||||
}
|
||||
app.get("bad-gateway") { _ -> String in
|
||||
throw Abort(.badGateway)
|
||||
}
|
||||
do { try await body(app) } catch {
|
||||
try? await app.asyncShutdown()
|
||||
throw error
|
||||
|
||||
+21
-4
@@ -87,7 +87,8 @@ store keeps its own copy of each.
|
||||
| `FN_HOST` | `127.0.0.1` | Not reachable from outside the host as shipped. Terminate TLS in front of it. |
|
||||
| `FN_PORT` | `8080` | |
|
||||
| `FN_LOG_LEVEL` | `error` | Never raise to `debug` on a machine with real accounts: it logs request URLs, and a protocol URL contains a device id. |
|
||||
| `FN_SENTRY_DSN` | unset | Error reporting to a Sentry-protocol collector you run (GlitchTip), as `http://<key>@<host>:<port>/<project>`. Unset sends nothing. Set, errors, criticals and 5xx responses each become one event: the message with account ids, device ids, addresses, tokens, emails and phone numbers removed, the error type, the route template (`/e2e/devices/:deviceID`, never the path asked for), method, status and the source line. No headers, bodies, query strings, addresses or logger metadata. Warnings stay on stdout; info and debug are never sent. At most 30 events a minute, one per fault per 60 seconds, 2 second timeout, no retries. The collector keeps what it receives for as long as it is configured to, so point it at one whose retention you have chosen. A value that is not a DSN stops the relay at start-up. |
|
||||
| `FN_SENTRY_DSN` | unset | Error reporting to a Sentry-protocol collector you run (GlitchTip), as `http://<key>@<host>:<port>/<project>`. Unset sends nothing. Set, errors, criticals and 5xx responses other than 503 (what a draining relay answers during a restart) each become one event: the message with account ids, device ids, addresses, tokens, emails and phone numbers removed, the error type, the route template (`/e2e/devices/:deviceID`, never the path asked for), method, status and the source line. No headers, bodies, query strings, addresses or logger metadata. Warnings stay on stdout; info and debug are never sent. At most 30 events a minute, one per fault per 60 seconds, 2 second timeout, no retries. The collector keeps what it receives for as long as it is configured to, so point it at one whose retention you have chosen. A value that is not a DSN stops the relay at start-up. |
|
||||
| `FN_RELEASE` | unset | The release error reports are tagged with. Baked into the image at build time, normally the commit sha: `docker build --build-arg FN_RELEASE=$(git rev-parse --short HEAD)`, or `FN_RELEASE=<sha> Scripts/staging-gate.sh`, which passes it on. Letters, digits and `._-+`, at most 64 characters; anything else, or unset, and reports carry no release. Read only when `FN_SENTRY_DSN` is set. |
|
||||
|
||||
**HTTPS is not optional in practice.** Clients hold key material, and serving
|
||||
them over plain HTTP hands it to anyone on the path. Forward the client address
|
||||
@@ -115,9 +116,25 @@ operators you do not run. That is the honest reason this is off by default. If
|
||||
your relay exists so one group can talk without anyone else involved, `closed`
|
||||
is the correct answer, not the cautious one.
|
||||
|
||||
A closed relay opens no outbound connections at all. The only outbound
|
||||
code in the relay is the SMTP client below, itself disabled unless configured.
|
||||
No telemetry, no update check, no directory to register with.
|
||||
The relay opens an outbound connection only for a feature you have switched
|
||||
on. Each one, and the setting that turns it on:
|
||||
|
||||
- Password-reset mail, over SMTP to `FN_SMTP_HOST`.
|
||||
- Error reports, to the collector named in `FN_SENTRY_DSN`. Off unless set.
|
||||
It sends only to that address, and only scrubbed errors and 5xx responses
|
||||
other than 503.
|
||||
- iOS wake-ups, to Apple's push service, when `FN_APNS_KEY_P8`,
|
||||
`FN_APNS_KEY_ID` and `FN_APNS_TEAM_ID` are all set.
|
||||
- Federation, to peer relays, when `FN_FEDERATION` is `allowlist` or `open`.
|
||||
Sending to a peer also needs `FN_FEDERATION_KEY`.
|
||||
- Donations, to `api.stripe.com`, when `FN_DONATE_SECRET` is set.
|
||||
- Attachments, to an S3-compatible store, when `FN_S3_ENDPOINT` is set.
|
||||
|
||||
It also connects to the databases you point it at: Postgres at
|
||||
`FN_POSTGRES_URL` and Redis at `FN_REDIS_URL`. Nothing reports to this
|
||||
project: there is no telemetry, no update check and no directory to register
|
||||
with. With every setting above unset, the relay keeps its data in a local
|
||||
SQLite file and opens no outbound connections.
|
||||
|
||||
## Shared FlameNet accounts
|
||||
|
||||
|
||||
+19
-3
@@ -128,9 +128,25 @@ FN_FEDERATION_PEERS=relay.friend.example
|
||||
FN_FEDERATION_KEY=<base64 Ed25519 seed>
|
||||
```
|
||||
|
||||
A closed relay opens no outbound connections at all. No telemetry, no
|
||||
update check, no directory to register with. The only outbound code is the SMTP
|
||||
client, itself disabled unless configured.
|
||||
The relay opens an outbound connection only for a feature you have switched
|
||||
on. Each one, and the setting that turns it on:
|
||||
|
||||
- Password-reset mail, over SMTP to `FN_SMTP_HOST`.
|
||||
- Error reports, to the collector named in `FN_SENTRY_DSN`. Off unless set.
|
||||
It sends only to that address, and only scrubbed errors and 5xx responses
|
||||
other than 503.
|
||||
- iOS wake-ups, to Apple's push service, when `FN_APNS_KEY_P8`,
|
||||
`FN_APNS_KEY_ID` and `FN_APNS_TEAM_ID` are all set.
|
||||
- Federation, to peer relays, when `FN_FEDERATION` is `allowlist` or `open`.
|
||||
Sending to a peer also needs `FN_FEDERATION_KEY`.
|
||||
- Donations, to `api.stripe.com`, when `FN_DONATE_SECRET` is set.
|
||||
- Attachments, to an S3-compatible store, when `FN_S3_ENDPOINT` is set.
|
||||
|
||||
It also connects to the databases you point it at: Postgres at
|
||||
`FN_POSTGRES_URL` and Redis at `FN_REDIS_URL`. Nothing reports to this
|
||||
project: there is no telemetry, no update check and no directory to register
|
||||
with. With every setting above unset, the relay keeps its data in a local
|
||||
SQLite file and opens no outbound connections.
|
||||
|
||||
**Federation costs metadata.** Each relay in an exchange learns the other had
|
||||
traffic for it, so a slice of your users' social graph is spread across
|
||||
|
||||
Reference in New Issue
Block a user