From cb867d885445cf204a59e1758b447f0a857647d1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 13:36:12 +0200 Subject: [PATCH 01/10] Friendlier error when NR APM API Key isn't configured. --- .../NewRelicFetchComponent/NewRelicFetchComponent.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx index 9b3791e062..4322bba10b 100644 --- a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx +++ b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx @@ -107,10 +107,15 @@ const NewRelicFetchComponent: FC<{}> = () => { const configApi = useApi(configApiRef); const apiBaseUrl = configApi.getString('newrelic.api.baseUrl'); const apiKey = configApi.getString('newrelic.api.key'); + const apiKeyIsSet = apiKey && apiKey !== 'NEW_RELIC_REST_API_KEY'; const { value, loading, error } = useAsync(async (): Promise< NewRelicApplication[] > => { + if (!apiKeyIsSet) { + return []; + } + const response = await fetch(`${apiBaseUrl}/applications.json`, { headers: { 'X-Api-Key': apiKey, @@ -126,6 +131,12 @@ const NewRelicFetchComponent: FC<{}> = () => { return ; } else if (error) { return {error.message}; + } else if (!apiKeyIsSet) { + return ( + + No REST API Key configured. Check your app-config.yaml. + + ); } return ; From 8e0016f7ad3d80681e95c7afcbce57d0a1485046 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:49:41 +0200 Subject: [PATCH 02/10] Introduce a New Relic API class for future expansion. --- plugins/newrelic/src/api/index.ts | 96 +++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 plugins/newrelic/src/api/index.ts diff --git a/plugins/newrelic/src/api/index.ts b/plugins/newrelic/src/api/index.ts new file mode 100644 index 0000000000..692cb19e6f --- /dev/null +++ b/plugins/newrelic/src/api/index.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createApiRef, DiscoveryApi } from '@backstage/core'; + +export type NewRelicApplication = { + id: number; + application_summary: NewRelicApplicationSummary; + name: string; + language: string; + health_status: string; + reporting: boolean; + settings: NewRelicApplicationSettings; + links?: NewRelicApplicationLinks; +}; + +export type NewRelicApplicationSummary = { + apdex_score: number; + error_rate: number; + host_count: number; + instance_count: number; + response_time: number; + throughput: number; +}; + +export type NewRelicApplicationSettings = { + app_apdex_threshold: number; + end_user_apdex_threshold: number; + enable_real_user_monitoring: boolean; + use_server_side_config: boolean; +}; + +export type NewRelicApplicationLinks = { + application_instances: Array; + servers: Array; + application_hosts: Array; +}; + +export type NewRelicApplications = { + applications: NewRelicApplication[]; +}; + +export const newRelicApiRef = createApiRef({ + id: 'plugin.newrelic.service', + description: 'Used by the NewRelic plugin to make requests', +}); + +const DEFAULT_PROXY_PATH_BASE = '/newrelic'; + +type Options = { + discoveryApi: DiscoveryApi; + /** + * Path to use for requests via the proxy, defaults to /newrelic + */ + proxyPathBase?: string; +}; + +export class NewRelicApi { + private readonly discoveryApi: DiscoveryApi; + private readonly proxyPathBase: string; + + constructor(options: Options) { + this.discoveryApi = options.discoveryApi; + this.proxyPathBase = options.proxyPathBase ?? DEFAULT_PROXY_PATH_BASE; + } + + async getApplications(): Promise { + const url = await this.getApiUrl('apm', 'applications.json'); + const response = await fetch(url); + const responseJson = await response.json(); + + if (response.status !== 200) { + throw new Error(responseJson?.error?.title || response.statusText); + } + + return responseJson; + } + + private async getApiUrl(product: string, path: string) { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + return `${proxyUrl}${this.proxyPathBase}/${product}/api/${path}`; + } +} From 2865fcf9cb017a363eec9a8d715965c5a1d78c71 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:51:47 +0200 Subject: [PATCH 03/10] Make the plugin aware of the NR API. --- plugins/newrelic/src/plugin.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/newrelic/src/plugin.ts b/plugins/newrelic/src/plugin.ts index 21aac69e8f..f247158fbd 100644 --- a/plugins/newrelic/src/plugin.ts +++ b/plugins/newrelic/src/plugin.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; +import { + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, +} from '@backstage/core'; +import { NewRelicApi, newRelicApiRef } from './api'; import NewRelicComponent from './components/NewRelicComponent'; export const rootRouteRef = createRouteRef({ @@ -24,6 +30,13 @@ export const rootRouteRef = createRouteRef({ export const plugin = createPlugin({ id: 'newrelic', + apis: [ + createApiFactory({ + api: newRelicApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new NewRelicApi({ discoveryApi }), + }), + ], register({ router }) { router.addRoute(rootRouteRef, NewRelicComponent); }, From 25645e25dbc8516d0cf89b3181fdca47b513d857 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:52:59 +0200 Subject: [PATCH 04/10] Refactor config/type usage out of component, use new API instead. --- .../NewRelicFetchComponent.tsx | 76 ++----------------- 1 file changed, 7 insertions(+), 69 deletions(-) diff --git a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx index 4322bba10b..62f52e7875 100644 --- a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx +++ b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx @@ -15,52 +15,10 @@ */ import React, { FC } from 'react'; -import { - configApiRef, - Progress, - Table, - TableColumn, - useApi, -} from '@backstage/core'; +import { Progress, Table, TableColumn, useApi } from '@backstage/core'; import Alert from '@material-ui/lab/Alert'; import { useAsync } from 'react-use'; - -type NewRelicApplication = { - id: number; - application_summary: NewRelicApplicationSummary; - name: string; - language: string; - health_status: string; - reporting: boolean; - settings: NewRelicApplicationSettings; - links?: NewRelicApplicationLinks; -}; - -type NewRelicApplicationSummary = { - apdex_score: number; - error_rate: number; - host_count: number; - instance_count: number; - response_time: number; - throughput: number; -}; - -type NewRelicApplicationSettings = { - app_apdex_threshold: number; - end_user_apdex_threshold: number; - enable_real_user_monitoring: boolean; - use_server_side_config: boolean; -}; - -type NewRelicApplicationLinks = { - application_instances: Array; - servers: Array; - application_hosts: Array; -}; - -type NewRelicApplications = { - applications: NewRelicApplication[]; -}; +import { newRelicApiRef, NewRelicApplications } from '../../api'; export const NewRelicAPMTable: FC = ({ applications, @@ -73,7 +31,7 @@ export const NewRelicAPMTable: FC = ({ { title: 'Instance Count', field: 'instanceCount' }, { title: 'Apdex', field: 'apdexScore' }, ]; - const data = applications.map((app: NewRelicApplication) => { + const data = applications.map(app => { const { name, application_summary: applicationSummary } = app; const { response_time: responseTime, @@ -104,25 +62,11 @@ export const NewRelicAPMTable: FC = ({ }; const NewRelicFetchComponent: FC<{}> = () => { - const configApi = useApi(configApiRef); - const apiBaseUrl = configApi.getString('newrelic.api.baseUrl'); - const apiKey = configApi.getString('newrelic.api.key'); - const apiKeyIsSet = apiKey && apiKey !== 'NEW_RELIC_REST_API_KEY'; + const api = useApi(newRelicApiRef); - const { value, loading, error } = useAsync(async (): Promise< - NewRelicApplication[] - > => { - if (!apiKeyIsSet) { - return []; - } - - const response = await fetch(`${apiBaseUrl}/applications.json`, { - headers: { - 'X-Api-Key': apiKey, - }, - }); - const data: NewRelicApplications = await response.json(); - return data.applications.filter((application: NewRelicApplication) => { + const { value, loading, error } = useAsync(async () => { + const data = await api.getApplications(); + return data.applications.filter(application => { return application.hasOwnProperty('application_summary'); }); }, []); @@ -131,12 +75,6 @@ const NewRelicFetchComponent: FC<{}> = () => { return ; } else if (error) { return {error.message}; - } else if (!apiKeyIsSet) { - return ( - - No REST API Key configured. Check your app-config.yaml. - - ); } return ; From c64a0a47ea0898eebc6f99b1eadd5db0ae806c84 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:54:15 +0200 Subject: [PATCH 05/10] Use new expected proxy configs to hit New Relic API. --- app-config.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 228eafc588..7dee71534f 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -34,6 +34,12 @@ proxy: $env: TRAVISCI_AUTH_TOKEN travis-api-version: 3 + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: + $env: NEW_RELIC_REST_API_KEY + organization: name: Spotify @@ -51,11 +57,6 @@ rollbar: accountToken: $env: ROLLBAR_ACCOUNT_TOKEN -newrelic: - api: - baseUrl: 'https://api.newrelic.com/v2' - key: NEW_RELIC_REST_API_KEY - lighthouse: baseUrl: http://localhost:3003 From 78fea5041efef33abdeeaff53142e914eb5ef93c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:54:48 +0200 Subject: [PATCH 06/10] Update README with new instructions on how to configure the plugin. --- plugins/newrelic/README.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md index 19bd24950c..e787d9c91f 100644 --- a/plugins/newrelic/README.md +++ b/plugins/newrelic/README.md @@ -7,16 +7,38 @@ Website: [https://newrelic.com](https://newrelic.com) ## Getting Started -Add New Relic REST API Key to `app-config.yaml` +This plugin uses the Backstage proxy to securely communicate with New Relic's +APIs. Add the following to your `app-config.yaml` to enable this configuration: ```yaml -newrelic: - api: - baseUrl: 'https://api.newrelic.com/v2' - key: +proxy: + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: + $env: NEW_RELIC_REST_API_KEY ``` -New Relic Plugin Path: [/newrelic](http://localhost:3000/newrelic) +In your production deployment of Backstage, you would also need to ensure that +you've set the `NEW_RELIC_REST_API_KEY` environment variable before starting +the backend. + +While working locally, you may wish to hard-code your API key in your +`app-config.local.yaml` like this: + +```yaml +# app-config.local.yaml +proxy: + 'newrelic/apm/api': + headers: + X-Api-Key: NRRA-YourActualApiKey +``` + +Read more about how to find or generate this key in +[New Relic's Documentation](https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys#rest-api-key). + +See if it's working by visiting the New Relic Plugin Path: +[/newrelic](http://localhost:3000/newrelic) ## Features From 4512b9967889ec6299742cae5c66494c3b7b5806 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 18:10:45 +0200 Subject: [PATCH 07/10] Marked minor, although it is major-ish... --- .changeset/old-falcons-jump.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .changeset/old-falcons-jump.md diff --git a/.changeset/old-falcons-jump.md b/.changeset/old-falcons-jump.md new file mode 100644 index 0000000000..107d75ff88 --- /dev/null +++ b/.changeset/old-falcons-jump.md @@ -0,0 +1,25 @@ +--- +'@backstage/plugin-newrelic': minor +--- + +The New Relic plugin now uses the Backstage proxy to communicate with New Relic's API. + +Please update your `app-config.yaml` as follows: + +```yaml +# Old Config +newrelic: + api: + baseUrl: 'https://api.newrelic.com/v2' + key: NEW_RELIC_REST_API_KEY +``` + +```yaml +# New Config +proxy: + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: + $env: NEW_RELIC_REST_API_KEY +``` From a6c0d9803ecde293b82a0464375a7dad6669ee52 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2020 11:08:16 +0200 Subject: [PATCH 08/10] Type with interface instead of class. --- plugins/newrelic/src/api/index.ts | 6 +++++- plugins/newrelic/src/plugin.ts | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/newrelic/src/api/index.ts b/plugins/newrelic/src/api/index.ts index 692cb19e6f..eb94acdbd1 100644 --- a/plugins/newrelic/src/api/index.ts +++ b/plugins/newrelic/src/api/index.ts @@ -68,7 +68,11 @@ type Options = { proxyPathBase?: string; }; -export class NewRelicApi { +export interface NewRelicApi { + getApplications(): Promise; +} + +export class NewRelicClient implements NewRelicApi { private readonly discoveryApi: DiscoveryApi; private readonly proxyPathBase: string; diff --git a/plugins/newrelic/src/plugin.ts b/plugins/newrelic/src/plugin.ts index f247158fbd..5f5ba88617 100644 --- a/plugins/newrelic/src/plugin.ts +++ b/plugins/newrelic/src/plugin.ts @@ -20,7 +20,7 @@ import { createRouteRef, discoveryApiRef, } from '@backstage/core'; -import { NewRelicApi, newRelicApiRef } from './api'; +import { NewRelicClient, newRelicApiRef } from './api'; import NewRelicComponent from './components/NewRelicComponent'; export const rootRouteRef = createRouteRef({ @@ -34,7 +34,7 @@ export const plugin = createPlugin({ createApiFactory({ api: newRelicApiRef, deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new NewRelicApi({ discoveryApi }), + factory: ({ discoveryApi }) => new NewRelicClient({ discoveryApi }), }), ], register({ router }) { From 1af3dbdb5ebaa8c283d4b2cb6eecd04aeb1949ba Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2020 12:37:50 +0200 Subject: [PATCH 09/10] Provide more context. Handle more edge cases. --- plugins/newrelic/src/api/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/newrelic/src/api/index.ts b/plugins/newrelic/src/api/index.ts index eb94acdbd1..d76a87875c 100644 --- a/plugins/newrelic/src/api/index.ts +++ b/plugins/newrelic/src/api/index.ts @@ -84,10 +84,20 @@ export class NewRelicClient implements NewRelicApi { async getApplications(): Promise { const url = await this.getApiUrl('apm', 'applications.json'); const response = await fetch(url); - const responseJson = await response.json(); + let responseJson; + + try { + responseJson = await response.json(); + } catch (e) { + responseJson = { applications: [] }; + } if (response.status !== 200) { - throw new Error(responseJson?.error?.title || response.statusText); + throw new Error( + `Error communicating with New Relic: ${ + responseJson?.error?.title || response.statusText + }`, + ); } return responseJson; From ee88f6d21023f9949248daf565057f22bb3e21ee Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2020 22:44:15 +0200 Subject: [PATCH 10/10] Fix example in README. --- plugins/newrelic/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md index e787d9c91f..e14acedef2 100644 --- a/plugins/newrelic/README.md +++ b/plugins/newrelic/README.md @@ -29,7 +29,7 @@ While working locally, you may wish to hard-code your API key in your ```yaml # app-config.local.yaml proxy: - 'newrelic/apm/api': + '/newrelic/apm/api': headers: X-Api-Key: NRRA-YourActualApiKey ```