@spotify/backstage-plugin-soundcheck-metrics-backend 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +658 -0
- package/config.d.ts +58 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.d.ts +90 -0
- package/dist/plugin.cjs.js +2 -0
- package/dist/service/BigQueryExporter.cjs.js +2 -0
- package/dist/service/EnvironmentContextService.cjs.js +2 -0
- package/dist/service/MetricsCollector.cjs.js +2 -0
- package/package.json +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @spotify/backstage-plugin-soundcheck-metrics-backend
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Soundcheck - Initial commit for the soundcheck-metrics-backend module.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependency `backstage` to `1.48.0`.
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
- Updated dependencies
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @spotify/backstage-plugin-soundcheck-node@0.11.0
|
package/README.md
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
# Soundcheck Metrics Backend
|
|
2
|
+
|
|
3
|
+
A Backstage backend module that exports Soundcheck usage metrics to BigQuery for analytics and monitoring.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This module extends the Soundcheck plugin to automatically collect and export aggregate metrics on a configurable schedule. It provides visibility into Soundcheck adoption, check pass rates, track completion, and group-level performance across your Backstage environments.
|
|
8
|
+
|
|
9
|
+
**Key Capabilities:**
|
|
10
|
+
|
|
11
|
+
- Scheduled metrics collection and export to BigQuery
|
|
12
|
+
- Group-based metric aggregation using Soundcheck's GroupHierarchy
|
|
13
|
+
- Track-level pass/fail metrics (entities completing entire tracks)
|
|
14
|
+
- Environment context tracking (installation ID, customer detection)
|
|
15
|
+
- Robust error handling with per-row retry logic
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
This is a **backend module** (not a standalone plugin) that extends the Soundcheck plugin:
|
|
20
|
+
|
|
21
|
+
- Module ID: `soundcheck.metrics`
|
|
22
|
+
- Shares the Soundcheck database (`backstage_plugin_soundcheck`)
|
|
23
|
+
- Integrates with Soundcheck's `GroupHierarchyService`
|
|
24
|
+
- Exports data to Google BigQuery via the `@google-cloud/bigquery` library
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
### 1. Install the Package
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd packages/backend
|
|
32
|
+
yarn add @spotify/backstage-plugin-soundcheck-metrics-backend
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Register the Module
|
|
36
|
+
|
|
37
|
+
Add the module to your backend in `packages/backend/src/index.ts`:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { soundcheckMetricsModule } from '@spotify/backstage-plugin-soundcheck-metrics-backend';
|
|
41
|
+
|
|
42
|
+
// Register with your backend
|
|
43
|
+
backend.add(soundcheckMetricsModule);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Important:** The Soundcheck plugin must be installed and registered before this module.
|
|
47
|
+
|
|
48
|
+
### 3. Configure the Module
|
|
49
|
+
|
|
50
|
+
Add configuration to `app-config.yaml`:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
soundcheckMetrics:
|
|
54
|
+
enabled: true
|
|
55
|
+
schedule: '0 2 * * *' # Daily at 2 AM UTC
|
|
56
|
+
|
|
57
|
+
# Group types to aggregate metrics for
|
|
58
|
+
groupTypesToAggregate:
|
|
59
|
+
- squad
|
|
60
|
+
- misison
|
|
61
|
+
- product area
|
|
62
|
+
- studio
|
|
63
|
+
|
|
64
|
+
# BigQuery configuration
|
|
65
|
+
bigquery:
|
|
66
|
+
project: my-gcp-project
|
|
67
|
+
dataset: backstage_metrics
|
|
68
|
+
table: soundcheck_usage
|
|
69
|
+
credentials: ${BIGQUERY_CREDENTIALS} # Optional: use ADC if omitted
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration Reference
|
|
73
|
+
|
|
74
|
+
### Core Settings
|
|
75
|
+
|
|
76
|
+
| Field | Type | Required | Default | Description |
|
|
77
|
+
| ----------------------- | -------- | -------- | ------------- | ---------------------------------------------- |
|
|
78
|
+
| `enabled` | boolean | Yes | - | Enable/disable metrics export |
|
|
79
|
+
| `schedule` | string | No | `'0 2 * * *'` | Cron expression for export schedule |
|
|
80
|
+
| `groupTypesToAggregate` | string[] | No | `[]` | Group types to create separate metric rows for |
|
|
81
|
+
|
|
82
|
+
### BigQuery Settings
|
|
83
|
+
|
|
84
|
+
| Field | Type | Required | Description |
|
|
85
|
+
| ---------------------- | ------ | -------- | ------------------------------------------------------------------ |
|
|
86
|
+
| `bigquery.project` | string | Yes | GCP project ID containing the BigQuery dataset |
|
|
87
|
+
| `bigquery.dataset` | string | Yes | BigQuery dataset name |
|
|
88
|
+
| `bigquery.table` | string | Yes | BigQuery table name |
|
|
89
|
+
| `bigquery.credentials` | string | No | Service account JSON (omit to use Application Default Credentials) |
|
|
90
|
+
|
|
91
|
+
### Schedule Format
|
|
92
|
+
|
|
93
|
+
The `schedule` field uses standard cron format: `minute hour day month weekday`
|
|
94
|
+
|
|
95
|
+
**Examples:**
|
|
96
|
+
|
|
97
|
+
- `'0 2 * * *'` - Daily at 2 AM UTC (default)
|
|
98
|
+
- `'0 */4 * * *'` - Every 4 hours
|
|
99
|
+
- `'*/15 * * * *'` - Every 15 minutes
|
|
100
|
+
- `'0 0 * * 0'` - Weekly on Sunday at midnight
|
|
101
|
+
- `'0 0 1 * *'` - Monthly on the 1st at midnight
|
|
102
|
+
|
|
103
|
+
**Task Timeout:** The export task has a 10-minute timeout. If collection or export exceeds this limit, the task will be terminated and retried on the next scheduled run.
|
|
104
|
+
|
|
105
|
+
### Group-Based Aggregation
|
|
106
|
+
|
|
107
|
+
Configure `groupTypesToAggregate` to export separate metric rows for each group of specified types:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
soundcheckMetrics:
|
|
111
|
+
groupTypesToAggregate:
|
|
112
|
+
- squad # Metrics for each squad
|
|
113
|
+
- mission # Metrics for each mission
|
|
114
|
+
- studio # Metrics for each studio
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**How it works:**
|
|
118
|
+
|
|
119
|
+
1. Queries `GroupHierarchyService` for all root groups of the specified types
|
|
120
|
+
2. For each group, collects metrics filtered to entities owned by that group (including descendant groups)
|
|
121
|
+
3. Exports a separate BigQuery row per group with:
|
|
122
|
+
- `group_name`: Name extracted from the group entity ref (e.g., "toast-infra")
|
|
123
|
+
- `group_level`: Group type (e.g., "squad", "mission")
|
|
124
|
+
- Check and campaign counts filtered to entities owned by the group
|
|
125
|
+
- Global counts for total campaigns, tracks, and checkers (not filtered by group)
|
|
126
|
+
|
|
127
|
+
**If omitted:** No metrics will be exported (the module requires at least one group type to be configured).
|
|
128
|
+
|
|
129
|
+
### Authentication
|
|
130
|
+
|
|
131
|
+
Three methods for BigQuery authentication:
|
|
132
|
+
|
|
133
|
+
#### Option 1: Application Default Credentials (Recommended for GCP)
|
|
134
|
+
|
|
135
|
+
When running in GCP (Cloud Run, GKE), omit the `credentials` field:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
soundcheckMetrics:
|
|
139
|
+
bigquery:
|
|
140
|
+
project: my-project
|
|
141
|
+
dataset: metrics
|
|
142
|
+
table: soundcheck_usage
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The module will automatically use the service account attached to the workload.
|
|
146
|
+
|
|
147
|
+
#### Option 2: Service Account JSON (Recommended for Non-GCP)
|
|
148
|
+
|
|
149
|
+
Provide service account credentials as a JSON string:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
soundcheckMetrics:
|
|
153
|
+
bigquery:
|
|
154
|
+
credentials: ${BIGQUERY_CREDENTIALS}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Set the environment variable:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
export BIGQUERY_CREDENTIALS='{"type":"service_account","project_id":"...","private_key":"..."}'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Option 3: Credentials File (Development Only)
|
|
164
|
+
|
|
165
|
+
Reference a local credentials file:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
soundcheckMetrics:
|
|
169
|
+
bigquery:
|
|
170
|
+
credentials: '${file:./credentials.json}'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## BigQuery Setup
|
|
174
|
+
|
|
175
|
+
### 1. Create the Table
|
|
176
|
+
|
|
177
|
+
Run this SQL in the BigQuery Console or via `bq` CLI:
|
|
178
|
+
|
|
179
|
+
```sql
|
|
180
|
+
CREATE TABLE `my-project.backstage_metrics.soundcheck_usage` (
|
|
181
|
+
-- Environment identification
|
|
182
|
+
environment_name STRING,
|
|
183
|
+
installation_id STRING,
|
|
184
|
+
organization_name STRING,
|
|
185
|
+
customer BOOL,
|
|
186
|
+
|
|
187
|
+
-- Group identification
|
|
188
|
+
group_name STRING,
|
|
189
|
+
group_level STRING,
|
|
190
|
+
|
|
191
|
+
-- Global counts (not filtered by group)
|
|
192
|
+
total_campaigns INT64,
|
|
193
|
+
total_tracks INT64,
|
|
194
|
+
total_checkers INT64,
|
|
195
|
+
|
|
196
|
+
-- Group-scoped metrics (filtered to entities owned by the group)
|
|
197
|
+
group_total_checks INT64,
|
|
198
|
+
group_passed_checks INT64,
|
|
199
|
+
group_failed_checks INT64,
|
|
200
|
+
group_active_campaigns INT64,
|
|
201
|
+
group_archived_campaigns INT64,
|
|
202
|
+
tracks_passed_by_entities_of_group INT64,
|
|
203
|
+
tracks_failed_by_entities_of_group INT64,
|
|
204
|
+
|
|
205
|
+
-- Timestamp
|
|
206
|
+
snapshot_timestamp TIMESTAMP
|
|
207
|
+
)
|
|
208
|
+
PARTITION BY DATE(snapshot_timestamp)
|
|
209
|
+
CLUSTER BY environment_name, customer, group_level
|
|
210
|
+
OPTIONS(
|
|
211
|
+
description="Soundcheck usage metrics exported from Backstage instances"
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### Schema Details
|
|
216
|
+
|
|
217
|
+
**Environment Context:**
|
|
218
|
+
|
|
219
|
+
- `environment_name`: Hostname prefix from `app.baseUrl` (e.g., "spc-acme-prod")
|
|
220
|
+
- `installation_id`: Value from `K_SERVICE` environment variable (or 'UNKNOWN')
|
|
221
|
+
- `organization_name`: From `organization.name` config
|
|
222
|
+
- `customer`: `true` if environment name starts with `spc-` (Spotify customer pattern)
|
|
223
|
+
|
|
224
|
+
**Group Context:**
|
|
225
|
+
|
|
226
|
+
- `group_name`: Group entity name (e.g., "toast-infra", "platform")
|
|
227
|
+
- `group_level`: Group type (e.g., "squad", "mission", "studio")
|
|
228
|
+
|
|
229
|
+
**Global Metrics (not filtered by group):**
|
|
230
|
+
|
|
231
|
+
- `total_campaigns`: Total campaigns in the Soundcheck database
|
|
232
|
+
- `total_tracks`: Total tracks/programs in the database
|
|
233
|
+
- `total_checkers`: Total checker definitions in the database
|
|
234
|
+
|
|
235
|
+
**Group-Scoped Metrics (filtered to entities owned by the group):**
|
|
236
|
+
|
|
237
|
+
- `group_total_checks`: Total check results for entities owned by this group
|
|
238
|
+
- `group_passed_checks`: Passed check results for group entities
|
|
239
|
+
- `group_failed_checks`: Failed check results for group entities
|
|
240
|
+
- `group_active_campaigns`: Non-archived campaigns for group entities
|
|
241
|
+
- `group_archived_campaigns`: Archived campaigns for group entities
|
|
242
|
+
- `tracks_passed_by_entities_of_group`: Track instances where a group entity passed all checks
|
|
243
|
+
- `tracks_failed_by_entities_of_group`: Track instances where a group entity failed any check
|
|
244
|
+
|
|
245
|
+
**Timestamp:**
|
|
246
|
+
|
|
247
|
+
- `snapshot_timestamp`: UTC timestamp when metrics were collected (truncated to date boundary)
|
|
248
|
+
|
|
249
|
+
#### Partitioning & Clustering
|
|
250
|
+
|
|
251
|
+
- **Partitioned by date:** Reduces query costs by scanning only relevant dates
|
|
252
|
+
- **Clustered by:** `environment_name`, `customer`, `group_level` for optimal query performance
|
|
253
|
+
|
|
254
|
+
### 2. Create a Service Account
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Create service account
|
|
258
|
+
gcloud iam service-accounts create soundcheck-metrics-exporter \
|
|
259
|
+
--display-name="Soundcheck Metrics Exporter" \
|
|
260
|
+
--project=my-project
|
|
261
|
+
|
|
262
|
+
# Grant BigQuery Data Editor role
|
|
263
|
+
gcloud projects add-iam-policy-binding my-project \
|
|
264
|
+
--member="serviceAccount:soundcheck-metrics-exporter@my-project.iam.gserviceaccount.com" \
|
|
265
|
+
--role="roles/bigquery.dataEditor"
|
|
266
|
+
|
|
267
|
+
# Create key (for non-GCP environments)
|
|
268
|
+
gcloud iam service-accounts keys create credentials.json \
|
|
269
|
+
--iam-account=soundcheck-metrics-exporter@my-project.iam.gserviceaccount.com
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Required Permissions:**
|
|
273
|
+
|
|
274
|
+
- `bigquery.tables.updateData` (insert rows)
|
|
275
|
+
- `bigquery.tables.get` (verify table exists)
|
|
276
|
+
|
|
277
|
+
The `roles/bigquery.dataEditor` role includes both permissions.
|
|
278
|
+
|
|
279
|
+
### 3. Test the Configuration
|
|
280
|
+
|
|
281
|
+
Restart your Backstage backend and check logs for:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
[SoundcheckMetricsBackend] - BigQuery exporter initialized { project: 'my-project', dataset: 'backstage_metrics', table: 'soundcheck_usage' }
|
|
285
|
+
[SoundcheckMetricsBackend] - Soundcheck metrics export module initialized! { schedule: '0 2 * * *' }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Metrics Reference
|
|
289
|
+
|
|
290
|
+
### Track-Level Metrics
|
|
291
|
+
|
|
292
|
+
Track metrics measure how many track instances (entity + track combinations) resulted in all checks passing or any check failing.
|
|
293
|
+
|
|
294
|
+
**Calculation Logic:**
|
|
295
|
+
|
|
296
|
+
For each track in `soundcheck_program`:
|
|
297
|
+
|
|
298
|
+
1. Get all check IDs associated with the track from `soundcheck_program_check_description`
|
|
299
|
+
2. For each entity owned by the group:
|
|
300
|
+
- Query `check_result` for results matching the entity and track's check IDs
|
|
301
|
+
- **Passed:** Entity has results for ALL checks in the track, and ALL results are `state='passed'`
|
|
302
|
+
- **Failed:** Entity has at least one result with `state='failed'` for the track's checks
|
|
303
|
+
|
|
304
|
+
**Example:**
|
|
305
|
+
|
|
306
|
+
Track "Security Baseline" has 3 checks: `tls-check`, `vuln-scan`, `secrets-scan`
|
|
307
|
+
|
|
308
|
+
| Entity | TLS Check | Vuln Scan | Secrets Scan | Result |
|
|
309
|
+
| --------- | --------- | ----------- | ------------ | ---------------------------- |
|
|
310
|
+
| service-a | passed | passed | passed | **Track passed** (1) |
|
|
311
|
+
| service-b | passed | failed | passed | **Track failed** (1) |
|
|
312
|
+
| service-c | passed | (no result) | passed | **Not counted** (incomplete) |
|
|
313
|
+
|
|
314
|
+
Result: `tracks_passed_by_entities_of_group = 1`, `tracks_failed_by_entities_of_group = 1`
|
|
315
|
+
|
|
316
|
+
### Metrics Summary Table
|
|
317
|
+
|
|
318
|
+
| Metric | Scope | Source | Description |
|
|
319
|
+
| ------------------------------------ | ------ | ----------------------------------------------------------------------- | ---------------------------------------------- |
|
|
320
|
+
| `total_campaigns` | Global | `soundcheck_campaign` | Total campaigns across all entities |
|
|
321
|
+
| `total_tracks` | Global | `soundcheck_program` | Total tracks/programs defined |
|
|
322
|
+
| `total_checkers` | Global | `checker` | Total checker definitions |
|
|
323
|
+
| `group_total_checks` | Group | `check_result` filtered by group entities | Total check results for group entities |
|
|
324
|
+
| `group_passed_checks` | Group | `check_result` WHERE `state='passed'` | Passed checks for group entities |
|
|
325
|
+
| `group_failed_checks` | Group | `check_result` WHERE `state='failed'` | Failed checks for group entities |
|
|
326
|
+
| `group_active_campaigns` | Group | `soundcheck_campaign` WHERE `archived=false` | Active campaigns for group entities |
|
|
327
|
+
| `group_archived_campaigns` | Group | `soundcheck_campaign` WHERE `archived=true` | Archived campaigns for group entities |
|
|
328
|
+
| `tracks_passed_by_entities_of_group` | Group | Calculated from `check_result` + `soundcheck_program_check_description` | Track instances fully passed by group entities |
|
|
329
|
+
| `tracks_failed_by_entities_of_group` | Group | Calculated from `check_result` + `soundcheck_program_check_description` | Track instances failed by group entities |
|
|
330
|
+
|
|
331
|
+
## Example Queries
|
|
332
|
+
|
|
333
|
+
### Latest Metrics Per Environment
|
|
334
|
+
|
|
335
|
+
```sql
|
|
336
|
+
SELECT
|
|
337
|
+
environment_name,
|
|
338
|
+
group_name,
|
|
339
|
+
group_level,
|
|
340
|
+
group_total_checks,
|
|
341
|
+
group_passed_checks,
|
|
342
|
+
group_failed_checks,
|
|
343
|
+
ROUND(SAFE_DIVIDE(group_passed_checks, group_total_checks) * 100, 2) AS pass_rate_pct,
|
|
344
|
+
tracks_passed_by_entities_of_group,
|
|
345
|
+
tracks_failed_by_entities_of_group,
|
|
346
|
+
snapshot_timestamp
|
|
347
|
+
FROM `my-project.backstage_metrics.soundcheck_usage`
|
|
348
|
+
WHERE DATE(snapshot_timestamp) = CURRENT_DATE()
|
|
349
|
+
ORDER BY environment_name, group_name;
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Pass Rate Trend Over Time
|
|
353
|
+
|
|
354
|
+
```sql
|
|
355
|
+
SELECT
|
|
356
|
+
DATE(snapshot_timestamp) AS date,
|
|
357
|
+
environment_name,
|
|
358
|
+
group_name,
|
|
359
|
+
SUM(group_passed_checks) AS total_passed,
|
|
360
|
+
SUM(group_failed_checks) AS total_failed,
|
|
361
|
+
ROUND(SAFE_DIVIDE(SUM(group_passed_checks), SUM(group_total_checks)) * 100, 2) AS pass_rate_pct
|
|
362
|
+
FROM `my-project.backstage_metrics.soundcheck_usage`
|
|
363
|
+
WHERE snapshot_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
|
|
364
|
+
GROUP BY date, environment_name, group_name
|
|
365
|
+
ORDER BY date DESC, environment_name, group_name;
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Customer vs Internal Comparison
|
|
369
|
+
|
|
370
|
+
```sql
|
|
371
|
+
SELECT
|
|
372
|
+
customer,
|
|
373
|
+
COUNT(DISTINCT environment_name) AS num_environments,
|
|
374
|
+
COUNT(DISTINCT group_name) AS num_groups,
|
|
375
|
+
AVG(group_total_checks) AS avg_checks_per_group,
|
|
376
|
+
AVG(SAFE_DIVIDE(group_passed_checks, group_total_checks)) AS avg_pass_rate,
|
|
377
|
+
SUM(tracks_passed_by_entities_of_group) AS total_tracks_passed,
|
|
378
|
+
SUM(tracks_failed_by_entities_of_group) AS total_tracks_failed
|
|
379
|
+
FROM `my-project.backstage_metrics.soundcheck_usage`
|
|
380
|
+
WHERE DATE(snapshot_timestamp) = CURRENT_DATE()
|
|
381
|
+
GROUP BY customer;
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Top Performing Groups
|
|
385
|
+
|
|
386
|
+
```sql
|
|
387
|
+
SELECT
|
|
388
|
+
group_level,
|
|
389
|
+
group_name,
|
|
390
|
+
environment_name,
|
|
391
|
+
group_total_checks,
|
|
392
|
+
group_passed_checks,
|
|
393
|
+
ROUND(SAFE_DIVIDE(group_passed_checks, group_total_checks) * 100, 2) AS pass_rate_pct,
|
|
394
|
+
tracks_passed_by_entities_of_group,
|
|
395
|
+
tracks_failed_by_entities_of_group
|
|
396
|
+
FROM `my-project.backstage_metrics.soundcheck_usage`
|
|
397
|
+
WHERE DATE(snapshot_timestamp) = CURRENT_DATE()
|
|
398
|
+
AND group_total_checks > 0
|
|
399
|
+
ORDER BY pass_rate_pct DESC
|
|
400
|
+
LIMIT 20;
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Track Completion Rates
|
|
404
|
+
|
|
405
|
+
```sql
|
|
406
|
+
SELECT
|
|
407
|
+
group_level,
|
|
408
|
+
group_name,
|
|
409
|
+
tracks_passed_by_entities_of_group,
|
|
410
|
+
tracks_failed_by_entities_of_group,
|
|
411
|
+
tracks_passed_by_entities_of_group + tracks_failed_by_entities_of_group AS total_track_instances,
|
|
412
|
+
ROUND(
|
|
413
|
+
SAFE_DIVIDE(
|
|
414
|
+
tracks_passed_by_entities_of_group,
|
|
415
|
+
tracks_passed_by_entities_of_group + tracks_failed_by_entities_of_group
|
|
416
|
+
) * 100,
|
|
417
|
+
2
|
|
418
|
+
) AS track_completion_rate_pct
|
|
419
|
+
FROM `my-project.backstage_metrics.soundcheck_usage`
|
|
420
|
+
WHERE DATE(snapshot_timestamp) = CURRENT_DATE()
|
|
421
|
+
AND (tracks_passed_by_entities_of_group + tracks_failed_by_entities_of_group) > 0
|
|
422
|
+
ORDER BY track_completion_rate_pct DESC;
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Monitoring & Logs
|
|
426
|
+
|
|
427
|
+
All log messages are prefixed with `[SoundcheckMetricsBackend] - ` for easy filtering.
|
|
428
|
+
|
|
429
|
+
### Successful Export
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
[SoundcheckMetricsBackend] - Soundcheck metrics export module initialized! { schedule: '0 2 * * *' }
|
|
433
|
+
[SoundcheckMetricsBackend] - Starting scheduled Soundcheck metrics export
|
|
434
|
+
[SoundcheckMetricsBackend] - Collecting Soundcheck metrics...
|
|
435
|
+
[SoundcheckMetricsBackend] - Environment context retrieved { environmentName: 'spc-acme-prod', ... }
|
|
436
|
+
[SoundcheckMetricsBackend] - Collecting metrics for group types: squad, product area, studio, mission
|
|
437
|
+
[SoundcheckMetricsBackend] - Collecting metrics for group: platform-squad (type: squad)
|
|
438
|
+
[SoundcheckMetricsBackend] - Exporting metrics to BigQuery { environment: 'spc-acme-prod', ... }
|
|
439
|
+
[SoundcheckMetricsBackend] - Successfully exported metrics to BigQuery
|
|
440
|
+
[SoundcheckMetricsBackend] - Metrics collected successfully for 12 groups
|
|
441
|
+
[SoundcheckMetricsBackend] - Completed scheduled Soundcheck metrics export: 12 succeeded, 0 failed
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Common Log Patterns
|
|
445
|
+
|
|
446
|
+
**Module initialization:**
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
[SoundcheckMetricsBackend] - BigQuery exporter initialized { project: '...', dataset: '...', table: '...' }
|
|
450
|
+
[SoundcheckMetricsBackend] - Soundcheck metrics export module initialized! { schedule: '...' }
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Per-group collection:**
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
[SoundcheckMetricsBackend] - Collecting metrics for group: toast-infra (type: squad)
|
|
457
|
+
[SoundcheckMetricsBackend] - Found 47 entities owned by toast-infra
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Export summary:**
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
[SoundcheckMetricsBackend] - Exporting 15 metrics row(s) to BigQuery
|
|
464
|
+
[SoundcheckMetricsBackend] - Completed scheduled Soundcheck metrics export: 15 succeeded, 0 failed
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Troubleshooting
|
|
468
|
+
|
|
469
|
+
### Module Not Initializing
|
|
470
|
+
|
|
471
|
+
**Symptom:** No initialization logs appear
|
|
472
|
+
|
|
473
|
+
**Possible Causes:**
|
|
474
|
+
|
|
475
|
+
1. `soundcheckMetrics.enabled` is not set to `true`
|
|
476
|
+
2. Configuration is missing or invalid
|
|
477
|
+
3. Module not registered in `packages/backend/src/index.ts`
|
|
478
|
+
|
|
479
|
+
**Check:**
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
[SoundcheckMetricsBackend] - Soundcheck metrics export is disabled.
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Solution:** Verify config and ensure `enabled: true`
|
|
486
|
+
|
|
487
|
+
### No Metrics Exported
|
|
488
|
+
|
|
489
|
+
**Symptom:** Export completes but 0 rows inserted
|
|
490
|
+
|
|
491
|
+
**Possible Causes:**
|
|
492
|
+
|
|
493
|
+
1. `groupTypesToAggregate` is empty or contains invalid group types
|
|
494
|
+
2. No groups exist for the specified types
|
|
495
|
+
3. All groups failed to collect metrics
|
|
496
|
+
|
|
497
|
+
**Check logs for:**
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
[SoundcheckMetricsBackend] - No groups found for types: squad, product area, studio, mission
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Solution:**
|
|
504
|
+
|
|
505
|
+
- Verify `groupTypesToAggregate` contains valid group types
|
|
506
|
+
- Confirm groups exist in Soundcheck's GroupHierarchy
|
|
507
|
+
- Check that GroupHierarchyService is properly configured
|
|
508
|
+
|
|
509
|
+
### BigQuery Insert Failures
|
|
510
|
+
|
|
511
|
+
**Symptom:** Errors during export
|
|
512
|
+
|
|
513
|
+
**Error:** `no such field: tracks_passed_by_entities_of_group`
|
|
514
|
+
|
|
515
|
+
**Solution:** The BigQuery table schema doesn't match the exported data structure. Run the `ALTER TABLE` commands:
|
|
516
|
+
|
|
517
|
+
```sql
|
|
518
|
+
ALTER TABLE `my-project.backstage_metrics.soundcheck_usage`
|
|
519
|
+
ADD COLUMN tracks_passed_by_entities_of_group INT64;
|
|
520
|
+
|
|
521
|
+
ALTER TABLE `my-project.backstage_metrics.soundcheck_usage`
|
|
522
|
+
ADD COLUMN tracks_failed_by_entities_of_group INT64;
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Error:** `Not found: Table my-project:backstage_metrics.soundcheck_usage`
|
|
526
|
+
|
|
527
|
+
**Solution:** Create the BigQuery table using the SQL in the "Create the Table" section.
|
|
528
|
+
|
|
529
|
+
**Error:** `Permission 'bigquery.tables.updateData' denied`
|
|
530
|
+
|
|
531
|
+
**Solution:** Grant the service account the BigQuery Data Editor role:
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
gcloud projects add-iam-policy-binding my-project \
|
|
535
|
+
--member="serviceAccount:soundcheck-metrics-exporter@my-project.iam.gserviceaccount.com" \
|
|
536
|
+
--role="roles/bigquery.dataEditor"
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Missing Soundcheck Tables
|
|
540
|
+
|
|
541
|
+
**Symptom:** Warnings about table queries failing
|
|
542
|
+
|
|
543
|
+
**Logs:**
|
|
544
|
+
|
|
545
|
+
```
|
|
546
|
+
[SoundcheckMetricsBackend] - Failed to query table soundcheck_campaign
|
|
547
|
+
[SoundcheckMetricsBackend] - Failed to load track-check mappings
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**Behavior:** The module uses defensive error handling. Missing tables result in 0 values for the corresponding metrics, but the export continues.
|
|
551
|
+
|
|
552
|
+
**Solution:**
|
|
553
|
+
|
|
554
|
+
- Verify Soundcheck plugin is installed and initialized
|
|
555
|
+
- Check that Soundcheck has created its database tables
|
|
556
|
+
- This is expected if Soundcheck is not fully configured in the environment
|
|
557
|
+
|
|
558
|
+
### Authentication Errors
|
|
559
|
+
|
|
560
|
+
**Error:** `Could not load the default credentials`
|
|
561
|
+
|
|
562
|
+
**Solution:** Provide credentials via one of three methods:
|
|
563
|
+
|
|
564
|
+
1. Set `soundcheckMetrics.bigquery.credentials` with service account JSON
|
|
565
|
+
2. Use Application Default Credentials (when running in GCP)
|
|
566
|
+
3. Set `GOOGLE_APPLICATION_CREDENTIALS` environment variable to a credentials file path
|
|
567
|
+
|
|
568
|
+
**Error:** `invalid_grant: Invalid JWT Signature`
|
|
569
|
+
|
|
570
|
+
**Solution:** The service account key may be corrupted or incorrectly formatted. Regenerate the key:
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
gcloud iam service-accounts keys create new-credentials.json \
|
|
574
|
+
--iam-account=soundcheck-metrics-exporter@my-project.iam.gserviceaccount.com
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Partial Export Failures
|
|
578
|
+
|
|
579
|
+
**Symptom:** Some groups succeed, others fail
|
|
580
|
+
|
|
581
|
+
**Logs:**
|
|
582
|
+
|
|
583
|
+
```
|
|
584
|
+
[SoundcheckMetricsBackend] - Failed to export metrics for group platform-squad { error: ... }
|
|
585
|
+
[SoundcheckMetricsBackend] - Completed scheduled Soundcheck metrics export: 10 succeeded, 2 failed
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Behavior:** The module uses per-row error handling. Individual group failures don't prevent other groups from being exported.
|
|
589
|
+
|
|
590
|
+
**Solution:** Check the error details in the logs to diagnose the specific group failure (e.g., entity ownership query failure, BigQuery validation error).
|
|
591
|
+
|
|
592
|
+
## Development
|
|
593
|
+
|
|
594
|
+
### Running Locally
|
|
595
|
+
|
|
596
|
+
1. Set up local BigQuery credentials:
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
export BIGQUERY_CREDENTIALS=$(cat credentials.json)
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
2. Configure `app-config.local.yaml`:
|
|
603
|
+
|
|
604
|
+
```yaml
|
|
605
|
+
soundcheckMetrics:
|
|
606
|
+
enabled: true
|
|
607
|
+
schedule: '*/5 * * * *' # Every 5 minutes for testing
|
|
608
|
+
groupTypesToAggregate:
|
|
609
|
+
- squad
|
|
610
|
+
bigquery:
|
|
611
|
+
project: my-dev-project
|
|
612
|
+
dataset: dev_metrics
|
|
613
|
+
table: soundcheck_usage
|
|
614
|
+
credentials: ${BIGQUERY_CREDENTIALS}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
3. Start the backend:
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
yarn workspace backend start
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Testing Metrics Collection
|
|
624
|
+
|
|
625
|
+
To test without waiting for the scheduled run, modify the schedule to run frequently:
|
|
626
|
+
|
|
627
|
+
```yaml
|
|
628
|
+
schedule: '*/1 * * * *' # Every minute
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Or trigger manually by restarting the backend (the initial export runs immediately).
|
|
632
|
+
|
|
633
|
+
### Debugging
|
|
634
|
+
|
|
635
|
+
Enable debug logs:
|
|
636
|
+
|
|
637
|
+
```yaml
|
|
638
|
+
backend:
|
|
639
|
+
logger:
|
|
640
|
+
level: debug
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Filter for module logs:
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
yarn workspace backend start | grep '\[SoundcheckMetricsBackend\]'
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Adding New Metrics
|
|
650
|
+
|
|
651
|
+
1. Update the `SoundcheckMetrics` interface in `src/types.ts`
|
|
652
|
+
2. Update the metric collection logic in `src/service/MetricsCollector.ts`
|
|
653
|
+
3. Update the BigQuery table schema (ALTER TABLE or recreate)
|
|
654
|
+
4. Update this README with the new metric documentation
|
|
655
|
+
|
|
656
|
+
## License
|
|
657
|
+
|
|
658
|
+
Copyright © 2024 Spotify AB. All rights reserved.
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for the Soundcheck Metrics backend module
|
|
4
|
+
*/
|
|
5
|
+
soundcheckMetrics?: {
|
|
6
|
+
/**
|
|
7
|
+
* Enable or disable the metrics export
|
|
8
|
+
* @visibility backend
|
|
9
|
+
*/
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cron schedule for metrics export (default: '0 2 * * *' - daily at 2 AM UTC)
|
|
14
|
+
* Format: minute hour day month weekday
|
|
15
|
+
* @visibility backend
|
|
16
|
+
*/
|
|
17
|
+
schedule?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Group types to aggregate metrics for
|
|
21
|
+
* When specified, exports separate metric rows for each group of these types
|
|
22
|
+
* Example: ['squad', 'product area', 'studio', 'mission']
|
|
23
|
+
* @visibility backend
|
|
24
|
+
*/
|
|
25
|
+
groupTypesToAggregate?: string[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* BigQuery configuration
|
|
29
|
+
* @visibility backend
|
|
30
|
+
*/
|
|
31
|
+
bigquery: {
|
|
32
|
+
/**
|
|
33
|
+
* GCP project ID
|
|
34
|
+
* @visibility backend
|
|
35
|
+
*/
|
|
36
|
+
project: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* BigQuery dataset ID
|
|
40
|
+
* @visibility backend
|
|
41
|
+
*/
|
|
42
|
+
dataset: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* BigQuery table ID
|
|
46
|
+
* @visibility backend
|
|
47
|
+
*/
|
|
48
|
+
table: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Optional service account credentials as JSON string
|
|
52
|
+
* If not provided, will use Application Default Credentials
|
|
53
|
+
* @visibility secret
|
|
54
|
+
*/
|
|
55
|
+
credentials?: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Soundcheck Metrics Backend Module
|
|
5
|
+
*
|
|
6
|
+
* A backend module that extends the Soundcheck plugin to export aggregate metrics
|
|
7
|
+
* to BigQuery on a configurable schedule. This enables tracking of Soundcheck usage
|
|
8
|
+
* and performance across Portal instances.
|
|
9
|
+
*
|
|
10
|
+
* The module collects metrics such as:
|
|
11
|
+
* - Total checks, campaigns, programs, and checkers
|
|
12
|
+
* - Pass/fail rates for checks
|
|
13
|
+
* - Environment identification and customer flags
|
|
14
|
+
*
|
|
15
|
+
* Configuration is provided via app-config.yaml under the 'soundcheckMetrics' key.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
declare const soundcheckMetricsModule: _backstage_backend_plugin_api.BackendFeature;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Soundcheck metrics data structure for BigQuery export
|
|
23
|
+
*
|
|
24
|
+
* Represents a single snapshot of Soundcheck usage metrics at a point in time.
|
|
25
|
+
* This structure maps directly to the BigQuery table schema.
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
interface SoundcheckMetrics {
|
|
30
|
+
/** Environment identifier extracted from app.baseUrl hostname */
|
|
31
|
+
environment_name: string;
|
|
32
|
+
/** Unique installation ID from K_SERVICE environment variable (or 'UNKNOWN' as fallback) */
|
|
33
|
+
installation_id: string;
|
|
34
|
+
/** Organization name from organization.name config */
|
|
35
|
+
organization_name: string;
|
|
36
|
+
/** Whether this is a Spotify customer environment (identified by spc-* naming pattern) */
|
|
37
|
+
customer: boolean;
|
|
38
|
+
/** Total number of campaigns in the soundcheck_campaign table (global, not filtered by group) */
|
|
39
|
+
total_campaigns: number;
|
|
40
|
+
/** Total number of tracks in the soundcheck_program table (global, not filtered by group) */
|
|
41
|
+
total_tracks: number;
|
|
42
|
+
/** Total number of checker definitions in the checker table (global, not filtered by group) */
|
|
43
|
+
total_checkers: number;
|
|
44
|
+
/** The name of the group for which metrics are being collected (e.g., 'toast-infra', 'PDX', etc.) */
|
|
45
|
+
group_name: string;
|
|
46
|
+
/** Group level classification (e.g., 'squad', 'product area', 'studio', 'mission') */
|
|
47
|
+
group_level: string;
|
|
48
|
+
/** Total number of check results for entities owned by this group */
|
|
49
|
+
group_total_checks: number;
|
|
50
|
+
/** Number of passed check results for entities owned by this group */
|
|
51
|
+
group_passed_checks: number;
|
|
52
|
+
/** Number of failed check results for entities owned by this group */
|
|
53
|
+
group_failed_checks: number;
|
|
54
|
+
/** Number of active (non-archived) campaigns for entities owned by this group */
|
|
55
|
+
group_active_campaigns: number;
|
|
56
|
+
/** Number of archived campaigns for entities owned by this group */
|
|
57
|
+
group_archived_campaigns: number;
|
|
58
|
+
/** Number of track instances where an entity owned by this group passed all checks in the track */
|
|
59
|
+
tracks_passed_by_entities_of_group: number;
|
|
60
|
+
/** Number of track instances where an entity owned by this group failed any check in the track */
|
|
61
|
+
tracks_failed_by_entities_of_group: number;
|
|
62
|
+
/** Number of campaign instances where an entity owned by this group passed all checks in the campaign */
|
|
63
|
+
campaigns_passed_by_entities_of_group: number;
|
|
64
|
+
/** Number of campaign instances where an entity owned by this group failed any check in the campaign */
|
|
65
|
+
campaigns_failed_by_entities_of_group: number;
|
|
66
|
+
/** Timestamp when this metrics snapshot was collected */
|
|
67
|
+
snapshot_timestamp: Date;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Environment context information
|
|
71
|
+
*
|
|
72
|
+
* Metadata about the current Backstage instance, used to identify the source
|
|
73
|
+
* of exported metrics in BigQuery.
|
|
74
|
+
*
|
|
75
|
+
* @public
|
|
76
|
+
*/
|
|
77
|
+
interface EnvironmentContext {
|
|
78
|
+
/** Environment name extracted from the first segment of app.baseUrl hostname */
|
|
79
|
+
environmentName: string;
|
|
80
|
+
/** Unique installation ID, or 'UNKNOWN' if unavailable */
|
|
81
|
+
installationId: string;
|
|
82
|
+
/** Organization name from organization.name config */
|
|
83
|
+
organizationName: string;
|
|
84
|
+
/** Whether this environment is identified as a customer Portal instance */
|
|
85
|
+
isCustomer: boolean;
|
|
86
|
+
/** Group types to aggregate metrics for (e.g., ['squad', 'product area']) */
|
|
87
|
+
groupTypesToAggregate: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { type EnvironmentContext, type SoundcheckMetrics, soundcheckMetricsModule as default };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var c=require("@backstage/backend-plugin-api"),m=require("@spotify/backstage-plugin-soundcheck-node"),M=require("./service/BigQueryExporter.cjs.js"),f=require("./service/EnvironmentContextService.cjs.js"),v=require("./service/MetricsCollector.cjs.js");const B=c.createBackendModule({pluginId:"soundcheck",moduleId:"metrics",register(s){s.registerInit({deps:{config:c.coreServices.rootConfig,logger:c.coreServices.logger,database:c.coreServices.database,scheduler:c.coreServices.scheduler,soundcheckBackendClient:m.soundcheckBackendClientServiceRef},async init({config:r,logger:e,database:u,scheduler:a,soundcheckBackendClient:l}){if(!r.getOptionalBoolean("soundcheckMetrics.enabled")){e.info("[SoundcheckMetricsBackend] - Soundcheck metrics export is disabled.");return}e.info("[SoundcheckMetricsBackend] - Initializing Soundcheck metrics export module...");const k=await u.getClient(),h=new f.EnvironmentContextService(r,e),g=new v.MetricsCollectorService(k,h,e,l),S=new M.BigQueryExporterService(r,e),t=r.getOptionalString("soundcheckMetrics.schedule")||"0 2 * * *";await a.scheduleTask({id:"soundcheck-metrics-export",frequency:{cron:t},timeout:{minutes:10},scope:"global",fn:async()=>{try{e.info("[SoundcheckMetricsBackend] - Starting scheduled Soundcheck metrics export");const o=await g.collectMetrics();e.info(`[SoundcheckMetricsBackend] - Exporting ${o.length} metrics row(s) to BigQuery`);let i=0,n=0;for(const d of o)try{await S.export(d),i++}catch(p){n++,e.error(`[SoundcheckMetricsBackend] - Failed to export metrics for group ${d.group_name}`,{error:p})}e.info(`[SoundcheckMetricsBackend] - Completed scheduled Soundcheck metrics export: ${i} succeeded, ${n} failed`)}catch(o){e.error("[SoundcheckMetricsBackend] - Error during scheduled metrics export",{error:o})}}}),e.info("[SoundcheckMetricsBackend] - Soundcheck metrics export module initialized!",{schedule:t})}})}});exports.soundcheckMetricsModule=B;
|
|
2
|
+
//# sourceMappingURL=plugin.cjs.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var c=require("@google-cloud/bigquery");class s{constructor(e,t){this.logger=t;const r=e.getString("soundcheckMetrics.bigquery.project");this.#e=e.getString("soundcheckMetrics.bigquery.dataset"),this.#t=e.getString("soundcheckMetrics.bigquery.table");const i=e.getOptionalString("soundcheckMetrics.bigquery.credentials");this.#r=new c.BigQuery({projectId:r,...i&&{credentials:JSON.parse(i)}}),this.logger.info("[SoundcheckMetricsBackend] - BigQuery exporter initialized",{project:r,dataset:this.#e,table:this.#t})}#r;#e;#t;async export(e){this.logger.info("[SoundcheckMetricsBackend] - Exporting metrics to BigQuery",{environment:e.environment_name,timestamp:e.snapshot_timestamp.toISOString()});try{await this.#r.dataset(this.#e).table(this.#t).insert([e]),this.logger.info("[SoundcheckMetricsBackend] - Successfully exported metrics to BigQuery")}catch(t){this.logger.error("[SoundcheckMetricsBackend] - Failed to export metrics to BigQuery",{error:t})}}}exports.BigQueryExporterService=s;
|
|
2
|
+
//# sourceMappingURL=BigQueryExporter.cjs.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";class s{constructor(n,t){this.config=n,this.logger=t}async getContext(){const n=this.config.getString("app.baseUrl"),t=new URL(n).hostname,e=process.env.K_SERVICE??"Spotify-Internal",o=this.config.getOptionalString("organization.name")||"Unknown",i=e.startsWith("spc-"),r=this.config.getOptionalStringArray("soundcheckMetrics.groupTypesToAggregate")||[];return this.logger.info("[SoundcheckMetricsBackend] - Environment context retrieved",{hostname:t,installationId:e,organizationName:o,isCustomer:i,groupTypesToAggregate:r}),{environmentName:t,installationId:e,organizationName:o,isCustomer:i,groupTypesToAggregate:r}}}exports.EnvironmentContextService=s;
|
|
2
|
+
//# sourceMappingURL=EnvironmentContextService.cjs.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";class k{constructor(e,i,t,c){this.database=e,this.environmentContextService=i,this.logger=t,this.soundcheckBackendClient=c}async#t(){try{const e=await this.soundcheckBackendClient.getTracks(),i=new Map;for(const t of e){const c=new Set;for(const r of t.levels)for(const s of r.checks)c.add(s.id);i.set(t.id,{checkIds:c,type:t.type})}return this.logger.debug(`[SoundcheckMetricsBackend] - Loaded ${i.size} tracks with check mappings and types`),i}catch(e){return this.logger.warn("[SoundcheckMetricsBackend] - Failed to load track information",{error:e}),new Map}}async#i(){try{return(await this.soundcheckBackendClient.getCheckIds()).length}catch(e){return this.logger.warn("[SoundcheckMetricsBackend] - Failed to get total checks",{error:e}),0}}async#c(e,i){if(!e||e.length===0)return{tracks_passed_by_entities:0,tracks_failed_by_entities:0,campaigns_passed_by_entities:0,campaigns_failed_by_entities:0};if(i.size===0)return{tracks_passed_by_entities:0,tracks_failed_by_entities:0,campaigns_passed_by_entities:0,campaigns_failed_by_entities:0};try{let t=0,c=0,r=0,s=0;for(const[n,a]of i.entries()){const o=Array.from(a.checkIds),g=a.type==="campaign";for(const h of e)try{const d=await this.database("check_result").where("entity_ref",h).whereIn("check_id",o).select("state");if(d.length===0)continue;const _=d.every(l=>l.state==="passed"),u=d.some(l=>l.state==="failed");_&&d.length===o.length?g?r++:t++:u&&(g?s++:c++)}catch(d){this.logger.debug(`[SoundcheckMetricsBackend] - Failed to get check results for entity ${h}`,{error:d})}}return{tracks_passed_by_entities:t,tracks_failed_by_entities:c,campaigns_passed_by_entities:r,campaigns_failed_by_entities:s}}catch(t){return this.logger.warn("[SoundcheckMetricsBackend] - Failed to calculate track metrics",{error:t}),{tracks_passed_by_entities:0,tracks_failed_by_entities:0,campaigns_passed_by_entities:0,campaigns_failed_by_entities:0}}}async#e(e,i,t){try{let c=this.database(e).count("* as count");if(i&&(c=c.where(i.field,i.value)),t!==void 0)if(t.length>0)c=c.whereIn("entity_ref",t);else return 0;const r=await c.first(),s=Number(r?.count||0);return this.logger.debug(`[SoundcheckMetricsBackend] - Queried ${e}: ${s} rows`,{whereClause:i,filteredByGroup:!!t}),s}catch(c){return this.logger.warn(`[SoundcheckMetricsBackend] - Failed to query table ${e}`,{error:c.message,code:c.code,table:e,where:i}),0}}async collectMetrics(){this.logger.info("[SoundcheckMetricsBackend] - Collecting Soundcheck metrics...");const e=await this.environmentContextService.getContext();if(e.groupTypesToAggregate.length===0)return this.logger.warn("[SoundcheckMetricsBackend] - No group types are configured, collecting global metrics..."),[];this.logger.info(`[SoundcheckMetricsBackend] - Collecting metrics for group types: ${e.groupTypesToAggregate.join(", ")}`);let i;try{i=await this.soundcheckBackendClient.getGroupsOfType(e.groupTypesToAggregate)}catch(n){return this.logger.error(`[SoundcheckMetricsBackend] - Failed to get groups of type [${e.groupTypesToAggregate.join(", ")}]`,n),[]}if(i.length===0)return this.logger.warn(`[SoundcheckMetricsBackend] - No groups found for types: ${e.groupTypesToAggregate.join(", ")}`),[];const t=await this.#t(),c=t.size,r=await this.#i(),s=[];for(const n of i){const a=n.entityRef.split("/"),o=a[a.length-1];this.logger.info(`[SoundcheckMetricsBackend] - Collecting metrics for group: ${o} (type: ${n.type})`);try{const g=await this.#s(e,n.entityRef,o,n.type,t,c,r);g?s.push(g):this.logger.warn(`[SoundcheckMetricsBackend] - Skipping metrics for group ${o} - collection returned no data`)}catch(g){this.logger.error(`[SoundcheckMetricsBackend] - Failed to collect metrics for group ${o}`,g)}}return this.logger.info(`[SoundcheckMetricsBackend] - Metrics collected successfully for ${s.length} groups`),s}async#s(e,i,t,c,r,s,n){let a;if(i)try{a=await this.soundcheckBackendClient.getOwnedEntityRefs(i,!0),this.logger.debug(`[SoundcheckMetricsBackend] - Found ${a?.length||0} entities owned by ${t}`)}catch(p){this.logger.warn(`[SoundcheckMetricsBackend] - Failed to get owned entities for group ${t}`,{error:p});return}const o=await this.#e("soundcheck_campaign"),g=await this.#e("check_result",void 0,a),h=await this.#e("soundcheck_campaign",{field:"archived",value:!1}),d=await this.#e("soundcheck_campaign",{field:"archived",value:!0}),_=await this.#e("check_result",{field:"state",value:"passed"},a),u=await this.#e("check_result",{field:"state",value:"failed"},a),l=await this.#c(a,r);return{environment_name:e.environmentName,installation_id:e.installationId,organization_name:e.organizationName,customer:e.isCustomer,total_campaigns:o,total_tracks:s,total_checkers:n,group_name:t,group_level:c,group_total_checks:g,group_passed_checks:_,group_failed_checks:u,group_active_campaigns:h,group_archived_campaigns:d,tracks_passed_by_entities_of_group:l.tracks_passed_by_entities,tracks_failed_by_entities_of_group:l.tracks_failed_by_entities,campaigns_passed_by_entities_of_group:l.campaigns_passed_by_entities,campaigns_failed_by_entities_of_group:l.campaigns_failed_by_entities,snapshot_timestamp:new Date(Date.UTC(new Date().getUTCFullYear(),new Date().getUTCMonth(),new Date().getUTCDate()))}}}exports.MetricsCollectorService=k;
|
|
2
|
+
//# sourceMappingURL=MetricsCollector.cjs.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spotify/backstage-plugin-soundcheck-metrics-backend",
|
|
3
|
+
"description": "Backstage backend module for exporting Soundcheck metrics to BigQuery",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
|
+
"homepage": "https://backstage.spotify.com",
|
|
7
|
+
"main": "dist/index.cjs.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"main": "dist/index.cjs.js",
|
|
11
|
+
"types": "dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"backstage": {
|
|
14
|
+
"role": "backend-plugin-module",
|
|
15
|
+
"pluginId": "soundcheck",
|
|
16
|
+
"pluginPackage": "@spotify/backstage-plugin-soundcheck-backend",
|
|
17
|
+
"features": {
|
|
18
|
+
".": "@backstage/BackendFeature"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "backstage-cli package build --minify",
|
|
23
|
+
"lint": "backstage-cli package lint --max-warnings 0",
|
|
24
|
+
"test": "backstage-cli package test",
|
|
25
|
+
"clean": "backstage-cli package clean",
|
|
26
|
+
"prepack": "backstage-cli package prepack",
|
|
27
|
+
"postpack": "backstage-cli package postpack"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@backstage/backend-plugin-api": "^1.7.0",
|
|
31
|
+
"@backstage/catalog-client": "^1.13.0",
|
|
32
|
+
"@backstage/config": "^1.3.6",
|
|
33
|
+
"@backstage/plugin-catalog-node": "^2.0.0",
|
|
34
|
+
"@google-cloud/bigquery": "^8.0.0",
|
|
35
|
+
"@spotify/backstage-plugin-soundcheck-node": "^0.11.0",
|
|
36
|
+
"knex": "^3.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@backstage/backend-test-utils": "^1.11.0",
|
|
40
|
+
"@backstage/cli": "^0.35.4"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"!dist/**/*.map",
|
|
45
|
+
"config.d.ts"
|
|
46
|
+
],
|
|
47
|
+
"configSchema": "config.d.ts",
|
|
48
|
+
"typesVersions": {
|
|
49
|
+
"*": {
|
|
50
|
+
"package.json": [
|
|
51
|
+
"package.json"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|