diff --git a/packages/elastic-apm-generator/src/lib/apm_error.ts b/packages/elastic-apm-generator/src/lib/apm_error.ts
new file mode 100644
index 0000000000000..5a48093a26db2
--- /dev/null
+++ b/packages/elastic-apm-generator/src/lib/apm_error.ts
@@ -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];
+  }
+}
diff --git a/packages/elastic-apm-generator/src/lib/base_span.ts b/packages/elastic-apm-generator/src/lib/base_span.ts
index 6288c16d339b6..f762bf730a717 100644
--- a/packages/elastic-apm-generator/src/lib/base_span.ts
+++ b/packages/elastic-apm-generator/src/lib/base_span.ts
@@ -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[] = [];
@@ -19,7 +19,7 @@ export class BaseSpan extends Serializable {
     super({
       ...fields,
       'event.outcome': 'unknown',
-      'trace.id': generateTraceId(),
+      'trace.id': generateLongId(),
       'processor.name': 'transaction',
     });
   }
diff --git a/packages/elastic-apm-generator/src/lib/entity.ts b/packages/elastic-apm-generator/src/lib/entity.ts
index 2a4beee652cf7..bf8fc10efd3a7 100644
--- a/packages/elastic-apm-generator/src/lib/entity.ts
+++ b/packages/elastic-apm-generator/src/lib/entity.ts
@@ -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;
@@ -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;
@@ -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) {
diff --git a/packages/elastic-apm-generator/src/lib/instance.ts b/packages/elastic-apm-generator/src/lib/instance.ts
index 4218a9e23f4b4..3570f497c9055 100644
--- a/packages/elastic-apm-generator/src/lib/instance.ts
+++ b/packages/elastic-apm-generator/src/lib/instance.ts
@@ -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';
 
@@ -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,
+    });
+  }
 }
diff --git a/packages/elastic-apm-generator/src/lib/metricset.ts b/packages/elastic-apm-generator/src/lib/metricset.ts
index f7abec6fde958..c1ebbea313123 100644
--- a/packages/elastic-apm-generator/src/lib/metricset.ts
+++ b/packages/elastic-apm-generator/src/lib/metricset.ts
@@ -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,
+    });
+  }
 }
diff --git a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts
index b4cae1b41b9a6..d90ce8e01f83d 100644
--- a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts
+++ b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts
@@ -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']}`,
diff --git a/packages/elastic-apm-generator/src/lib/span.ts b/packages/elastic-apm-generator/src/lib/span.ts
index 36f7f44816d01..3c8d90f56b78e 100644
--- a/packages/elastic-apm-generator/src/lib/span.ts
+++ b/packages/elastic-apm-generator/src/lib/span.ts
@@ -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(),
     });
   }
 
diff --git a/packages/elastic-apm-generator/src/lib/transaction.ts b/packages/elastic-apm-generator/src/lib/transaction.ts
index f615f46710996..3a8d32e1843f8 100644
--- a/packages/elastic-apm-generator/src/lib/transaction.ts
+++ b/packages/elastic-apm-generator/src/lib/transaction.ts
@@ -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;
@@ -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);
   }
 }
diff --git a/packages/elastic-apm-generator/src/lib/utils/generate_id.ts b/packages/elastic-apm-generator/src/lib/utils/generate_id.ts
index 6c8b33fc19077..cc372a56209aa 100644
--- a/packages/elastic-apm-generator/src/lib/utils/generate_id.ts
+++ b/packages/elastic-apm-generator/src/lib/utils/generate_id.ts
@@ -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);
 }
diff --git a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts
index 7aae2986919c8..f6aad154532c2 100644
--- a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts
+++ b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts
@@ -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)
@@ -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),
diff --git a/packages/elastic-apm-generator/src/test/scenarios/05_transactions_with_errors.test.ts b/packages/elastic-apm-generator/src/test/scenarios/05_transactions_with_errors.test.ts
new file mode 100644
index 0000000000000..289fdfa6cf565
--- /dev/null
+++ b/packages/elastic-apm-generator/src/test/scenarios/05_transactions_with_errors.test.ts
@@ -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"`);
+  });
+});
diff --git a/packages/elastic-apm-generator/src/test/scenarios/06_application_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/06_application_metrics.test.ts
new file mode 100644
index 0000000000000..59ca8f0edbe88
--- /dev/null
+++ b/packages/elastic-apm-generator/src/test/scenarios/06_application_metrics.test.ts
@@ -0,0 +1,64 @@
+/*
+ * 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('application metrics', () => {
+  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 application metricsets', () => {
+    const events = instance
+      .appMetrics({
+        'system.memory.actual.free': 80,
+        'system.memory.total': 100,
+      })
+      .timestamp(timestamp)
+      .serialize();
+
+    const appMetrics = events.filter((event) => event['processor.event'] === 'metric');
+
+    expect(appMetrics.length).toEqual(1);
+
+    expect(
+      pick(
+        appMetrics[0],
+        '@timestamp',
+        'agent.name',
+        'container.id',
+        'metricset.name',
+        'processor.event',
+        'processor.name',
+        'service.environment',
+        'service.name',
+        'service.node.name',
+        'system.memory.actual.free',
+        'system.memory.total'
+      )
+    ).toEqual({
+      '@timestamp': timestamp,
+      'metricset.name': 'app',
+      'processor.event': 'metric',
+      'processor.name': 'metric',
+      'system.memory.actual.free': 80,
+      'system.memory.total': 100,
+      ...pick(
+        instance.fields,
+        'agent.name',
+        'container.id',
+        'service.environment',
+        'service.name',
+        'service.node.name'
+      ),
+    });
+  });
+});