Skip to content

Commit 3cfa21d

Browse files
authored
[APM] generator: support error events and application metrics (#115311)
1 parent c6be6c0 commit 3cfa21d

File tree

12 files changed

+273
-27
lines changed

12 files changed

+273
-27
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { Fields } from './entity';
10+
import { Serializable } from './serializable';
11+
import { generateLongId, generateShortId } from './utils/generate_id';
12+
13+
export class ApmError extends Serializable {
14+
constructor(fields: Fields) {
15+
super({
16+
...fields,
17+
'processor.event': 'error',
18+
'processor.name': 'error',
19+
'error.id': generateShortId(),
20+
});
21+
}
22+
23+
serialize() {
24+
const [data] = super.serialize();
25+
data['error.grouping_key'] = generateLongId(
26+
this.fields['error.grouping_name'] || this.fields['error.exception']?.[0]?.message
27+
);
28+
return [data];
29+
}
30+
}

packages/elastic-apm-generator/src/lib/base_span.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Fields } from './entity';
1010
import { Serializable } from './serializable';
1111
import { Span } from './span';
1212
import { Transaction } from './transaction';
13-
import { generateTraceId } from './utils/generate_id';
13+
import { generateLongId } from './utils/generate_id';
1414

1515
export class BaseSpan extends Serializable {
1616
private readonly _children: BaseSpan[] = [];
@@ -19,7 +19,7 @@ export class BaseSpan extends Serializable {
1919
super({
2020
...fields,
2121
'event.outcome': 'unknown',
22-
'trace.id': generateTraceId(),
22+
'trace.id': generateLongId(),
2323
'processor.name': 'transaction',
2424
});
2525
}

packages/elastic-apm-generator/src/lib/entity.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
* Side Public License, v 1.
77
*/
88

9+
export type ApplicationMetricFields = Partial<{
10+
'system.process.memory.size': number;
11+
'system.memory.actual.free': number;
12+
'system.memory.total': number;
13+
'system.cpu.total.norm.pct': number;
14+
'system.process.memory.rss.bytes': number;
15+
'system.process.cpu.total.norm.pct': number;
16+
}>;
17+
18+
export interface Exception {
19+
message: string;
20+
}
21+
922
export type Fields = Partial<{
1023
'@timestamp': number;
1124
'agent.name': string;
@@ -14,6 +27,10 @@ export type Fields = Partial<{
1427
'ecs.version': string;
1528
'event.outcome': string;
1629
'event.ingested': number;
30+
'error.id': string;
31+
'error.exception': Exception[];
32+
'error.grouping_name': string;
33+
'error.grouping_key': string;
1734
'host.name': string;
1835
'metricset.name': string;
1936
'observer.version': string;
@@ -46,7 +63,8 @@ export type Fields = Partial<{
4663
'span.destination.service.response_time.count': number;
4764
'span.self_time.count': number;
4865
'span.self_time.sum.us': number;
49-
}>;
66+
}> &
67+
ApplicationMetricFields;
5068

5169
export class Entity {
5270
constructor(public readonly fields: Fields) {

packages/elastic-apm-generator/src/lib/instance.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
* Side Public License, v 1.
77
*/
88

9-
import { Entity } from './entity';
9+
import { ApmError } from './apm_error';
10+
import { ApplicationMetricFields, Entity } from './entity';
11+
import { Metricset } from './metricset';
1012
import { Span } from './span';
1113
import { Transaction } from './transaction';
1214

@@ -27,4 +29,20 @@ export class Instance extends Entity {
2729
'span.subtype': spanSubtype,
2830
});
2931
}
32+
33+
error(message: string, type?: string, groupingName?: string) {
34+
return new ApmError({
35+
...this.fields,
36+
'error.exception': [{ message, ...(type ? { type } : {}) }],
37+
'error.grouping_name': groupingName || message,
38+
});
39+
}
40+
41+
appMetrics(metrics: ApplicationMetricFields) {
42+
return new Metricset({
43+
...this.fields,
44+
'metricset.name': 'app',
45+
...metrics,
46+
});
47+
}
3048
}

packages/elastic-apm-generator/src/lib/metricset.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
* Side Public License, v 1.
77
*/
88

9+
import { Fields } from './entity';
910
import { Serializable } from './serializable';
1011

11-
export class Metricset extends Serializable {}
12-
13-
export function metricset(name: string) {
14-
return new Metricset({
15-
'metricset.name': name,
16-
});
12+
export class Metricset extends Serializable {
13+
constructor(fields: Fields) {
14+
super({
15+
'processor.event': 'metric',
16+
'processor.name': 'metric',
17+
...fields,
18+
});
19+
}
1720
}

packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export function toElasticsearchOutput(events: Fields[], versionOverride?: string
2525
const document = {};
2626
// eslint-disable-next-line guard-for-in
2727
for (const key in values) {
28-
set(document, key, values[key as keyof typeof values]);
28+
const val = values[key as keyof typeof values];
29+
set(document, key, val);
2930
}
3031
return {
3132
_index: `apm-${versionOverride || values['observer.version']}-${values['processor.event']}`,

packages/elastic-apm-generator/src/lib/span.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88

99
import { BaseSpan } from './base_span';
1010
import { Fields } from './entity';
11-
import { generateEventId } from './utils/generate_id';
11+
import { generateShortId } from './utils/generate_id';
1212

1313
export class Span extends BaseSpan {
1414
constructor(fields: Fields) {
1515
super({
1616
...fields,
1717
'processor.event': 'span',
18-
'span.id': generateEventId(),
18+
'span.id': generateShortId(),
1919
});
2020
}
2121

packages/elastic-apm-generator/src/lib/transaction.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,48 @@
66
* Side Public License, v 1.
77
*/
88

9+
import { ApmError } from './apm_error';
910
import { BaseSpan } from './base_span';
1011
import { Fields } from './entity';
11-
import { generateEventId } from './utils/generate_id';
12+
import { generateShortId } from './utils/generate_id';
1213

1314
export class Transaction extends BaseSpan {
1415
private _sampled: boolean = true;
16+
private readonly _errors: ApmError[] = [];
1517

1618
constructor(fields: Fields) {
1719
super({
1820
...fields,
1921
'processor.event': 'transaction',
20-
'transaction.id': generateEventId(),
22+
'transaction.id': generateShortId(),
2123
'transaction.sampled': true,
2224
});
2325
}
2426

27+
parent(span: BaseSpan) {
28+
super.parent(span);
29+
30+
this._errors.forEach((error) => {
31+
error.fields['trace.id'] = this.fields['trace.id'];
32+
error.fields['transaction.id'] = this.fields['transaction.id'];
33+
error.fields['transaction.type'] = this.fields['transaction.type'];
34+
});
35+
36+
return this;
37+
}
38+
39+
errors(...errors: ApmError[]) {
40+
errors.forEach((error) => {
41+
error.fields['trace.id'] = this.fields['trace.id'];
42+
error.fields['transaction.id'] = this.fields['transaction.id'];
43+
error.fields['transaction.type'] = this.fields['transaction.type'];
44+
});
45+
46+
this._errors.push(...errors);
47+
48+
return this;
49+
}
50+
2551
duration(duration: number) {
2652
this.fields['transaction.duration.us'] = duration * 1000;
2753
return this;
@@ -35,11 +61,13 @@ export class Transaction extends BaseSpan {
3561
serialize() {
3662
const [transaction, ...spans] = super.serialize();
3763

64+
const errors = this._errors.flatMap((error) => error.serialize());
65+
3866
const events = [transaction];
3967
if (this._sampled) {
4068
events.push(...spans);
4169
}
4270

43-
return events;
71+
return events.concat(errors);
4472
}
4573
}

packages/elastic-apm-generator/src/lib/utils/generate_id.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ let seq = 0;
1212

1313
const namespace = 'f38d5b83-8eee-4f5b-9aa6-2107e15a71e3';
1414

15-
function generateId() {
16-
return uuidv5(String(seq++), namespace).replace(/-/g, '');
15+
function generateId(seed?: string) {
16+
return uuidv5(seed ?? String(seq++), namespace).replace(/-/g, '');
1717
}
1818

19-
export function generateEventId() {
20-
return generateId().substr(0, 16);
19+
export function generateShortId(seed?: string) {
20+
return generateId(seed).substr(0, 16);
2121
}
2222

23-
export function generateTraceId() {
24-
return generateId().substr(0, 32);
23+
export function generateLongId(seed?: string) {
24+
return generateId(seed).substr(0, 32);
2525
}

packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ export function simpleTrace(from: number, to: number) {
1414

1515
const range = timerange(from, to);
1616

17-
const transactionName = '100rpm (80% success) failed 1000ms';
17+
const transactionName = '240rpm/60% 1000ms';
1818

1919
const successfulTraceEvents = range
20-
.interval('30s')
21-
.rate(40)
20+
.interval('1s')
21+
.rate(3)
2222
.flatMap((timestamp) =>
2323
instance
2424
.transaction(transactionName)
@@ -38,21 +38,39 @@ export function simpleTrace(from: number, to: number) {
3838
);
3939

4040
const failedTraceEvents = range
41-
.interval('30s')
42-
.rate(10)
41+
.interval('1s')
42+
.rate(1)
4343
.flatMap((timestamp) =>
4444
instance
4545
.transaction(transactionName)
4646
.timestamp(timestamp)
4747
.duration(1000)
4848
.failure()
49+
.errors(
50+
instance.error('[ResponseError] index_not_found_exception').timestamp(timestamp + 50)
51+
)
4952
.serialize()
5053
);
5154

55+
const metricsets = range
56+
.interval('30s')
57+
.rate(1)
58+
.flatMap((timestamp) =>
59+
instance
60+
.appMetrics({
61+
'system.memory.actual.free': 800,
62+
'system.memory.total': 1000,
63+
'system.cpu.total.norm.pct': 0.6,
64+
'system.process.cpu.total.norm.pct': 0.7,
65+
})
66+
.timestamp(timestamp)
67+
.serialize()
68+
);
5269
const events = successfulTraceEvents.concat(failedTraceEvents);
5370

5471
return [
5572
...events,
73+
...metricsets,
5674
...getTransactionMetrics(events),
5775
...getSpanDestinationMetrics(events),
5876
...getBreakdownMetrics(events),

0 commit comments

Comments
 (0)