Skip to content

Commit 07a5a55

Browse files
committed
refactor(scorecard): logic to load and cleanup metrics
Signed-off-by: Ihor Mykhno <[email protected]>
1 parent 68c0b71 commit 07a5a55

25 files changed

+1294
-344
lines changed

workspaces/scorecard/plugins/scorecard-backend-module-github/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,12 @@ Thresholds define conditions that determine which category a metric value belong
9393

9494
## Schedule Configuration
9595

96-
The Scorecard plugin uses Backstage's built-in scheduler service to automatically collect metrics from all registered providers.
96+
The Scorecard plugin uses Backstage's built-in scheduler service to automatically collect metrics from all registered providers every hour by default. However, this configuration can be changed in the `app-config.yaml` file. Here is an example of how to do that:
9797

9898
```yaml
9999
scorecard:
100100
plugins:
101-
jira:
101+
github:
102102
open_prs:
103103
schedule:
104104
frequency:

workspaces/scorecard/plugins/scorecard-backend-module-jira/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ scorecard:
100100
101101
## Schedule Configuration
102102
103-
The Scorecard plugin uses Backstage's built-in scheduler service to automatically collect metrics from all registered providers.
103+
The Scorecard plugin uses Backstage's built-in scheduler service to automatically collect metrics from all registered providers every hour by default. However, this configuration can be changed in the `app-config.yaml` file. Here is an example of how to do that:
104104

105105
```yaml
106106
scorecard:

workspaces/scorecard/plugins/scorecard-backend-module-jira/src/clients/base.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717
import type { Config } from '@backstage/config';
1818
import type { Entity } from '@backstage/catalog-model';
1919
import { JiraEntityFilters, JiraOptions, RequestOptions } from './types';
20-
import {
21-
JIRA_MANDATORY_FILTER,
22-
OPEN_ISSUES_CONFIG_PATH,
23-
} from '../constants';
20+
import { JIRA_MANDATORY_FILTER, OPEN_ISSUES_CONFIG_PATH } from '../constants';
2421
import { ScorecardJiraAnnotations } from '../annotations';
2522
import { sanitizeValue, validateIdentifier, validateJQLValue } from './utils';
2623
import { ConnectionStrategy } from '../strategies/ConnectionStrategy';

workspaces/scorecard/plugins/scorecard-backend/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ Thresholds are evaluated in order, and the first matching rule determines the ca
9797

9898
For comprehensive threshold configuration guide, examples, and best practices, see [thresholds.md](./docs/thresholds.md).
9999

100+
## Configuration cleanup Job
101+
102+
The plugin has a predefined job that runs every day to check and clean old metrics. By default, metrics are saved for **365 days**, however, this period can be changed in the `app-config.yaml` file. Here is an example of how to do that:
103+
104+
```YAML app-config.yaml
105+
---
106+
scorecard:
107+
dataRetentionDays: 12
108+
```
109+
100110
## Development
101111

102112
This plugin backend can be started in a standalone mode from directly in this

workspaces/scorecard/plugins/scorecard-backend/__fixtures__/mockDatabase.ts renamed to workspaces/scorecard/plugins/scorecard-backend/__fixtures__/mockDatabaseMetricValues.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { MetricValuesStore } from '../src/database/MetricValuesStore';
17+
import { DatabaseMetricValues } from '../src/database/DatabaseMetricValues';
1818

