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

Commit d38efe7

Browse files
djonathascardosokjin
authored andcommitted
feat: add stats support to zpages exporter (#112)
1 parent 13cab60 commit d38efe7

22 files changed

+1396
-56
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 * as express from 'express';
18+
19+
import {StatsParams} from '../../zpages';
20+
21+
import {RpczPageHandler} from './../page-handlers/rpcz.page-handler';
22+
23+
/**
24+
* An Express middleware that renders the RPCz view.
25+
* @param statsParams An object with all registered views, registered measures
26+
* and recorded data from stats
27+
*/
28+
export function createRpczHandler(statsParams: StatsParams): express.Handler {
29+
const html = new RpczPageHandler(statsParams);
30+
return (req: express.Request, res: express.Response) => {
31+
res.send(html.emitHtml(req.query.json === '1'));
32+
};
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 * as express from 'express';
18+
19+
import {StatsParams} from '../../zpages';
20+
21+
import {StatszPageHandler} from './../page-handlers/statsz.page-handler';
22+
23+
/**
24+
* An Express middleware that renders the Statsz view.
25+
* @param statsParams An object with all registered views, registered measures
26+
* and recorded data from stats
27+
*/
28+
export function createStatszHandler(statsParams: StatsParams): express.Handler {
29+
const html = new StatszPageHandler(statsParams);
30+
return (req: express.Request, res: express.Response) => {
31+
res.send(html.emitHtml(req.query, req.query.json === '1'));
32+
};
33+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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, CountData, DistributionData} from '@opencensus/core';
18+
19+
import {StatsParams} from '../../zpages';
20+
21+
const ejs = require('ejs');
22+
23+
import * as pkgDir from 'pkg-dir';
24+
const templatesDir = `${pkgDir.sync(__dirname)}/templates`;
25+
26+
const FIXED_SIZE = 3;
27+
28+
export interface ZMeasureOrders {
29+
min: number;
30+
hr: number;
31+
tot: number;
32+
}
33+
34+
export interface ZMeasure {
35+
method: string;
36+
count: ZMeasureOrders;
37+
avgLatency: ZMeasureOrders;
38+
rate: ZMeasureOrders;
39+
input: ZMeasureOrders;
40+
output: ZMeasureOrders;
41+
errors: ZMeasureOrders;
42+
}
43+
44+
/**
45+
* Information used to render the Rpcz UI.
46+
*/
47+
export interface RpczData {
48+
measuresSent: {[key: string]: ZMeasure};
49+
measuresReceived: {[key: string]: ZMeasure};
50+
}
51+
52+
enum DefaultMeasures {
53+
CLIENT_SENT_MESSAGES_PER_RPC = 'grpc.io/client/sent_messages_per_rpc',
54+
CLIENT_SENT_BYTES_PER_RPC = 'grpc.io/client/sent_bytes_per_rpc',
55+
CLIENT_RECEIVED_MESSAGES_PER_RPC = 'grpc.io/client/received_messages_per_rpc',
56+
CLIENT_RECEIVED_BYTES_PER_RPC = 'grpc.io/client/received_bytes_per_rpc',
57+
CLIENT_ROUDTRIP_LATENCY = 'grpc.io/client/roundtrip_latency',
58+
CLIENT_SERVER_LATENCY = 'grpc.io/client/server_latency',
59+
CLIENT_STARTED_RPCS = 'grpc.io/client/started_rpcs',
60+
SERVER_RECEIVED_MESSAGES_PER_RPC = 'grpc.io/server/received_messages_per_rpc',
61+
SERVER_RECEIVED_BYTES_PER_RPC = 'grpc.io/server/received_bytes_per_rpc',
62+
SERVER_SENT_MESSAGES_PER_RPC = 'grpc.io/server/sent_messages_per_rpc',
63+
SERVER_SENT_BYTES_PER_RPC = 'grpc.io/server/sent_bytes_per_rpc',
64+
SERVER_SERVER_LATENCY = 'grpc.io/server/server_latency',
65+
SERVER_STARTED_RPCS = 'grpc.io/server/started_rpcs'
66+
}
67+
68+
enum DefaultViews {
69+
CLIENT_SENT_BYTES_PER_RPC = 'grpc.io/client/sent_bytes_per_rpc',
70+
CLIENT_RECEIVED_BYTES_PER_RPC = 'grpc.io/client/received_bytes_per_rpc',
71+
CLIENT_ROUDTRIP_LATENCY = 'grpc.io/client/roundtrip_latency',
72+
CLIENT_COMPLETED_RPCS = 'grpc.io/client/completed_rpcs',
73+
CLIENT_STARTED_RPCS = 'grpc.io/client/started_rpcs',
74+
SERVER_RECEIVED_BYTES_PER_RPC = 'grpc.io/server/received_bytes_per_rpc',
75+
SERVER_SENT_BYTES_PER_RPC = 'grpc.io/server/sent_bytes_per_rpc',
76+
SERVER_SERVER_LATENCY = 'grpc.io/server/server_latency',
77+
SERVER_COMPLETED_RPCS = 'grpc.io/server/completed_rpcs',
78+
SERVER_STARTED_RPCS = 'grpc.io/server/started_rpcs'
79+
}
80+
81+
export class RpczPageHandler {
82+
constructor(private statsParams: StatsParams) {}
83+
84+
/**
85+
* Generate Zpages RPC HTML Page
86+
* @param json If true, JSON will be emitted instead. Used for testing only.
87+
* @returns output HTML
88+
*/
89+
emitHtml(json: boolean): string {
90+
/** template HTML */
91+
const rpczFile =
92+
ejs.fileLoader(`${templatesDir}/rpcz.ejs`, 'utf8').toString();
93+
/** CSS styles file */
94+
const stylesFile =
95+
ejs.fileLoader(`${templatesDir}/styles.min.css`).toString();
96+
/** EJS render options */
97+
const options = {delimiter: '?'};
98+
99+
const rpcViews = this.statsParams.registeredViews.filter(
100+
view => view.name.indexOf('http') < 0);
101+
const rpczData: RpczData = {measuresSent: {}, measuresReceived: {}};
102+
103+
for (const view of rpcViews) {
104+
const recordedData = this.statsParams.recordedData[view.name];
105+
for (const snapshot of recordedData) {
106+
let method = snapshot.tags['grpc_client_method'];
107+
let zMeasures = rpczData.measuresSent;
108+
109+
// Switches to received data if it's a server
110+
if (!method) {
111+
method = snapshot.tags['grpc_server_method'];
112+
zMeasures = rpczData.measuresReceived;
113+
}
114+
115+
if (method) {
116+
if (!zMeasures[method]) {
117+
zMeasures[method] = this.newEmptyZMeasure();
118+
}
119+
120+
if (snapshot.type === AggregationType.DISTRIBUTION) {
121+
// Fills the output columns for that method
122+
if (view.name === DefaultViews.CLIENT_SENT_BYTES_PER_RPC ||
123+
view.name === DefaultViews.SERVER_SENT_BYTES_PER_RPC) {
124+
zMeasures[method].output.tot += snapshot.sum / 1024;
125+
zMeasures[method].output.min =
126+
this.getRate(
127+
zMeasures[method].output.tot, new Date(view.startTime)) *
128+
60;
129+
zMeasures[method].output.hr = zMeasures[method].output.min * 60;
130+
}
131+
132+
// Fills the input columns for that method
133+
if (view.name === DefaultViews.CLIENT_RECEIVED_BYTES_PER_RPC ||
134+
view.name === DefaultViews.SERVER_RECEIVED_BYTES_PER_RPC) {
135+
zMeasures[method].input.tot += snapshot.sum / 1024;
136+
zMeasures[method].input.min =
137+
this.getRate(
138+
zMeasures[method].input.tot, new Date(view.startTime)) *
139+
60;
140+
zMeasures[method].input.hr = zMeasures[method].input.min * 60;
141+
}
142+
}
143+
144+
if (snapshot.type === AggregationType.COUNT &&
145+
(view.name === DefaultViews.CLIENT_COMPLETED_RPCS ||
146+
view.name === DefaultViews.SERVER_COMPLETED_RPCS)) {
147+
// Fills the count columns for that method
148+
zMeasures[method].count.tot += snapshot.value;
149+
zMeasures[method].count.min =
150+
this.getRate(
151+
zMeasures[method].count.tot, new Date(view.startTime)) *
152+
60;
153+
zMeasures[method].count.hr = zMeasures[method].count.min * 60;
154+
155+
// Fills the rate columns for that method
156+
zMeasures[method].rate.tot = this.getRate(
157+
zMeasures[method].count.tot, new Date(view.startTime));
158+
zMeasures[method].rate.min = zMeasures[method].rate.tot * 60;
159+
zMeasures[method].rate.hr = zMeasures[method].rate.min * 60;
160+
161+
// Fills the error columns for that method
162+
const error = (snapshot.tags['grpc_client_status'] ||
163+
snapshot.tags['grpc_server_status']) &&
164+
(snapshot.tags['grpc_client_status'] !== 'OK' ||
165+
snapshot.tags['grpc_server_status'] !== 'OK');
166+
if (error) {
167+
zMeasures[method].errors.tot += snapshot.value;
168+
zMeasures[method].errors.min =
169+
this.getRate(
170+
zMeasures[method].errors.tot, new Date(view.startTime)) *
171+
60;
172+
zMeasures[method].errors.hr = zMeasures[method].errors.min * 60;
173+
}
174+
}
175+
176+
// Fills the avgLatency columns for that method
177+
if (snapshot.type === AggregationType.DISTRIBUTION &&
178+
(view.name === DefaultViews.CLIENT_ROUDTRIP_LATENCY ||
179+
view.name === DefaultViews.SERVER_SERVER_LATENCY)) {
180+
zMeasures[method].avgLatency.tot = snapshot.mean;
181+
zMeasures[method].avgLatency.min =
182+
this.getRate(
183+
zMeasures[method].avgLatency.tot,
184+
new Date(view.startTime)) *
185+
60;
186+
zMeasures[method].avgLatency.hr =
187+
zMeasures[method].avgLatency.min * 60;
188+
}
189+
}
190+
}
191+
}
192+
if (json) {
193+
return JSON.stringify(rpczData, null, 2);
194+
} else {
195+
return ejs.render(
196+
rpczFile, {
197+
styles: stylesFile,
198+
zMeasuresSent: rpczData.measuresSent,
199+
zMeasuresReceived: rpczData.measuresReceived,
200+
FIXED_SIZE
201+
},
202+
options);
203+
}
204+
}
205+
206+
/**
207+
* Returns the rate of a value in seconds.
208+
* @param value The value to get a rate from
209+
* @param startTime The value's colection start time
210+
*/
211+
private getRate(value: number, startTime: Date): number {
212+
return value / this.timeDiff(startTime, new Date());
213+
}
214+
215+
/** Creates an empty zPages' format measure */
216+
private newEmptyZMeasure(): ZMeasure {
217+
return {
218+
method: '',
219+
count: {min: 0, hr: 0, tot: 0},
220+
avgLatency: {min: 0, hr: 0, tot: 0},
221+
rate: {min: 0, hr: 0, tot: 0},
222+
input: {min: 0, hr: 0, tot: 0},
223+
output: {min: 0, hr: 0, tot: 0},
224+
errors: {min: 0, hr: 0, tot: 0}
225+
};
226+
}
227+
228+
/**
229+
* Calculates the difference between two timestamps in seconds.
230+
* @param start The start time
231+
* @param end The end time
232+
*/
233+
private timeDiff(start: Date, end: Date) {
234+
// Gets the days' difference in seconds
235+
return (end.getTime() - start.getTime()) * 1000;
236+
}
237+
}

0 commit comments

Comments
 (0)