Add Periskop plugin

Signed-off-by: Julio Zynger <julio.zynger@soundcloud.com>
This commit is contained in:
Julio Zynger
2022-02-25 11:46:26 +01:00
committed by Fredrik Adelöw
parent c8f5f5c1d2
commit 7ef026339d
34 changed files with 1197 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
/*
* 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';
import { AggregatedError, NotFoundInLocation } from '../types';
import fetch from 'node-fetch';
export type Options = {
config: Config;
};
type PeriskopLocation = {
name: string;
host: string;
};
export class PeriskopApi {
private readonly locations: PeriskopLocation[];
constructor(options: Options) {
this.locations = options.config
.getConfigArray('periskop.locations')
.flatMap(locConf => {
const name = locConf.getString('name');
const host = locConf.getString('host');
return { name: name, host: host };
});
}
private getApiUrl(locationName: string): string | undefined {
return this.locations.find(loc => loc.name === locationName)?.host;
}
async getErrors(
locationName: string,
serviceName: string,
): Promise<AggregatedError[] | NotFoundInLocation> {
const apiUrl = this.getApiUrl(locationName);
if (!apiUrl) {
throw new Error(
`failed to fetch data, no periskop location with name ${locationName}`,
);
}
const response = await fetch(`${apiUrl}/services/${serviceName}/errors/`);
if (!response.ok) {
if (response.status === 404) {
return {
body: await response.text(),
};
}
throw new Error(
`failed to fetch data, status ${response.status}: ${response.statusText}`,
);
}
return response.json();
}
}
+17
View File
@@ -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 * from './service/router';
+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,56 @@
/*
* 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 { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
describe('createRouter', () => {
let app: express.Express;
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
config: new ConfigReader({
periskop: {
locations: [
{
name: 'db',
host: 'http://periskop-db',
},
],
},
}),
});
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
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' });
});
});
});
@@ -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 { errorHandler } from '@backstage/backend-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { PeriskopApi } from '../api/index';
import { Config } from '@backstage/config';
/**
* @public
*/
export interface RouterOptions {
logger: Logger;
config: Config;
}
/**
* @public
*/
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, config } = options;
const periskopApi = new PeriskopApi({ config });
const router = Router();
router.use(express.json());
router.get('/health', (_, response) => {
logger.info('PONG!');
response.send({ status: 'ok' });
});
router.get('/:locationName/:serviceName', async (request, response) => {
const { locationName, serviceName } = request.params;
logger.info(`Periskop got queried for ${serviceName} in ${locationName}`);
const errors = await periskopApi.getErrors(locationName, serviceName);
response.status(200).json(errors);
});
router.use(errorHandler());
return router;
}
@@ -0,0 +1,56 @@
/*
* 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 {
createServiceBuilder,
loadBackendConfig,
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'periskop-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
logger.debug('Starting application server...');
const router = await createRouter({
logger,
config,
});
let service = createServiceBuilder(module)
.setPort(options.port)
.addRouter('/periskop', 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 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 {};
+48
View File
@@ -0,0 +1,48 @@
/*
* 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 interface AggregatedError {
aggregation_key: string;
total_count: number;
severity: string;
latest_errors: ErrorInstance[];
}
export interface ErrorInstance {
error: Error;
uuid: string;
timestamp: number;
severity: string;
http_context: HttpContext;
}
export interface Error {
class: string;
message: string;
stacktrace?: string[];
cause?: Error | null;
}
export interface HttpContext {
request_method: string;
request_url: string;
request_headers: RequestHeaders;
request_body: string;
}
export interface RequestHeaders {
[k: string]: string;
}
export interface NotFoundInLocation {
body: string;
}