19-
export const mockMetricValuesStore = {
19+
export const mockDatabaseMetricValues = {
2020
createMetricValues: jest.fn(),
21-
readMetricValues: jest.fn(),
2221
readLatestEntityMetricValues: jest.fn(),
23-
} as unknown as jest.Mocked<MetricValuesStore>;
22+
cleanupExpiredMetrics: jest.fn(),
23+
} as unknown as jest.Mocked<DatabaseMetricValues>;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { MetricProvidersRegistry } from '../src/providers/MetricProvidersRegistry';
18+
19+
export const mockMetricProvidersRegistry = {
20+
register: jest.fn(),
21+
getProvider: jest.fn(),
22+
getMetric: jest.fn(),
23+
calculateMetric: jest.fn(),
24+
calculateMetrics: jest.fn(),
25+
listProviders: jest.fn().mockReturnValue([]),
26+
listMetrics: jest.fn().mockReturnValue([]),
27+
listMetricsByDatasource: jest.fn().mockReturnValue([]),
28+
} as unknown as jest.Mocked<MetricProvidersRegistry>;
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
mockServices,
19+
TestDatabaseId,
20+
TestDatabases,
21+
} from '@backstage/backend-test-utils';
22+
import { DatabaseMetricValues } from './DatabaseMetricValues';
23+
import { DbMetricValue } from './types';
24+
import { migrate } from './migration';
25+
26+
jest.setTimeout(60000);
27+
28+
const metricValues: Omit<DbMetricValue, 'id'>[] = [
29+
{
30+
catalog_entity_ref: 'component:default/test-service',
31+
metric_id: 'github.metric1',
32+
value: 41,
33+
timestamp: new Date('2023-01-01T00:00:00Z'),
34+
error_message: undefined,
35+
},
36+
{
37+
catalog_entity_ref: 'component:default/another-service',
38+
metric_id: 'github.metric1',
39+
value: 25,
40+
timestamp: new Date('2023-01-01T00:00:00Z'),
41+
error_message: undefined,
42+
},
43+
{
44+
catalog_entity_ref: 'component:default/another-service',
45+
metric_id: 'github.metric2',
46+
value: undefined,
47+
timestamp: new Date('2023-01-01T00:00:00Z'),
48+
error_message: 'Failed to fetch metric',
49+
},
50+
];
51+
52+
describe('DatabaseMetricValues', () => {
53+
const databases = TestDatabases.create({
54+
ids: ['SQLITE_3', 'POSTGRES_15'],
55+
});
56+
57+
async function createDatabase(databaseId: TestDatabaseId) {
58+
const client = await databases.init(databaseId);
59+
const mockDatabaseService = mockServices.database.mock({
60+
getClient: async () => client,
61+
migrations: { skip: false },
62+
});
63+
64+
await migrate(mockDatabaseService);
65+
66+
return {
67+
client,
68+
db: new DatabaseMetricValues(client),
69+
};
70+
}
71+
72+
describe('createMetricValues', () => {
73+
it.each(databases.eachSupportedId())(
74+
'should successfully insert metric values - %p',
75+
async databaseId => {
76+
const { client, db } = await createDatabase(databaseId);
77+
78+
await expect(
79+
db.createMetricValues(metricValues),
80+
).resolves.not.toThrow();
81+
82+
const insertedValues = await client('metric_values').select('*');
83+
84+
expect(insertedValues).toHaveLength(3);
85+
86+
expect(insertedValues[0]).toMatchObject({
87+
catalog_entity_ref: 'component:default/test-service',
88+
metric_id: 'github.metric1',
89+
value: 41,
90+
error_message: null,
91+
});
92+
93+
expect(insertedValues[1]).toMatchObject({
94+
catalog_entity_ref: 'component:default/another-service',
95+
metric_id: 'github.metric1',
96+
value: 25,
97+
error_message: null,
98+
});
99+
100+
expect(insertedValues[2]).toMatchObject({
101+
catalog_entity_ref: 'component:default/another-service',
102+
metric_id: 'github.metric2',
103+
value: null,
104+
error_message: 'Failed to fetch metric',
105+
});
106+
},
107+
);
108+
});
109+
110+
describe('readLatestEntityMetricValues', () => {
111+
it.each(databases.eachSupportedId())(
112+
'should return latest metric values for entity and metrics - %p',
113+
async databaseId => {
114+
const { client, db } = await createDatabase(databaseId);
115+
116+
const baseTime = new Date('2023-01-01T00:00:00Z');
117+
const laterTime = new Date('2023-01-01T01:00:00Z');
118+
119+
await client('metric_values').insert([
120+
{
121+
...metricValues[0],
122+
timestamp: baseTime, // older time
123+
},
124+
{
125+
...metricValues[1],
126+
timestamp: laterTime, // newer time, value should be returned
127+
},
128+
{
129+
...metricValues[2],
130+
timestamp: laterTime, // newer time, different entity
131+
},
132+
{
133+
catalog_entity_ref: 'component:default/test-service',
134+
metric_id: 'github.metric2',
135+
value: undefined,
136+
timestamp: baseTime,
137+
error_message: 'Failed to fetch metric',
138+
},
139+
]);
140+
141+
const result = await db.readLatestEntityMetricValues(
142+
'component:default/test-service',
143+
['github.metric1', 'github.metric2'],
144+
);
145+
146+
expect(result).toHaveLength(2);
147+
148+
const metric1Result = result.find(
149+
r => r.metric_id === 'github.metric1',
150+
);
151+
const metric2Result = result.find(
152+
r => r.metric_id === 'github.metric2',
153+
);
154+
155+
expect(metric1Result).toMatchObject({
156+
catalog_entity_ref: 'component:default/test-service',
157+
metric_id: 'github.metric1',
158+
value: 41,
159+
});
160+
161+
expect(metric2Result).toMatchObject({
162+
catalog_entity_ref: 'component:default/test-service',
163+
metric_id: 'github.metric2',
164+
value: null,
165+
});
166+
},
167+
);
168+
});
169+
170+
describe('cleanupExpiredMetrics', () => {
171+
it.each(databases.eachSupportedId())(
172+
'should delete metric values that are older than the given date - %p',
173+
async databaseId => {
174+
const { client, db } = await createDatabase(databaseId);
175+
176+
await client('metric_values').insert([
177+
{
178+
...metricValues[0],
179+
timestamp: new Date('2022-01-01T00:00:00Z'),
180+
},
181+
{
182+
...metricValues[1],
183+
},
184+
]);
185+
186+
const result = await db.cleanupExpiredMetrics(
187+
new Date('2023-01-01T00:00:00Z'),
188+
);
189+
190+
expect(result).toBe(1);
191+
},
192+
);
193+
});
194+
});
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@
1515
*/
1616

