feat: add a new system metadata service

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
aramissennyeydd
2024-12-12 21:54:22 -07:00
parent d5db4cd62f
commit 957397ceeb
24 changed files with 4184 additions and 4 deletions
+4
View File
@@ -21,6 +21,7 @@
"exports": {
".": "./src/index.ts",
"./auditor": "./src/entrypoints/auditor/index.ts",
"./alpha/systemMetadata": "./src/entrypoints/systemMetadata/index.ts",
"./auth": "./src/entrypoints/auth/index.ts",
"./cache": "./src/entrypoints/cache/index.ts",
"./database": "./src/entrypoints/database/index.ts",
@@ -49,6 +50,9 @@
"auditor": [
"src/entrypoints/auditor/index.ts"
],
"alpha/systemMetadata": [
"src/entrypoints/systemMetadata/index.ts"
],
"auth": [
"src/entrypoints/auth/index.ts"
],
@@ -0,0 +1,17 @@
/*
* Copyright 2024 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 { systemMetadataServiceFactory } from './systemMetadataServiceFactory';
export { DefaultSystemMetadataService } from './lib/DefaultSystemMetadataService';
@@ -0,0 +1,61 @@
/*
* Copyright 2024 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 {
LoggerService,
RootConfigService,
} from '@backstage/backend-plugin-api';
import {
BackstageInstance,
SystemMetadataService,
} from '@backstage/backend-plugin-api/alpha';
export class DefaultSystemMetadataService implements SystemMetadataService {
private readonly logger: LoggerService;
private readonly config: RootConfigService;
constructor(options: { logger: LoggerService; config: RootConfigService }) {
this.logger = options.logger;
this.config = options.config;
}
public static create(pluginEnv: {
logger: LoggerService;
config: RootConfigService;
}) {
return new DefaultSystemMetadataService(pluginEnv);
}
listInstances() {
const endpoints =
this.config.getOptionalConfigArray('discovery.instances') ?? [];
const instances: BackstageInstance[] = [];
for (const endpoint of endpoints) {
const baseUrl = endpoint.getOptionalString('baseUrl');
if (baseUrl) {
this.logger.info(`Found instance at ${baseUrl}`);
instances.push({ url: baseUrl });
} else {
this.logger.warn(
`Instance ${endpoint.get(
'target',
)} is missing a 'baseUrl' property. This is required for the system metadata service.`,
);
}
}
this.logger.info(`Found ${instances.length} instances.`);
return Promise.resolve(instances);
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2024 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 {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { DefaultSystemMetadataService } from './lib/DefaultSystemMetadataService';
import { systemMetadataServiceRef } from '@backstage/backend-plugin-api/alpha';
/**
* Metadata about an entire Backstage system, a collection of Backstage instances.
*
* @alpha
*/
export const systemMetadataServiceFactory = createServiceFactory({
service: systemMetadataServiceRef,
deps: {
logger: coreServices.logger,
config: coreServices.rootConfig,
},
async factory({ logger, config }) {
return DefaultSystemMetadataService.create({
logger,
config,
});
},
});
@@ -23,3 +23,16 @@ export type {
export type { ActionsService, ActionsServiceAction } from './ActionsService';
export { actionsRegistryServiceRef, actionsServiceRef } from './refs';
import { createServiceRef } from '@backstage/backend-plugin-api';
export const systemMetadataServiceRef = createServiceRef<
import('./services/definitions/SystemMetadataService').SystemMetadataService
>({
id: 'core.systemMetadata',
});
export type {
BackstageInstance,
SystemMetadataService,
} from './services/definitions/SystemMetadataService';
@@ -0,0 +1,25 @@
/*
* Copyright 2024 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.
*/
type Target = string | { internal: string; external: string };
export interface BackstageInstance {
url: Target;
}
export interface SystemMetadataService {
listInstances(): Promise<BackstageInstance[]>;
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
# example-backend
This package is an EXAMPLE of a Backstage backend using the [new backend system](https://backstage.io/docs/backend-system/).
The main purpose of this package is to provide a test bed for Backstage split deployment work. You can deploy both this package and the main `packages/backend` together by running the `start:split` command in both packages. This will run the following backends:
1. `packages/backend` running on `:7007` with the default plugins installed.
2. `packages/backend-split` running on `:7008` with a subset of plugins installed for testing.
@@ -0,0 +1,14 @@
backend:
baseUrl: http://localhost:7008
listen:
port: 7008
discovery:
endpoints:
- target: http://localhost:7007/api/{{pluginId}}
plugins: [proxy]
- target: http://localhost:7008/api/{{pluginId}}
plugins: [catalog]
instances:
- baseUrl: http://localhost:7007
- baseUrl: http://localhost:7008
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-backend-split
title: example-backend-split
spec:
lifecycle: experimental
type: backstage-backend
owner: maintainers
+12
View File
@@ -0,0 +1,12 @@
# Knip report
## Unused dependencies (5)
| Name | Location | Severity |
| :----------------------------------------------- | :----------- | :------- |
| @backstage/plugin-catalog-backend-module-openapi | package.json | error |
| @backstage/plugin-search-backend-node | package.json | error |
| @backstage/plugin-permission-common | package.json | error |
| @backstage/plugin-permission-node | package.json | error |
| @backstage/backend-tasks | package.json | error |
+73
View File
@@ -0,0 +1,73 @@
{
"name": "example-backend-split",
"version": "0.0.33-next.2",
"backstage": {
"role": "backend"
},
"private": true,
"keywords": [
"backstage"
],
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/backend"
},
"license": "Apache-2.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"start": "backstage-cli package start --require ./src/instrumentation.js",
"start:split": "backstage-cli package start --require ./src/instrumentation.js --config ../../app-config.yaml --config app-config.split.yaml",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/plugin-app-backend": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^",
"@backstage/plugin-catalog-backend-module-openapi": "workspace:^",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^",
"@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^",
"@backstage/plugin-devtools-backend": "workspace:^",
"@backstage/plugin-events-backend": "workspace:^",
"@backstage/plugin-kubernetes-backend": "workspace:^",
"@backstage/plugin-notifications-backend": "workspace:^",
"@backstage/plugin-permission-backend": "workspace:^",
"@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/plugin-proxy-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-github": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-notifications": "workspace:^",
"@backstage/plugin-search-backend": "workspace:^",
"@backstage/plugin-search-backend-module-catalog": "workspace:^",
"@backstage/plugin-search-backend-module-explore": "workspace:^",
"@backstage/plugin-search-backend-module-techdocs": "workspace:^",
"@backstage/plugin-search-backend-node": "workspace:^",
"@backstage/plugin-signals-backend": "workspace:^",
"@backstage/plugin-techdocs-backend": "workspace:^",
"@opentelemetry/auto-instrumentations-node": "^0.54.0",
"@opentelemetry/exporter-prometheus": "^0.54.0",
"@opentelemetry/sdk-node": "^0.54.0",
"example-app": "link:../app",
"express-promise-router": "^4.1.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
}
}
@@ -0,0 +1,5 @@
GET http://localhost:7007/.backstage/systemInfo/features/installed
###
GET http://localhost:7008/.backstage/systemInfo/features/installed
@@ -26,14 +26,22 @@ export default createBackendPlugin({
deps: {
instanceMetadata: coreServices.rootInstanceMetadata,
logger: coreServices.logger,
httpRouter: coreServices.rootHttpRouter,
},
async init({ instanceMetadata, logger }) {
const plugins = await instanceMetadata.getInstalledPlugins();
async init({ instanceMetadata, logger, httpRouter }) {
logger.info(
`Installed plugins on this instance: ${plugins
.map(e => e.pluginId)
.join(', ')}`,
);
const router = Router();
router.get('/features/installed', (_, res) => {
res.json({ items: instanceMetadata.getInstalledFeatures() });
});
httpRouter.use('/.backstage/instanceInfo', router);
},
});
},
@@ -0,0 +1,93 @@
/*
* Copyright 2024 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 {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import {
BackendFeatureMeta,
systemMetadataServiceRef,
} from '@backstage/backend-plugin-api/alpha';
import Router from 'express-promise-router';
// Example usage of the instance metadata service to log the installed features.
export default createBackendPlugin({
pluginId: 'system-metadata-logging',
register(env) {
env.registerInit({
deps: {
systemMetadata: systemMetadataServiceRef,
logger: coreServices.logger,
httpRouter: coreServices.rootHttpRouter,
},
async init({ systemMetadata, logger, httpRouter }) {
logger.info(
`Instances in this system: ${JSON.stringify(
await systemMetadata.listInstances(),
)}`,
);
const router = Router();
router.get('/instances', async (_, res) => {
res.json(await systemMetadata.listInstances());
});
router.get('/features/installed', async (_, res) => {
const instances = await systemMetadata.listInstances();
const featurePromises = await Promise.allSettled(
instances.map(async instance => {
const response = await fetch(
`${instance.url}/.backstage/instanceInfo/features/installed`,
);
if (response.ok) {
return { instance, response: await response.json() };
}
throw new Error(
`Failed to fetch installed features from ${instance.url}`,
);
}),
);
const pluginByInstance: Record<string, string[]> = {};
for (const result of featurePromises) {
if (result.status !== 'fulfilled') {
logger.error(
`Failed to fetch installed features: ${result.reason}`,
);
continue;
}
const instance = result.value.instance;
const installedFeatures = result.value.response
.items as BackendFeatureMeta[];
for (const feature of installedFeatures) {
if (feature.type === 'plugin') {
if (!pluginByInstance[feature.pluginId]) {
pluginByInstance[feature.pluginId] = [];
}
pluginByInstance[feature.pluginId].push(
`${instance.url}/api/${feature.pluginId}`,
);
}
}
}
res.json(pluginByInstance);
});
httpRouter.use('/.backstage/systemInfo', router);
},
});
},
});
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright 2022 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 { createBackend } from '@backstage/backend-defaults';
import { systemMetadataServiceFactory } from '@backstage/backend-defaults/alpha/systemMetadata';
const backend = createBackend();
backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
);
backend.add(import('@backstage/plugin-permission-backend'));
backend.add(import('./experimental/instanceMetadata'));
backend.add(import('./experimental/systemMetadata'));
backend.add(systemMetadataServiceFactory);
backend.start();
@@ -0,0 +1,34 @@
/*
* Copyright 2024 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.
*/
const { NodeSDK } = require('@opentelemetry/sdk-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');
// Expose opentelemetry metrics using a Prometheus exporter on
// http://localhost:9464/metrics. See packages/backend/prometheus.yml for
// more information on how to scrape it.
const prometheus = new PrometheusExporter();
const sdk = new NodeSDK({
// traceExporter: ...,
metricReader: prometheus,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
+9
View File
@@ -0,0 +1,9 @@
discovery:
endpoints:
- target: http://localhost:7007/api/{{pluginId}}
plugins: [proxy]
- target: http://localhost:7008/api/{{pluginId}}
plugins: [catalog]
instances:
- baseUrl: http://localhost:7007
- baseUrl: http://localhost:7008
+3 -1
View File
@@ -26,6 +26,7 @@
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"start": "backstage-cli package start --require ./src/instrumentation.js",
"start:split": "backstage-cli package start --require ./src/instrumentation.js --config ../../app-config.yaml --config app-config.split.yaml",
"start:prometheus": "docker run --mount type=bind,source=./prometheus.yml,destination=/etc/prometheus/prometheus.yml --publish published=9090,target=9090,protocol=tcp prom/prometheus",
"test": "backstage-cli package test"
},
@@ -69,7 +70,8 @@
"@opentelemetry/auto-instrumentations-node": "^0.61.0",
"@opentelemetry/exporter-prometheus": "^0.54.0",
"@opentelemetry/sdk-node": "^0.54.0",
"example-app": "link:../app"
"example-app": "link:../app",
"express-promise-router": "^4.1.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
@@ -0,0 +1,50 @@
/*
* Copyright 2024 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 {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha';
import Router from 'express-promise-router';
// Example usage of the instance metadata service to log the installed features.
export default createBackendPlugin({
pluginId: 'instance-metadata-logging',
register(env) {
env.registerInit({
deps: {
instanceMetadata: instanceMetadataServiceRef,
logger: coreServices.logger,
httpRouter: coreServices.rootHttpRouter,
},
async init({ instanceMetadata, logger, httpRouter }) {
logger.info(
`Installed features on this instance: ${JSON.stringify(
instanceMetadata.getInstalledFeatures(),
)}`,
);
const router = Router();
router.get('/features/installed', (_, res) => {
res.json({ items: instanceMetadata.getInstalledFeatures() });
});
httpRouter.use('/.backstage/instanceInfo', router);
},
});
},
});
@@ -0,0 +1,93 @@
/*
* Copyright 2024 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 {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import {
BackendFeatureMeta,
systemMetadataServiceRef,
} from '@backstage/backend-plugin-api/alpha';
import Router from 'express-promise-router';
// Example usage of the instance metadata service to log the installed features.
export default createBackendPlugin({
pluginId: 'system-metadata-logging',
register(env) {
env.registerInit({
deps: {
systemMetadata: systemMetadataServiceRef,
logger: coreServices.logger,
httpRouter: coreServices.rootHttpRouter,
},
async init({ systemMetadata, logger, httpRouter }) {
logger.info(
`Instances in this system: ${JSON.stringify(
await systemMetadata.listInstances(),
)}`,
);
const router = Router();
router.get('/instances', async (_, res) => {
res.json(await systemMetadata.listInstances());
});
router.get('/features/installed', async (_, res) => {
const instances = await systemMetadata.listInstances();
const featurePromises = await Promise.allSettled(
instances.map(async instance => {
const response = await fetch(
`${instance.url}/.backstage/instanceInfo/features/installed`,
);
if (response.ok) {
return { instance, response: await response.json() };
}
throw new Error(
`Failed to fetch installed features from ${instance.url}`,
);
}),
);
const pluginByInstance: Record<string, string[]> = {};
for (const result of featurePromises) {
if (result.status !== 'fulfilled') {
logger.error(
`Failed to fetch installed features: ${result.reason}`,
);
continue;
}
const instance = result.value.instance;
const installedFeatures = result.value.response
.items as BackendFeatureMeta[];
for (const feature of installedFeatures) {
if (feature.type === 'plugin') {
if (!pluginByInstance[feature.pluginId]) {
pluginByInstance[feature.pluginId] = [];
}
pluginByInstance[feature.pluginId].push(
`${instance.url}/api/${feature.pluginId}`,
);
}
}
}
res.json(pluginByInstance);
});
httpRouter.use('/.backstage/systemInfo', router);
},
});
},
});
+4 -1
View File
@@ -19,6 +19,7 @@ import {
coreServices,
createBackendFeatureLoader,
} from '@backstage/backend-plugin-api';
import { systemMetadataServiceFactory } from '@backstage/backend-defaults/alpha/systemMetadata';
const backend = createBackend();
@@ -69,7 +70,9 @@ backend.add(searchLoader);
backend.add(import('@backstage/plugin-techdocs-backend'));
backend.add(import('@backstage/plugin-signals-backend'));
backend.add(import('@backstage/plugin-notifications-backend'));
backend.add(import('./instanceMetadata'));
backend.add(import('./experimental/instanceMetadata'));
backend.add(import('./experimental/systemMetadata'));
backend.add(systemMetadataServiceFactory);
backend.add(import('@backstage/plugin-events-backend-module-google-pubsub'));
backend.add(import('@backstage/plugin-mcp-actions-backend'));
+52
View File
@@ -30232,6 +30232,12 @@ __metadata:
languageName: node
linkType: soft
"example-app@link:../app::locator=example-backend-split%40workspace%3Apackages%2Fbackend-split":
version: 0.0.0-use.local
resolution: "example-app@link:../app::locator=example-backend-split%40workspace%3Apackages%2Fbackend-split"
languageName: node
linkType: soft
"example-app@workspace:packages/app":
version: 0.0.0-use.local
resolution: "example-app@workspace:packages/app"
@@ -30300,6 +30306,51 @@ __metadata:
languageName: unknown
linkType: soft
"example-backend-split@workspace:packages/backend-split":
version: 0.0.0-use.local
resolution: "example-backend-split@workspace:packages/backend-split"
dependencies:
"@backstage/backend-defaults": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/plugin-app-backend": "workspace:^"
"@backstage/plugin-auth-backend": "workspace:^"
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
"@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^"
"@backstage/plugin-catalog-backend-module-openapi": "workspace:^"
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^"
"@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^"
"@backstage/plugin-devtools-backend": "workspace:^"
"@backstage/plugin-events-backend": "workspace:^"
"@backstage/plugin-kubernetes-backend": "workspace:^"
"@backstage/plugin-notifications-backend": "workspace:^"
"@backstage/plugin-permission-backend": "workspace:^"
"@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^"
"@backstage/plugin-permission-common": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/plugin-proxy-backend": "workspace:^"
"@backstage/plugin-scaffolder-backend": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-github": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-notifications": "workspace:^"
"@backstage/plugin-search-backend": "workspace:^"
"@backstage/plugin-search-backend-module-catalog": "workspace:^"
"@backstage/plugin-search-backend-module-explore": "workspace:^"
"@backstage/plugin-search-backend-module-techdocs": "workspace:^"
"@backstage/plugin-search-backend-node": "workspace:^"
"@backstage/plugin-signals-backend": "workspace:^"
"@backstage/plugin-techdocs-backend": "workspace:^"
"@opentelemetry/auto-instrumentations-node": "npm:^0.54.0"
"@opentelemetry/exporter-prometheus": "npm:^0.54.0"
"@opentelemetry/sdk-node": "npm:^0.54.0"
example-app: "link:../app"
express-promise-router: "npm:^4.1.0"
languageName: unknown
linkType: soft
"example-backend@workspace:packages/backend":
version: 0.0.0-use.local
resolution: "example-backend@workspace:packages/backend"
@@ -30345,6 +30396,7 @@ __metadata:
"@opentelemetry/exporter-prometheus": "npm:^0.54.0"
"@opentelemetry/sdk-node": "npm:^0.54.0"
example-app: "link:../app"
express-promise-router: "npm:^4.1.0"
languageName: unknown
linkType: soft