Add Periskop plugin
Signed-off-by: Julio Zynger <julio.zynger@soundcloud.com>
This commit is contained in:
committed by
Fredrik Adelöw
parent
c8f5f5c1d2
commit
7ef026339d
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-periskop': patch
|
||||
'@backstage/plugin-periskop-backend': patch
|
||||
---
|
||||
|
||||
Add periskop and periskop-backend plugin, for usage with exception aggregation tool https://periskop.io/
|
||||
@@ -211,6 +211,8 @@ Patrik
|
||||
Peloton
|
||||
performant
|
||||
Performant
|
||||
periskop
|
||||
Periskop
|
||||
plantuml
|
||||
Platformize
|
||||
Podman
|
||||
|
||||
@@ -456,3 +456,10 @@ apacheAirflow:
|
||||
|
||||
gocd:
|
||||
baseUrl: https://your.gocd.instance.com
|
||||
|
||||
periskop:
|
||||
locations:
|
||||
- name: <name of the location 1>
|
||||
host: <HTTP/S host for the Periskop API location>
|
||||
- name: <name of the location 2>
|
||||
host: <HTTP/S host for the Periskop API location>
|
||||
|
||||
@@ -222,6 +222,20 @@ definition.
|
||||
Specifying this annotation will enable GoCD related features in Backstage for
|
||||
that entity.
|
||||
|
||||
### periskop.io/name
|
||||
|
||||
```yaml
|
||||
# Example:
|
||||
metadata:
|
||||
annotations:
|
||||
periskop.io/name: pump-station
|
||||
```
|
||||
|
||||
The value of this annotation is the periskop project name for the given entity.
|
||||
|
||||
Specifying this annotation will enable [Periskop](https://periskop.io/) related features in Backstage for
|
||||
that entity if the periskop plugin is installed.
|
||||
|
||||
### sentry.io/project-slug
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Periskop
|
||||
author: Periskop
|
||||
authorUrl: https://periskop.io/
|
||||
category: Monitoring
|
||||
description: Periskop is a pull-based, language agnostic exception aggregator for microservice environments.
|
||||
documentation: https://github.com/backstage/backstage/tree/master/plugins/periskop
|
||||
iconUrl: https://raw.githubusercontent.com/periskop-dev/periskop/master/docs/assets/periskop-logo.png
|
||||
npmPackageName: '@backstage/plugin-periskop'
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
# periskop-backend
|
||||
|
||||
Simple plugin that proxies requests to the Periskop server APIs.
|
||||
|
||||
To be used with the [Periskop plugin](../periskop/README.md).
|
||||
@@ -0,0 +1,22 @@
|
||||
## API Report File for "@backstage/plugin-periskop-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 (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RouterOptions {
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
logger: Logger_2;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@backstage/plugin-periskop-backend",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"homepage": "https://periskop.io",
|
||||
"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.8",
|
||||
"@backstage/config": "^0.1.14",
|
||||
"@types/express": "*",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.14.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^0.35.0",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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 {};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
# periskop
|
||||
|
||||
[Periskop](https://periskop.io/) is a pull-based, language agnostic exception aggregator for microservice environments.
|
||||
|
||||

|
||||
|
||||
### `PeriskopErrorsTable`
|
||||
|
||||
The Periskop Backstage Plugin exposes an entity tab component named `PeriskopErrorsTable`. Each of the entries in the table will direct you to the error details in your deployed Periskop instance location.
|
||||
|
||||

|
||||
|
||||
Now your plugin should be visible as a tab at the top of the entity pages.
|
||||
However, it warns of a missing `periskop.io/name` annotation.
|
||||
|
||||
Add the annotation to your component descriptor file as shown in the highlighted example below:
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
periskop.io/name: '<THE NAME OF THE PERISKOP APP>'
|
||||
```
|
||||
|
||||
### Locations
|
||||
|
||||
The periskop plugin can be configured to fetch aggregated errors from multiple deployment locations. This is especially useful if you have a multi-zone deployment, or a federated setup and would like to drill deeper into a single instance of the federation. Each of the configured locations will be included in the plugin's UI via a dropdown on the errors table.
|
||||
|
||||
The plugin requires to configure _at least one_ Periskop API location in the [app-config.yaml](https://github.com/backstage/backstage/blob/master/app-config.yaml):
|
||||
|
||||
```yaml
|
||||
periskop:
|
||||
locations:
|
||||
- name: <name of the location>
|
||||
host: <HTTP/S host for the Periskop API location>
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
## API Report File for "@backstage/plugin-periskop"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public
|
||||
export const isPeriskopAvailable: (entity: Entity) => boolean;
|
||||
|
||||
// @public
|
||||
export const PERISKOP_NAME_ANNOTATION = 'periskop.io/name';
|
||||
|
||||
// @public
|
||||
export class PeriskopApi {
|
||||
// Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts
|
||||
constructor(options: Options);
|
||||
// Warning: (ae-forgotten-export) The symbol "AggregatedError" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
getErrorInstanceUrl(
|
||||
locationName: string,
|
||||
serviceName: string,
|
||||
error: AggregatedError,
|
||||
): string;
|
||||
// Warning: (ae-forgotten-export) The symbol "NotFoundInLocation" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
getErrors(
|
||||
locationName: string,
|
||||
serviceName: string,
|
||||
): Promise<AggregatedError[] | NotFoundInLocation>;
|
||||
// (undocumented)
|
||||
getLocationNames(): string[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const periskopApiRef: ApiRef<PeriskopApi>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const PeriskopErrorsTable: () => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const periskopPlugin: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 Config {
|
||||
/**
|
||||
* Configuration options for the periskop plugin
|
||||
*/
|
||||
periskop: {
|
||||
/**
|
||||
* Integration configuration for the periskop servers
|
||||
* @visibility frontend
|
||||
*/
|
||||
locations: Array<{
|
||||
/**
|
||||
* The name of the given Periskop instance
|
||||
* @visibility frontend
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The hostname of the given Periskop instance
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 { createDevApp } from '@backstage/dev-utils';
|
||||
import { periskopPlugin } from '../src/plugin';
|
||||
|
||||
createDevApp().registerPlugin(periskopPlugin).render();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@backstage/plugin-periskop",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"homepage": "https://periskop.io",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"start": "backstage-cli plugin:serve",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.11.0",
|
||||
"@backstage/core-components": "^0.8.9",
|
||||
"@backstage/core-plugin-api": "^0.6.1",
|
||||
"@backstage/plugin-catalog-react": "^0.7.0",
|
||||
"@backstage/theme": "^0.2.15",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"moment": "^2.29.1",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.14.0",
|
||||
"@backstage/core-app-api": "^0.5.3",
|
||||
"@backstage/dev-utils": "^0.2.22",
|
||||
"@backstage/test-utils": "^0.2.5",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "*",
|
||||
"@types/node": "*",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"msw": "^0.35.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { AggregatedError, NotFoundInLocation } from '../types';
|
||||
|
||||
type Options = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
configApi: ConfigApi;
|
||||
};
|
||||
|
||||
type PeriskopLocation = {
|
||||
name: string;
|
||||
host: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* API abstraction to interact with Periskop's backends.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class PeriskopApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly locations: PeriskopLocation[];
|
||||
|
||||
constructor(options: Options) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.locations = options.configApi
|
||||
.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;
|
||||
}
|
||||
|
||||
getLocationNames(): string[] {
|
||||
return this.locations.map(e => e.name);
|
||||
}
|
||||
|
||||
getErrorInstanceUrl(
|
||||
locationName: string,
|
||||
serviceName: string,
|
||||
error: AggregatedError,
|
||||
): string {
|
||||
return `${this.getApiUrl(
|
||||
locationName,
|
||||
)}/#/${serviceName}/errors/${encodeURIComponent(error.aggregation_key)}`;
|
||||
}
|
||||
|
||||
async getErrors(
|
||||
locationName: string,
|
||||
serviceName: string,
|
||||
): Promise<AggregatedError[] | NotFoundInLocation> {
|
||||
const apiUrl = `${await this.discoveryApi.getBaseUrl(
|
||||
'periskop',
|
||||
)}/${locationName}/${serviceName}`;
|
||||
const response = await fetch(apiUrl);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
Table,
|
||||
TableColumn,
|
||||
Progress,
|
||||
StatusWarning,
|
||||
StatusError,
|
||||
StatusPending,
|
||||
Select,
|
||||
EmptyState,
|
||||
Link,
|
||||
} from '@backstage/core-components';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { periskopApiRef } from '../..';
|
||||
import { AggregatedError, NotFoundInLocation } from '../../types';
|
||||
|
||||
/**
|
||||
* Constant storing Periskop project name.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PERISKOP_NAME_ANNOTATION = 'periskop.io/name';
|
||||
|
||||
/**
|
||||
* Returns true if Periskop annotation is present in the given entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const isPeriskopAvailable = (entity: Entity): boolean =>
|
||||
Boolean(entity.metadata.annotations?.[PERISKOP_NAME_ANNOTATION]);
|
||||
|
||||
const renderKey = (
|
||||
error: AggregatedError,
|
||||
linkTarget: string,
|
||||
): React.ReactNode => {
|
||||
return (
|
||||
<Link to={linkTarget} target="_blank" rel="noreferrer">
|
||||
{error.aggregation_key}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSeverity = (severity: string): React.ReactNode => {
|
||||
if (severity.toLocaleLowerCase('en-US') === 'warning') {
|
||||
return <StatusWarning>{severity}</StatusWarning>;
|
||||
} else if (severity.toLocaleLowerCase('en-US') === 'error') {
|
||||
return <StatusError>{severity}</StatusError>;
|
||||
}
|
||||
return <StatusPending>{severity}</StatusPending>;
|
||||
};
|
||||
|
||||
const renderLastOccurrence = (error: AggregatedError): React.ReactNode => {
|
||||
return moment(new Date(error.latest_errors[0].timestamp * 1000)).fromNow();
|
||||
};
|
||||
|
||||
function isNotFoundInLocation(
|
||||
apiResult: AggregatedError[] | NotFoundInLocation | undefined,
|
||||
): apiResult is NotFoundInLocation {
|
||||
return (apiResult as NotFoundInLocation)?.body !== undefined;
|
||||
}
|
||||
|
||||
export const PeriskopErrorsTable = () => {
|
||||
const { entity } = useEntity();
|
||||
const entityPeriskopName: string =
|
||||
(entity.metadata.annotations?.[PERISKOP_NAME_ANNOTATION] as
|
||||
| string
|
||||
| undefined) ?? entity.metadata.name;
|
||||
|
||||
const periskopApi = useApi(periskopApiRef);
|
||||
const locations = periskopApi.getLocationNames();
|
||||
const [locationOption, setLocationOption] = React.useState<string>(
|
||||
locations[0],
|
||||
);
|
||||
const {
|
||||
value: aggregatedErrors,
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(async (): Promise<AggregatedError[] | NotFoundInLocation> => {
|
||||
return periskopApi.getErrors(locationOption, entityPeriskopName);
|
||||
}, [locationOption]);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
}
|
||||
|
||||
const columns: TableColumn<AggregatedError>[] = [
|
||||
{
|
||||
title: 'Key',
|
||||
field: 'aggregation_key',
|
||||
highlight: true,
|
||||
sorting: false,
|
||||
render: aggregatedError => {
|
||||
const errorUrl = periskopApi.getErrorInstanceUrl(
|
||||
locationOption,
|
||||
entityPeriskopName,
|
||||
aggregatedError,
|
||||
);
|
||||
return renderKey(aggregatedError, errorUrl);
|
||||
},
|
||||
},
|
||||
{ title: 'Occurrences', field: 'total_count', sorting: true },
|
||||
{
|
||||
title: 'Last Occurrence',
|
||||
render: aggregatedError => renderLastOccurrence(aggregatedError),
|
||||
defaultSort: 'asc',
|
||||
customSort: (a, b) =>
|
||||
b.latest_errors[0].timestamp - a.latest_errors[0].timestamp,
|
||||
type: 'datetime',
|
||||
},
|
||||
{
|
||||
title: 'Severity',
|
||||
field: 'severity',
|
||||
render: aggregatedError => renderSeverity(aggregatedError.severity),
|
||||
sorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
const sortingSelect = (
|
||||
<Select
|
||||
selected={locationOption}
|
||||
label="Location"
|
||||
items={locations.map(e => ({
|
||||
label: e,
|
||||
value: e,
|
||||
}))}
|
||||
onChange={el => setLocationOption(el.toString())}
|
||||
/>
|
||||
);
|
||||
|
||||
const contentHeader = (
|
||||
<ContentHeader title="Periskop" description={entityPeriskopName}>
|
||||
{sortingSelect}
|
||||
</ContentHeader>
|
||||
);
|
||||
|
||||
if (isNotFoundInLocation(aggregatedErrors)) {
|
||||
return (
|
||||
<Content noPadding>
|
||||
{contentHeader}
|
||||
<EmptyState
|
||||
title="Periskop returned an error"
|
||||
description={aggregatedErrors.body}
|
||||
missing="data"
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content noPadding>
|
||||
{contentHeader}
|
||||
<Table
|
||||
options={{
|
||||
search: false,
|
||||
paging: false,
|
||||
filtering: true,
|
||||
padding: 'dense',
|
||||
toolbar: false,
|
||||
}}
|
||||
columns={columns}
|
||||
data={aggregatedErrors || []}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 {
|
||||
PeriskopErrorsTable,
|
||||
isPeriskopAvailable,
|
||||
PERISKOP_NAME_ANNOTATION,
|
||||
} from './PeriskopErrorsTable';
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 { periskopPlugin } from './plugin';
|
||||
import { createComponentExtension } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PeriskopErrorsTable = periskopPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'PeriskopErrorsTable',
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/PeriskopErrorsTable').then(
|
||||
m => m.PeriskopErrorsTable,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { periskopPlugin, periskopApiRef } from './plugin';
|
||||
export { PeriskopErrorsTable } from './extensions';
|
||||
export {
|
||||
isPeriskopAvailable,
|
||||
PERISKOP_NAME_ANNOTATION,
|
||||
} from './components/PeriskopErrorsTable';
|
||||
export * from './api/index';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { periskopPlugin } from './plugin';
|
||||
|
||||
describe('periskop', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(periskopPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 {
|
||||
discoveryApiRef,
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
createApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { PeriskopApi } from './api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const periskopApiRef = createApiRef<PeriskopApi>({
|
||||
id: 'plugin.periskop.service',
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const periskopPlugin = createPlugin({
|
||||
id: 'periskop',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: periskopApiRef,
|
||||
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
|
||||
factory: ({ configApi, discoveryApi }) =>
|
||||
new PeriskopApi({ configApi, discoveryApi }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'periskop',
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user