refactor: reorg sections for better flow of content
Signed-off-by: Kurt King <kurtaking@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user