diff --git a/.gitignore b/.gitignore index a34474f..c18bc9f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ dist .env .env.prod -dbinit.sql \ No newline at end of file +dbinit.sql +Errors OpenPodcast Default Stack-logs-*.txt diff --git a/src/api/StatusApi.test.ts b/src/api/StatusApi.test.ts index 1ae8483..ef6bc35 100644 --- a/src/api/StatusApi.test.ts +++ b/src/api/StatusApi.test.ts @@ -1,6 +1,8 @@ import { StatusApi } from './StatusApi' import { StatusRepository } from '../db/StatusRepository' +const GRAFANA_EXPLORE_URL = 'https://openpodcast.grafana.net/explore' + describe('StatusApi', () => { beforeEach(() => { jest.useFakeTimers().setSystemTime(new Date(2026, 6, 22, 12, 0, 0)) @@ -49,6 +51,7 @@ describe('StatusApi', () => { severity: 'serious', severityLabel: 'Needs attention', guidance: 'Check the import pipeline and provider credentials.', + grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL), }, { podcastId: 2, @@ -59,6 +62,7 @@ describe('StatusApi', () => { severity: 'concerning', severityLabel: 'Concerning', guidance: 'Check whether the pipeline is delayed or stuck.', + grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL), }, { podcastId: 1, @@ -70,6 +74,7 @@ describe('StatusApi', () => { severityLabel: 'Stale', guidance: 'Watch this import; it is older than the daily window.', + grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL), }, ]) expect(report.endpointGroups).toEqual([ @@ -80,6 +85,7 @@ describe('StatusApi', () => { affectedPodcastCount: 2, worstAgeHours: 80, severity: 'serious', + grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL), }), expect.objectContaining({ provider: 'apple', @@ -88,6 +94,7 @@ describe('StatusApi', () => { affectedPodcastCount: 1, worstAgeHours: 52, severity: 'concerning', + grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL), }), ]) expect(report.endpointGroups[0].issues).toEqual([ @@ -101,6 +108,7 @@ describe('StatusApi', () => { affectedEndpointCount: 2, worstAgeHours: 80, severity: 'serious', + grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL), }), expect.objectContaining({ podcastId: 1, @@ -108,6 +116,7 @@ describe('StatusApi', () => { affectedEndpointCount: 1, worstAgeHours: 30, severity: 'stale', + grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL), }), ]) expect(report.podcastGroups[0].issues).toEqual([ diff --git a/src/api/StatusApi.ts b/src/api/StatusApi.ts index a67f0ea..7b4da3b 100644 --- a/src/api/StatusApi.ts +++ b/src/api/StatusApi.ts @@ -16,6 +16,7 @@ type ImportIssue = { severity: 'stale' | 'concerning' | 'serious' severityLabel: string guidance: string + grafanaUrl: string } type ImportIssueSummary = { @@ -34,6 +35,7 @@ type EndpointIssueGroup = { worstAgeHours: number severity: ImportIssue['severity'] severityLabel: string + grafanaUrl: string issues: ImportIssue[] } @@ -44,6 +46,7 @@ type PodcastIssueGroup = { worstAgeHours: number severity: ImportIssue['severity'] severityLabel: string + grafanaUrl: string issues: ImportIssue[] } @@ -54,6 +57,49 @@ type ImportIssueReport = { issues: ImportIssue[] } +const GRAFANA_EXPLORE_URL = 'https://openpodcast.grafana.net/explore' +const GRAFANA_LOGS_DATASOURCE_UID = 'grafanacloud-logs' +const GRAFANA_SWARM_STACK = 'openpod-engineeringkiosk' + +const getGrafanaLogsUrl = () => { + const pane = { + datasource: GRAFANA_LOGS_DATASOURCE_UID, + queries: [ + { + refId: 'A', + expr: `{swarm_stack="${GRAFANA_SWARM_STACK}"} |~ \`(?i)error\``, + queryType: 'range', + datasource: { + type: 'loki', + uid: GRAFANA_LOGS_DATASOURCE_UID, + }, + editorMode: 'code', + direction: 'backward', + }, + ], + range: { + from: 'now-7d', + to: 'now', + }, + panelsState: { + logs: { + sortOrder: 'Descending', + }, + }, + compact: false, + } + const panes = { + importErrors: pane, + } + const params = new URLSearchParams({ + schemaVersion: '1', + panes: JSON.stringify(panes), + orgId: '1', + }) + + return `${GRAFANA_EXPLORE_URL}?${params.toString()}` +} + const getEndpointAgeHours = (endpointDate: Date) => { return Math.round( (new Date().getTime() - new Date(endpointDate).getTime()) / @@ -163,6 +209,7 @@ const getEndpointIssueGroups = (issues: ImportIssue[]) => { worstAgeHours: worstIssue.ageHours, severity: worstIssue.severity, severityLabel: worstIssue.severityLabel, + grafanaUrl: worstIssue.grafanaUrl, issues: groupIssues, } }) @@ -209,6 +256,7 @@ const getPodcastIssueGroups = (issues: ImportIssue[]) => { worstAgeHours: worstIssue.ageHours, severity: worstIssue.severity, severityLabel: worstIssue.severityLabel, + grafanaUrl: worstIssue.grafanaUrl, issues: groupIssues, } } @@ -292,6 +340,7 @@ class StatusApi { ): ImportIssueReport { const affectedPodcasts = new Set() const affectedEndpoints = new Set() + const grafanaUrl = getGrafanaLogsUrl() const issues: ImportIssue[] = [] Object.entries(status).forEach(([podcastIdStr, podcastData]) => { @@ -320,6 +369,7 @@ class StatusApi { severity, severityLabel: getIssueSeverityLabel(severity), guidance: getIssueGuidance(severity), + grafanaUrl, }) } ) diff --git a/views/statistics.hbs b/views/statistics.hbs index 594eb0c..44e2998 100644 --- a/views/statistics.hbs +++ b/views/statistics.hbs @@ -151,6 +151,19 @@ margin-top: 4px; } + .grafana-link { + color: #0969da; + display: inline-flex; + font-size: 0.9rem; + font-weight: 700; + margin-top: 10px; + text-decoration: none; + } + + .grafana-link:hover { + text-decoration: underline; + } + .panel { background: #fff; border: 1px solid #d8dee4; @@ -194,12 +207,23 @@ } .timestamp, - .guidance { + .guidance, + .logs-link { color: #6b7280; font-size: 0.92rem; line-height: 1.4; } + .logs-link a { + color: #0969da; + font-weight: 700; + text-decoration: none; + } + + .logs-link a:hover { + text-decoration: underline; + } + .severity { border-radius: 999px; display: inline-flex; @@ -357,6 +381,7 @@
  • #{{podcastId}} · {{ageHours}}h since {{latestUpdateAt}}
  • {{/each}} + Open Grafana logs {{/each}} @@ -377,6 +402,7 @@
  • {{provider}}/{{endpoint}} · {{ageHours}}h since {{latestUpdateAt}}
  • {{/each}} + Open Grafana logs {{/each}} @@ -394,6 +420,7 @@ Last success Severity Guidance + Logs @@ -407,6 +434,9 @@ {{severityLabel}} {{guidance}} + + Grafana + {{/each}}