move cost insights example client to plugin

This commit is contained in:
Ryan Vazquez
2020-10-26 15:00:46 -04:00
parent b173dacb46
commit 0eac8f037d
9 changed files with 35 additions and 37 deletions
-3
View File
@@ -37,7 +37,6 @@
"@roadiehq/backstage-plugin-github-insights": "^0.2.7",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.5.2",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.3",
"dayjs": "^1.9.1",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
@@ -46,7 +45,6 @@
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"regression": "^2.0.1",
"zen-observable": "^0.8.15"
},
"devDependencies": {
@@ -58,7 +56,6 @@
"@types/jquery": "^3.3.34",
"@types/node": "^12.0.0",
"@types/react-dom": "^16.9.8",
"@types/regression": "^2.0.0",
"@types/zen-observable": "^0.8.0",
"cross-env": "^7.0.0",
"cypress": "^4.2.0",
+1 -2
View File
@@ -35,8 +35,7 @@ import {
githubPullRequestsApiRef,
} from '@roadiehq/backstage-plugin-github-pull-requests';
import { costInsightsApiRef } from '@backstage/plugin-cost-insights';
import { ExampleCostInsightsClient } from './plugins/cost-insights';
import { costInsightsApiRef, ExampleCostInsightsClient } from '@backstage/plugin-cost-insights';
export const apis = [
createApiFactory({
@@ -1,321 +0,0 @@
/*
* 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.
*/
/* eslint-disable no-restricted-imports */
import dayjs from 'dayjs';
import regression, { DataPoint } from 'regression';
import {
Alert,
ChangeStatistic,
Cost,
CostInsightsApi,
DateAggregation,
DEFAULT_DATE_FORMAT,
Duration,
exclusiveEndDateOf,
Group,
inclusiveStartDateOf,
Maybe,
MetricData,
ProductCost,
Project,
ProjectGrowthAlert,
ProjectGrowthData,
Trendline,
UnlabeledDataflowAlert,
UnlabeledDataflowData,
} from '@backstage/plugin-cost-insights';
type IntervalFields = {
duration: Duration;
endDate: string;
};
function parseIntervals(intervals: string): IntervalFields {
const match = intervals.match(
/\/(?<duration>P\d+[DM])\/(?<date>\d{4}-\d{2}-\d{2})/,
);
if (Object.keys(match?.groups || {}).length !== 2) {
throw new Error(`Invalid intervals: ${intervals}`);
}
const { duration, date } = match!.groups!;
return {
duration: duration as Duration,
endDate: date,
};
}
function aggregationFor(
intervals: string,
baseline: number,
): DateAggregation[] {
const { duration, endDate } = parseIntervals(intervals);
const days = dayjs(exclusiveEndDateOf(duration, endDate)).diff(
inclusiveStartDateOf(duration, endDate),
'day',
);
return [...Array(days).keys()].reduce(
(values: DateAggregation[], i: number): DateAggregation[] => {
const last = values.length ? values[values.length - 1].amount : baseline;
values.push({
date: dayjs(inclusiveStartDateOf(duration, endDate))
.add(i, 'day')
.format(DEFAULT_DATE_FORMAT),
amount: Math.max(0, last + (baseline / 20) * (Math.random() * 2 - 1)),
});
return values;
},
[],
);
}
function trendlineOf(aggregation: DateAggregation[]): Trendline {
const data: ReadonlyArray<DataPoint> = aggregation.map(a => [
Date.parse(a.date) / 1000,
a.amount,
]);
const result = regression.linear(data, { precision: 5 });
return {
slope: result.equation[0],
intercept: result.equation[1],
};
}
function changeOf(aggregation: DateAggregation[]): ChangeStatistic {
const half = Math.ceil(aggregation.length / 2);
const before = aggregation
.slice(0, half)
.reduce((sum, a) => sum + a.amount, 0);
const after = aggregation
.slice(half, aggregation.length)
.reduce((sum, a) => sum + a.amount, 0);
return {
ratio: (after - before) / before,
amount: after - before,
};
}
export class ExampleCostInsightsClient implements CostInsightsApi {
private request(_: any, res: any): Promise<any> {
return new Promise(resolve => setTimeout(resolve, 0, res));
}
getLastCompleteBillingDate(): Promise<string> {
return Promise.resolve(
dayjs().subtract(1, 'day').format(DEFAULT_DATE_FORMAT),
);
}
async getUserGroups(userId: string): Promise<Group[]> {
const groups: Group[] = await this.request({ userId }, [
{ id: 'pied-piper' },
]);
return groups;
}
async getGroupProjects(group: string): Promise<Project[]> {
const projects: Project[] = await this.request({ group }, [
{ id: 'project-a' },
{ id: 'project-b' },
{ id: 'project-c' },
]);
return projects;
}
async getDailyMetricData(
metric: string,
intervals: string,
): Promise<MetricData> {
const aggregation = aggregationFor(intervals, 100_000).map(entry => ({
...entry,
amount: Math.round(entry.amount),
}));
const cost: MetricData = await this.request(
{ metric, intervals },
{
format: 'number',
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
},
);
return cost;
}
async getGroupDailyCost(group: string, intervals: string): Promise<Cost> {
const aggregation = aggregationFor(intervals, 8_000);
const groupDailyCost: Cost = await this.request(
{ group, intervals },
{
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
},
);
return groupDailyCost;
}
async getProjectDailyCost(project: string, intervals: string): Promise<Cost> {
const aggregation = aggregationFor(intervals, 1_500);
const projectDailyCost: Cost = await this.request(
{ project, intervals },
{
id: 'project-a',
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
},
);
return projectDailyCost;
}
async getProductInsights(
product: string,
group: string,
duration: Duration,
project: Maybe<string>,
): Promise<ProductCost> {
const projectProductInsights = await this.request(
{ product, group, duration, project },
{
aggregation: [80_000, 110_000],
change: {
ratio: 0.375,
amount: 30_000,
},
entities: [
{
id: null, // entities with null ids will be appear as "Unlabeled" in product panels
aggregation: [45_000, 50_000],
},
{
id: 'entity-a',
aggregation: [15_000, 20_000],
},
{
id: 'entity-b',
aggregation: [20_000, 30_000],
},
{
id: 'entity-e',
aggregation: [0, 10_000],
},
],
},
);
const productInsights: ProductCost = await this.request(
{ product, group, duration, project },
{
aggregation: [200_000, 250_000],
change: {
ratio: 0.2,
amount: 50_000,
},
entities: [
{
id: null, // entities with null ids will be appear as "Unlabeled" in product panels
aggregation: [15_000, 30_000],
},
{
id: 'entity-a',
aggregation: [15_000, 20_000],
},
{
id: 'entity-b',
aggregation: [20_000, 30_000],
},
{
id: 'entity-c',
aggregation: [18_000, 25_000],
},
{
id: 'entity-d',
aggregation: [36_000, 42_000],
},
{
id: 'entity-e',
aggregation: [0, 10_000],
},
{
id: 'entity-f',
aggregation: [17_000, 19_000],
},
{
id: 'entity-g',
aggregation: [49_000, 30_000],
},
{
id: 'entity-h',
aggregation: [0, 34_000],
},
],
},
);
return project ? projectProductInsights : productInsights;
}
async getAlerts(group: string): Promise<Alert[]> {
const projectGrowthData: ProjectGrowthData = {
project: 'example-project',
periodStart: '2020-Q2',
periodEnd: '2020-Q3',
aggregation: [60_000, 120_000],
change: {
ratio: 1,
amount: 60000,
},
products: [
{ id: 'Compute Engine', aggregation: [58_000, 118_000] },
{ id: 'Cloud Dataflow', aggregation: [1200, 1500] },
{ id: 'Cloud Storage', aggregation: [800, 500] },
],
};
const unlabeledDataflowData: UnlabeledDataflowData = {
periodStart: '2020-09-01',
periodEnd: '2020-09-30',
labeledCost: 6_200,
unlabeledCost: 7_000,
projects: [
{
id: 'example-project-1',
unlabeledCost: 5_000,
labeledCost: 3_000,
},
{
id: 'example-project-2',
unlabeledCost: 2_000,
labeledCost: 3_200,
},
],
};
const alerts: Alert[] = await this.request({ group }, [
new ProjectGrowthAlert(projectGrowthData),
new UnlabeledDataflowAlert(unlabeledDataflowData),
]);
return alerts;
}
}
@@ -1,17 +0,0 @@
/*
* 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.
*/
export { ExampleCostInsightsClient } from './ExampleCostInsightsClient';