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
+6
View File
@@ -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/
+2
View File
@@ -211,6 +211,8 @@ Patrik
Peloton
performant
Performant
periskop
Periskop
plantuml
Platformize
Podman
+7
View File
@@ -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
+9
View File
@@ -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'
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+5
View File
@@ -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).
+22
View File
@@ -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)
```
+43
View File
@@ -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"
]
}
+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;
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+34
View File
@@ -0,0 +1,34 @@
# periskop
[Periskop](https://periskop.io/) is a pull-based, language agnostic exception aggregator for microservice environments.
![periskop-logo](https://i.imgur.com/z8BLePO.png)
### `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.
![periskop-errors-card](./docs/periskop-plugin-screenshot.png)
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>
```
+59
View File
@@ -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)
```
+39
View File
@@ -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;
}>;
};
}
+20
View File
@@ -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

+58
View File
@@ -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"
}
+89
View File
@@ -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';
+33
View File
@@ -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,
),
},
}),
);
+23
View File
@@ -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';
+23
View File
@@ -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();
});
});
+51
View File
@@ -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 }),
}),
],
});
+21
View File
@@ -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',
});
+18
View File
@@ -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';
+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;
}