Skip to content

Commit 014bc47

Browse files
authored
Fix APM metrics accuracy: server-side filtering, chart totals, and throughput normalization (#2623)
* update ppl timestamp format and use date format from settings Signed-off-by: ps48 <pshenoy36@gmail.com> * update metrics to support server vs client metrics Signed-off-by: ps48 <pshenoy36@gmail.com> * update RED metrics chart calculation for places which show absolute numbers Signed-off-by: ps48 <pshenoy36@gmail.com> * add support for window duration in APM settings Signed-off-by: ps48 <pshenoy36@gmail.com> * fix unit tests Signed-off-by: ps48 <pshenoy36@gmail.com> * resolve comments Signed-off-by: ps48 <pshenoy36@gmail.com> --------- Signed-off-by: ps48 <pshenoy36@gmail.com>
1 parent 38ba31c commit 014bc47

File tree

19 files changed

+581
-266
lines changed

19 files changed

+581
-266
lines changed

common/types/observability_saved_object_attributes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface ApmConfigEntity {
3939
tracesDataset?: { id: string };
4040
serviceMapDataset?: { id: string };
4141
prometheusDataSource?: { id: string };
42+
windowDuration?: number; // Data Prepper window_duration in seconds
4243
}
4344

4445
export interface ApmConfigAttributes extends SavedObjectAttributes {
@@ -69,4 +70,5 @@ export interface ResolvedApmConfig extends Omit<ApmConfigAttributes, 'entities'>
6970
name: string; // ConnectionId (for PromQL dataset.id and display)
7071
meta?: Record<string, unknown>;
7172
} | null;
73+
windowDuration: number; // Data Prepper window_duration in seconds (default 60)
7274
}

public/components/apm/common/__tests__/format_utils.test.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,29 +127,36 @@ describe('format_utils', () => {
127127
expect(formatThroughput(-Infinity)).toBe('-');
128128
});
129129

130-
it('formats small numbers with req/int suffix', () => {
131-
expect(formatThroughput(100)).toBe('100 req/int');
132-
expect(formatThroughput(0)).toBe('0 req/int');
133-
expect(formatThroughput(999)).toBe('999 req/int');
130+
it('formats fractional values with 1 decimal place', () => {
131+
expect(formatThroughput(0.33)).toBe('0.3 req/s');
132+
expect(formatThroughput(0.5)).toBe('0.5 req/s');
133+
expect(formatThroughput(0.05)).toBe('0.1 req/s');
134+
expect(formatThroughput(0.99)).toBe('1.0 req/s');
135+
});
136+
137+
it('formats small numbers with req/s suffix', () => {
138+
expect(formatThroughput(100)).toBe('100 req/s');
139+
expect(formatThroughput(0)).toBe('0 req/s');
140+
expect(formatThroughput(999)).toBe('999 req/s');
134141
});
135142

136143
it('formats thousands with K suffix', () => {
137-
expect(formatThroughput(1500)).toBe('1.5K req/int');
138-
expect(formatThroughput(1000)).toBe('1.0K req/int');
139-
expect(formatThroughput(2500)).toBe('2.5K req/int');
144+
expect(formatThroughput(1500)).toBe('1.5K req/s');
145+
expect(formatThroughput(1000)).toBe('1.0K req/s');
146+
expect(formatThroughput(2500)).toBe('2.5K req/s');
140147
});
141148

142149
it('formats millions with M suffix', () => {
143-
expect(formatThroughput(1500000)).toBe('1.5M req/int');
144-
expect(formatThroughput(1000000)).toBe('1.0M req/int');
145-
expect(formatThroughput(2500000)).toBe('2.5M req/int');
150+
expect(formatThroughput(1500000)).toBe('1.5M req/s');
151+
expect(formatThroughput(1000000)).toBe('1.0M req/s');
152+
expect(formatThroughput(2500000)).toBe('2.5M req/s');
146153
});
147154

148155
it('handles edge cases at boundaries', () => {
149-
expect(formatThroughput(999)).toBe('999 req/int');
150-
expect(formatThroughput(1000)).toBe('1.0K req/int');
151-
expect(formatThroughput(999999)).toBe('1000.0K req/int');
152-
expect(formatThroughput(1000000)).toBe('1.0M req/int');
156+
expect(formatThroughput(999)).toBe('999 req/s');
157+
expect(formatThroughput(1000)).toBe('1.0K req/s');
158+
expect(formatThroughput(999999)).toBe('1000.0K req/s');
159+
expect(formatThroughput(1000000)).toBe('1.0M req/s');
153160
});
154161
});
155162
});

