commit 8008ede24ab1dde56c14ed552d91babc7a473269
parent 1bec5be07224d26f8e0c478aa33dd7ccf263846a
Author: mtmn <miro@haravara.org>
Date: Sat, 18 Apr 2026 17:35:33 +0200
fix: update docs; add metrics switch, fixes
.
Diffstat:
6 files changed, 208 insertions(+), 161 deletions(-)
diff --git a/docs/architecture.md b/docs/architecture.md
@@ -11,6 +11,7 @@ The server is built with PureScript running on Node.js. It handles several core
- **Last.fm Sync**: A background process that polls the Last.fm API every 60 seconds to fetch new scrobbles. Both syncs write to the same `scrobbles` table; duplicate timestamps are silently ignored.
- **Metadata Enrichment**: A background process that identifies scrobbles with missing metadata (genres, labels, release years) and fetches information from MusicBrainz, Last.fm, and Discogs.
- **Cover Art Proxy**: A specialized endpoint that fetches, caches, and serves cover art, utilizing a multi-source fallback strategy (CAA → Last.fm → Discogs).
+- **Observability**: Prometheus metrics exposed at `/metrics` and optional OpenTelemetry tracing of all HTTP requests.
### Frontend
A Single Page Application (SPA) built with [Elm](https://elm-lang.org).
@@ -36,6 +37,8 @@ Corpus runs as a single server process serving multiple users. User configuratio
### Routing
- `/` and `/~<slug>` — serve the Elm SPA for the root user and named users respectively
- `/proxy?user=<slug>`, `/stats?user=<slug>`, `/cover?user=<slug>` — shared API endpoints, user selected via query parameter
+- `/healthz?user=<slug>` — liveness check; pings the user's DuckDB connection
+- `/metrics` — Prometheus metrics (no user parameter; covers all users; only available when `METRICS_ENABLED=true`)
### Configuration
User configuration is split into two layers:
@@ -63,6 +66,9 @@ Each user gets their own `UserContext` with an independent DuckDB connection, sy
| `AWS_ENDPOINT_URL` | — | S3 endpoint (for S3-compatible storage) |
| `AWS_S3_ADDRESSING_STYLE` | — | `virtual` or `path` |
| `PORT` | `8000` | HTTP listen port |
+| `METRICS_ENABLED` | `false` | Set to `true` to enable the Prometheus `/metrics` endpoint |
+| `OTEL_EXPORTER_OTLP_ENDPOINT` | — | If set, enables OpenTelemetry tracing and exports spans to this OTLP HTTP endpoint |
+| `OTEL_SERVICE_NAME` | `corpus` | Service name reported in OTel spans |
### users.dhall Fields
@@ -110,6 +116,31 @@ When a cover is requested:
- Final fallback to **Discogs** search API.
3. If found in any source, the image is proxied to the client and uploaded to S3 in the background.
+## Observability
+
+### Prometheus Metrics
+
+Prometheus metrics are **disabled by default**. Set `METRICS_ENABLED=true` to enable them. When enabled, all HTTP requests are instrumented via `Metrics.wrapRequest` and background work is tracked with dedicated counters and gauges. When disabled, the `/metrics` endpoint returns 404 and all metric-increment calls are no-ops with no runtime overhead.
+
+| Metric | Type | Labels | Description |
+|---|---|---|---|
+| `corpus_http_requests_total` | Counter | `method`, `path`, `status` | HTTP requests |
+| `corpus_http_request_duration_seconds` | Histogram | `method`, `path` | HTTP request latency |
+| `corpus_sync_runs_total` | Counter | `user`, `source`, `result` | Sync loop iterations |
+| `corpus_sync_scrobbles_added_total` | Counter | `user`, `source` | Scrobbles inserted per sync |
+| `corpus_sync_last_success_seconds` | Gauge | `user`, `source` | Timestamp of last successful sync |
+| `corpus_enrichment_fetches_total` | Counter | `user`, `source`, `result` | Metadata enrichment API calls |
+| `corpus_enrichment_queue_size` | Gauge | `user`, `type` | Releases pending enrichment |
+| `corpus_cover_requests_total` | Counter | `user`, `source`, `result` | Cover art requests |
+| `corpus_db_backup_runs_total` | Counter | `user`, `result` | Database backup runs |
+| `corpus_db_backup_last_success_seconds` | Gauge | `user` | Timestamp of last successful backup |
+
+Node.js default metrics (GC, event loop, memory) are also collected via `prom-client`'s `collectDefaultMetrics`.
+
+### OpenTelemetry Tracing
+
+Tracing is opt-in: the OTel SDK starts only when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. Spans are exported over OTLP/HTTP. Each incoming HTTP request becomes a server span; outbound `fetch`/`undici` calls made during that request automatically become child spans via the `HttpInstrumentation` and `UndiciInstrumentation` auto-instrumentation packages. W3C `traceparent` headers are extracted from incoming requests for distributed trace propagation.
+
## Tech Stack
- **Language**: [PureScript](https://purescript.org) (server), [Elm](https://elm-lang.org) (frontend)
@@ -127,6 +158,7 @@ Corpus relies on FFI to interact with the Node.js ecosystem where native PureScr
- **Cloud Storage (`S3.js`)**: AWS SDK (`@aws-sdk/client-s3`) for cover art caching. Takes explicit config structs rather than reading `process.env`.
- **System Utilities (`Main.js`)**: Bridges PureScript with Node.js — `dotenv` loading and request helpers.
- **Config (`Config.js`)**: Reads and parses `users.json` from the path given by `CORPUS_USERS_FILE`.
+- **Observability (`Metrics.js`)**: Initialises `prom-client` (Prometheus) and the OpenTelemetry Node SDK. Exports metric-increment helpers called from PureScript and the `wrapRequest` function that wraps each HTTP handler in an OTel server span.
## System Flow
diff --git a/docs/duckdb.md b/docs/duckdb.md
@@ -7,7 +7,7 @@ Corpus uses [DuckDB](https://duckdb.org/) as its primary analytical database. Du
The database consists of two main tables:
### `scrobbles`
-Stores the raw listening history synced from ListenBrainz.
+Stores the raw listening history synced from ListenBrainz and/or Last.fm.
| Column | Type | Description |
| :--- | :--- | :--- |
diff --git a/src/Config.purs b/src/Config.purs
@@ -40,6 +40,7 @@ type UserEntry =
type AppConfig =
{ port :: Int
+ , metricsEnabled :: Boolean
, users :: Array UserEntry
}
@@ -88,6 +89,7 @@ loadConfig path = do
awsEndpointUrl <- liftEffect $ lookupEnv "AWS_ENDPOINT_URL"
awsS3AddressingStyle <- liftEffect $ lookupEnv "AWS_S3_ADDRESSING_STYLE"
databasePath <- liftEffect $ lookupEnv "DATABASE_PATH"
+ metricsEnabledStr <- liftEffect $ lookupEnv "METRICS_ENABLED"
defaultPath <- liftEffect cwd
let
resolvePath file = case databasePath of
@@ -106,7 +108,8 @@ loadConfig path = do
}
let
port = fromMaybe 8000 (portStr >>= Data.Int.fromString)
- fullConfig = { port, users: map (\u -> u { config = fillCreds u.config }) rawUsers }
+ metricsEnabled = metricsEnabledStr == Just "true"
+ fullConfig = { port, metricsEnabled, users: map (\u -> u { config = fillCreds u.config }) rawUsers }
case validateConfig fullConfig of
Left msg -> makeAff \cb -> cb (Left (error msg)) *> pure nonCanceler
Right cfg -> pure cfg
diff --git a/src/Db.purs b/src/Db.purs
@@ -98,21 +98,25 @@ backupDb conn dbFile s3cfg intervalMs slug = forever do
Right _ -> pure unit
-- Acquires the write lock, runs the action inside a transaction, then releases.
--- If the action throws, the transaction is rolled back before the lock is released.
+-- The lock is always released: on success, on action failure (rollback), and on
+-- BEGIN/COMMIT failure. This prevents deadlock if the database throws unexpectedly.
withTransaction :: forall a. Connection -> AVar Unit -> Aff a -> Aff a
withTransaction conn lock action = do
Avar.take lock
- run conn "BEGIN TRANSACTION" []
- result <- try action
+ result <- try do
+ run conn "BEGIN TRANSACTION" []
+ r <- try action
+ case r of
+ Left err -> do
+ void $ try $ run conn "ROLLBACK" []
+ throwError err
+ Right v -> do
+ run conn "COMMIT" []
+ pure v
+ Avar.put unit lock
case result of
- Left err -> do
- void $ try $ run conn "ROLLBACK" []
- Avar.put unit lock
- throwError err
- Right r -> do
- run conn "COMMIT" []
- Avar.put unit lock
- pure r
+ Left err -> throwError err
+ Right v -> pure v
queryAll :: Connection -> String -> Array Foreign -> Aff (Array Json)
queryAll conn sql params = makeAff \cb -> do
@@ -129,11 +133,9 @@ initDb conn = do
checkExists :: Connection -> Int -> Aff Boolean
checkExists conn ts = do
rows <- queryAll conn "SELECT 1 FROM scrobbles WHERE listened_at = ?" [ unsafeCoerce ts ]
- pure $ fromMaybe false $ do
- arr <- Just rows
- case arr of
- [] -> Just false
- _ -> Just true
+ pure case rows of
+ [] -> false
+ _ -> true
getOldestTs :: Connection -> Aff (Maybe Int)
getOldestTs conn = do
@@ -141,7 +143,8 @@ getOldestTs conn = do
pure $ do
row <- rows !! 0
obj <- toObject row
- ts <- Object.lookup "min_ts" obj >>= (unsafeCoerce >>> Just)
+ v <- Object.lookup "min_ts" obj
+ ts <- toMaybe (unsafeCoerce v :: Nullable Int)
if ts == 0 then Nothing else Just ts
upsertScrobble :: Connection -> Listen -> Aff Unit
diff --git a/src/Main.purs b/src/Main.purs
@@ -70,9 +70,8 @@ type UserContext =
listenBrainzUrl :: String -> String
listenBrainzUrl username = "https://api.listenbrainz.org/1/user/" <> username <> "/listens"
-fetchListenBrainzData :: String -> Int -> Aff String
-fetchListenBrainzData username count = withRetry "ListenBrainz fetch" $ makeAff \callback -> do
- let url = listenBrainzUrl username <> "?count=" <> show count
+fetchListenBrainzUrl :: String -> Aff String
+fetchListenBrainzUrl url = withRetry "ListenBrainz fetch" $ makeAff \callback -> do
req <- HTTPS.get url
req # on_ Client.responseH \res -> do
@@ -86,35 +85,7 @@ fetchListenBrainzData username count = withRetry "ListenBrainz fetch" $ makeAff
callback (Left $ Exception.error $ "ListenBrainz API returned status " <> show statusCode)
let errorH = EventHandle "error" mkEffectFn1
- on_ errorH
- ( \err -> do
- callback (Left err)
- )
- (unsafeCoerce req)
-
- pure nonCanceler
-
-fetchListenBrainzDataBefore :: String -> Int -> Aff String
-fetchListenBrainzDataBefore username maxTs = withRetry "ListenBrainz fetch" $ makeAff \callback -> do
- let url = listenBrainzUrl username <> "?count=100&max_ts=" <> show maxTs
- req <- HTTPS.get url
-
- req # on_ Client.responseH \res -> do
- launchAff_ do
- body <- readableToStringUtf8 (IM.toReadable res)
- let statusCode = IM.statusCode res
- liftEffect $
- if statusCode == 200 then
- callback (Right body)
- else
- callback (Left $ Exception.error $ "ListenBrainz API returned status " <> show statusCode)
-
- let errorH = EventHandle "error" mkEffectFn1
- on_ errorH
- ( \err -> do
- callback (Left err)
- )
- (unsafeCoerce req)
+ on_ errorH (\err -> callback (Left err)) (unsafeCoerce req)
pure nonCanceler
@@ -194,7 +165,7 @@ lbSyncOnce conn username slug writeLock initialSyncEnabled = void performFullSyn
where
performFullSync = do
when initialSyncEnabled $ Log.info $ "Starting ListenBrainz sync for " <> username
- result <- try $ fetchListenBrainzData username 100
+ result <- try $ fetchListenBrainzUrl (listenBrainzUrl username <> "?count=100")
case result of
Right body -> do
case parseJson body >>= decodeJson of
@@ -209,15 +180,11 @@ lbSyncOnce conn username slug writeLock initialSyncEnabled = void performFullSyn
let allExist = hitCount == length listens && length listens > 0
if allExist || length listens == 0 || not initialSyncEnabled then do
when (added > 0) $ Log.info $ "ListenBrainz sync complete. Added " <> show added <> " new scrobbles."
- liftEffect $ Metrics.incSyncRuns slug "listenbrainz" "success"
- when (added > 0) $ liftEffect $ Metrics.incSyncScrobbles slug "listenbrainz" added
- liftEffect $ Metrics.setSyncLastSuccess slug "listenbrainz"
+ recordSuccess added
else do
total <- paginateUntilDone 2 minTs added
Log.info $ "ListenBrainz sync complete. Added " <> show total <> " new scrobbles."
- liftEffect $ Metrics.incSyncRuns slug "listenbrainz" "success"
- when (total > 0) $ liftEffect $ Metrics.incSyncScrobbles slug "listenbrainz" total
- liftEffect $ Metrics.setSyncLastSuccess slug "listenbrainz"
+ recordSuccess total
Left err -> do
Log.error $ "Sync fetch error: " <> Exception.message err
liftEffect $ Metrics.incSyncRuns slug "listenbrainz" "error"
@@ -226,7 +193,7 @@ lbSyncOnce conn username slug writeLock initialSyncEnabled = void performFullSyn
Nothing -> pure acc
Just ts -> do
Log.info $ "Fetching ListenBrainz batch " <> show batchNum <> " (before " <> show ts <> ")..."
- result <- try $ fetchListenBrainzDataBefore username ts
+ result <- try $ fetchListenBrainzUrl (listenBrainzUrl username <> "?count=100&max_ts=" <> show ts)
case result of
Right body -> do
case parseJson body >>= decodeJson of
@@ -245,6 +212,11 @@ lbSyncOnce conn username slug writeLock initialSyncEnabled = void performFullSyn
Log.error $ "Sync fetch error: " <> Exception.message err
pure acc
+ recordSuccess n = do
+ liftEffect $ Metrics.incSyncRuns slug "listenbrainz" "success"
+ when (n > 0) $ liftEffect $ Metrics.incSyncScrobbles slug "listenbrainz" n
+ liftEffect $ Metrics.setSyncLastSuccess slug "listenbrainz"
+
processListens listens = do
syncRecursive 0 Nothing 0 listens
@@ -283,15 +255,11 @@ lfSyncOnce conn apiKey lfmUser slug writeLock initialSyncEnabled = do
allExist = hitCount == length validTracks && length validTracks > 0
if allExist || totalPages <= 1 || not initialSyncEnabled then do
when (added > 0) $ Log.info $ "Last.fm sync complete. Added " <> show added <> " new scrobbles."
- liftEffect $ Metrics.incSyncRuns slug "lastfm" "success"
- when (added > 0) $ liftEffect $ Metrics.incSyncScrobbles slug "lastfm" added
- liftEffect $ Metrics.setSyncLastSuccess slug "lastfm"
+ recordSuccess added
else do
total <- paginateLastfmUntilDone 2 totalPages Nothing added
Log.info $ "Last.fm sync complete. Added " <> show total <> " new scrobbles."
- liftEffect $ Metrics.incSyncRuns slug "lastfm" "success"
- when (total > 0) $ liftEffect $ Metrics.incSyncScrobbles slug "lastfm" total
- liftEffect $ Metrics.setSyncLastSuccess slug "lastfm"
+ recordSuccess total
paginateLastfmUntilDone page totalPages mTo acc
| page > totalPages = pure acc
@@ -323,6 +291,11 @@ lfSyncOnce conn apiKey lfmUser slug writeLock initialSyncEnabled = do
total <- paginateLastfmUntilDone 2 totalPages (Just (oldestTs - 1)) added
Log.info $ "Last.fm backfill complete. Added " <> show total <> " older scrobbles."
+ recordSuccess n = do
+ liftEffect $ Metrics.incSyncRuns slug "lastfm" "success"
+ when (n > 0) $ liftEffect $ Metrics.incSyncScrobbles slug "lastfm" n
+ liftEffect $ Metrics.setSyncLastSuccess slug "lastfm"
+
processLastfmTracks tracks = syncLastfmRecursive 0 0 (mapMaybe lastfmTrackToListen tracks)
syncLastfmRecursive acc hitCount listens = case uncons listens of
@@ -795,8 +768,8 @@ normalizePath path = case stripPrefix (Pattern "/~") path of
-- Request handler
-- API endpoints (/proxy, /stats, /cover, /healthz) select the user via ?user=<slug>.
-- Index pages are served at / (root user) and /~<slug> (named users).
-handleRequest :: Array UserContext -> Request -> Response -> Effect Unit
-handleRequest contexts req res = do
+handleRequest :: Boolean -> Array UserContext -> Request -> Response -> Effect Unit
+handleRequest metricsEnabled contexts req res = do
let method = IM.method req
let rawUrl = IM.url req
case URL.fromRelative rawUrl "http://localhost" of
@@ -808,7 +781,11 @@ handleRequest contexts req res = do
"/client.js" -> serveClientJs res
"/favicon.png" -> serveAsset "image/png" "assets/favicon.png" res
"/" -> serveIndex "" res
- "/metrics" -> serveMetrics res
+ "/metrics" ->
+ if metricsEnabled then serveMetrics res
+ else do
+ Log.warn "Path not found: /metrics"
+ serveNotFound res
"/proxy" -> withUser url \ctx -> serveProxy ctx.conn url res
"/stats" -> withUser url \ctx -> serveStats ctx.conn url res
"/cover" -> withUser url \ctx -> serveCover ctx.config ctx.slug url res
@@ -1352,7 +1329,7 @@ main = do
contexts <- traverse startUser appConfig.users
liftEffect $ do
server <- createServer
- server # on_ Server.requestH (handleRequest contexts)
+ server # on_ Server.requestH (handleRequest appConfig.metricsEnabled contexts)
let netServer = Server.toNetServer server
netServer # on_ listeningH do
diff --git a/src/Metrics.js b/src/Metrics.js
@@ -18,83 +18,98 @@ import {
} from "@opentelemetry/api";
// ---------------------------------------------------------------------------
-// Prometheus metrics
+// Prometheus metrics — only active when METRICS_ENABLED=true
// ---------------------------------------------------------------------------
-const registry = new Registry();
-
-collectDefaultMetrics({ register: registry });
-
-const httpRequestsTotal = new Counter({
- name: "corpus_http_requests_total",
- help: "Total number of HTTP requests",
- labelNames: ["method", "path", "status"],
- registers: [registry],
-});
-
-const httpRequestDurationSeconds = new Histogram({
- name: "corpus_http_request_duration_seconds",
- help: "HTTP request duration in seconds",
- labelNames: ["method", "path"],
- buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5],
- registers: [registry],
-});
-
-const syncRunsTotal = new Counter({
- name: "corpus_sync_runs_total",
- help: "Total number of sync runs",
- labelNames: ["user", "source", "result"],
- registers: [registry],
-});
-
-const syncScrobblesAddedTotal = new Counter({
- name: "corpus_sync_scrobbles_added_total",
- help: "Total scrobbles added during syncs",
- labelNames: ["user", "source"],
- registers: [registry],
-});
-
-const syncLastSuccessSeconds = new Gauge({
- name: "corpus_sync_last_success_seconds",
- help: "Unix timestamp of last successful sync run",
- labelNames: ["user", "source"],
- registers: [registry],
-});
-
-const enrichmentFetchesTotal = new Counter({
- name: "corpus_enrichment_fetches_total",
- help: "Total metadata enrichment fetches by user, source, and result",
- labelNames: ["user", "source", "result"],
- registers: [registry],
-});
-
-const enrichmentQueueSize = new Gauge({
- name: "corpus_enrichment_queue_size",
- help: "Number of releases pending metadata enrichment per user",
- labelNames: ["user", "type"],
- registers: [registry],
-});
-
-const coverRequestsTotal = new Counter({
- name: "corpus_cover_requests_total",
- help: "Total cover art requests by user, source, and result",
- labelNames: ["user", "source", "result"],
- registers: [registry],
-});
-
-const dbBackupRunsTotal = new Counter({
- name: "corpus_db_backup_runs_total",
- help: "Total database backup runs",
- labelNames: ["user", "result"],
- registers: [registry],
-});
-
-const dbBackupLastSuccessSeconds = new Gauge({
- name: "corpus_db_backup_last_success_seconds",
- help: "Unix timestamp of last successful database backup",
- labelNames: ["user"],
- registers: [registry],
-});
+const metricsEnabled = process.env.METRICS_ENABLED === "true";
+
+let registry = null;
+let httpRequestsTotal = null;
+let httpRequestDurationSeconds = null;
+let syncRunsTotal = null;
+let syncScrobblesAddedTotal = null;
+let syncLastSuccessSeconds = null;
+let enrichmentFetchesTotal = null;
+let enrichmentQueueSize = null;
+let coverRequestsTotal = null;
+let dbBackupRunsTotal = null;
+let dbBackupLastSuccessSeconds = null;
+
+if (metricsEnabled) {
+ registry = new Registry();
+ collectDefaultMetrics({ register: registry });
+
+ httpRequestsTotal = new Counter({
+ name: "corpus_http_requests_total",
+ help: "Total number of HTTP requests",
+ labelNames: ["method", "path", "status"],
+ registers: [registry],
+ });
+
+ httpRequestDurationSeconds = new Histogram({
+ name: "corpus_http_request_duration_seconds",
+ help: "HTTP request duration in seconds",
+ labelNames: ["method", "path"],
+ buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5],
+ registers: [registry],
+ });
+
+ syncRunsTotal = new Counter({
+ name: "corpus_sync_runs_total",
+ help: "Total number of sync runs",
+ labelNames: ["user", "source", "result"],
+ registers: [registry],
+ });
+
+ syncScrobblesAddedTotal = new Counter({
+ name: "corpus_sync_scrobbles_added_total",
+ help: "Total scrobbles added during syncs",
+ labelNames: ["user", "source"],
+ registers: [registry],
+ });
+
+ syncLastSuccessSeconds = new Gauge({
+ name: "corpus_sync_last_success_seconds",
+ help: "Unix timestamp of last successful sync run",
+ labelNames: ["user", "source"],
+ registers: [registry],
+ });
+
+ enrichmentFetchesTotal = new Counter({
+ name: "corpus_enrichment_fetches_total",
+ help: "Total metadata enrichment fetches by user, source, and result",
+ labelNames: ["user", "source", "result"],
+ registers: [registry],
+ });
+
+ enrichmentQueueSize = new Gauge({
+ name: "corpus_enrichment_queue_size",
+ help: "Number of releases pending metadata enrichment per user",
+ labelNames: ["user", "type"],
+ registers: [registry],
+ });
+
+ coverRequestsTotal = new Counter({
+ name: "corpus_cover_requests_total",
+ help: "Total cover art requests by user, source, and result",
+ labelNames: ["user", "source", "result"],
+ registers: [registry],
+ });
+
+ dbBackupRunsTotal = new Counter({
+ name: "corpus_db_backup_runs_total",
+ help: "Total database backup runs",
+ labelNames: ["user", "result"],
+ registers: [registry],
+ });
+
+ dbBackupLastSuccessSeconds = new Gauge({
+ name: "corpus_db_backup_last_success_seconds",
+ help: "Unix timestamp of last successful database backup",
+ labelNames: ["user"],
+ registers: [registry],
+ });
+}
// ---------------------------------------------------------------------------
// OpenTelemetry — only active when OTEL_EXPORTER_OTLP_ENDPOINT is set
@@ -151,13 +166,18 @@ function endHttpSpan(span, statusCode) {
// ---------------------------------------------------------------------------
export const getMetricsImpl = (onSuccess) => (onError) => () => {
+ if (!registry) {
+ onSuccess("")();
+ return;
+ }
registry.metrics().then(
(s) => onSuccess(s)(),
(err) => onError(err.message)(),
);
};
-export const getContentType = () => registry.contentType;
+export const getContentType = () =>
+ registry ? registry.contentType : "text/plain; version=0.0.4; charset=utf-8";
// Runs `handler` inside the active context of a server span so that any
// outbound HTTP/fetch calls made during handling automatically become child
@@ -184,26 +204,38 @@ export const wrapRequest =
}
};
-export const incSyncRuns = (user) => (source) => (result) => () =>
- syncRunsTotal.inc({ user, source, result });
+export const incSyncRuns = (user) => (source) => (result) => () => {
+ if (syncRunsTotal) syncRunsTotal.inc({ user, source, result });
+};
-export const incSyncScrobbles = (user) => (source) => (count) => () =>
- syncScrobblesAddedTotal.inc({ user, source }, count);
+export const incSyncScrobbles = (user) => (source) => (count) => () => {
+ if (syncScrobblesAddedTotal)
+ syncScrobblesAddedTotal.inc({ user, source }, count);
+};
-export const setSyncLastSuccess = (user) => (source) => () =>
- syncLastSuccessSeconds.setToCurrentTime({ user, source });
+export const setSyncLastSuccess = (user) => (source) => () => {
+ if (syncLastSuccessSeconds)
+ syncLastSuccessSeconds.setToCurrentTime({ user, source });
+};
-export const incEnrichmentFetch = (user) => (source) => (result) => () =>
- enrichmentFetchesTotal.inc({ user, source, result });
+export const incEnrichmentFetch = (user) => (source) => (result) => () => {
+ if (enrichmentFetchesTotal)
+ enrichmentFetchesTotal.inc({ user, source, result });
+};
-export const setEnrichmentQueueSize = (user) => (type) => (size) => () =>
- enrichmentQueueSize.set({ user, type }, size);
+export const setEnrichmentQueueSize = (user) => (type) => (size) => () => {
+ if (enrichmentQueueSize) enrichmentQueueSize.set({ user, type }, size);
+};
-export const incCoverRequest = (user) => (source) => (result) => () =>
- coverRequestsTotal.inc({ user, source, result });
+export const incCoverRequest = (user) => (source) => (result) => () => {
+ if (coverRequestsTotal) coverRequestsTotal.inc({ user, source, result });
+};
-export const incDbBackupRun = (user) => (result) => () =>
- dbBackupRunsTotal.inc({ user, result });
+export const incDbBackupRun = (user) => (result) => () => {
+ if (dbBackupRunsTotal) dbBackupRunsTotal.inc({ user, result });
+};
-export const setDbBackupLastSuccess = (user) => () =>
- dbBackupLastSuccessSeconds.setToCurrentTime({ user });
+export const setDbBackupLastSuccess = (user) => () => {
+ if (dbBackupLastSuccessSeconds)
+ dbBackupLastSuccessSeconds.setToCurrentTime({ user });
+};