diff --git a/beps/0012-metrics-service/README.md b/beps/0012-metrics-service/README.md index c621a25faf..9948f22cba 100644 --- a/beps/0012-metrics-service/README.md +++ b/beps/0012-metrics-service/README.md @@ -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; }>; @@ -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