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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@red-hat-developer-hub/backstage-plugin-scorecard-common': minor
'@red-hat-developer-hub/backstage-plugin-scorecard-node': minor
'@red-hat-developer-hub/backstage-plugin-scorecard-backend': minor
---

Add optional `enabled` flag for metrics and `isEnabled()` method for metric providers, allowing them to be disabled by default. Administrators can override these defaults via app-config. Disabled metrics are excluded from scheduling, API responses, and scaffolder actions.
15 changes: 15 additions & 0 deletions workspaces/scorecard/plugins/scorecard-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ To use these providers, install the corresponding backend modules:

Administrators can disable metric checks globally via `scorecard.disabledMetrics`, and users can disable them per entity via the `scorecard.io/disabled-metrics` annotation. Whether that annotation is honored is controlled by `scorecard.entityAnnotations.enabled` (global switch for all scorecard entity annotations) and `scorecard.entityAnnotations.disabledMetrics` (`enabled` / `except`). For more details, see [disabled-metrics-logic.md](./docs/disabled-metrics-logic.md).

Providers and individual metrics can also be disabled via the `enabled` config key at the provider or metric level:

```yaml
scorecard:
metricProviders:
github:
openPRs:
enabled: false # disable the entire provider
metrics:
openPRs:
enabled: true # re-enable a specific metric
```