public/components/apm/common/format_utils.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ export const formatLatency = (valueMs: number | undefined): string => {
4747
};
4848

4949
/**
50-
* Format throughput values with req/int unit (requests per interval).
51-
* The interval is determined by the window_duration option configured during Data Prepper ingestion.
52-
* 1500 → 1.5K req/int
50+
* Format throughput values with req/s unit (requests per second).
51+
* Values are normalized by dividing gauge values by the configured window duration.
52+
* 1500 → 1.5K req/s
5353
*/
5454
export const formatThroughput = (value: number | undefined): string => {
5555
if (value === undefined || isNaN(value) || !isFinite(value)) return '-';
56-
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M req/int`;
57-
if (value >= 1000) return `${(value / 1000).toFixed(1)}K req/int`;
58-
return `${value.toFixed(0)} req/int`;
56+
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M req/s`;
57+
if (value >= 1000) return `${(value / 1000).toFixed(1)}K req/s`;
58+
if (value > 0 && value < 1) return `${value.toFixed(1)} req/s`;
59+
return `${value.toFixed(0)} req/s`;
5960
};

public/components/apm/config/apm_settings_modal.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
EuiFlexGroup,
2727
EuiFlexItem,
2828
EuiToolTip,
29+
EuiFieldNumber,
2930
} from '@elastic/eui';
3031
import { i18n } from '@osd/i18n';
3132
import { NotificationsStart } from '../../../../../../src/core/public';
@@ -63,6 +64,13 @@ export interface ApmSettingsModalProps {
6364
notifications: NotificationsStart;
6465
}
6566

