Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit f7e053f

Browse files
eduardoemerykjin
authored andcommitted
feat: add stackdriver stats exporter (#94)
* feat: add stackdriver stats exporter * refactor(fix): changes to address review comments
1 parent 5882009 commit f7e053f

File tree

7 files changed

+717
-36
lines changed

7 files changed

+717
-36
lines changed

packages/opencensus-exporter-stackdriver/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
export * from './stackdriver';
17+
export * from './stackdriver-cloudtrace';
18+
export * from './stackdriver-monitoring';
19+
export * from './types';

packages/opencensus-exporter-stackdriver/src/stackdriver.ts renamed to packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,11 @@ import {logger, Logger} from '@opencensus/core';
1919
import {auth, JWT} from 'google-auth-library';
2020
import {google} from 'googleapis';
2121

22+
import {StackdriverExporterOptions, TracesWithCredentials, TranslatedSpan, TranslatedTrace} from './types';
23+
2224
google.options({headers: {'x-opencensus-outgoing-request': 0x1}});
2325
const cloudTrace = google.cloudtrace('v1');
2426

25-
type TranslatedTrace = {
26-
projectId: string,
27-
traceId: string,
28-
spans: TranslatedSpan[]
29-
};
30-
31-
type TranslatedSpan = {
32-
name: string,
33-
kind: string,
34-
spanId: string,
35-
startTime: Date,
36-
endTime: Date,
37-
labels: Record<string, string>
38-
};
39-
40-
41-
/**
42-
* Options for stackdriver configuration
43-
*/
44-
export interface StackdriverExporterOptions extends ExporterConfig {
45-
/**
46-
* projectId project id defined to stackdriver
47-
*/
48-
projectId: string;
49-
}
50-
51-
interface TracesWithCredentials {
52-
projectId: string;
53-
resource: {traces: {}};
54-
auth: JWT;
55-
}
56-
5727
/** Format and sends span information to Stackdriver */
5828
export class StackdriverTraceExporter implements Exporter {
5929
projectId: string;
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/**
2+
* Copyright 2018, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {AggregationType, Bucket, DistributionData, logger, Logger, Measurement, MeasureType, StatsEventListener, View} from '@opencensus/core';
18+
import {auth, JWT} from 'google-auth-library';
19+
import {google} from 'googleapis';
20+
21+
import {Distribution, LabelDescriptor, MetricDescriptor, MetricKind, Point, StackdriverExporterOptions, TimeSeries, ValueType} from './types';
22+
23+
google.options({headers: {'x-opencensus-outgoing-request': 0x1}});
24+
const monitoring = google.monitoring('v3');
25+
26+
/** Format and sends Stats to Stackdriver */
27+
export class StackdriverStatsExporter implements StatsEventListener {
28+
private projectId: string;
29+
logger: Logger;
30+
31+
constructor(options: StackdriverExporterOptions) {
32+
this.projectId = options.projectId;
33+
this.logger = options.logger || logger.logger();
34+
}
35+
36+
/**
37+
* Is called whenever a view is registered.
38+
* @param view The registered view.
39+
*/
40+
onRegisterView(view: View) {
41+
return this.authorize().then((authClient) => {
42+
const request = {
43+
name: `projects/${this.projectId}`,
44+
resource: this.createMetricDescriptorData(view),
45+
auth: authClient
46+
};
47+
48+
return new Promise((resolve, reject) => {
49+
monitoring.projects.metricDescriptors.create(request, (err: Error) => {
50+
this.logger.debug('sent metric descriptor', request.resource);
51+
err ? reject(err) : resolve();
52+
});
53+
});
54+
});
55+
}
56+
57+
/**
58+
* Is called whenever a measure is recorded.
59+
* @param views The views associated with the measure
60+
* @param measurement The measurement recorded
61+
*/
62+
onRecord(views: View[], measurement: Measurement) {
63+
const timeSeries = views.map(view => {
64+
return this.createTimeSeriesData(view, measurement);
65+
});
66+
67+
return this.authorize().then(authClient => {
68+
const request = {
69+
name: `projects/${this.projectId}`,
70+
resource: {timeSeries},
71+
auth: authClient
72+
};
73+
74+
return new Promise((resolve, reject) => {
75+
monitoring.projects.timeSeries.create(request, (err: Error) => {
76+
this.logger.debug('sent time series', request.resource.timeSeries);
77+
err ? reject(err) : resolve();
78+
});
79+
});
80+
});
81+
}
82+
83+
/**
84+
* Gets the Google Application Credentials from the environment variables
85+
* and authenticates the client.
86+
*/
87+
private authorize(): Promise<JWT> {
88+
return auth.getApplicationDefault()
89+
.then((client) => {
90+
let authClient = client.credential as JWT;
91+
92+
if (authClient.createScopedRequired &&
93+
authClient.createScopedRequired()) {
94+
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
95+
authClient = authClient.createScoped(scopes);
96+
}
97+
98+
return authClient;
99+
})
100+
.catch((err) => {
101+
err.message = `authorize error: ${err.message}`;
102+
throw (err);
103+
});
104+
}
105+
106+
/**
107+
* Creates a Stackdriver TimeSeries from a given view and metric value.
108+
* @param view The view to get TimeSeries information from
109+
* @param measurement The measurement to get TimeSeries information from
110+
*/
111+
private createTimeSeriesData(view: View, measurement: Measurement):
112+
TimeSeries {
113+
const aggregationData = view.getSnapshot(measurement.tags);
114+
115+
const resourceLabels:
116+
{[key: string]: string} = {project_id: this.projectId};
117+
118+
// For non Sum Aggregations, the end time should be the same as the start
119+
// time.
120+
const endTime = (new Date(aggregationData.timestamp)).toISOString();
121+
const startTime = view.aggregation === AggregationType.SUM ?
122+
(new Date(view.startTime)).toISOString() :
123+
endTime;
124+
125+
let value;
126+
if (view.measure.type === MeasureType.INT64) {
127+
value = {int64Value: measurement.value.toString()};
128+
} else if (aggregationData.type === AggregationType.DISTRIBUTION) {
129+
value = {distributionValue: this.createDistribution(aggregationData)};
130+
} else {
131+
value = {doubleValue: measurement.value};
132+
}
133+
134+
return {
135+
metric: {
136+
type: `custom.googleapis.com/${view.name}`,
137+
labels: measurement.tags
138+
},
139+
resource: {type: 'global', labels: resourceLabels},
140+
metricKind: this.createMetricKind(view.aggregation),
141+
valueType: this.createValueType(view),
142+
points: [{interval: {startTime, endTime}, value}]
143+
};
144+
}
145+
146+
/**
147+
* Formats an OpenCensus Distribution to Stackdriver's format.
148+
* @param distribution The OpenCensus Distribution Data
149+
*/
150+
private createDistribution(distribution: DistributionData): Distribution {
151+
return {
152+
count: distribution.count.toString(),
153+
mean: distribution.mean,
154+
sumOfSquaredDeviation: distribution.sumSquaredDeviations,
155+
range: {min: distribution.min, max: distribution.max},
156+
bucketOptions: {
157+
explicitBuckets:
158+
{bounds: this.getBucketBoundaries(distribution.buckets)}
159+
},
160+
bucketCounts: this.getBucketCounts(distribution.buckets)
161+
};
162+
}
163+
164+
/**
165+
* Gets the bucket boundaries in an monotonicaly increasing order.
166+
* @param buckets The bucket list to get the boundaries from
167+
*/
168+
private getBucketBoundaries(buckets: Bucket[]): number[] {
169+
return [...buckets.map(bucket => bucket.lowBoundary), Infinity];
170+
}
171+
172+
/**
173+
* Gets the count value for each bucket
174+
* @param buckets The bucket list to get the count values from
175+
*/
176+
private getBucketCounts(buckets: Bucket[]): number[] {
177+
return buckets.map(bucket => bucket.count);
178+
}
179+
180+
/**
181+
* Creates a Stackdriver LabelDescriptor from given Tags.
182+
* @param tag The Tags to get TimeSeries information from.
183+
*/
184+
private createLabelDescriptor(tags: string[]): LabelDescriptor[] {
185+
return tags.map(labelKey => {
186+
return {key: labelKey, valueType: 'STRING', description: ''} as
187+
LabelDescriptor;
188+
});
189+
}
190+
191+
/**
192+
* Creates a Stackdriver MetricDescriptor from a given view.
193+
* @param view The view to get MetricDescriptor information from
194+
*/
195+
private createMetricDescriptorData(view: View): MetricDescriptor {
196+
return {
197+
type: `custom.googleapis.com/${view.name}`,
198+
description: view.description || view.measure.description,
199+
displayName: view.measure.name,
200+
metricKind: this.createMetricKind(view.aggregation),
201+
valueType: this.createValueType(view),
202+
unit: view.measure.unit,
203+
labels: this.createLabelDescriptor(view.getColumns())
204+
} as MetricDescriptor;
205+
}
206+
207+
/**
208+
* Creates a Stackdriver ValueType from a given view.
209+
* @param view The view to extract data from
210+
*/
211+
private createValueType(view: View): ValueType {
212+
if (view.measure.type === MeasureType.INT64) {
213+
return ValueType.INT64;
214+
} else if (view.aggregation === AggregationType.DISTRIBUTION) {
215+
return ValueType.DISTRIBUTION;
216+
}
217+
return ValueType.DOUBLE;
218+
}
219+
220+
/**
221+
* Creates a Stackdriver MetricKind from a given aggregation.
222+
* @param aggregationType The aggregation type to get MetricKind information
223+
* from.
224+
*/
225+
private createMetricKind(aggregationType: AggregationType): MetricKind {
226+
if (aggregationType === AggregationType.SUM) {
227+
return MetricKind.CUMULATIVE;
228+
}
229+
return MetricKind.GAUGE;
230+
}
231+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright 2018, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {ExporterConfig} from '@opencensus/core';
18+
import {JWT} from 'google-auth-library';
19+
20+
export type TranslatedTrace = {
21+
projectId: string,
22+
traceId: string,
23+
spans: TranslatedSpan[]
24+
};
25+
26+
export type TranslatedSpan = {
27+
name: string,
28+
kind: string,
29+
spanId: string,
30+
startTime: Date,
31+
endTime: Date,
32+
labels: Record<string, string>
33+
};
34+
35+
/**
36+
* Options for stackdriver configuration
37+
*/
38+
export interface StackdriverExporterOptions extends ExporterConfig {
39+
/**
40+
* projectId project id defined to stackdriver
41+
*/
42+
projectId: string;
43+
}
44+
45+
export interface TracesWithCredentials {
46+
projectId: string;
47+
resource: {traces: {}};
48+
auth: JWT;
49+
}
50+
51+
export enum MetricKind {
52+
UNSPECIFIED = 'METRIC_KIND_UNSPECIFIED',
53+
GAUGE = 'GAUGE',
54+
DELTA = 'DELTA',
55+
CUMULATIVE = 'CUMULATIVE'
56+
}
57+
58+
export enum ValueType {
59+
VALUE_TYPE_UNSPECIFIED,
60+
BOOL,
61+
INT64,
62+
DOUBLE,
63+
STRING,
64+
DISTRIBUTION,
65+
MONEY
66+
}
67+
68+
export interface LabelDescriptor {
69+
key: string;
70+
valueType: string;
71+
description: string;
72+
}
73+
74+
export interface MetricDescriptor {
75+
description: string;
76+
displayName: string;
77+
type: string;
78+
metricKind: MetricKind;
79+
valueType: ValueType;
80+
unit: string;
81+
labels: LabelDescriptor[];
82+
}
83+
84+
export interface Distribution {
85+
count: string;
86+
mean: number;
87+
sumOfSquaredDeviation: number;
88+
range: {min: number; max: number;};
89+
bucketOptions: {explicitBuckets: {bounds: number[];}};
90+
bucketCounts: number[];
91+
}
92+
93+
export interface Point {
94+
interval: {endTime: string, startTime: string};
95+
value: {
96+
boolValue?: boolean;
97+
int64Value?: string;
98+
doubleValue?: number;
99+
stringValue?: string;
100+
distributionValue?: Distribution;
101+
};
102+
}
103+
104+
export interface TimeSeries {
105+
metric: {type: string; labels: {[key: string]: string};};
106+
resource: {type: 'global', labels: {[key: string]: string}};
107+
metricKind: MetricKind;
108+
valueType: ValueType;
109+
points: Point[];
110+
}

0 commit comments

Comments
 (0)