Config overrides take precedence over code-level defaults. See [providers.md](./docs/providers.md#disabling-providers-and-metrics-by-default) for provider authoring details and [disabled-metrics-logic.md](./docs/disabled-metrics-logic.md#enabled-by-default-system) for the full resolution chain.

## Thresholds

Thresholds define conditions to assign metric values to specific visual categories (`success`, `warning`, `error` or any custom category). The Scorecard plugin provides multiple ways to configure thresholds:
Expand Down
18 changes: 18 additions & 0 deletions workspaces/scorecard/plugins/scorecard-backend/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ export interface Config {
* Use the local name without datasource prefix (e.g., `openPRs` instead of `github.openPRs`).
*/
[providerName: string]: {
/**
* Whether this metric provider is enabled. Overrides the
* provider's code-level `isEnabled()` default. When `false`,
* the provider and all its metrics are disabled (unless
* individual metrics are re-enabled via their own `enabled`
* flag). When `true`, a provider that is disabled by default
* in code is re-enabled.
*/
enabled?: boolean;
/** How often metrics will be calculated for this provider. */
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
/**
Expand All @@ -151,6 +160,15 @@ export interface Config {
* Use the local name without datasource prefix (e.g., 'openPRs' instead of 'github.openPRs').
*/
[metricName: string]: {
/**
* Whether this metric is enabled. Overrides both the
* metric's code-level `enabled` default and the
* provider-level enabled state. Set to `true` to
* re-enable a metric that is disabled by default; set
* to `false` to disable a metric that is normally
* enabled.
*/
enabled?: boolean;
/**
* How metric values are categorized for this metric.
* Overrides provider-level thresholds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@ The following table describes the result for each combination of app-config and

`—`: means this setting is not consulted for that row.

## Enabled-by-default system

In addition to the `disabledMetrics` list and entity annotations described above, providers and individual metrics can be disabled **by default** in code using the `isEnabled()` method on `MetricProvider` and the `enabled` field on `Metric`. Administrators can override these defaults via `app-config.yaml`:

```yaml
scorecard:
metricProviders:
myDatasource:
exampleProvider:
enabled: false # disable the entire provider
metrics:
experimentalMetric:
enabled: true # re-enable a specific metric
```

The enabled-by-default resolution uses a five-level precedence chain (first defined value wins):

1. **Config metric `enabled`** — most specific config override
2. **Config provider `enabled`** — provider-level config override
3. **Code metric `enabled` field** — code-level default
4. **Code provider `isEnabled()`** — provider code default
5. **`true`** — backward-compatible default

Config overrides always take precedence over code defaults. Disabled metrics are excluded from scheduled data collection, API responses, and scaffolder actions. Old data for disabled metrics remains in the database.

**Interaction with `disabledMetrics`:** The enabled-by-default system and the `disabledMetrics` list are independent mechanisms. A metric must pass both checks to be active: it must be enabled by the resolution chain above **and** not appear in `scorecard.disabledMetrics`. The `disabledMetrics` list is a per-entity check evaluated at collection time, while the enabled-by-default system is a global check applied at startup and request time.

## Summary

- **`scorecard.disabledMetrics`**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,45 @@ export class MyMetricProvider implements MetricProvider<'number'> {
- Each metric carries its own `type` and `thresholds`
- Configuration for metric providers follows the schema in [`config.d.ts`](../config.d.ts) under `scorecard.metricProviders.<datasource>.<providerName>` (e.g., for schedule and threshold configurations)

### Disabling providers and metrics by default

Providers and individual metrics can be disabled by default in code. Administrators can override these defaults via `app-config.yaml`.

**Provider-level:** Implement the optional `isEnabled()` method on `MetricProvider`. When it returns `false`, the provider and all its metrics are disabled unless overridden by config:

```typescript
isEnabled(): boolean {
return false; // disabled by default; admins can re-enable via config
}
```

**Metric-level:** Set the optional `enabled` field on a `Metric` object. When `false`, the individual metric is disabled by default:

```typescript
getMetrics(): Metric<'number'>[] {
return [
{
id: 'myDatasource.experimentalMetric',
title: 'Experimental Metric',
description: 'This metric is disabled by default.',
type: 'number',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
enabled: false,
},
];
}
```

The enabled state is resolved using a five-level precedence chain (first defined value wins):

1. Config metric `enabled` — most specific config override
2. Config provider `enabled` — provider-level config override
3. Code metric `enabled` field — code-level default
4. Code provider `isEnabled()` — provider code default
5. `true` — backward-compatible default

Config overrides always take precedence over code defaults. Disabled metrics are excluded from scheduling, API responses, and scaffolder actions. See [disabled-metrics-logic.md](./disabled-metrics-logic.md) for the full disabled-metrics system.

## Updating the Module

Update the module registration in `module.ts` to register your metric provider:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthService, PermissionsService } from '@backstage/backend-plugin-api';
import type { Config } from '@backstage/config';
import {
AuthService,
LoggerService,
PermissionsService,
} from '@backstage/backend-plugin-api';
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';
Expand All @@ -27,6 +32,8 @@ export { createListMetricsAction } from './listMetrics';
export const createScorecardActions = (options: {
actionsRegistry: ActionsRegistryService;
auth: AuthService;
config: Config;
logger: LoggerService;
permissions: PermissionsService;
catalog: CatalogService;
metricProvidersRegistry: MetricProvidersRegistry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';
describe('createListMetricsAction', () => {
const mockRegistry = {
listMetrics: jest.fn(),
getProvider: jest.fn().mockReturnValue({
getProviderId: () => 'github.openPRs',
getProviderDatasourceId: () => 'github',
}),
} as unknown as MetricProvidersRegistry;

beforeEach(() => {
Expand Down Expand Up @@ -56,8 +60,10 @@ describe('createListMetricsAction', () => {

createListMetricsAction({
actionsRegistry: mockActionsRegistry,
config: mockServices.rootConfig({ data: {} }),
permissions: mockPermissions,
metricProvidersRegistry: mockRegistry,
logger: mockServices.logger.mock(),
});

const result = await mockActionsRegistry.invoke({
Expand All @@ -78,8 +84,10 @@ describe('createListMetricsAction', () => {

createListMetricsAction({
actionsRegistry: mockActionsRegistry,
config: mockServices.rootConfig({ data: {} }),
permissions: mockPermissions,
metricProvidersRegistry: mockRegistry,
logger: mockServices.logger.mock(),
});

await expect(
Expand Down Expand Up @@ -126,8 +134,10 @@ describe('createListMetricsAction', () => {

createListMetricsAction({
actionsRegistry: mockActionsRegistry,
config: mockServices.rootConfig({ data: {} }),
permissions: mockPermissions,
metricProvidersRegistry: mockRegistry,
logger: mockServices.logger.mock(),
});

const result = await mockActionsRegistry.invoke({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PermissionsService } from '@backstage/backend-plugin-api';
import type { Config } from '@backstage/config';
import {
LoggerService,
PermissionsService,
} from '@backstage/backend-plugin-api';
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
import { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
import { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';
import {
authorizeConditional,
filterAuthorizedMetrics,
} from '../permissions/permissionUtils';
import { filterEnabledMetrics } from '../utils/metricUtils';

export const createListMetricsAction = ({
actionsRegistry,
config,
permissions,
metricProvidersRegistry,
logger,
}: {
actionsRegistry: ActionsRegistryService;
config: Config;
permissions: PermissionsService;
metricProvidersRegistry: MetricProvidersRegistry;
logger: LoggerService;
}) => {
actionsRegistry.register({
name: 'list-metrics',
Expand Down Expand Up @@ -66,7 +75,12 @@ export const createListMetricsAction = ({
scorecardMetricReadPermission,
);

const allMetrics = metricProvidersRegistry.listMetrics();
const allMetrics = filterEnabledMetrics(
config,
metricProvidersRegistry.listMetrics(),
metricId => metricProvidersRegistry.getProvider(metricId),
logger,
);
const metrics = filterAuthorizedMetrics(allMetrics, conditions);

return { output: { metrics } };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const scorecardPlugin = createBackendPlugin({
);

const catalogMetricService = new CatalogMetricService({
config,
catalog,
auth,
registry: metricProvidersRegistry,
Expand Down Expand Up @@ -159,6 +160,8 @@ export const scorecardPlugin = createBackendPlugin({
createScorecardActions({
actionsRegistry,
auth,
config,
logger,
permissions,
catalog,
metricProvidersRegistry,
Expand All @@ -167,6 +170,7 @@ export const scorecardPlugin = createBackendPlugin({

httpRouter.use(
await createRouter({
config,
metricProvidersRegistry,
service,
catalog,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,51 @@ describe('Scheduler', () => {

expect(initializedTasks).toEqual([]);
});

it('should skip providers whose metrics are all disabled by config', async () => {
const disabledConfig = mockServices.rootConfig({
data: {
scorecard: {
metricProviders: {
github: {
testMetric: { enabled: false },
},
},
},
},
});

const disabledScheduler = Scheduler.create({
auth: mockAuth,
catalog: mockCatalog,
config: disabledConfig,
logger: mockLogger,
scheduler: mockScheduler,
database: mockDatabase,
metricProvidersRegistry: mockRegistry,
thresholdEvaluator: new ThresholdEvaluator(),
thresholdResolver: new ThresholdResolver(
disabledConfig,
mockRegistry.listProviders(),
),
});

(disabledScheduler as any).initializeTasksByProviders();

const initializedTasks = (disabledScheduler as any).tasks;

// Only jira.testMetric should have a task; github.testMetric is disabled
expect(initializedTasks).toEqual([
{
name: 'jira.testMetric',
task: mockPullTask,
},
]);

expect(mockLogger.info).toHaveBeenCalledWith(
"Skipping provider 'github.testMetric': all metrics disabled",
);
});
});

describe('startTask', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { SchedulerOptions, SchedulerTask } from './types';
import { DatabaseMetricValues } from '../database/DatabaseMetricValues';
import { ThresholdEvaluator } from '../threshold/ThresholdEvaluator';
import { ThresholdResolver } from '../threshold/ThresholdResolver';
import { isMetricEnabled } from '../utils/metricUtils';

export class Scheduler {
private readonly auth: AuthService;
Expand Down Expand Up @@ -101,6 +102,17 @@ export class Scheduler {
const providers = this.metricProvidersRegistry.listProviders();

for (const provider of providers) {
const hasEnabledMetric = provider
Comment thread
christoph-jerolimov marked this conversation as resolved.
.getMetrics()
.some(m => isMetricEnabled(this.config, m, provider));

if (!hasEnabledMetric) {
this.logger.info(
`Skipping provider '${provider.getProviderId()}': all metrics disabled`,
);
continue;
}

this.tasks.push({
name: provider.getProviderId(),
task: new PullMetricsByProviderTask(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import type { Config } from '@backstage/config';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';
import { isMetricIdDisabled } from '../../utils/metricUtils';
import { isMetricIdDisabled, isMetricEnabled } from '../../utils/metricUtils';
import { randomUUID } from 'node:crypto';
import { normalizeOwnerRef } from '../../utils/normalizeOwnerRef';
import { resolveScheduleFromConfig } from '../../utils/metricProviderConfigKeys';
Expand Down Expand Up @@ -124,7 +124,23 @@ export class PullMetricsByProviderTask implements SchedulerTask {
let totalProcessed = 0;
let cursor: string | undefined = undefined;

const metrics = provider.getMetrics();
const allMetrics = provider.getMetrics();
const metrics = allMetrics.filter(m =>
isMetricEnabled(this.config, m, provider),
);

if (metrics.length < allMetrics.length) {
const skipped = allMetrics.length - metrics.length;
logger.info(
`Skipping ${skipped} disabled metric(s) for ${this.providerId}`,
);
}

if (metrics.length === 0) {
logger.info(`No enabled metrics for ${this.providerId}, skipping pull`);
return;
}

const metricsById = new Map<string, Metric>(metrics.map(m => [m.id, m]));
const metricIds = metrics.map(m => m.id);

Expand Down
Loading
Loading