Merge pull request #9390 from moltenice-forks/airbrake-backend

Airbrake Backend Plugin
This commit is contained in:
Patrik Oldsberg
2022-02-17 10:20:06 +01:00
committed by GitHub
16 changed files with 511 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-airbrake-backend': minor
---
Created the Airbrake backend plugin that proxies requests to the Airbrake API
+3
View File
@@ -464,3 +464,6 @@ apacheAirflow:
gocd:
baseUrl: https://your.gocd.instance.com
airbrake:
apiKey: ${AIRBRAKE_API_KEY}
+1
View File
@@ -33,6 +33,7 @@
"@backstage/catalog-model": "^0.9.10",
"@backstage/config": "^0.1.13",
"@backstage/integration": "^0.7.2",
"@backstage/plugin-airbrake-backend": "^0.0.0",
"@backstage/plugin-app-backend": "^0.3.24",
"@backstage/plugin-auth-backend": "^0.10.0",
"@backstage/plugin-auth-node": "^0.1.0",
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+16
View File
@@ -0,0 +1,16 @@
# airbrake-backend
Welcome to the airbrake-backend backend plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/airbrake-backend](http://localhost:7007/airbrake-backend).
Here is an example endpoint: http://localhost:7007/airbrake-backend/health
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development.
+26
View File
@@ -0,0 +1,26 @@
## API Report File for "@backstage/plugin-airbrake-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import express from 'express';
import { Logger as Logger_2 } from 'winston';
// @public
export interface AirbrakeConfig {
apiKey: string;
}
// @public
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public
export function extractAirbrakeConfig(config: Config): AirbrakeConfig;
// @public
export interface RouterOptions {
airbrakeConfig: AirbrakeConfig;
logger: Logger_2;
}
```
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2021 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 interface Config {
/** Configuration options for the Airbrake plugin */
airbrake: {
/**
* The API Key to authenticate requests. More details on how to get this here: https://airbrake.io/docs/api/#authentication
*/
apiKey: string;
};
}
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@backstage/plugin-airbrake-backend",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.10.7",
"@backstage/config": "^0.1.13",
"@types/express": "*",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"http-proxy-middleware": "^2.0.0",
"winston": "^3.2.1",
"cross-fetch": "^3.0.6",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.2",
"@types/http-proxy-middleware": "^0.19.3",
"@types/supertest": "^2.0.8",
"supertest": "^6.1.6",
"msw": "^0.35.0"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts",
"jest": {
"coverageThreshold": {
"global": {
"functions": 100,
"lines": 100,
"statements": 100
}
},
"coveragePathIgnorePatterns": [
"standaloneServer.ts",
"index.ts",
"run.ts"
]
}
}
@@ -0,0 +1,41 @@
/*
* 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 { Config } from '@backstage/config';
/**
* The configuration needed for the airbrake-backend plugin
*
* @public
*/
export interface AirbrakeConfig {
/**
* The API Key to authenticate requests. More details on how to get this here: https://airbrake.io/docs/api/#authentication
*/
apiKey: string;
}
/**
* Extract the Airbrake config from a config object
*
* @public
*
* @param config - The config object to extract from
*/
export function extractAirbrakeConfig(config: Config): AirbrakeConfig {
return {
apiKey: config.getString('airbrake.apiKey'),
};
}
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './ExtractAirbrakeConfig';
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright 2020 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.
*/
/**
* The Airbrake Backend plugin used by \@backstage/plugin-airbrake
*
* @packageDocumentation
*/
export * from './service/router';
export * from './config';
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright 2020 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 { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -0,0 +1,89 @@
/*
* Copyright 2020 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 { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { ConfigReader } from '@backstage/config';
import {
createRouter,
generateAirbrakePathRewrite,
RouterOptions,
} from './router';
import { AirbrakeConfig, extractAirbrakeConfig } from '../config';
describe('createRouter', () => {
let app: express.Express;
let airbrakeConfig: AirbrakeConfig;
beforeEach(async () => {
jest.resetAllMocks();
const config = new ConfigReader({
airbrake: {
apiKey: 'fakeApiKey',
},
});
airbrakeConfig = extractAirbrakeConfig(config);
const router = await createRouter({
logger: getVoidLogger(),
airbrakeConfig,
});
app = express().use(router);
});
describe('GET /health', () => {
it('returns ok', async () => {
const response = await request(app).get('/health');
expect(response.status).toEqual(200);
expect(response.body).toEqual({ status: 'ok' });
});
});
describe('GET /api', () => {
it('appends the API Key properly with no other url parameters', () => {
const options: RouterOptions = {
logger: getVoidLogger(),
airbrakeConfig,
};
const pathRewrite = generateAirbrakePathRewrite(options) as (
path: string,
) => string;
expect(pathRewrite('/airbrake-backend/api/v4/random/endpoint')).toBe(
'/v4/random/endpoint?key=fakeApiKey',
);
});
it('appends the API Key properly despite there being other URL parameters', () => {
const options: RouterOptions = {
logger: getVoidLogger(),
airbrakeConfig,
};
const pathRewrite = generateAirbrakePathRewrite(options) as (
path: string,
) => string;
expect(
pathRewrite(
'/airbrake-backend/api/v4/random/endpoint?param1=123&param2=abc',
),
).toBe('/v4/random/endpoint?param1=123&param2=abc&key=fakeApiKey');
});
});
});
@@ -0,0 +1,96 @@
/*
* Copyright 2020 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 { errorHandler } from '@backstage/backend-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { AirbrakeConfig } from '../config';
import { Options } from 'http-proxy-middleware/dist/types';
/**
* The router options that are needed when creating a router.
*
* @public
*/
export interface RouterOptions {
/**
* A logger object
*/
logger: Logger;
/**
* The Airbrake config obtained from {@link extractAirbrakeConfig}
*/
airbrakeConfig: AirbrakeConfig;
}
/**
* Mainly used internally to generate the path.
*
* @internal
*
* @param options - Router options
*/
export const generateAirbrakePathRewrite = (
options: RouterOptions,
): Options['pathRewrite'] => {
const apiKey = options.airbrakeConfig.apiKey;
return path => {
let newPath = path.replace(/.+?(\/api)/g, '');
if (newPath.includes('?')) {
newPath += `&key=${apiKey}`;
} else {
newPath += `?key=${apiKey}`;
}
return newPath;
};
};
/**
* Create the Airbrake Router, used for making API calls to the Airbrake API.
*
* @public
*
* @param options - Router options
*/
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger } = options;
const router = Router();
router.use(express.json());
router.get('/health', (_, response) => {
logger.info('PONG!');
response.send({ status: 'ok' });
});
router.use(
'/api',
createProxyMiddleware({
target: 'https://api.airbrake.io/api',
changeOrigin: true,
pathRewrite: generateAirbrakePathRewrite(options),
}),
);
router.use(errorHandler());
return router;
}
@@ -0,0 +1,57 @@
/*
* Copyright 2020 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 {
createServiceBuilder,
loadBackendConfig,
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
import { extractAirbrakeConfig } from '../config';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'airbrake-backend-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
const airbrakeConfig = extractAirbrakeConfig(config);
logger.debug('Starting application server...');
const router = await createRouter({
logger,
airbrakeConfig,
});
let service = createServiceBuilder(module)
.setPort(options.port)
.addRouter('/airbrake-backend', router);
if (options.enableCors) {
service = service.enableCors({ origin: 'http://localhost:3000' });
}
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 {};