1717
import { Knex } from 'knex';
18-
import { DbMetricValue, MetricValuesStore } from './MetricValuesStore';
18+
import { DbMetricValue } from './types';
1919

20-
export class DatabaseMetricValuesStore implements MetricValuesStore {
20+
export class DatabaseMetricValues {
2121
private readonly tableName = 'metric_values';
2222

23-
constructor(private readonly knex: Knex<any, any[]>) {}
23+
constructor(private readonly dbClient: Knex<any, any[]>) {}
2424

2525
/**
2626
* Insert multiple metric values
2727
*/
2828
async createMetricValues(
2929
metricValues: Omit<DbMetricValue, 'id'>[],
3030
): Promise<void> {
31-
await this.knex(this.tableName).insert(metricValues);
31+
await this.dbClient(this.tableName).insert(metricValues);
3232
}
3333

3434
/**
@@ -38,11 +38,11 @@ export class DatabaseMetricValuesStore implements MetricValuesStore {
3838
catalog_entity_ref: string,
3939
metric_ids: string[],
4040
): Promise<DbMetricValue[]> {
41-
return await this.knex(this.tableName)
41+
return await this.dbClient(this.tableName)
4242
.select('*')
4343
.whereIn(
4444
'id',
45-
this.knex(this.tableName)
45+
this.dbClient(this.tableName)
4646
.max('id')
4747
.whereIn('metric_id', metric_ids)
4848
.where('catalog_entity_ref', catalog_entity_ref)
@@ -54,7 +54,7 @@ export class DatabaseMetricValuesStore implements MetricValuesStore {
5454
* Delete metric values that are older than the given date
5555
*/
5656
async cleanupExpiredMetrics(olderThan: Date): Promise<number> {
57-
return await this.knex(this.tableName)
57+
return await this.dbClient(this.tableName)
5858
.where('timestamp', '<', olderThan)
5959
.del();
6060
}

0 commit comments

Comments
 (0)