67+
interface ApmSettingsFormData {
68+
tracesDatasetId: string;
69+
serviceMapDatasetId: string;
70+
prometheusDataSourceId: string;
71+
windowDuration: string;
72+
}
73+
6674
export const ApmSettingsModal = (props: ApmSettingsModalProps) => {
6775
const { onClose, notifications } = props;
6876

@@ -73,10 +81,11 @@ export const ApmSettingsModal = (props: ApmSettingsModalProps) => {
7381
);
7482

7583
// Form state - minimal fields only
76-
const [formData, setFormData] = useState({
84+
const [formData, setFormData] = useState<ApmSettingsFormData>({
7785
tracesDatasetId: '',
7886
serviceMapDatasetId: '',
7987
prometheusDataSourceId: '',
88+
windowDuration: '60',
8089
});
8190

8291
const [selectedTracesDataset, setSelectedTracesDataset] = useState([]);
@@ -151,6 +160,7 @@ export const ApmSettingsModal = (props: ApmSettingsModalProps) => {
151160
tracesDatasetId: existingConfig.tracesDataset.id,
152161
serviceMapDatasetId: existingConfig.serviceMapDataset.id,
153162
prometheusDataSourceId: existingConfig.prometheusDataSource.id,
163+
windowDuration: String(existingConfig.windowDuration ?? 60),
154164
});
155165

156166
// Set selected options
@@ -290,20 +300,26 @@ export const ApmSettingsModal = (props: ApmSettingsModalProps) => {
290300
const client = OSDSavedApmConfigClient.getInstance();
291301

292302
if (existingConfig?.objectId) {
303+
const windowDuration = Math.max(1, parseInt(formData.windowDuration, 10) || 60);
304+
293305
// Update existing config in place (atomic operation)
294306
await client.update({
295307
objectId: existingConfig.objectId,
296308
tracesDatasetId: formData.tracesDatasetId,
297309
serviceMapDatasetId: formData.serviceMapDatasetId,
298310
prometheusDataSourceId: formData.prometheusDataSourceId,
311+
windowDuration,
299312
});
300313
} else {
314+
const windowDuration = Math.max(1, parseInt(formData.windowDuration, 10) || 60);
315+
301316
// Create new config only when none exists
302317
await client.create({
303318
workspaceId,
304319
tracesDatasetId: formData.tracesDatasetId,
305320
serviceMapDatasetId: formData.serviceMapDatasetId,
306321
prometheusDataSourceId: formData.prometheusDataSourceId,
322+
windowDuration,
307323
});
308324
}
309325

@@ -713,6 +729,33 @@ export const ApmSettingsModal = (props: ApmSettingsModalProps) => {
713729
fullWidth
714730
/>
715731
</EuiFormRow>
732+
733+
<EuiSpacer size="m" />
734+
735+
{/* Window Duration */}
736+
<EuiFormRow
737+
label={i18n.translate('observability.apm.settings.windowDurationLabel', {
738+
defaultMessage: 'Window Duration (seconds)',
739+
})}
740+
helpText={i18n.translate('observability.apm.settings.windowDurationHelpText', {
741+
defaultMessage:
742+
'Set to match your APM service map processor window_duration config. For accurate counts, ensure your Prometheus remote write/scrape interval matches this value.',
743+
})}
744+
fullWidth
745+
>
746+
<EuiFieldNumber
747+
compressed
748+
min={1}
749+
value={formData.windowDuration}
750+
onChange={(e) =>
751+
setFormData({
752+
...formData,
753+
windowDuration: e.target.value,
754+
})
755+
}
756+
fullWidth
757+
/>
758+
</EuiFormRow>
716759
</EuiForm>
717760
</EuiModalBody>
718761

public/components/apm/pages/service_details/service_dependencies.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
import { useDependencies } from '../../shared/hooks/use_dependencies';
4545
import { useDependencyMetrics } from '../../shared/hooks/use_dependency_metrics';
4646
import { parseTimeRange } from '../../shared/utils/time_utils';
47+
import { useChartStepWindow } from '../../shared/hooks/use_chart_step_window';
4748
import { DependencyFilterSidebar } from '../../shared/components/dependency_filter_sidebar';
4849
import { ActiveFilterBadges, FilterBadge } from '../../shared/components/active_filter_badges';
4950
import { formatCount, formatLatency } from '../../common/format_utils';
@@ -164,6 +165,8 @@ export const ServiceDependencies: React.FC<ServiceDependenciesProps> = ({
164165
expandedRowsRef.current = expandedRows;
165166
const hasAutoExpandedRef = useRef(false);
166167

168+
const chartStepWindow = useChartStepWindow(timeRange);
169+
167170
// Helper to parse URL params from hash
168171
const getUrlParams = () => {
169172
const hash = window.location.hash;
@@ -855,7 +858,8 @@ export const ServiceDependencies: React.FC<ServiceDependenciesProps> = ({
855858
environment,
856859
serviceName,
857860
dependency.serviceName,
858-
dependency.remoteOperation
861+
dependency.remoteOperation,
862+
chartStepWindow
859863
)}
860864
prometheusConnectionId={prometheusConnectionId}
861865
timeRange={timeRange}
@@ -898,7 +902,15 @@ export const ServiceDependencies: React.FC<ServiceDependenciesProps> = ({
898902
});
899903

900904
return map;
901-
}, [expandedRows, dependencies, environment, serviceName, timeRange, prometheusConnectionId]);
905+
}, [
906+
expandedRows,
907+
dependencies,
908+
environment,
909+
serviceName,
910+
timeRange,
911+
prometheusConnectionId,
912+
chartStepWindow,
913+
]);
902914

903915
if (error) {
904916
return (

public/components/apm/pages/service_details/service_operations.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
import { useOperations } from '../../shared/hooks/use_operations';
4646
import { useOperationMetrics } from '../../shared/hooks/use_operation_metrics';
4747
import { parseTimeRange } from '../../shared/utils/time_utils';
48+
import { useChartStepWindow } from '../../shared/hooks/use_chart_step_window';
4849
import { OperationFilterSidebar } from '../../shared/components/operation_filter_sidebar';
4950
import { ActiveFilterBadges, FilterBadge } from '../../shared/components/active_filter_badges';
5051
import { formatCount, formatLatency } from '../../common/format_utils';
@@ -181,6 +182,8 @@ export const ServiceOperations: React.FC<ServiceOperationsProps> = ({
181182
expandedRowsRef.current = expandedRows;
182183
const hasAutoExpandedRef = useRef(false);
183184

185+
const chartStepWindow = useChartStepWindow(timeRange);
186+
184187
// Filter sidebar state
185188
// Sidebar state is now managed by EuiResizableContainer
186189
const [selectedOperations, setSelectedOperations] = useState<string[]>([]);
@@ -793,7 +796,8 @@ export const ServiceOperations: React.FC<ServiceOperationsProps> = ({
793796
promqlQuery={getQueryOperationRequestsOverTime(
794797
environment,
795798
serviceName,
796-
operationName
799+
operationName,
800+
chartStepWindow
797801
)}
798802
prometheusConnectionId={prometheusConnectionId}
799803
timeRange={timeRange}
@@ -833,7 +837,7 @@ export const ServiceOperations: React.FC<ServiceOperationsProps> = ({
833837
});
834838

835839
return map;
836-
}, [expandedRows, environment, serviceName, timeRange, prometheusConnectionId]);
840+
}, [expandedRows, environment, serviceName, timeRange, prometheusConnectionId, chartStepWindow]);
837841

838842
if (error) {
839843
return (

public/components/apm/pages/service_details/service_overview.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ import {
3939
formatPercentageValue,
4040
formatLatency,
4141
} from '../../common/format_utils';
42+
import { useApmConfig } from '../../config/apm_config_context';
4243
import { navigateToServiceDetails } from '../../shared/utils/navigation_utils';
4344
import { RESOLUTION_LOW } from '../../shared/utils/step_utils';
45+
import { useChartStepWindow } from '../../shared/hooks/use_chart_step_window';
4446

4547
export interface ServiceOverviewProps {
4648
serviceName: string;
@@ -71,6 +73,9 @@ export const ServiceOverview: React.FC<ServiceOverviewProps> = ({
7173
serviceMapDataset: _serviceMapDataset,
7274
refreshTrigger,
7375
}) => {
76+
const { config } = useApmConfig();
77+
const windowDuration = config?.windowDuration ?? 60;
78+
7479
// State for latency percentile selector
7580
const [latencyPercentile, setLatencyPercentile] = useState<'p99' | 'p90' | 'p50'>('p99');
7681

@@ -80,6 +85,8 @@ export const ServiceOverview: React.FC<ServiceOverviewProps> = ({
8085
const [errorRateTopK, setErrorRateTopK] = useState<number>(3);
8186
const [availabilityBottomK, setAvailabilityBottomK] = useState<number>(3);
8287

88+
const chartStepWindow = useChartStepWindow(timeRange);
89+
8390
// Flyout state
8491
const [flyoutOpen, setFlyoutOpen] = useState(false);
8592
const [flyoutInitialTab, setFlyoutInitialTab] = useState<'spans' | 'logs' | 'attributes'>(
@@ -206,11 +213,11 @@ export const ServiceOverview: React.FC<ServiceOverviewProps> = ({
206213
<EuiFlexItem>
207214
<PromQLMetricCard
208215
title={i18n.translate('observability.apm.serviceOverview.throughput', {
209-
defaultMessage: 'Throughput (req/int)',
216+
defaultMessage: 'Throughput (req/s)',
210217
})}
211218
titleTooltip={i18n.translate('observability.apm.serviceOverview.throughputTooltip', {
212219
defaultMessage:
213-
'Average request count per interval. Interval is determined by the window_duration option set during Data Prepper ingestion.',
220+
'Average requests per second. Normalized using the Window Duration configured in APM Settings.',
214221
})}
215222
subtitle={i18n.translate('observability.apm.serviceOverview.avg', {
216223
defaultMessage: 'Avg',
@@ -221,6 +228,7 @@ export const ServiceOverview: React.FC<ServiceOverviewProps> = ({
221228
formatValue={formatCount}
222229
refreshTrigger={refreshTrigger}
223230
showTotal
231+
divisor={windowDuration}
224232
/>
225233
</EuiFlexItem>
226234
<EuiFlexItem>
@@ -410,7 +418,12 @@ export const ServiceOverview: React.FC<ServiceOverviewProps> = ({
410418
</EuiFlexGroup>
411419
<EuiSpacer size="s" />
412420
<PromQLLineChart
413-
promqlQuery={getQueryTopOperationsByVolume(environment, serviceName, requestsTopK)}
421+
promqlQuery={getQueryTopOperationsByVolume(
422+
environment,
423+
serviceName,
424+
requestsTopK,
425+
chartStepWindow
426+
)}
414427
timeRange={timeRange}
415428
prometheusConnectionId={prometheusConnectionId}
416429
formatValue={formatCount}

0 commit comments

Comments
 (0)