Skip to content

Commit f810912

Browse files
authored
[jaeger] Add Service Performance Monitoring (#428)
It is now possible to view the Jaeger Metrics for Service Performance Monitoring within kobs. Service Performance Monitoring allows users to view the latency, error rate and request rate for all operation of a service. It is also possible to switch from the monitor view directly to the corresponding traces view of a selected operation.
1 parent 26c7a7a commit f810912

18 files changed

+1181
-2
lines changed

plugins/plugin-jaeger/cmd/jaeger.go

+45
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,50 @@ func (router *Router) getTrace(w http.ResponseWriter, r *http.Request) {
154154
render.JSON(w, r, body)
155155
}
156156

157+
func (router *Router) getMetrics(w http.ResponseWriter, r *http.Request) {
158+
name := r.Header.Get("x-kobs-plugin")
159+
metric := r.URL.Query().Get("metric")
160+
service := r.URL.Query().Get("service")
161+
groupByOperation := r.URL.Query().Get("groupByOperation")
162+
quantile := r.URL.Query().Get("quantile")
163+
ratePer := r.URL.Query().Get("ratePer")
164+
step := r.URL.Query().Get("step")
165+
timeEnd := r.URL.Query().Get("timeEnd")
166+
timeStart := r.URL.Query().Get("timeStart")
167+
168+
log.Debug(r.Context(), "Get metrics parameters", zap.String("name", name), zap.String("metric", metric), zap.String("service", service), zap.String("groupByOperation", groupByOperation), zap.String("quantile", quantile), zap.String("ratePer", ratePer), zap.String("step", step), zap.String("timeEnd", timeEnd), zap.String("timeStart", timeStart))
169+
170+
i := router.getInstance(name)
171+
if i == nil {
172+
log.Error(r.Context(), "Could not find instance name", zap.String("name", name))
173+
errresponse.Render(w, r, nil, http.StatusBadRequest, "Could not find instance name")
174+
return
175+
}
176+
177+
parsedTimeStart, err := strconv.ParseInt(timeStart, 10, 64)
178+
if err != nil {
179+
log.Error(r.Context(), "Could not parse start time", zap.Error(err))
180+
errresponse.Render(w, r, nil, http.StatusBadRequest, "Could not parse start time")
181+
return
182+
}
183+
184+
parsedTimeEnd, err := strconv.ParseInt(timeEnd, 10, 64)
185+
if err != nil {
186+
log.Error(r.Context(), "Could not parse end time", zap.Error(err))
187+
errresponse.Render(w, r, nil, http.StatusBadRequest, "Could not parse end time")
188+
return
189+
}
190+
191+
body, err := i.GetMetrics(r.Context(), metric, service, groupByOperation, quantile, ratePer, step, parsedTimeStart, parsedTimeEnd)
192+
if err != nil {
193+
log.Error(r.Context(), "Could not get metrics", zap.Error(err))
194+
errresponse.Render(w, r, err, http.StatusInternalServerError, "Could not get metrics")
195+
return
196+
}
197+
198+
render.JSON(w, r, body)
199+
}
200+
157201
// Mount mounts the Jaeger plugin routes in the plugins router of a kobs satellite instance.
158202
func Mount(instances []plugin.Instance, clustersClient clusters.Client) (chi.Router, error) {
159203
var jaegerInstances []instance.Instance
@@ -176,6 +220,7 @@ func Mount(instances []plugin.Instance, clustersClient clusters.Client) (chi.Rou
176220
router.Get("/operations", router.getOperations)
177221
router.Get("/traces", router.getTraces)
178222
router.Get("/trace", router.getTrace)
223+
router.Get("/metrics", router.getMetrics)
179224

180225
return router, nil
181226
}

plugins/plugin-jaeger/pkg/instance/instance.go

+9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Instance interface {
3838
GetOperations(ctx context.Context, service string) (map[string]any, error)
3939
GetTraces(ctx context.Context, limit, maxDuration, minDuration, operation, service, tags string, timeStart, timeEnd int64) (map[string]any, error)
4040
GetTrace(ctx context.Context, traceID string) (map[string]any, error)
41+
GetMetrics(ctx context.Context, metric, service, groupByOperation, quantile, ratePer, step string, timeStart, timeEnd int64) (map[string]any, error)
4142
}
4243

4344
type instance struct {
@@ -111,6 +112,14 @@ func (i *instance) GetTrace(ctx context.Context, traceID string) (map[string]any
111112
return i.doRequest(ctx, fmt.Sprintf("/api/traces/%s", traceID))
112113
}
113114

115+
func (i *instance) GetMetrics(ctx context.Context, metric, service, groupByOperation, quantile, ratePer, step string, timeStart, timeEnd int64) (map[string]any, error) {
116+
timeStart = timeStart * 1000
117+
timeEnd = timeEnd * 1000
118+
lookback := timeEnd - timeStart
119+
120+
return i.doRequest(ctx, fmt.Sprintf("/api/metrics/%s?service=%s&endTs=%d&lookback=%d&groupByOperation=%s&quantile=%s&ratePer=%s&step=%s&spanKind=unspecified&spanKind=internal&spanKind=server&spanKind=client&spanKind=producer&spanKind=consumer", metric, service, timeEnd, lookback, groupByOperation, quantile, ratePer, step))
121+
}
122+
114123
// New returns a new Jaeger instance for the given configuration.
115124
func New(name string, options map[string]any) (Instance, error) {
116125
var config Config

plugins/plugin-jaeger/pkg/instance/instance_mock.go

+23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Grid, GridItem } from '@patternfly/react-core';
2+
import React, { useEffect, useState } from 'react';
3+
import { useLocation, useNavigate } from 'react-router-dom';
4+
5+
import { IPluginInstance, PageContentSection, PageHeaderSection, PluginPageTitle } from '@kobsio/shared';
6+
import { IMonitorOptions } from '../../utils/interfaces';
7+
import MonitorActions from './MonitorActions';
8+
import MonitorOperations from '../panel/MonitorOperations';
9+
import MonitorServiceCalls from '../panel/MonitorServiceCalls';
10+
import MonitorServiceErrors from '../panel/MonitorServiceErrors';
11+
import MonitorServiceLatency from '../panel/MonitorServiceLatency';
12+
import MonitorToolbar from './MonitorToolbar';
13+
import { defaultDescription } from '../../utils/constants';
14+
import { getInitialMonitorOptions } from '../../utils/helpers';
15+
16+
interface IMonitorProps {
17+
instance: IPluginInstance;
18+
}
19+
20+
const Monitor: React.FunctionComponent<IMonitorProps> = ({ instance }: IMonitorProps) => {
21+
const location = useLocation();
22+
const navigate = useNavigate();
23+
const [options, setOptions] = useState<IMonitorOptions>();
24+
const [details, setDetails] = useState<React.ReactNode>(undefined);
25+
26+
const changeOptions = (opts: IMonitorOptions): void => {
27+
navigate(
28+
`${location.pathname}?service=${encodeURIComponent(opts.service)}&time=${opts.times.time}&timeEnd=${
29+
opts.times.timeEnd
30+
}&timeStart=${opts.times.timeStart}`,
31+
);
32+
};
33+
34+
useEffect(() => {
35+
setOptions((prevOptions) => getInitialMonitorOptions(location.search, !prevOptions));
36+
}, [location.search]);
37+
38+
if (!options) {
39+
return null;
40+
}
41+
42+
return (
43+
<React.Fragment>
44+
<PageHeaderSection
45+
component={
46+
<PluginPageTitle
47+
satellite={instance.satellite}
48+
name={instance.name}
49+
description={instance.description || defaultDescription}
50+
actions={<MonitorActions instance={instance} />}
51+
/>
52+
}
53+
/>
54+
55+
<PageContentSection
56+
hasPadding={true}
57+
hasDivider={true}
58+
toolbarContent={<MonitorToolbar instance={instance} options={options} setOptions={changeOptions} />}
59+
panelContent={details}
60+
>
61+
{options.service ? (
62+
<Grid hasGutter={true}>
63+
<GridItem style={{ height: '300px' }} sm={12} md={12} lg={4} xl={4} xl2={4}>
64+
<MonitorServiceLatency
65+
title="Latency (ms)"
66+
instance={instance}
67+
service={options.service}
68+
times={options.times}
69+
/>
70+
</GridItem>
71+
<GridItem style={{ height: '300px' }} sm={12} md={12} lg={4} xl={4} xl2={4}>
72+
<MonitorServiceErrors
73+
title="Error Rate (%)"
74+
instance={instance}
75+
service={options.service}
76+
times={options.times}
77+
/>
78+
</GridItem>
79+
<GridItem style={{ height: '300px' }} sm={12} md={12} lg={4} xl={4} xl2={4}>
80+
<MonitorServiceCalls
81+
title="Request Rate (req/s)"
82+
instance={instance}
83+
service={options.service}
84+
times={options.times}
85+
/>
86+
</GridItem>
87+
<GridItem span={12}>
88+
<MonitorOperations
89+
title="Operations"
90+
instance={instance}
91+
service={options.service}
92+
times={options.times}
93+
setDetails={setDetails}
94+
/>
95+
</GridItem>
96+
</Grid>
97+
) : (
98+
<div></div>
99+
)}
100+
</PageContentSection>
101+
</React.Fragment>
102+
);
103+
};
104+
105+
export default Monitor;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core';
2+
import React, { useState } from 'react';
3+
import { Link } from 'react-router-dom';
4+
5+
import { IPluginInstance, pluginBasePath } from '@kobsio/shared';
6+
7+
interface IMonitorActionsProps {
8+
instance: IPluginInstance;
9+
}
10+
11+
export const MonitorActions: React.FunctionComponent<IMonitorActionsProps> = ({ instance }: IMonitorActionsProps) => {
12+
const [show, setShow] = useState<boolean>(false);
13+
14+
return (
15+
<Dropdown
16+
style={{ zIndex: 400 }}
17+
toggle={<KebabToggle onToggle={(): void => setShow(!show)} />}
18+
isOpen={show}
19+
isPlain={true}
20+
position="right"
21+
dropdownItems={[
22+
<DropdownItem key={0} component={<Link to={pluginBasePath(instance)}>Traces</Link>} />,
23+
<DropdownItem key={1} component={<Link to={`${pluginBasePath(instance)}/trace`}>Compare Traces</Link>} />,
24+
]}
25+
/>
26+
);
27+
};
28+
29+
export default MonitorActions;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { useState } from 'react';
2+
3+
import { IOptionsAdditionalFields, IPluginInstance, ITimes, Options, Toolbar, ToolbarItem } from '@kobsio/shared';
4+
import { IMonitorOptions } from '../../utils/interfaces';
5+
import TracesToolbarServices from './TracesToolbarServices';
6+
7+
interface IMonitorToolbarProps {
8+
instance: IPluginInstance;
9+
options: IMonitorOptions;
10+
setOptions: (data: IMonitorOptions) => void;
11+
}
12+
13+
const MonitorToolbar: React.FunctionComponent<IMonitorToolbarProps> = ({
14+
instance,
15+
options,
16+
setOptions,
17+
}: IMonitorToolbarProps) => {
18+
const [service, setService] = useState<string>(options.service);
19+
20+
const changeOptions = (times: ITimes, additionalFields: IOptionsAdditionalFields[] | undefined): void => {
21+
setOptions({
22+
service: service,
23+
times: times,
24+
});
25+
};
26+
27+
return (
28+
<Toolbar usePageInsets={true}>
29+
<ToolbarItem grow={true}>
30+
<TracesToolbarServices instance={instance} service={service} setService={(value): void => setService(value)} />
31+
</ToolbarItem>
32+
33+
<Options times={options.times} showOptions={true} showSearchButton={true} setOptions={changeOptions} />
34+
</Toolbar>
35+
);
36+
};
37+
38+
export default MonitorToolbar;

plugins/plugin-jaeger/src/components/page/Page.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IPluginPageProps } from '@kobsio/shared';
66

77
const Trace = lazy(() => import('./Trace'));
88
const Traces = lazy(() => import('./Traces'));
9+
const Monitor = lazy(() => import('./Monitor'));
910

1011
const Page: React.FunctionComponent<IPluginPageProps> = ({ instance }: IPluginPageProps) => {
1112
return (
@@ -16,6 +17,7 @@ const Page: React.FunctionComponent<IPluginPageProps> = ({ instance }: IPluginPa
1617
<Route path="/" element={<Traces instance={instance} />} />
1718
<Route path="/trace/" element={<Trace instance={instance} />} />
1819
<Route path="/trace/:traceID" element={<Trace instance={instance} />} />
20+
<Route path="/monitor" element={<Monitor instance={instance} />} />
1921
</Routes>
2022
</Suspense>
2123
);

plugins/plugin-jaeger/src/components/page/TracesActions.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const TracesActions: React.FunctionComponent<ITracesActionsProps> = ({ in
2020
position="right"
2121
dropdownItems={[
2222
<DropdownItem key={0} component={<Link to={`${pluginBasePath(instance)}/trace`}>Compare Traces</Link>} />,
23+
<DropdownItem key={1} component={<Link to={`${pluginBasePath(instance)}/monitor`}>Monitor</Link>} />,
2324
]}
2425
/>
2526
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
Chart,
3+
ChartAxis,
4+
ChartGroup,
5+
ChartLegendTooltip,
6+
ChartLine,
7+
ChartThemeColor,
8+
createContainer,
9+
} from '@patternfly/react-charts';
10+
import React, { useRef } from 'react';
11+
12+
import { IChartData, IChartDatum } from '../../utils/interfaces';
13+
import {
14+
ITimes,
15+
chartAxisStyle,
16+
chartFormatLabel,
17+
chartTickFormatDate,
18+
formatTime,
19+
useDimensions,
20+
} from '@kobsio/shared';
21+
22+
interface IMonitorChartProps {
23+
data: IChartData[];
24+
unit: string;
25+
times: ITimes;
26+
}
27+
28+
const MonitorChart: React.FunctionComponent<IMonitorChartProps> = ({ data, unit, times }: IMonitorChartProps) => {
29+
const refChart = useRef<HTMLDivElement>(null);
30+
const chartSize = useDimensions(refChart);
31+
32+
const legendData = data.map((metric, index) => {
33+
return { childName: metric.name, name: metric.name };
34+
});
35+
36+
const CursorVoronoiContainer = createContainer('voronoi', 'cursor');
37+
38+
return (
39+
<div style={{ height: '100%', width: '100%' }} ref={refChart}>
40+
<Chart
41+
containerComponent={
42+
<CursorVoronoiContainer
43+
cursorDimension="x"
44+
labels={({ datum }: { datum: IChartDatum }): string =>
45+
chartFormatLabel(datum.y != null ? `${datum.y} ${unit}` : '')
46+
}
47+
labelComponent={
48+
<ChartLegendTooltip
49+
themeColor={ChartThemeColor.multiOrdered}
50+
legendData={legendData}
51+
title={(datum: IChartDatum): string => formatTime(Math.floor((datum.x as Date).getTime() / 1000))}
52+
/>
53+
}
54+
mouseFollowTooltips={true}
55+
voronoiDimension="x"
56+
voronoiPadding={0}
57+
/>
58+
}
59+
height={chartSize.height}
60+
padding={{ bottom: 20, left: 50, right: 0, top: 0 }}
61+
scale={{ x: 'time', y: 'linear' }}
62+
themeColor={ChartThemeColor.multiOrdered}
63+
width={chartSize.width}
64+
domain={{ x: [new Date(times.timeStart * 1000), new Date(times.timeEnd * 1000)] }}
65+
>
66+
<ChartAxis dependentAxis={false} tickFormat={chartTickFormatDate} showGrid={true} style={chartAxisStyle} />
67+
<ChartAxis dependentAxis={true} showGrid={true} style={chartAxisStyle} />
68+
69+
<ChartGroup>
70+
{data.map((metric) => (
71+
<ChartLine key={metric.name} data={metric.data} name={metric.name} interpolation="monotoneX" />
72+
))}
73+
</ChartGroup>
74+
</Chart>
75+
</div>
76+
);
77+
};
78+
79+
export default MonitorChart;

0 commit comments

Comments
 (0)