Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] generator: support error events and application metrics #115311

Merged
merged 5 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/elastic-apm-generator/src/lib/apm_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { Fields } from './entity';
import { Serializable } from './serializable';
import { generateLongId, generateShortId } from './utils/generate_id';

export class ApmError extends Serializable {
constructor(fields: Fields) {
super({
...fields,
'processor.event': 'error',
'processor.name': 'error',
'error.id': generateShortId(),
});
}

serialize() {
const [data] = super.serialize();
data['error.grouping_key'] = generateLongId(
this.fields['error.grouping_name'] || this.fields['error.exception']?.[0]?.message
);
return [data];
}
}
4 changes: 2 additions & 2 deletions packages/elastic-apm-generator/src/lib/base_span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Fields } from './entity';
import { Serializable } from './serializable';
import { Span } from './span';
import { Transaction } from './transaction';
import { generateTraceId } from './utils/generate_id';
import { generateLongId } from './utils/generate_id';

export class BaseSpan extends Serializable {
private readonly _children: BaseSpan[] = [];
Expand All @@ -19,7 +19,7 @@ export class BaseSpan extends Serializable {
super({
...fields,
'event.outcome': 'unknown',
'trace.id': generateTraceId(),
'trace.id': generateLongId(),
'processor.name': 'transaction',
});
}
Expand Down
20 changes: 19 additions & 1 deletion packages/elastic-apm-generator/src/lib/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
* Side Public License, v 1.
*/

export type ApplicationMetricFields = Partial<{
'system.process.memory.size': number;
'system.memory.actual.free': number;
'system.memory.total': number;
'system.cpu.total.norm.pct': number;
'system.process.memory.rss.bytes': number;
'system.process.cpu.total.norm.pct': number;
}>;

export interface Exception {
message: string;
}

export type Fields = Partial<{
'@timestamp': number;
'agent.name': string;
Expand All @@ -14,6 +27,10 @@ export type Fields = Partial<{
'ecs.version': string;
'event.outcome': string;
'event.ingested': number;
'error.id': string;
'error.exception': Exception[];
'error.grouping_name': string;
'error.grouping_key': string;
'host.name': string;
'metricset.name': string;
'observer.version': string;
Expand Down Expand Up @@ -46,7 +63,8 @@ export type Fields = Partial<{
'span.destination.service.response_time.count': number;
'span.self_time.count': number;
'span.self_time.sum.us': number;
}>;
}> &
ApplicationMetricFields;

export class Entity {
constructor(public readonly fields: Fields) {
Expand Down
20 changes: 19 additions & 1 deletion packages/elastic-apm-generator/src/lib/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* Side Public License, v 1.
*/

import { Entity } from './entity';
import { ApmError } from './apm_error';
import { ApplicationMetricFields, Entity } from './entity';
import { Metricset } from './metricset';
import { Span } from './span';
import { Transaction } from './transaction';

Expand All @@ -27,4 +29,20 @@ export class Instance extends Entity {
'span.subtype': spanSubtype,
});
}

error(message: string, type?: string, groupingName?: string) {
return new ApmError({
...this.fields,
'error.exception': [{ message, ...(type ? { type } : {}) }],
'error.grouping_name': groupingName || message,
});
}

appMetrics(metrics: ApplicationMetricFields) {
return new Metricset({
...this.fields,
'metricset.name': 'app',
...metrics,
});
}
}
15 changes: 9 additions & 6 deletions packages/elastic-apm-generator/src/lib/metricset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
* Side Public License, v 1.
*/

import { Fields } from './entity';
import { Serializable } from './serializable';

export class Metricset extends Serializable {}

export function metricset(name: string) {
return new Metricset({
'metricset.name': name,
});
export class Metricset extends Serializable {
constructor(fields: Fields) {
super({
'processor.event': 'metric',
'processor.name': 'metric',
...fields,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export function toElasticsearchOutput(events: Fields[], versionOverride?: string
const document = {};
// eslint-disable-next-line guard-for-in
for (const key in values) {
set(document, key, values[key as keyof typeof values]);
const val = values[key as keyof typeof values];
set(document, key, val);
}
return {
_index: `apm-${versionOverride || values['observer.version']}-${values['processor.event']}`,
Expand Down
4 changes: 2 additions & 2 deletions packages/elastic-apm-generator/src/lib/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

import { BaseSpan } from './base_span';
import { Fields } from './entity';
import { generateEventId } from './utils/generate_id';
import { generateShortId } from './utils/generate_id';

export class Span extends BaseSpan {
constructor(fields: Fields) {
super({
...fields,
'processor.event': 'span',
'span.id': generateEventId(),
'span.id': generateShortId(),
});
}

Expand Down
34 changes: 31 additions & 3 deletions packages/elastic-apm-generator/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,48 @@
* Side Public License, v 1.
*/

import { ApmError } from './apm_error';
import { BaseSpan } from './base_span';
import { Fields } from './entity';
import { generateEventId } from './utils/generate_id';
import { generateShortId } from './utils/generate_id';

export class Transaction extends BaseSpan {
private _sampled: boolean = true;
private readonly _errors: ApmError[] = [];

constructor(fields: Fields) {
super({
...fields,
'processor.event': 'transaction',
'transaction.id': generateEventId(),
'transaction.id': generateShortId(),
'transaction.sampled': true,
});
}

parent(span: BaseSpan) {
super.parent(span);

this._errors.forEach((error) => {
error.fields['trace.id'] = this.fields['trace.id'];
error.fields['transaction.id'] = this.fields['transaction.id'];
error.fields['transaction.type'] = this.fields['transaction.type'];
});

return this;
}

errors(...errors: ApmError[]) {
errors.forEach((error) => {
error.fields['trace.id'] = this.fields['trace.id'];
error.fields['transaction.id'] = this.fields['transaction.id'];
error.fields['transaction.type'] = this.fields['transaction.type'];
});

this._errors.push(...errors);

return this;
}

duration(duration: number) {
this.fields['transaction.duration.us'] = duration * 1000;
return this;
Expand All @@ -35,11 +61,13 @@ export class Transaction extends BaseSpan {
serialize() {
const [transaction, ...spans] = super.serialize();

const errors = this._errors.flatMap((error) => error.serialize());

const events = [transaction];
if (this._sampled) {
events.push(...spans);
}

return events;
return events.concat(errors);
}
}
12 changes: 6 additions & 6 deletions packages/elastic-apm-generator/src/lib/utils/generate_id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ let seq = 0;

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

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

export function generateEventId() {
return generateId().substr(0, 16);
export function generateShortId(seed?: string) {
return generateId(seed).substr(0, 16);
}

export function generateTraceId() {
return generateId().substr(0, 32);
export function generateLongId(seed?: string) {
return generateId(seed).substr(0, 32);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export function simpleTrace(from: number, to: number) {

const range = timerange(from, to);

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

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

const failedTraceEvents = range
.interval('30s')
.rate(10)
.interval('1s')
.rate(1)
.flatMap((timestamp) =>
instance
.transaction(transactionName)
.timestamp(timestamp)
.duration(1000)
.failure()
.errors(
instance.error('[ResponseError] index_not_found_exception').timestamp(timestamp + 50)
)
.serialize()
);

const metricsets = range
.interval('30s')
.rate(1)
.flatMap((timestamp) =>
instance
.appMetrics({
'system.memory.actual.free': 800,
'system.memory.total': 1000,
'system.cpu.total.norm.pct': 0.6,
'system.process.cpu.total.norm.pct': 0.7,
})
.timestamp(timestamp)
.serialize()
);
const events = successfulTraceEvents.concat(failedTraceEvents);

return [
...events,
...metricsets,
...getTransactionMetrics(events),
...getSpanDestinationMetrics(events),
...getBreakdownMetrics(events),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pick } from 'lodash';
import { service } from '../../index';
import { Instance } from '../../lib/instance';

describe('transactions with errors', () => {
let instance: Instance;
const timestamp = new Date('2021-01-01T00:00:00.000Z').getTime();

beforeEach(() => {
instance = service('opbeans-java', 'production', 'java').instance('instance');
});
it('generates error events', () => {
const events = instance
.transaction('GET /api')
.timestamp(timestamp)
.errors(instance.error('test error').timestamp(timestamp))
.serialize();

const errorEvents = events.filter((event) => event['processor.event'] === 'error');

expect(errorEvents.length).toEqual(1);

expect(
pick(errorEvents[0], 'processor.event', 'processor.name', 'error.exception', '@timestamp')
).toEqual({
'processor.event': 'error',
'processor.name': 'error',
'@timestamp': timestamp,
'error.exception': [{ message: 'test error' }],
});
});

it('sets the transaction and trace id', () => {
const [transaction, error] = instance
.transaction('GET /api')
.timestamp(timestamp)
.errors(instance.error('test error').timestamp(timestamp))
.serialize();

const keys = ['transaction.id', 'trace.id', 'transaction.type'];

expect(pick(error, keys)).toEqual({
'transaction.id': transaction['transaction.id'],
'trace.id': transaction['trace.id'],
'transaction.type': 'request',
});
});

it('sets the error grouping key', () => {
const [, error] = instance
.transaction('GET /api')
.timestamp(timestamp)
.errors(instance.error('test error').timestamp(timestamp))
.serialize();

expect(error['error.grouping_name']).toEqual('test error');
expect(error['error.grouping_key']).toMatchInlineSnapshot(`"8b96fa10a7f85a5d960198627bf50840"`);
});
});
Loading