Skip to content

Commit f0a16bd

Browse files
authored
Merge pull request #9 from cloudgraphdev/feature/CG-1259
feat(service): Added GCP Billing service
2 parents 8985c32 + 3a457f9 commit f0a16bd

13 files changed

Lines changed: 613 additions & 26 deletions

File tree

src/enums/schemasMap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default {
1212
[services.bigQueryReservationCapacityCommitment]: 'gcpBigQueryReservationCapacityCommitment',
1313
[services.bigQueryDataTransfer]: 'gcpBigQueryDataTransfer',
1414
[services.bigQueryDataTransferRun]: 'gcpBigQueryDataTransferRun',
15+
[services.billing]: 'gcpBilling',
1516
[services.kmsKeyRing]: 'gcpKmsKeyRing',
1617
[services.kmsCryptoKeys]: 'gcpKmsCryptoKey',
1718
[services.cdnBackendBucket]: 'gcpCdnBackendBucket',

src/enums/serviceMap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import GcpApiGatewayApiConfig from '../services/apiGatewayApiConfig'
4949
import GcpFirestoreDatabase from '../services/firestore'
5050
import GcpLabel from '../services/label'
5151
import GcpTag from '../services/tag'
52+
import GcpBilling from '../services/billing'
5253

5354
/**
5455
* serviceMap is an object that contains all currently supported services
@@ -63,6 +64,7 @@ export default {
6364
[services.bigQueryReservationCapacityCommitment]: GcpBigQueryReservationCapacityCommitment,
6465
[services.bigQueryDataTransfer]: GcpBigQueryDataTransfer,
6566
[services.bigQueryDataTransferRun]: GcpBigQueryDataTransferRun,
67+
[services.billing]: GcpBilling,
6668
[services.kmsKeyRing]: GcpKmsKeyRing,
6769
[services.kmsCryptoKeys]: GcpKmsCryptoKey,
6870
[services.cloudRouter]: GcpCloudRouter,

src/enums/services.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default {
1616
// recommender: 'recommender',
1717
// services: 'services',
1818
// anthos: 'anthos',
19-
// billing: 'billing',
19+
billing: 'billing',
2020
// artifacts: 'artifacts',
2121
// builds: 'builds',
2222
// scheduler: 'scheduler',

src/properties/logger.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,48 @@
11
export default {
22
platform: 'Platform: GCP',
3-
fetchingResourceData: (resource: string): string => `Fetching ${resource} data...`,
3+
fetchingResourceData: (resource: string): string =>
4+
`Fetching ${resource} data...`,
45
doneFetchingResourceData: (resource: string, num: number): string =>
56
`✅ Done fetching ${resource} data in ${num}s ✅`,
6-
foundResources: (resource: string, num: number): string => `Found ${num} ${resource}`,
7+
foundResources: (resource: string, num: number): string =>
8+
`Found ${num} ${resource}`,
79
lookingForResourcesAddToRegion: (region: string, resource: string): string =>
810
`Looking for ${resource} to add to Region ${region}...`,
9-
addingServiceToRegion: (region: string, resource: string, num: number): string =>
10-
`Adding ${num} ${resource} to Region ${region}`,
11+
addingServiceToRegion: (
12+
region: string,
13+
resource: string,
14+
num: number
15+
): string => `Adding ${num} ${resource} to Region ${region}`,
1116
lookingForResourcesAddToProject: (resource: string): string =>
1217
`Looking for ${resource} to add to the Project...`,
1318
addingServiceToProject: (resource: string, num: number): string =>
1419
`Adding ${num} ${resource} to the Project`,
1520
/**
16-
* VPC
17-
*/
21+
* VPC
22+
*/
1823
foundVpcs: (num: number): string => `Found ${num} VPCs`,
1924
/**
20-
* IAM
21-
*/
25+
* IAM
26+
*/
2227
foundPolicies: (num: number): string => `Found ${num} IAM Policies`,
2328
/**
24-
* Logging
25-
*/
29+
* Logging
30+
*/
2631
foundLogBuckets: (num: number): string => `Found ${num} Log Buckets`,
2732
foundLogSinks: (num: number): string => `Found ${num} Log Sinks`,
28-
foundLogViews: (num: number): string => `Found ${num} Log Views`
33+
foundLogViews: (num: number): string => `Found ${num} Log Views`,
34+
/**
35+
* Billing
36+
*/
37+
fetchingAggregateFinOpsData:
38+
'Fetching aggregate FinOps data for this GCP account via the Azure SDK...',
39+
unableToFindFinOpsAggregateData:
40+
'❌ Unable to get billing data for this GCP account, billing data was missing. ❌',
41+
queryingAggregateFinOpsDataForRegion: (
42+
region: string,
43+
type: string
44+
): string =>
45+
`Querying aggregate FinOps data for the ${region} region using the ${type} method...`,
46+
doneFetchingAggregateFinOpsData: (num: number): string =>
47+
`🕒 Done fetching aggregate FinOps data in ${num} 🕘`,
2948
}

