refactor: reorg sections for better flow of content

Signed-off-by: Kurt King <kurtaking@gmail.com>
This commit is contained in:
Kurt King
2025-07-09 20:59:59 -06:00
parent a9ad6932a2
commit 86e1dfb411
+83 -86
View File
@@ -11,16 +11,22 @@ project-areas:
creation-date: 2025-06-23
---
# BEP: Backstage Metrics Service
## Table of Contents
- [Summary](#summary)
- [Motivation](#motivation)
- [Goals](#goals)
- [Non-Goals](#non-goals)
- [Proposal](#proposal)
- [Naming Conventions](#naming-conventions)
- [Design Details](#design-details)
- [References](#references)
- [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)
- [Dependencies](#dependencies)
- [Alternatives](#alternatives)
@@ -58,24 +64,68 @@ The catalog and scaffolder plugins will be updated to use the new metrics servic
## Proposal
Following similar patterns to other core services, create a new `RootMetricsService` responsible for initializing metrics, root-level concerns, and the creation of plugin-specific `MetricsService` instances. The root service delegates to the plugin-scoped `MetricsService` to initialize a meter for each registered plugin based on the `service.name` and optional `service.version` provided in the app's `app-config.yaml` file. This is based on the recommendation in the OpenTelemetry [documentation](https://opentelemetry.io/docs/languages/js/instrumentation/#acquiring-a-meter).
Following similar patterns to other core services, create a new `RootMetricsService` responsible for initializing the OpenTelemetry SDK, root-level concerns, and the creation of plugin-specific `MetricsService` instances. The root service delegates to the plugin-scoped `MetricsService` to initialize a meter for each registered plugin based on the `service.name` and optional `service.version` provided in the app's `app-config.yaml` file. This is based on the recommendation in the OpenTelemetry [documentation](https://opentelemetry.io/docs/languages/js/instrumentation/#acquiring-a-meter).
## Naming Conventions
All Backstage metrics follow this hierarchical pattern:
`backstage.{scope}.{scope_name}.{metric_name}`
**Where:**
- `backstage` is the root namespace for all Backstage metrics
- `{scope}` is the system scope (either **plugin** or **core**)
- `{scope_name}` is the name of the plugin or core service (e.g., `catalog`, `scaffolder`, `database`)
- `{metric_name}` is the hierarchical metric name as provided by the plugin author (e.g., `entities.processed.total`, `tasks.completed.total`)
### Scope
The `scope` represents where it belongs in the Backstage ecosystem.
- `plugin` - A plugin-specific metric (e.g. `backstage.plugin.catalog.entities.count`)
- `core` - A core service metric (e.g. `backstage.core.database.connections.active`)
### Plugin-Scoped Metrics
Pattern: `backstage.plugin.{pluginId}.{metric_name}`
```yaml
# Examples
backstage.plugin.catalog.entities.processed.total
backstage.plugin.scaffolder.tasks.completed.total
backstage.plugin.techdocs.builds.active
backstage.plugin.auth.sessions.active.total # todo: technically a core service and a backend plugin
```
### Core Metrics
Pattern: `backstage.core.{core_service}.{metric_name}`
```yaml
# Examples
backstage.core.database.connections.active
backstage.core.scheduler.tasks.queued.total
backstage.core.httpRouter.requests.total
```
## Design Details
### References
- [General naming considerations](https://opentelemetry.io/docs/specs/semconv/general/naming/#general-naming-considerations)
- [Acquiring a meter](https://opentelemetry.io/docs/languages/js/instrumentation/#acquiring-a-meter)
### Integration with OpenTelemetry Auto-Instrumentation
The `RootMetricsService` will automatically enable instrumentation for known libraries leveraged by the Backstage framework.
The `RootMetricsService` will automatically enable instrumentation for known libraries leveraged by the Backstage framework. Configuration will be provided to enable or disable auto-instrumentation via inclusion or exclusion lists.
- Express
- Knex
- Winston
- etc.
Configuration will be provided to enable or disable auto-instrumentation via inclusion or exclusion lists.
The `MetricsService` **complements** rather than duplicates auto-instrumentation by focusing on **application-level metrics** that only Backstage can provide.
For example, the catalog plugin may want to track the number of entities processed by the `refresh` operation and the kind of entity being processed.
The `MetricsService` **complements** rather than duplicates auto-instrumentation by focusing on **application-level metrics** that only Backstage can provide. For example, the catalog plugin may want to track the number of entities processed by the `refresh` operation and the kind of entity being processed.
```ts
// Auto-instrumentation provides (automatically):
@@ -107,7 +157,6 @@ interface MetricsConfig {
exporters: Array<{
type: 'prometheus' | 'otlp' | 'console' | '...';
enabled: boolean;
config?: Record<string, any>;
}>;
@@ -135,26 +184,19 @@ backend:
exporters:
- type: prometheus
enabled: true
config:
port: 9464
# ...
- type: console
enabled: true
config:
logLevel: debug
autoInstrumentation:
enabled: true
include:
- 'some other library'
exclude:
- 'express'
exclude: ['express']
```
### Interface
Provide a thin wrapper around OpenTelemetry's API while re-exporting the types from the `@opentelemetry/api` package. This introduces concepts already familiar to both the Backstage community and those familiar with OpenTelemetry. The OTEL maintainers state this is the recommended approach and that we can rely on `@opentelemetry/api` to be the source of truth and not to have breaking changes.
Provide a wrapper around OpenTelemetry's API while re-exporting the types from the `@opentelemetry/api` package. This introduces concepts already familiar to both the Backstage community and those familiar with OpenTelemetry.
```ts
interface MetricsService {
@@ -179,9 +221,15 @@ interface MetricsService {
}
```
#### Root Metrics Service
The `RootMetricsService` is responsible for initializing the OpenTelemetry SDK and creating plugin-scoped metrics services. If the end user wants to initialize their own SDK, they are responsible for initializing the OpenTelemetry SDK with their own configuration. The `RootMetricsService` is responsible for providing metrics to other root services and creating plugin-scoped metrics services.
```ts
interface RootMetricsService {
forPlugin(pluginId: string): MetricsService;
}
export const rootMetricsServiceFactory = createServiceFactory({
// depends on as little as possible so that it can be initialized as early as possible.
service: rootMetricsServiceRef,
@@ -194,13 +242,7 @@ export const rootMetricsServiceFactory = createServiceFactory({
});
```
It also provides a method for creating a plugin-scoped `MetricsService` for each registered plugin.
```ts
interface RootMetricsService extends MetricsService {
forPlugin(pluginId: string): MetricsService;
}
class DefaultRootMetricsService implements RootMetricsService {
private sdk: NodeSDK;
@@ -234,15 +276,16 @@ class DefaultRootMetricsService implements RootMetricsService {
}
```
Each plugin receives a metrics service that automatically namespaces all metrics to match the naming conventions outlined in the section below.
#### Plugin Metrics Service
Each plugin receives a metrics service that automatically namespaces all metrics to match the naming conventions.
```ts
const metricsServiceFactory = createServiceFactory({
service: metricsServiceRef,
deps: {
rootConfig: coreServices.rootConfig,
pluginMetadata: coreServices.pluginMetadata,
rootMetrics: coreServices.rootMetrics,
pluginMetadata: coreServices.pluginMetadata,
},
factory: ({ rootMetrics, pluginMetadata }) => {
return rootMetrics.forPlugin(pluginMetadata.getId());
@@ -255,11 +298,6 @@ class PluginMetricsService implements MetricsService {
this.meter = metrics.getMeter(`backstage.plugin.${pluginId}`);
}
createCounter(name: string, options?: MetricOptions): Counter {
const fullName = this.prefixMetricName(name);
return this.meter.createCounter(fullName, options);
}
// ... other interface methods
private prefixMetricName(name: string): string {
@@ -268,68 +306,23 @@ class PluginMetricsService implements MetricsService {
}
```
### Example
##### Example
````ts
const entitiesProcessed = metricsService.createCounter('entities.processed.total', {
description: 'Total entities processed during refresh',
unit: '{entity}',
});
```ts
const entitiesProcessed = metricsService.createCounter(
'entities.processed.total',
{
description: 'Total entities processed during refresh',
unit: '{entity}',
},
);
entitiesProcessed.add(100);
// ...
// metric is now available as `backstage.plugin.catalog.entities.processed.total`
### Naming Conventions
All Backstage metrics follow this hierarchical pattern:
`backstage.{plugin|core}.{scope_name}.{category}.{metric_name}`
**Where:**
- `backstage` is the root namespace for all Backstage metrics
- `{scope}` is the system scope (either **plugin** or **core**)
- `{scope_name}` is the name of the plugin or core service (e.g. `my-plugin`, `catalog`, `scaffolder`, etc.)
- `{category}` is the logical grouping within the `{scope}`
- `{metric_name}` is the name of the metric as provided by the plugin author.
#### Scope
The `scope` represents where it belongs in the Backstage ecosystem.
- `plugin` - A plugin-specific metric (e.g. `backstage.plugin.catalog.entities.count`)
- `core` - A core service metric (e.g. `backstage.core.database.connections.active`)
#### Plugin-Scoped Metrics
Pattern: `backstage.plugin.{pluginId}.{category}.{metric_name}`
```yaml
# Examples
backstage.plugin.catalog.entities.processed.total
backstage.plugin.scaffolder.tasks.completed.total
backstage.plugin.techdocs.builds.active
backstage.plugin.auth.sessions.active.total # todo: technically a core service and a backend plugin
````
#### Core Metrics
Pattern: `backstage.core.{core_service}.{category}.{metric_name}`
```yaml
# Examples
backstage.core.database.connections.active
backstage.core.scheduler.tasks.queued.total
backstage.core.http.requests.total
```
### References
- [General naming considerations](https://opentelemetry.io/docs/specs/semconv/general/naming/#general-naming-considerations)
- [Acquiring a meter](https://opentelemetry.io/docs/languages/js/instrumentation/#acquiring-a-meter)
## Release Plan
1. Create a new `RootMetricsService` that initializes the OpenTelemetry SDK and creates plugin-scoped metrics services.
@@ -344,6 +337,10 @@ backstage.core.http.requests.total
1. Create follow-up action items to integrate the new metrics service into the core system.
1. Fully deprecate all existing metrics implementations like the existing Prometheus one-off implementations.
## Deprecation Plan
TBD
## Dependencies
1. The root metrics service MUST BE initialized as EARLY as possible to prevent dependents from receiving no-op meters