Skip to content

Commit

Permalink
[APM] generator: support error events and application metrics (elasti…
Browse files Browse the repository at this point in the history
  • Loading branch information
dgieselaar authored Oct 18, 2021
1 parent c6be6c0 commit 3cfa21d
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 27 deletions.
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

0 comments on commit 3cfa21d

Please sign in to comment.