diff --git a/changelog.d/681.misc b/changelog.d/681.misc new file mode 100644 index 00000000..86ee5b11 --- /dev/null +++ b/changelog.d/681.misc @@ -0,0 +1 @@ +Add `feed_failing` metric to track the number of feeds failing to be read or parsed. \ No newline at end of file diff --git a/docs/metrics.md b/docs/metrics.md index 88d1f7ea..504c00b1 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -43,6 +43,7 @@ Below is the generated list of Prometheus metrics for Hookshot. |--------|------|--------| | feed_count | The number of RSS feeds that hookshot is subscribed to | | | feed_fetch_ms | The time taken for hookshot to fetch all feeds | | +| feed_failing | The number of RSS feeds that hookshot is failing to read | reason | ## process | Metric | Help | Labels | |--------|------|--------| diff --git a/src/Metrics.ts b/src/Metrics.ts index 1216eac2..2fec2456 100644 --- a/src/Metrics.ts +++ b/src/Metrics.ts @@ -26,6 +26,7 @@ export class Metrics { public readonly feedsCount = new Gauge({ name: "feed_count", help: "The number of RSS feeds that hookshot is subscribed to", labelNames: [], registers: [this.registry]}); public readonly feedFetchMs = new Gauge({ name: "feed_fetch_ms", help: "The time taken for hookshot to fetch all feeds", labelNames: [], registers: [this.registry]}); + public readonly feedsFailing = new Gauge({ name: "feed_failing", help: "The number of RSS feeds that hookshot is failing to read", labelNames: ["reason"], registers: [this.registry]}); constructor(private registry: Registry = register) { diff --git a/src/feeds/FeedReader.ts b/src/feeds/FeedReader.ts index 2c1f2dea..6f37730c 100644 --- a/src/feeds/FeedReader.ts +++ b/src/feeds/FeedReader.ts @@ -104,6 +104,11 @@ export class FeedReader { private seenEntries: Map = new Map(); // A set of last modified times for each url. private cacheTimes: Map = new Map(); + + // Reason failures to url map. + private feedsFailingHttp = new Set(); + private feedsFailingParsing = new Set(); + static readonly seenEntriesEventType = "uk.half-shot.matrix-hookshot.feed.reader.seenEntries"; constructor( @@ -199,6 +204,8 @@ export class FeedReader { // We don't want to wait forever for the feed. timeout: this.config.pollTimeoutSeconds * 1000, }); + // Clear any HTTP failures + this.feedsFailingHttp.delete(url); // Store any entity tags/cache times. if (res.headers.ETag) { @@ -208,6 +215,8 @@ export class FeedReader { } const feed = await this.parser.parseString(res.data); + this.feedsFailingParsing.delete(url); + let initialSync = false; let seenGuids = this.seenEntries.get(url); if (!seenGuids) { @@ -268,6 +277,9 @@ export class FeedReader { if (err.response?.status === StatusCodes.NOT_MODIFIED) { continue; } + this.feedsFailingHttp.add(url); + } else { + this.feedsFailingParsing.add(url); } const error = err instanceof Error ? err : new Error(`Unknown error ${err}`); const feedError = new FeedError(url.toString(), error, fetchKey); @@ -275,6 +287,10 @@ export class FeedReader { this.queue.push({ eventName: 'feed.error', sender: 'FeedReader', data: feedError}); } } + + Metrics.feedsFailing.set({ reason: "http" }, this.feedsFailingHttp.size ); + Metrics.feedsFailing.set({ reason: "parsing" }, this.feedsFailingParsing.size); + if (seenEntriesChanged) await this.saveSeenEntries(); const elapsed = Date.now() - fetchingStarted;