src/services/billing/data.ts

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import CloudGraph from '@cloudgraph/sdk'
2+
import { BigQuery, RowMetadata } from '@google-cloud/bigquery'
3+
import * as _ from 'lodash'
4+
import { GLOBAL_REGION } from '../../config/constants'
5+
import gcpLoggerText from '../../properties/logger'
6+
import { GcpServiceInput } from '../../types'
7+
import { generateGcpErrorLog, initTestEndpoint } from '../../utils'
8+
import {
9+
createDiffSecs,
10+
getCurrentDayOfMonth,
11+
getDaysAgo,
12+
getFirstDayOfMonth,
13+
} from '../../utils/dateutils'
14+
import {
15+
formatAmmountAndUnit,
16+
getCurrency,
17+
getTotalCost,
18+
RawGcpTotalCost,
19+
totalCostQuery,
20+
totalCostGroupByServiceQuery,
21+
} from './utils'
22+
23+
const lt = { ...gcpLoggerText }
24+
const { logger } = CloudGraph
25+
const serviceName = 'Billing'
26+
const apiEndpoint = initTestEndpoint(serviceName)
27+
28+
export interface costInterface {
29+
cost?: number
30+
currency?: string
31+
formattedCost?: string
32+
}
33+
34+
export interface RawGcpBilling {
35+
totalCostLast30Days: costInterface
36+
totalCostMonthToDate: costInterface
37+
monthToDateDailyAverage: { [key: string]: costInterface }
38+
last30DaysDailyAverage: { [key: string]: costInterface }
39+
monthToDate: { [key: string]: costInterface }
40+
last30Days: { [key: string]: costInterface }
41+
individualData: { [key: string]: costInterface }
42+
}
43+
44+
export const listBillingData = async (
45+
client: BigQuery,
46+
projectId: string,
47+
billings: RawGcpTotalCost[],
48+
billingAccountId: string,
49+
dataset: string,
50+
groupBy?: boolean,
51+
startDate?: string,
52+
endDate?: string
53+
): Promise<void> =>
54+
new Promise<void>(async resolve => {
55+
try {
56+
const billingTable = `${projectId}.${dataset}.gcp_billing_export_resource_v1_${billingAccountId}`
57+
58+
let sqlQuery = ''
59+
if (groupBy) {
60+
sqlQuery = totalCostGroupByServiceQuery(billingTable, startDate, endDate)
61+
} else {
62+
sqlQuery = totalCostQuery(billingTable, startDate, endDate)
63+
}
64+
65+
const options = {
66+
query: sqlQuery,
67+
}
68+
69+
// Run the query
70+
const [rows] = await client.query(options)
71+
72+
if (!_.isEmpty(rows)) {
73+
billings.push(...rows)
74+
}
75+
} catch (error) {
76+
generateGcpErrorLog(serviceName, 'bigQuery:getTotalCost', error)
77+
}
78+
79+
resolve()
80+
})
81+
82+
export default async ({
83+
config,
84+
}: GcpServiceInput): Promise<{
85+
[region: string]: RawGcpBilling[]
86+
}> => {
87+
const startDate = new Date()
88+
const region = GLOBAL_REGION
89+
const results: RawGcpBilling = {
90+
totalCostLast30Days: {},
91+
totalCostMonthToDate: {},
92+
monthToDateDailyAverage: {},
93+
last30DaysDailyAverage: {},
94+
monthToDate: {},
95+
last30Days: {},
96+
individualData: {},
97+
}
98+
const resultPromises = []
99+
const {
100+
projectId,
101+
billing: { billingAccountId, bigQueryDataset },
102+
} = config
103+
104+
try {
105+
const client = new BigQuery({ ...config, apiEndpoint })
106+
107+
const listAggregateFinOpsData = async ({
108+
resolve,
109+
type,
110+
groupBy = true,
111+
individualData = false,
112+
timePeriod: TimePeriod,
113+
}: {
114+
resolve: () => void
115+
type: string
116+
groupBy?: boolean
117+
individualData?: boolean
118+
timePeriod: { Start: string; End: string }
119+
}): Promise<void> => {
120+
logger.debug(lt.queryingAggregateFinOpsDataForRegion(region, type))
121+
const billingData: RowMetadata[] = []
122+
123+
await listBillingData(
124+
client,
125+
projectId,
126+
billingData,
127+
billingAccountId,
128+
bigQueryDataset,
129+
groupBy,
130+
TimePeriod.Start,
131+
TimePeriod.End
132+
)
133+
134+
if (_.isEmpty(billingData)) {
135+
logger.debug(lt.unableToFindFinOpsAggregateData)
136+
return resolve()
137+
}
138+
139+
if (groupBy || individualData) {
140+
const services = _.groupBy(billingData, u => u.service)
141+
Object.keys(services).map(name => {
142+
const serviceUsages = services[name]
143+
const currency = getCurrency(serviceUsages)
144+
const cost = getTotalCost(serviceUsages)
145+
const costData = {
146+
cost,
147+
currency,
148+
formattedCost: formatAmmountAndUnit({
149+
Amount: cost,
150+
Unit: currency,
151+
}),
152+
}
153+
if (individualData) {
154+
results.individualData[name] = costData
155+
} else {
156+
results[type][name] = costData
157+
}
158+
})
159+
} else {
160+
const currency = getCurrency(billingData)
161+
const cost = getTotalCost(billingData)
162+
results[type] = {
163+
cost,
164+
currency,
165+
formattedCost: formatAmmountAndUnit({
166+
Amount: cost,
167+
Unit: currency,
168+
}),
169+
}
170+
}
171+
172+
resolve()
173+
}
174+
175+
/**
176+
* Now we make 4 queries to the api in order to get aggregate pricing data sliced in various ways
177+
*/
178+
179+
const today = new Date().toLocaleDateString('en-ca')
180+
const startOfMonth = getFirstDayOfMonth()
181+
182+
const commonArgs = {
183+
timePeriod: {
184+
Start: getDaysAgo(60), // TODO: change to 30 !!!
185+
End: today,
186+
},
187+
}
188+
189+
/**
190+
* Breakdown by service types and spend for last 30 days
191+
*/
192+
const last30DaysData = new Promise<void>(resolve =>
193+
listAggregateFinOpsData({
194+
...commonArgs,
195+
resolve,
196+
type: 'last30Days',
197+
})
198+
)
199+
resultPromises.push(last30DaysData)
200+
201+
/**
202+
* Breakdown by service types and spend since the beginning of the month
203+
*/
204+
if (!(today === startOfMonth)) {
205+
const monthToDateData = new Promise<void>(resolve =>
206+
listAggregateFinOpsData({
207+
resolve,
208+
type: 'monthToDate',
209+
timePeriod: {
210+
Start: startOfMonth,
211+
End: today,
212+
},
213+
})
214+
)
215+
resultPromises.push(monthToDateData)
216+
}
217+
218+
/**
219+
* The single total cost of everything in the last 30 days
220+
*/
221+
const totalCostLast30Days = new Promise<void>(resolve =>
222+
listAggregateFinOpsData({
223+
...commonArgs,
224+
resolve,
225+
type: 'totalCostLast30Days',
226+
groupBy: false,
227+
})
228+
)
229+
resultPromises.push(totalCostLast30Days)
230+
231+
/**
232+
* The single total cost of everything in the current month
233+
*/
234+
if (!(today === startOfMonth)) {
235+
const totalCostMonthToDate = new Promise<void>(resolve =>
236+
listAggregateFinOpsData({
237+
resolve,
238+
type: 'totalCostMonthToDate',
239+
groupBy: false,
240+
timePeriod: {
241+
Start: startOfMonth,
242+
End: today,
243+
},
244+
})
245+
)
246+
resultPromises.push(totalCostMonthToDate)
247+
}
248+
249+
const individualDataPromise = new Promise<void>(resolve =>
250+
listAggregateFinOpsData({
251+
resolve,
252+
type: 'individualData',
253+
individualData: true,
254+
timePeriod: {
255+
Start: getDaysAgo(1), // i.e. get the daily cost
256+
End: today,
257+
},
258+
})
259+
)
260+
resultPromises.push(individualDataPromise)
261+
262+
await Promise.all(resultPromises)
263+
264+
/**
265+
* Create Daily Averages
266+
*/
267+
268+
const createDailyAverage = ({
269+
days,
270+
resultMonthlyData,
271+
resultAverageData,
272+
}): void[] =>
273+
Object.keys(resultMonthlyData).map(service => {
274+
const { cost: aggregateCost, currency } = resultMonthlyData[service]
275+
const cost = parseFloat((aggregateCost / days).toFixed(10))
276+
results[resultAverageData][service] = {
277+
cost,
278+
currency,
279+
formattedCost: formatAmmountAndUnit({ Amount: cost, Unit: currency }),
280+
}
281+
})
282+
283+
if (!_.isEmpty(results.monthToDate)) {
284+
createDailyAverage({
285+
days: parseInt(getCurrentDayOfMonth(), 10),
286+
resultMonthlyData: results.monthToDate,
287+
resultAverageData: 'monthToDateDailyAverage',
288+
})
289+
}
290+
if (!_.isEmpty(results.last30Days)) {
291+
createDailyAverage({
292+
days: 30,
293+
resultMonthlyData: results.last30Days,
294+
resultAverageData: 'last30DaysDailyAverage',
295+
})
296+
}
297+
298+
logger.debug(lt.doneFetchingAggregateFinOpsData(createDiffSecs(startDate)))
299+
return { [region]: [results] }
300+
} catch (e) {
301+
logger.error(e)
302+
}
303+
304+
logger.debug(lt.doneFetchingAggregateFinOpsData(createDiffSecs(startDate)))
305+
return { [region]: [results] }
306+
}

0 commit comments

Comments
 (0)