Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
dist
.env
.env.prod
dbinit.sql
dbinit.sql
Errors OpenPodcast Default Stack-logs-*.txt
9 changes: 9 additions & 0 deletions src/api/StatusApi.test.ts
Original file line number Diff line number Diff line change
@@ -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))
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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([
Expand All @@ -80,6 +85,7 @@ describe('StatusApi', () => {
affectedPodcastCount: 2,
worstAgeHours: 80,
severity: 'serious',
grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL),
}),
expect.objectContaining({
provider: 'apple',
Expand All @@ -88,6 +94,7 @@ describe('StatusApi', () => {
affectedPodcastCount: 1,
worstAgeHours: 52,
severity: 'concerning',
grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL),
}),
])
expect(report.endpointGroups[0].issues).toEqual([
Expand All @@ -101,13 +108,15 @@ describe('StatusApi', () => {
affectedEndpointCount: 2,
worstAgeHours: 80,
severity: 'serious',
grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL),
}),
expect.objectContaining({
podcastId: 1,
issueCount: 1,
affectedEndpointCount: 1,
worstAgeHours: 30,
severity: 'stale',
grafanaUrl: expect.stringContaining(GRAFANA_EXPLORE_URL),
}),
])
expect(report.podcastGroups[0].issues).toEqual([
Expand Down
50 changes: 50 additions & 0 deletions src/api/StatusApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type ImportIssue = {
severity: 'stale' | 'concerning' | 'serious'
severityLabel: string
guidance: string
grafanaUrl: string
}

type ImportIssueSummary = {
Expand All @@ -34,6 +35,7 @@ type EndpointIssueGroup = {
worstAgeHours: number
severity: ImportIssue['severity']
severityLabel: string
grafanaUrl: string
issues: ImportIssue[]
}

Expand All @@ -44,6 +46,7 @@ type PodcastIssueGroup = {
worstAgeHours: number
severity: ImportIssue['severity']
severityLabel: string
grafanaUrl: string
issues: ImportIssue[]
}

Expand All @@ -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()) /
Expand Down Expand Up @@ -163,6 +209,7 @@ const getEndpointIssueGroups = (issues: ImportIssue[]) => {
worstAgeHours: worstIssue.ageHours,
severity: worstIssue.severity,
severityLabel: worstIssue.severityLabel,
grafanaUrl: worstIssue.grafanaUrl,
issues: groupIssues,
}
})
Expand Down Expand Up @@ -209,6 +256,7 @@ const getPodcastIssueGroups = (issues: ImportIssue[]) => {
worstAgeHours: worstIssue.ageHours,
severity: worstIssue.severity,
severityLabel: worstIssue.severityLabel,
grafanaUrl: worstIssue.grafanaUrl,
issues: groupIssues,
}
}
Expand Down Expand Up @@ -292,6 +340,7 @@ class StatusApi {
): ImportIssueReport {
const affectedPodcasts = new Set<number>()
const affectedEndpoints = new Set<string>()
const grafanaUrl = getGrafanaLogsUrl()
const issues: ImportIssue[] = []

Object.entries(status).forEach(([podcastIdStr, podcastData]) => {
Expand Down Expand Up @@ -320,6 +369,7 @@ class StatusApi {
severity,
severityLabel: getIssueSeverityLabel(severity),
guidance: getIssueGuidance(severity),
grafanaUrl,
})
}
)
Expand Down
32 changes: 31 additions & 1 deletion views/statistics.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -357,6 +381,7 @@
<li>#{{podcastId}} · {{ageHours}}h since {{latestUpdateAt}}</li>
{{/each}}
</ol>
<a class="grafana-link" href="{{grafanaUrl}}" target="_blank" rel="noopener noreferrer">Open Grafana logs</a>
</article>
{{/each}}
</section>
Expand All @@ -377,6 +402,7 @@
<li>{{provider}}/{{endpoint}} · {{ageHours}}h since {{latestUpdateAt}}</li>
{{/each}}
</ol>
<a class="grafana-link" href="{{grafanaUrl}}" target="_blank" rel="noopener noreferrer">Open Grafana logs</a>
</article>
{{/each}}
</section>
Expand All @@ -394,6 +420,7 @@
<th>Last success</th>
<th>Severity</th>
<th>Guidance</th>
<th>Logs</th>
</tr>
</thead>
<tbody>
Expand All @@ -407,6 +434,9 @@
<span class="severity {{severity}}">{{severityLabel}}</span>
</td>
<td class="guidance" data-label="Guidance">{{guidance}}</td>
<td class="logs-link" data-label="Logs">
<a href="{{grafanaUrl}}" target="_blank" rel="noopener noreferrer">Grafana</a>
</td>
</tr>
{{/each}}
</tbody>
Expand Down
Loading