feat(metrics): Implement MetricsService (#32977)
* feat: add MetricsService alpha release Introduces MetricsService as a new @alpha core service wrapping @opentelemetry/api. Includes migration of existing catalog metrics to use the new service. Signed-off-by: benjdlambert <ben@blam.sh> * chore: duplicate otel types, add plugin-scoped factory and tests Signed-off-by: benjdlambert <ben@blam.sh> * chore: update BEP-0012 metrics service design Signed-off-by: Kurt King <kurtaking@gmail.com> * chore: address PR feedback from freben and rugvip Rename instrument types with MetricsService prefix for namespace clarity, move config to backend.metrics.plugin.{pluginId}, add config.d.ts schema, and improve factory test assertions. Signed-off-by: benjdlambert <ben@blam.sh> --------- Signed-off-by: benjdlambert <ben@blam.sh> Signed-off-by: Kurt King <kurtaking@gmail.com> Co-authored-by: Kurt King <kurtaking@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Migrates existing catalog metrics to use the alpha MetricsService. This release is a 1:1 migration with no breaking changes.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Adds a new metrics service mock to be leveraged in tests
|
||||
@@ -23,7 +23,6 @@ creation-date: 2025-06-23
|
||||
- [Integration with OpenTelemetry Auto-Instrumentation](#integration-with-opentelemetry-auto-instrumentation)
|
||||
- [Configuration](#configuration)
|
||||
- [Interface](#interface)
|
||||
- [Root Metrics Service](#root-metrics-service)
|
||||
- [Plugin Metrics Service](#plugin-metrics-service)
|
||||
- [Example](#example)
|
||||
- [Release Plan](#release-plan)
|
||||
@@ -36,7 +35,7 @@ Add a core `MetricsService` to Backstage's framework to provide a unified interf
|
||||
|
||||
## Motivation
|
||||
|
||||
While individual plugins may implement their own metrics, there's no standardized approach leading to inconsistent metrics patterns across the ecosystem. For example, both `catalog_entities_count` and `catalog.processed.entities.count` are examples of existing metric patterns. Ideally, these would be standardized to `backstage.plugin.catalog.entities.count` and `backstage.plugin.catalog.entities.processed.total` respectively.
|
||||
While individual plugins may implement their own metrics, there's no standardized approach leading to inconsistent metrics patterns across the ecosystem and incompatibility with OpenTelemetry semantic conventions. For example, a plugin implementing MCP functionality might incorrectly namespace metrics as `backstage_mcp_client_duration` when OpenTelemetry semantic conventions explicitly define `mcp.client.operation.duration` as the standard.
|
||||
|
||||
By providing a core metrics service:
|
||||
|
||||
@@ -45,7 +44,7 @@ By providing a core metrics service:
|
||||
|
||||
### Goals
|
||||
|
||||
- Plugin-scoped metric namespacing
|
||||
- Plugin identification via OpenTelemetry Instrumentation Scope
|
||||
- Consistent metrics patterns across all plugins
|
||||
- Aligned with OpenTelemetry industry standards
|
||||
- Provide a familiar interface as other core services
|
||||
@@ -124,9 +123,12 @@ The `MetricsService` **complements** rather than duplicates auto-instrumentation
|
||||
|
||||
// MetricsService provides (manually):
|
||||
const entityMetrics = metricsService.createCounter('entities.processed.total');
|
||||
entityMetrics.add(entities.length, { operation: 'refresh', kind: 'Component' });
|
||||
entityMetrics.add(entities.length, {
|
||||
operation: 'refresh',
|
||||
'entity.kind': 'Component',
|
||||
});
|
||||
|
||||
// Metric is now available as `backstage.plugin.catalog.entities.processed.total`
|
||||
// Metric is now available as `entities.processed.total`
|
||||
```
|
||||
|
||||
### Configuration
|
||||
@@ -162,43 +164,21 @@ interface MetricsService {
|
||||
}
|
||||
```
|
||||
|
||||
#### Root Metrics Service
|
||||
|
||||
The `RootMetricsService` is responsible for providing metrics to other root services and creating both plugin-scoped and core-scoped `MetricsService` instances.
|
||||
|
||||
```ts
|
||||
interface RootMetricsService {
|
||||
// note: no config is provided to the root service.
|
||||
static forRoot(): RootMetricsService;
|
||||
forPlugin(pluginId: string): MetricsService;
|
||||
|
||||
// final implementation will be similar to
|
||||
forService(serviceName: string, scope: 'plugin' | 'core'): MetricsService;
|
||||
}
|
||||
|
||||
export const rootMetricsServiceFactory = createServiceFactory({
|
||||
// depends on as little as possible so that it can be initialized as early as possible.
|
||||
service: rootMetricsServiceRef,
|
||||
deps: {},
|
||||
factory: () => {
|
||||
return DefaultRootMetricsService.forRoot();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Plugin Metrics Service
|
||||
|
||||
Each plugin receives a metrics service that automatically namespaces all metrics to match the naming conventions.
|
||||
Each plugin receives a metrics service that automatically configures the Instrumentation Scope to identify the plugin. The scope name follows the pattern `backstage-plugin-{pluginId}`.
|
||||
|
||||
```ts
|
||||
const metricsServiceFactory = createServiceFactory({
|
||||
service: metricsServiceRef,
|
||||
export const metricsServiceFactory = createServiceFactory({
|
||||
service: coreServices.metrics,
|
||||
deps: {
|
||||
rootMetrics: coreServices.rootMetrics,
|
||||
pluginMetadata: coreServices.pluginMetadata,
|
||||
},
|
||||
factory: ({ rootMetrics, pluginMetadata }) => {
|
||||
return rootMetrics.forPlugin(pluginMetadata.getId());
|
||||
factory: ({ pluginMetadata }) => {
|
||||
const pluginId = pluginMetadata.getId();
|
||||
const scopeName = `backstage-plugin-${pluginId}`;
|
||||
|
||||
return new DefaultMetricsService(scopeName, version, ...);
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -248,3 +228,22 @@ entitiesProcessed.add(100);
|
||||
|
||||
- Plugin authors continue to implement their own metrics as they see fit.
|
||||
- A combined TelemetryService that provides both metrics and tracing.
|
||||
|
||||
### Rejected: Forced Namespace Prefixes
|
||||
|
||||
Prepend `backstage.plugin.{pluginId}.` to all metric names. This was the original proposal but conflicts with OpenTelemetry semantic conventions.
|
||||
|
||||
**Problems:**
|
||||
|
||||
- Makes it impossible to use standard semantic conventions like `mcp.*`, `gen_ai.*`, `http.*`
|
||||
- Breaks compatibility with industry-standard observability tooling
|
||||
- Prevents cross-service metric aggregation
|
||||
- Goes against OpenTelemetry best practices and official guidance
|
||||
|
||||
**Example of conflict:**
|
||||
|
||||
```ts
|
||||
// Plugin wants to emit: mcp.client.operation.duration
|
||||
// Framework forces: backstage.plugin.mcp-actions.mcp.client.operation.duration
|
||||
// This violates the semantic convention and breaks tooling
|
||||
```
|
||||
|
||||
+30
@@ -1127,6 +1127,36 @@ export interface Config {
|
||||
headers?: { [name: string]: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for the metrics service.
|
||||
*/
|
||||
metrics?: {
|
||||
/**
|
||||
* Plugin-specific metrics configuration. Each plugin can override meter metadata.
|
||||
*/
|
||||
plugin?: {
|
||||
[pluginId: string]: {
|
||||
/**
|
||||
* Meter configuration for this plugin.
|
||||
*/
|
||||
meter?: {
|
||||
/**
|
||||
* Custom meter name. If not set, defaults to backstage-plugin-{pluginId}.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Version for the meter.
|
||||
*/
|
||||
version?: string;
|
||||
/**
|
||||
* Schema URL for the meter.
|
||||
*/
|
||||
schemaUrl?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to configure the default RootLoggerService.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
```ts
|
||||
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { RootSystemMetadataService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
@@ -22,6 +23,13 @@ export const actionsServiceFactory: ServiceFactory<
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// @alpha
|
||||
export const metricsServiceFactory: ServiceFactory<
|
||||
MetricsService,
|
||||
'plugin',
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// @alpha
|
||||
export const rootSystemMetadataServiceFactory: ServiceFactory<
|
||||
RootSystemMetadataService,
|
||||
|
||||
@@ -38,6 +38,7 @@ import { eventsServiceFactory } from '@backstage/plugin-events-node';
|
||||
import {
|
||||
actionsRegistryServiceFactory,
|
||||
actionsServiceFactory,
|
||||
metricsServiceFactory,
|
||||
} from '@backstage/backend-defaults/alpha';
|
||||
import { instanceMetadataServiceFactory } from './alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory';
|
||||
|
||||
@@ -66,6 +67,7 @@ export const defaultServiceFactories = [
|
||||
// alpha services
|
||||
actionsRegistryServiceFactory,
|
||||
actionsServiceFactory,
|
||||
metricsServiceFactory,
|
||||
|
||||
// Unexported alpha services kept around for compatibility reasons
|
||||
instanceMetadataServiceFactory,
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { metrics } from '@opentelemetry/api';
|
||||
import { DefaultMetricsService } from './DefaultMetricsService';
|
||||
|
||||
const mockGetMeter = jest.spyOn(metrics, 'getMeter');
|
||||
|
||||
describe('DefaultMetricsService', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMeter.mockClear();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a MetricsService with name only', () => {
|
||||
const service = DefaultMetricsService.create({ name: 'test-meter' });
|
||||
|
||||
expect(mockGetMeter).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetMeter).toHaveBeenCalledWith('test-meter', undefined, {
|
||||
schemaUrl: undefined,
|
||||
});
|
||||
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a MetricsService with name, version, and schemaUrl', () => {
|
||||
const service = DefaultMetricsService.create({
|
||||
name: 'test-meter',
|
||||
version: '1.2.3',
|
||||
schemaUrl: 'https://example.com/schema',
|
||||
});
|
||||
|
||||
expect(mockGetMeter).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetMeter).toHaveBeenCalledWith('test-meter', '1.2.3', {
|
||||
schemaUrl: 'https://example.com/schema',
|
||||
});
|
||||
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('metric instruments', () => {
|
||||
it('should create a counter', () => {
|
||||
const service = DefaultMetricsService.create({ name: 'test' });
|
||||
const counter = service.createCounter('my_counter', {
|
||||
description: 'A test counter',
|
||||
unit: 'bytes',
|
||||
});
|
||||
|
||||
expect(counter).toBeDefined();
|
||||
expect(counter.add).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create an up-down counter', () => {
|
||||
const service = DefaultMetricsService.create({ name: 'test' });
|
||||
const upDownCounter = service.createUpDownCounter('my_updown');
|
||||
|
||||
expect(upDownCounter).toBeDefined();
|
||||
expect(upDownCounter.add).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a histogram', () => {
|
||||
const service = DefaultMetricsService.create({ name: 'test' });
|
||||
const histogram = service.createHistogram('my_histogram');
|
||||
|
||||
expect(histogram).toBeDefined();
|
||||
expect(histogram.record).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a gauge', () => {
|
||||
const service = DefaultMetricsService.create({ name: 'test' });
|
||||
const gauge = service.createGauge('my_gauge');
|
||||
|
||||
expect(gauge).toBeDefined();
|
||||
expect(gauge.record).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create an observable counter', () => {
|
||||
const service = DefaultMetricsService.create({ name: 'test' });
|
||||
const counter = service.createObservableCounter('my_observable_counter');
|
||||
|
||||
expect(counter).toBeDefined();
|
||||
expect(counter.addCallback).toBeDefined();
|
||||
expect(counter.removeCallback).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create an observable up-down counter', () => {
|
||||
const service = DefaultMetricsService.create({ name: 'test' });
|
||||
const counter = service.createObservableUpDownCounter(
|
||||
'my_observable_updown',
|
||||
);
|
||||
|
||||
expect(counter).toBeDefined();
|
||||
expect(counter.addCallback).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create an observable gauge', () => {
|
||||
const service = DefaultMetricsService.create({ name: 'test' });
|
||||
const gauge = service.createObservableGauge('my_observable_gauge');
|
||||
|
||||
expect(gauge).toBeDefined();
|
||||
expect(gauge.addCallback).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Meter, metrics } from '@opentelemetry/api';
|
||||
import {
|
||||
MetricsService,
|
||||
MetricAttributes,
|
||||
MetricOptions,
|
||||
MetricsServiceCounter,
|
||||
MetricsServiceUpDownCounter,
|
||||
MetricsServiceHistogram,
|
||||
MetricsServiceGauge,
|
||||
MetricsServiceObservableCounter,
|
||||
MetricsServiceObservableGauge,
|
||||
MetricsServiceObservableUpDownCounter,
|
||||
} from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
/**
|
||||
* Options for creating a {@link DefaultMetricsService}.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface DefaultMetricsServiceOptions {
|
||||
name: string;
|
||||
version?: string;
|
||||
schemaUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of the {@link MetricsService} interface.
|
||||
*
|
||||
* This implementation provides a thin wrapper around the OpenTelemetry Meter API.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export class DefaultMetricsService implements MetricsService {
|
||||
private readonly meter: Meter;
|
||||
|
||||
private constructor(opts: DefaultMetricsServiceOptions) {
|
||||
// The meter name sets the OpenTelemetry Instrumentation Scope which identifies the source of metrics in telemetry backends.
|
||||
this.meter = metrics.getMeter(opts.name, opts.version, {
|
||||
schemaUrl: opts.schemaUrl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MetricsService} instance.
|
||||
*
|
||||
* @param opts - Options for configuring the meter scope
|
||||
* @returns A new MetricsService instance
|
||||
*/
|
||||
static create(opts: DefaultMetricsServiceOptions): MetricsService {
|
||||
return new DefaultMetricsService(opts);
|
||||
}
|
||||
|
||||
createCounter<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceCounter<TAttributes> {
|
||||
return this.meter.createCounter(name, opts);
|
||||
}
|
||||
|
||||
createUpDownCounter<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceUpDownCounter<TAttributes> {
|
||||
return this.meter.createUpDownCounter(name, opts);
|
||||
}
|
||||
|
||||
createHistogram<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceHistogram<TAttributes> {
|
||||
return this.meter.createHistogram(name, opts);
|
||||
}
|
||||
|
||||
createGauge<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceGauge<TAttributes> {
|
||||
return this.meter.createGauge(name, opts);
|
||||
}
|
||||
|
||||
createObservableCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableCounter<TAttributes> {
|
||||
return this.meter.createObservableCounter(name, opts);
|
||||
}
|
||||
|
||||
createObservableUpDownCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableUpDownCounter<TAttributes> {
|
||||
return this.meter.createObservableUpDownCounter(name, opts);
|
||||
}
|
||||
|
||||
createObservableGauge<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableGauge<TAttributes> {
|
||||
return this.meter.createObservableGauge(name, opts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { metricsServiceFactory } from './metricsServiceFactory';
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
mockServices,
|
||||
ServiceFactoryTester,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { metricsServiceFactory } from './metricsServiceFactory';
|
||||
import { DefaultMetricsService } from './DefaultMetricsService';
|
||||
|
||||
describe('metricsServiceFactory', () => {
|
||||
let createSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
createSpy = jest.spyOn(DefaultMetricsService, 'create');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const defaultServices = [
|
||||
mockServices.rootConfig.factory(),
|
||||
metricsServiceFactory,
|
||||
];
|
||||
|
||||
it('should use backstage-plugin-{pluginId} as meter name when no config is set', async () => {
|
||||
await ServiceFactoryTester.from(metricsServiceFactory, {
|
||||
dependencies: defaultServices,
|
||||
}).getSubject('my-plugin');
|
||||
|
||||
expect(createSpy).toHaveBeenCalledWith({
|
||||
name: 'backstage-plugin-my-plugin',
|
||||
version: undefined,
|
||||
schemaUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom name from config', async () => {
|
||||
await ServiceFactoryTester.from(metricsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
metrics: {
|
||||
plugin: {
|
||||
'my-plugin': {
|
||||
meter: {
|
||||
name: 'custom-metrics-name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
metricsServiceFactory,
|
||||
],
|
||||
}).getSubject('my-plugin');
|
||||
|
||||
expect(createSpy).toHaveBeenCalledWith({
|
||||
name: 'custom-metrics-name',
|
||||
version: undefined,
|
||||
schemaUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept version and schemaUrl from config', async () => {
|
||||
await ServiceFactoryTester.from(metricsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
metrics: {
|
||||
plugin: {
|
||||
'my-plugin': {
|
||||
meter: {
|
||||
name: 'my-plugin-metrics',
|
||||
version: '1.2.3',
|
||||
schemaUrl: 'https://example.com/schema',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
metricsServiceFactory,
|
||||
],
|
||||
}).getSubject('my-plugin');
|
||||
|
||||
expect(createSpy).toHaveBeenCalledWith({
|
||||
name: 'my-plugin-metrics',
|
||||
version: '1.2.3',
|
||||
schemaUrl: 'https://example.com/schema',
|
||||
});
|
||||
});
|
||||
|
||||
it('should implement the full MetricsService interface', async () => {
|
||||
const subject = await ServiceFactoryTester.from(metricsServiceFactory, {
|
||||
dependencies: defaultServices,
|
||||
}).getSubject('test-plugin');
|
||||
|
||||
expect(createSpy).toHaveBeenCalledWith({
|
||||
name: 'backstage-plugin-test-plugin',
|
||||
version: undefined,
|
||||
schemaUrl: undefined,
|
||||
});
|
||||
|
||||
expect(subject.createCounter).toBeDefined();
|
||||
expect(subject.createUpDownCounter).toBeDefined();
|
||||
expect(subject.createHistogram).toBeDefined();
|
||||
expect(subject.createGauge).toBeDefined();
|
||||
expect(subject.createObservableCounter).toBeDefined();
|
||||
expect(subject.createObservableUpDownCounter).toBeDefined();
|
||||
expect(subject.createObservableGauge).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { DefaultMetricsService } from './DefaultMetricsService';
|
||||
|
||||
/**
|
||||
* Service factory for collecting plugin-scoped metrics.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const metricsServiceFactory = createServiceFactory({
|
||||
service: metricsServiceRef,
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
pluginMetadata: coreServices.pluginMetadata,
|
||||
},
|
||||
factory: ({ config, pluginMetadata }) => {
|
||||
const pluginId = pluginMetadata.getId();
|
||||
|
||||
const meterConfig = config.getOptionalConfig(
|
||||
`backend.metrics.plugin.${pluginId}.meter`,
|
||||
);
|
||||
const scopeName = `backstage-plugin-${pluginId}`;
|
||||
const name = meterConfig?.getOptionalString('name') ?? scopeName;
|
||||
const version = meterConfig?.getOptionalString('version');
|
||||
const schemaUrl = meterConfig?.getOptionalString('schemaUrl');
|
||||
|
||||
return DefaultMetricsService.create({ name, version, schemaUrl });
|
||||
},
|
||||
});
|
||||
@@ -16,4 +16,5 @@
|
||||
|
||||
export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry';
|
||||
export { actionsServiceFactory } from './entrypoints/actions';
|
||||
export { metricsServiceFactory } from './entrypoints/metrics';
|
||||
export { rootSystemMetadataServiceFactory } from './entrypoints/rootSystemMetadata';
|
||||
|
||||
@@ -103,6 +103,150 @@ export const actionsServiceRef: ServiceRef<
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// @alpha
|
||||
export interface MetricAdvice {
|
||||
explicitBucketBoundaries?: number[];
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface MetricAttributes {
|
||||
// (undocumented)
|
||||
[attributeKey: string]: MetricAttributeValue | undefined;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export type MetricAttributeValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<null | undefined | string>
|
||||
| Array<null | undefined | number>
|
||||
| Array<null | undefined | boolean>;
|
||||
|
||||
// @alpha
|
||||
export interface MetricOptions {
|
||||
advice?: MetricAdvice;
|
||||
description?: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface MetricsService {
|
||||
createCounter<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceCounter<TAttributes>;
|
||||
createGauge<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceGauge<TAttributes>;
|
||||
createHistogram<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceHistogram<TAttributes>;
|
||||
createObservableCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableCounter<TAttributes>;
|
||||
createObservableGauge<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableGauge<TAttributes>;
|
||||
createObservableUpDownCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableUpDownCounter<TAttributes>;
|
||||
createUpDownCounter<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceUpDownCounter<TAttributes>;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface MetricsServiceCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
// (undocumented)
|
||||
add(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface MetricsServiceGauge<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
// (undocumented)
|
||||
record(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface MetricsServiceHistogram<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
// (undocumented)
|
||||
record(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface MetricsServiceObservable<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
// (undocumented)
|
||||
addCallback(callback: MetricsServiceObservableCallback<TAttributes>): void;
|
||||
// (undocumented)
|
||||
removeCallback(callback: MetricsServiceObservableCallback<TAttributes>): void;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export type MetricsServiceObservableCallback<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> = (
|
||||
observableResult: MetricsServiceObservableResult<TAttributes>,
|
||||
) => void | Promise<void>;
|
||||
|
||||
// @alpha
|
||||
export type MetricsServiceObservableCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> = MetricsServiceObservable<TAttributes>;
|
||||
|
||||
// @alpha
|
||||
export type MetricsServiceObservableGauge<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> = MetricsServiceObservable<TAttributes>;
|
||||
|
||||
// @alpha
|
||||
export interface MetricsServiceObservableResult<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
// (undocumented)
|
||||
observe(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export type MetricsServiceObservableUpDownCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> = MetricsServiceObservable<TAttributes>;
|
||||
|
||||
// @alpha
|
||||
export const metricsServiceRef: ServiceRef<
|
||||
MetricsService,
|
||||
'plugin',
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// @alpha
|
||||
export interface MetricsServiceUpDownCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
// (undocumented)
|
||||
add(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RootSystemMetadataService {
|
||||
// (undocumented)
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Attribute values that can be attached to metric measurements.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export type MetricAttributeValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<null | undefined | string>
|
||||
| Array<null | undefined | number>
|
||||
| Array<null | undefined | boolean>;
|
||||
|
||||
/**
|
||||
* A set of key-value pairs that can be attached to metric measurements.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricAttributes {
|
||||
[attributeKey: string]: MetricAttributeValue | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advisory options that influence aggregation configuration.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricAdvice {
|
||||
/**
|
||||
* Hint the explicit bucket boundaries for histogram aggregation.
|
||||
*/
|
||||
explicitBucketBoundaries?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a metric instrument.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricOptions {
|
||||
/**
|
||||
* The description of the Metric.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* The unit of the Metric values.
|
||||
*/
|
||||
unit?: string;
|
||||
/**
|
||||
* Advisory options that influence aggregation configuration.
|
||||
*/
|
||||
advice?: MetricAdvice;
|
||||
}
|
||||
|
||||
/**
|
||||
* A counter metric that only supports non-negative increments.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricsServiceCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
add(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A counter metric that supports both positive and negative increments.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricsServiceUpDownCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
add(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A histogram metric for recording distributions of values.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricsServiceHistogram<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
record(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A gauge metric for recording instantaneous values.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricsServiceGauge<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
record(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result object passed to observable metric callbacks.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricsServiceObservableResult<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
observe(value: number, attributes?: TAttributes): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback function for observable metrics. Called whenever a metric
|
||||
* collection is initiated.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export type MetricsServiceObservableCallback<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> = (
|
||||
observableResult: MetricsServiceObservableResult<TAttributes>,
|
||||
) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* An observable metric instrument that reports values via callbacks.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricsServiceObservable<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> {
|
||||
addCallback(callback: MetricsServiceObservableCallback<TAttributes>): void;
|
||||
removeCallback(callback: MetricsServiceObservableCallback<TAttributes>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable counter metric that reports non-negative sums via callbacks.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export type MetricsServiceObservableCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> = MetricsServiceObservable<TAttributes>;
|
||||
|
||||
/**
|
||||
* An observable counter metric that reports sums that can go up or down
|
||||
* via callbacks.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export type MetricsServiceObservableUpDownCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> = MetricsServiceObservable<TAttributes>;
|
||||
|
||||
/**
|
||||
* An observable gauge metric that reports instantaneous values via callbacks.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export type MetricsServiceObservableGauge<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
> = MetricsServiceObservable<TAttributes>;
|
||||
|
||||
/**
|
||||
* A service that provides a facility for emitting metrics.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MetricsService {
|
||||
/**
|
||||
* Creates a new counter metric.
|
||||
*
|
||||
* @param name - The name of the metric.
|
||||
* @param opts - The options for the metric.
|
||||
* @returns The counter metric.
|
||||
*/
|
||||
createCounter<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceCounter<TAttributes>;
|
||||
|
||||
/**
|
||||
* Creates a new up-down counter metric.
|
||||
*
|
||||
* @param name - The name of the metric.
|
||||
* @param opts - The options for the metric.
|
||||
* @returns The up-down counter metric.
|
||||
*/
|
||||
createUpDownCounter<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceUpDownCounter<TAttributes>;
|
||||
|
||||
/**
|
||||
* Creates a new histogram metric.
|
||||
*
|
||||
* @param name - The name of the metric.
|
||||
* @param opts - The options for the metric.
|
||||
* @returns The histogram metric.
|
||||
*/
|
||||
createHistogram<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceHistogram<TAttributes>;
|
||||
|
||||
/**
|
||||
* Creates a new gauge metric.
|
||||
*
|
||||
* @param name - The name of the metric.
|
||||
* @param opts - The options for the metric.
|
||||
* @returns The gauge metric.
|
||||
*/
|
||||
createGauge<TAttributes extends MetricAttributes = MetricAttributes>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceGauge<TAttributes>;
|
||||
|
||||
/**
|
||||
* Creates a new observable counter metric.
|
||||
*
|
||||
* @param name - The name of the metric.
|
||||
* @param opts - The options for the metric.
|
||||
* @returns The observable counter metric.
|
||||
*/
|
||||
createObservableCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableCounter<TAttributes>;
|
||||
|
||||
/**
|
||||
* Creates a new observable up-down counter metric.
|
||||
*
|
||||
* @param name - The name of the metric.
|
||||
* @param opts - The options for the metric.
|
||||
* @returns The observable up-down counter metric.
|
||||
*/
|
||||
createObservableUpDownCounter<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableUpDownCounter<TAttributes>;
|
||||
|
||||
/**
|
||||
* Creates a new observable gauge metric.
|
||||
*
|
||||
* @param name - The name of the metric.
|
||||
* @param opts - The options for the metric.
|
||||
* @returns The observable gauge metric.
|
||||
*/
|
||||
createObservableGauge<
|
||||
TAttributes extends MetricAttributes = MetricAttributes,
|
||||
>(
|
||||
name: string,
|
||||
opts?: MetricOptions,
|
||||
): MetricsServiceObservableGauge<TAttributes>;
|
||||
}
|
||||
@@ -27,8 +27,27 @@ export type {
|
||||
|
||||
export type { ActionsService, ActionsServiceAction } from './ActionsService';
|
||||
|
||||
export type {
|
||||
MetricsService,
|
||||
MetricAdvice,
|
||||
MetricAttributes,
|
||||
MetricAttributeValue,
|
||||
MetricOptions,
|
||||
MetricsServiceCounter,
|
||||
MetricsServiceUpDownCounter,
|
||||
MetricsServiceHistogram,
|
||||
MetricsServiceGauge,
|
||||
MetricsServiceObservable,
|
||||
MetricsServiceObservableCallback,
|
||||
MetricsServiceObservableCounter,
|
||||
MetricsServiceObservableGauge,
|
||||
MetricsServiceObservableResult,
|
||||
MetricsServiceObservableUpDownCounter,
|
||||
} from './MetricsService';
|
||||
|
||||
export {
|
||||
actionsRegistryServiceRef,
|
||||
actionsServiceRef,
|
||||
metricsServiceRef,
|
||||
rootSystemMetadataServiceRef,
|
||||
} from './refs';
|
||||
|
||||
@@ -56,3 +56,14 @@ export const rootSystemMetadataServiceRef = createServiceRef<
|
||||
id: 'alpha.core.rootSystemMetadata',
|
||||
scope: 'root',
|
||||
});
|
||||
|
||||
/**
|
||||
* Service for managing metrics.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const metricsServiceRef = createServiceRef<
|
||||
import('./MetricsService').MetricsService
|
||||
>({
|
||||
id: 'alpha.core.metrics',
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
@@ -43,6 +44,16 @@ export namespace actionsServiceMock {
|
||||
) => ServiceMock<ActionsService>;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export namespace metricsServiceMock {
|
||||
const // (undocumented)
|
||||
factory: () => ServiceFactory<MetricsService, 'plugin', 'singleton'>;
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?: Partial<MetricsService> | undefined,
|
||||
) => ServiceMock<MetricsService>;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export class MockActionsRegistry
|
||||
implements ActionsRegistryService, ActionsService
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createServiceMock } from './alphaCreateServiceMock';
|
||||
import {
|
||||
MetricsService,
|
||||
metricsServiceRef,
|
||||
} from '@backstage/backend-plugin-api/alpha';
|
||||
import { metricsServiceFactory } from '@backstage/backend-defaults/alpha';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export namespace metricsServiceMock {
|
||||
export const factory = () => metricsServiceFactory;
|
||||
|
||||
export const mock = createServiceMock<MetricsService>(
|
||||
metricsServiceRef,
|
||||
() => ({
|
||||
createCounter: jest.fn().mockImplementation(() => ({
|
||||
add: jest.fn(),
|
||||
})),
|
||||
createUpDownCounter: jest.fn().mockImplementation(() => ({
|
||||
add: jest.fn(),
|
||||
})),
|
||||
createHistogram: jest.fn().mockImplementation(() => ({
|
||||
record: jest.fn(),
|
||||
})),
|
||||
createGauge: jest.fn().mockImplementation(() => ({
|
||||
record: jest.fn(),
|
||||
})),
|
||||
createObservableCounter: jest.fn().mockImplementation(() => ({
|
||||
addCallback: jest.fn(),
|
||||
removeCallback: jest.fn(),
|
||||
})),
|
||||
createObservableUpDownCounter: jest.fn().mockImplementation(() => ({
|
||||
addCallback: jest.fn(),
|
||||
removeCallback: jest.fn(),
|
||||
})),
|
||||
createObservableGauge: jest.fn().mockImplementation(() => ({
|
||||
addCallback: jest.fn(),
|
||||
removeCallback: jest.fn(),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -17,4 +17,5 @@
|
||||
export { actionsRegistryServiceMock } from './ActionsRegistryServiceMock';
|
||||
export { MockActionsRegistry } from './MockActionsRegistry';
|
||||
export { actionsServiceMock } from './ActionsServiceMock';
|
||||
export { metricsServiceMock } from './MetricsServiceMock';
|
||||
export { type ServiceMock } from './alphaCreateServiceMock';
|
||||
|
||||
@@ -43,6 +43,7 @@ import { HostDiscovery } from '@backstage/backend-defaults/discovery';
|
||||
import {
|
||||
actionsRegistryServiceMock,
|
||||
actionsServiceMock,
|
||||
metricsServiceMock,
|
||||
} from '../alpha/services';
|
||||
|
||||
/** @public */
|
||||
@@ -92,6 +93,7 @@ export const defaultServiceFactories = [
|
||||
// Alpha services
|
||||
actionsRegistryServiceMock.factory(),
|
||||
actionsServiceMock.factory(),
|
||||
metricsServiceMock.factory(),
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,7 @@ import { createRandomProcessingInterval } from '../processing/refresh';
|
||||
import { timestampToDateTime } from './conversion';
|
||||
import { generateStableHash } from './util';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { metricsServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
@@ -59,6 +60,7 @@ describe('DefaultProcessingDatabase', () => {
|
||||
maxSeconds: 150,
|
||||
}),
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import { DateTime } from 'luxon';
|
||||
import { CATALOG_CONFLICTS_TOPIC } from '../constants';
|
||||
import { CatalogConflictEventPayload } from '../catalog/types';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
// The number of items that are sent per batch to the database layer, when
|
||||
// doing .batchInsert calls to knex. This needs to be low enough to not cause
|
||||
@@ -60,6 +61,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
|
||||
logger: LoggerService;
|
||||
refreshInterval: ProcessingIntervalFunction;
|
||||
events: EventsService;
|
||||
metrics: MetricsService;
|
||||
};
|
||||
|
||||
constructor(options: {
|
||||
@@ -67,9 +69,10 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
|
||||
logger: LoggerService;
|
||||
refreshInterval: ProcessingIntervalFunction;
|
||||
events: EventsService;
|
||||
metrics: MetricsService;
|
||||
}) {
|
||||
this.options = options;
|
||||
initDatabaseMetrics(options.database);
|
||||
initDatabaseMetrics(options.database, options.metrics);
|
||||
}
|
||||
|
||||
async updateProcessedEntity(
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { createGaugeMetric } from '../util/metrics';
|
||||
import { DbRelationsRow, DbLocationsRow, DbSearchRow } from './tables';
|
||||
import { metrics } from '@opentelemetry/api';
|
||||
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
export function initDatabaseMetrics(knex: Knex) {
|
||||
export function initDatabaseMetrics(knex: Knex, metrics: MetricsService) {
|
||||
const seenProm = new Set<string>();
|
||||
const seen = new Set<string>();
|
||||
const meter = metrics.getMeter('default');
|
||||
|
||||
return {
|
||||
entities_count_prom: createGaugeMetric({
|
||||
name: 'catalog_entities_count',
|
||||
@@ -69,7 +69,7 @@ export function initDatabaseMetrics(knex: Knex) {
|
||||
this.set(Number(total[0].count));
|
||||
},
|
||||
}),
|
||||
entities_count: meter
|
||||
entities_count: metrics
|
||||
.createObservableGauge('catalog_entities_count', {
|
||||
description: 'Total amount of entities in the catalog',
|
||||
})
|
||||
@@ -93,7 +93,7 @@ export function initDatabaseMetrics(knex: Knex) {
|
||||
}
|
||||
});
|
||||
}),
|
||||
registered_locations: meter
|
||||
registered_locations: metrics
|
||||
.createObservableGauge('catalog_registered_locations_count', {
|
||||
description: 'Total amount of registered locations in the catalog',
|
||||
})
|
||||
@@ -113,7 +113,7 @@ export function initDatabaseMetrics(knex: Knex) {
|
||||
gauge.observe(Number(total[0].count));
|
||||
}
|
||||
}),
|
||||
relations: meter
|
||||
relations: metrics
|
||||
.createObservableGauge('catalog_relations_count', {
|
||||
description: 'Total amount of relations between entities',
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ import { CatalogProcessingOrchestrator } from './types';
|
||||
import { Stitcher } from '../stitching/types';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { metricsServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
describe('DefaultCatalogProcessingEngine', () => {
|
||||
const db = {
|
||||
@@ -72,6 +73,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
createHash: () => hash,
|
||||
scheduler: mockServices.scheduler(),
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
@@ -141,6 +143,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
scheduler: mockServices.scheduler(),
|
||||
createHash: () => hash,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
@@ -226,6 +229,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
scheduler: mockServices.scheduler(),
|
||||
createHash: () => hash,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
@@ -305,6 +309,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
scheduler: mockServices.scheduler(),
|
||||
createHash: () => hash,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
@@ -367,6 +372,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
createHash: () => hash,
|
||||
pollingIntervalMs: 100,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
@@ -484,6 +490,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
createHash: () => hash,
|
||||
pollingIntervalMs: 100,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
@@ -591,6 +598,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
createHash: () => hash,
|
||||
pollingIntervalMs: 100,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
@@ -676,6 +684,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
createHash: () => hash,
|
||||
pollingIntervalMs: 100,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
@@ -766,6 +775,7 @@ describe('DefaultCatalogProcessingEngine', () => {
|
||||
createHash: () => hash,
|
||||
pollingIntervalMs: 100,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
|
||||
@@ -23,7 +23,7 @@ import { assertError, serializeError, stringifyError } from '@backstage/errors';
|
||||
import { Hash } from 'node:crypto';
|
||||
import stableStringify from 'fast-json-stable-stringify';
|
||||
import { Knex } from 'knex';
|
||||
import { metrics, trace } from '@opentelemetry/api';
|
||||
import { trace } from '@opentelemetry/api';
|
||||
import { ProcessingDatabase, RefreshStateItem } from '../database/types';
|
||||
import { createCounterMetric, createSummaryMetric } from '../util/metrics';
|
||||
import { CatalogProcessingOrchestrator, EntityProcessingResult } from './types';
|
||||
@@ -39,6 +39,7 @@ import { deleteOrphanedEntities } from '../database/operations/util/deleteOrphan
|
||||
import { EventsService } from '@backstage/plugin-events-node';
|
||||
import { CATALOG_ERRORS_TOPIC } from '../constants';
|
||||
import { LoggerService, SchedulerService } from '@backstage/backend-plugin-api';
|
||||
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
const CACHE_TTL = 5;
|
||||
|
||||
@@ -94,6 +95,7 @@ export class DefaultCatalogProcessingEngine {
|
||||
}) => Promise<void> | void;
|
||||
tracker?: ProgressTracker;
|
||||
events: EventsService;
|
||||
metrics: MetricsService;
|
||||
}) {
|
||||
this.config = options.config;
|
||||
this.scheduler = options.scheduler;
|
||||
@@ -106,7 +108,7 @@ export class DefaultCatalogProcessingEngine {
|
||||
this.pollingIntervalMs = options.pollingIntervalMs ?? 1_000;
|
||||
this.orphanCleanupIntervalMs = options.orphanCleanupIntervalMs ?? 30_000;
|
||||
this.onProcessingError = options.onProcessingError;
|
||||
this.tracker = options.tracker ?? progressTracker();
|
||||
this.tracker = options.tracker ?? progressTracker(options.metrics);
|
||||
this.events = options.events;
|
||||
|
||||
this.stopFunc = undefined;
|
||||
@@ -386,7 +388,7 @@ export class DefaultCatalogProcessingEngine {
|
||||
}
|
||||
|
||||
// Helps wrap the timing and logging behaviors
|
||||
function progressTracker() {
|
||||
function progressTracker(metrics: MetricsService) {
|
||||
// prom-client metrics are deprecated in favour of OpenTelemetry metrics.
|
||||
const promProcessedEntities = createCounterMetric({
|
||||
name: 'catalog_processed_entities_count',
|
||||
@@ -408,13 +410,12 @@ function progressTracker() {
|
||||
help: 'The amount of delay between being scheduled for processing, and the start of actually being processed, DEPRECATED, use OpenTelemetry metrics instead',
|
||||
});
|
||||
|
||||
const meter = metrics.getMeter('default');
|
||||
const processedEntities = meter.createCounter(
|
||||
const processedEntities = metrics.createCounter(
|
||||
'catalog.processed.entities.count',
|
||||
{ description: 'Amount of entities processed' },
|
||||
);
|
||||
|
||||
const processingDuration = meter.createHistogram(
|
||||
const processingDuration = metrics.createHistogram(
|
||||
'catalog.processing.duration',
|
||||
{
|
||||
description: 'Time spent executing the full processing flow',
|
||||
@@ -422,7 +423,7 @@ function progressTracker() {
|
||||
},
|
||||
);
|
||||
|
||||
const processorsDuration = meter.createHistogram(
|
||||
const processorsDuration = metrics.createHistogram(
|
||||
'catalog.processors.duration',
|
||||
{
|
||||
description: 'Time spent executing catalog processors',
|
||||
@@ -430,7 +431,7 @@ function progressTracker() {
|
||||
},
|
||||
);
|
||||
|
||||
const processingQueueDelay = meter.createHistogram(
|
||||
const processingQueueDelay = metrics.createHistogram(
|
||||
'catalog.processing.queue.delay',
|
||||
{
|
||||
description:
|
||||
|
||||
@@ -117,6 +117,7 @@ import {
|
||||
import { filterAndSortProcessors, filterProviders } from './util';
|
||||
import { GenericScmEventRefreshProvider } from '../providers/GenericScmEventRefreshProvider';
|
||||
import { readScmEventHandlingConfig } from '../util/readScmEventHandlingConfig';
|
||||
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
export type CatalogEnvironment = {
|
||||
logger: LoggerService;
|
||||
@@ -131,6 +132,7 @@ export type CatalogEnvironment = {
|
||||
auditor: AuditorService;
|
||||
events: EventsService;
|
||||
catalogScmEvents: CatalogScmEventsService;
|
||||
metrics: MetricsService;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -429,6 +431,7 @@ export class CatalogBuilder {
|
||||
httpAuth,
|
||||
events,
|
||||
catalogScmEvents,
|
||||
metrics,
|
||||
} = this.env;
|
||||
|
||||
const enableRelationsCompatibility = Boolean(
|
||||
@@ -448,6 +451,7 @@ export class CatalogBuilder {
|
||||
const stitcher = DefaultStitcher.fromConfig(config, {
|
||||
knex: dbClient,
|
||||
logger,
|
||||
metrics,
|
||||
});
|
||||
|
||||
const processingDatabase = new DefaultProcessingDatabase({
|
||||
@@ -455,6 +459,7 @@ export class CatalogBuilder {
|
||||
logger,
|
||||
events,
|
||||
refreshInterval: this.processingInterval,
|
||||
metrics,
|
||||
});
|
||||
const providerDatabase = new DefaultProviderDatabase({
|
||||
database: dbClient,
|
||||
@@ -577,6 +582,7 @@ export class CatalogBuilder {
|
||||
this.onProcessingError?.(event);
|
||||
},
|
||||
events,
|
||||
metrics,
|
||||
});
|
||||
|
||||
const locationAnalyzer =
|
||||
|
||||
@@ -43,10 +43,12 @@ import {
|
||||
} from '@backstage/plugin-catalog-node/alpha';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { Permission } from '@backstage/plugin-permission-common';
|
||||
import { metrics } from '@opentelemetry/api';
|
||||
import { merge } from 'lodash';
|
||||
import { CatalogBuilder } from './CatalogBuilder';
|
||||
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
import {
|
||||
actionsRegistryServiceRef,
|
||||
metricsServiceRef,
|
||||
} from '@backstage/backend-plugin-api/alpha';
|
||||
import { createCatalogActions } from '../actions';
|
||||
import type { EntityProviderEntry } from '../processing/connectEntityProviders';
|
||||
|
||||
@@ -220,6 +222,7 @@ export const catalogPlugin = createBackendPlugin({
|
||||
catalog: catalogServiceRef,
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
catalogScmEvents: catalogScmEventsServiceRef,
|
||||
metrics: metricsServiceRef,
|
||||
},
|
||||
async init({
|
||||
logger,
|
||||
@@ -238,6 +241,7 @@ export const catalogPlugin = createBackendPlugin({
|
||||
auditor,
|
||||
events,
|
||||
catalogScmEvents,
|
||||
metrics,
|
||||
}) {
|
||||
const builder = await CatalogBuilder.create({
|
||||
config,
|
||||
@@ -252,6 +256,7 @@ export const catalogPlugin = createBackendPlugin({
|
||||
auditor,
|
||||
events,
|
||||
catalogScmEvents,
|
||||
metrics,
|
||||
});
|
||||
|
||||
if (onProcessingError) {
|
||||
@@ -303,9 +308,7 @@ export const catalogPlugin = createBackendPlugin({
|
||||
actionsRegistry,
|
||||
});
|
||||
|
||||
// Track SCM event message counts as a metric
|
||||
const meter = metrics.getMeter('default');
|
||||
const scmEventsMessagesCounter = meter.createCounter<{
|
||||
const scmEventsMessagesCounter = metrics.createCounter<{
|
||||
eventType: string;
|
||||
}>('catalog.events.scm.messages', {
|
||||
description:
|
||||
|
||||
@@ -38,6 +38,7 @@ import { DefaultRefreshService } from './DefaultRefreshService';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { DefaultStitcher } from '../stitching/DefaultStitcher';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { metricsServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
@@ -58,6 +59,7 @@ describe('DefaultRefreshService', () => {
|
||||
logger,
|
||||
refreshInterval: () => 100,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
}),
|
||||
catalogDb: new DefaultCatalogDatabase({
|
||||
database: knex,
|
||||
@@ -115,6 +117,7 @@ describe('DefaultRefreshService', () => {
|
||||
const stitcher = DefaultStitcher.fromConfig(new ConfigReader({}), {
|
||||
knex,
|
||||
logger: defaultLogger,
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
const engine = new DefaultCatalogProcessingEngine({
|
||||
config: new ConfigReader({}),
|
||||
@@ -164,6 +167,7 @@ describe('DefaultRefreshService', () => {
|
||||
createHash: () => createHash('sha1'),
|
||||
pollingIntervalMs: 50,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
return engine;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
DbSearchRow,
|
||||
} from '../database/tables';
|
||||
import { DefaultStitcher } from './DefaultStitcher';
|
||||
import { metricsServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
@@ -42,6 +43,7 @@ describe('Stitcher', () => {
|
||||
knex: db,
|
||||
logger,
|
||||
strategy: { mode: 'immediate' },
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
let entities: DbFinalEntitiesRow[];
|
||||
let entity: Entity;
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
stitchingStrategyFromConfig,
|
||||
} from './types';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
type DeferredStitchItem = Awaited<
|
||||
ReturnType<typeof getDeferredStitchableEntities>
|
||||
@@ -55,11 +56,13 @@ export class DefaultStitcher implements Stitcher {
|
||||
options: {
|
||||
knex: Knex;
|
||||
logger: LoggerService;
|
||||
metrics: MetricsService;
|
||||
},
|
||||
): DefaultStitcher {
|
||||
return new DefaultStitcher({
|
||||
knex: options.knex,
|
||||
logger: options.logger,
|
||||
metrics: options.metrics,
|
||||
strategy: stitchingStrategyFromConfig(config),
|
||||
});
|
||||
}
|
||||
@@ -67,12 +70,17 @@ export class DefaultStitcher implements Stitcher {
|
||||
constructor(options: {
|
||||
knex: Knex;
|
||||
logger: LoggerService;
|
||||
metrics: MetricsService;
|
||||
strategy: StitchingStrategy;
|
||||
}) {
|
||||
this.knex = options.knex;
|
||||
this.logger = options.logger;
|
||||
this.strategy = options.strategy;
|
||||
this.tracker = progressTracker(options.knex, options.logger);
|
||||
this.tracker = progressTracker(
|
||||
options.knex,
|
||||
options.logger,
|
||||
options.metrics,
|
||||
);
|
||||
}
|
||||
|
||||
async stitch(options: {
|
||||
|
||||
@@ -15,31 +15,33 @@
|
||||
*/
|
||||
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
import { metrics } from '@opentelemetry/api';
|
||||
import { Knex } from 'knex';
|
||||
import { DateTime } from 'luxon';
|
||||
import { DbRefreshStateRow } from '../database/tables';
|
||||
import { createCounterMetric } from '../util/metrics';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
// Helps wrap the timing and logging behaviors
|
||||
export function progressTracker(knex: Knex, logger: LoggerService) {
|
||||
export function progressTracker(
|
||||
knex: Knex,
|
||||
logger: LoggerService,
|
||||
metrics: MetricsService,
|
||||
) {
|
||||
// prom-client metrics are deprecated in favour of OpenTelemetry metrics.
|
||||
const promStitchedEntities = createCounterMetric({
|
||||
name: 'catalog_stitched_entities_count',
|
||||
help: 'Amount of entities stitched. DEPRECATED, use OpenTelemetry metrics instead',
|
||||
});
|
||||
|
||||
const meter = metrics.getMeter('default');
|
||||
|
||||
const stitchedEntities = meter.createCounter(
|
||||
const stitchedEntities = metrics.createCounter(
|
||||
'catalog.stitched.entities.count',
|
||||
{
|
||||
description: 'Amount of entities stitched',
|
||||
},
|
||||
);
|
||||
|
||||
const stitchingDuration = meter.createHistogram(
|
||||
const stitchingDuration = metrics.createHistogram(
|
||||
'catalog.stitching.duration',
|
||||
{
|
||||
description: 'Time spent executing the full stitching flow',
|
||||
@@ -47,7 +49,7 @@ export function progressTracker(knex: Knex, logger: LoggerService) {
|
||||
},
|
||||
);
|
||||
|
||||
const stitchingQueueCount = meter.createObservableGauge(
|
||||
const stitchingQueueCount = metrics.createObservableGauge(
|
||||
'catalog.stitching.queue.length',
|
||||
{ description: 'Number of entities currently in the stitching queue' },
|
||||
);
|
||||
@@ -58,7 +60,7 @@ export function progressTracker(knex: Knex, logger: LoggerService) {
|
||||
result.observe(Number(total[0].count));
|
||||
});
|
||||
|
||||
const stitchingQueueDelay = meter.createHistogram(
|
||||
const stitchingQueueDelay = metrics.createHistogram(
|
||||
'catalog.stitching.queue.delay',
|
||||
{
|
||||
description:
|
||||
|
||||
@@ -56,6 +56,7 @@ import { mockServices, TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { entitiesResponseToObjects } from '../service/response';
|
||||
import { deleteOrphanedEntities } from '../database/operations/util/deleteOrphanedEntities';
|
||||
import { metricsServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
const voidLogger = mockServices.logger.mock();
|
||||
|
||||
@@ -248,6 +249,7 @@ class TestHarness {
|
||||
logger,
|
||||
events: mockServices.events.mock(),
|
||||
refreshInterval: () => 0.05,
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
@@ -280,6 +282,7 @@ class TestHarness {
|
||||
const stitcher = DefaultStitcher.fromConfig(config, {
|
||||
knex: options.db,
|
||||
logger,
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
const catalog = new DefaultEntitiesCatalog({
|
||||
database: options.db,
|
||||
@@ -306,6 +309,7 @@ class TestHarness {
|
||||
},
|
||||
tracker: proxyProgressTracker,
|
||||
events: mockServices.events.mock(),
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
const refresh = new DefaultRefreshService({ database: catalogDatabase });
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ import { Knex } from 'knex';
|
||||
import { DefaultProcessingDatabase } from '../../database/DefaultProcessingDatabase';
|
||||
import { applyDatabaseMigrations } from '../../database/migrations';
|
||||
import { describePerformanceTest, performanceTraceEnabled } from './lib/env';
|
||||
import { metricsServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
// #region Helpers
|
||||
|
||||
@@ -81,6 +82,7 @@ describePerformanceTest('getProcessableEntities', () => {
|
||||
logger: mockServices.logger.mock(),
|
||||
events: mockServices.events.mock(),
|
||||
refreshInterval: () => 0,
|
||||
metrics: metricsServiceMock.mock(),
|
||||
});
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user