@drarzter/kafka-client 0.9.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +625 -8
- package/dist/chunk-CMO7SMVK.mjs +4814 -0
- package/dist/chunk-CMO7SMVK.mjs.map +1 -0
- package/dist/cli/dlq.d.ts +119 -0
- package/dist/cli/dlq.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/{chunk-TPIP5VV7.mjs → cli/index.js} +965 -265
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +355 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/config/from-env.d.ts +188 -0
- package/dist/client/config/from-env.d.ts.map +1 -0
- package/dist/client/config/index.d.ts +2 -0
- package/dist/client/config/index.d.ts.map +1 -0
- package/dist/client/errors.d.ts +67 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/kafka.client/admin/ops.d.ts +114 -0
- package/dist/client/kafka.client/admin/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/delayed.d.ts +24 -0
- package/dist/client/kafka.client/consumer/features/delayed.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts +52 -0
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/routed.d.ts +4 -0
- package/dist/client/kafka.client/consumer/features/routed.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/snapshot.d.ts +10 -0
- package/dist/client/kafka.client/consumer/features/snapshot.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/window.d.ts +5 -0
- package/dist/client/kafka.client/consumer/features/window.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/handler.d.ts +149 -0
- package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/ops.d.ts +51 -0
- package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/pipeline.d.ts +167 -0
- package/dist/client/kafka.client/consumer/pipeline.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/queue.d.ts +37 -0
- package/dist/client/kafka.client/consumer/queue.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/retry-topic.d.ts +65 -0
- package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/setup.d.ts +63 -0
- package/dist/client/kafka.client/consumer/setup.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/start.d.ts +7 -0
- package/dist/client/kafka.client/consumer/start.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/stop.d.ts +19 -0
- package/dist/client/kafka.client/consumer/stop.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/subscribe-retry.d.ts +4 -0
- package/dist/client/kafka.client/consumer/subscribe-retry.d.ts.map +1 -0
- package/dist/client/kafka.client/context.d.ts +72 -0
- package/dist/client/kafka.client/context.d.ts.map +1 -0
- package/dist/client/kafka.client/index.d.ts +155 -0
- package/dist/client/kafka.client/index.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts +61 -0
- package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/dedup.store.d.ts +28 -0
- package/dist/client/kafka.client/infra/dedup.store.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/inflight.tracker.d.ts +22 -0
- package/dist/client/kafka.client/infra/inflight.tracker.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/metrics.manager.d.ts +67 -0
- package/dist/client/kafka.client/infra/metrics.manager.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/lifecycle.d.ts +41 -0
- package/dist/client/kafka.client/producer/lifecycle.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/ops.d.ts +70 -0
- package/dist/client/kafka.client/producer/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/send.d.ts +21 -0
- package/dist/client/kafka.client/producer/send.d.ts.map +1 -0
- package/dist/client/kafka.client/validate-options.d.ts +11 -0
- package/dist/client/kafka.client/validate-options.d.ts.map +1 -0
- package/dist/client/message/envelope.d.ts +105 -0
- package/dist/client/message/envelope.d.ts.map +1 -0
- package/dist/client/message/schema-registry.d.ts +105 -0
- package/dist/client/message/schema-registry.d.ts.map +1 -0
- package/dist/client/message/topic.d.ts +138 -0
- package/dist/client/message/topic.d.ts.map +1 -0
- package/dist/client/message/versioned-schema.d.ts +53 -0
- package/dist/client/message/versioned-schema.d.ts.map +1 -0
- package/dist/client/outbox/index.d.ts +4 -0
- package/dist/client/outbox/index.d.ts.map +1 -0
- package/dist/client/outbox/outbox.relay.d.ts +90 -0
- package/dist/client/outbox/outbox.relay.d.ts.map +1 -0
- package/dist/client/outbox/outbox.store.d.ts +42 -0
- package/dist/client/outbox/outbox.store.d.ts.map +1 -0
- package/dist/client/outbox/outbox.types.d.ts +144 -0
- package/dist/client/outbox/outbox.types.d.ts.map +1 -0
- package/dist/client/security/acl.d.ts +108 -0
- package/dist/client/security/acl.d.ts.map +1 -0
- package/dist/client/security/index.d.ts +5 -0
- package/dist/client/security/index.d.ts.map +1 -0
- package/dist/client/security/providers.d.ts +88 -0
- package/dist/client/security/providers.d.ts.map +1 -0
- package/dist/client/security/resolve-security.d.ts +19 -0
- package/dist/client/security/resolve-security.d.ts.map +1 -0
- package/dist/client/security/security.types.d.ts +76 -0
- package/dist/client/security/security.types.d.ts.map +1 -0
- package/dist/client/transport/confluent.transport.d.ts +32 -0
- package/dist/client/transport/confluent.transport.d.ts.map +1 -0
- package/dist/client/transport/transport.interface.d.ts +216 -0
- package/dist/client/transport/transport.interface.d.ts.map +1 -0
- package/dist/client/types/admin.interface.d.ts +174 -0
- package/dist/client/types/admin.interface.d.ts.map +1 -0
- package/dist/client/types/admin.types.d.ts +140 -0
- package/dist/client/types/admin.types.d.ts.map +1 -0
- package/dist/client/types/client.d.ts +21 -0
- package/dist/client/types/client.d.ts.map +1 -0
- package/dist/client/types/common.d.ts +84 -0
- package/dist/client/types/common.d.ts.map +1 -0
- package/dist/client/types/config.types.d.ts +150 -0
- package/dist/client/types/config.types.d.ts.map +1 -0
- package/dist/client/types/consumer.interface.d.ts +115 -0
- package/dist/client/types/consumer.interface.d.ts.map +1 -0
- package/dist/{consumer.types-fFCag3VJ.d.mts → client/types/consumer.types.d.ts} +62 -383
- package/dist/client/types/consumer.types.d.ts.map +1 -0
- package/dist/client/types/dedup.types.d.ts +50 -0
- package/dist/client/types/dedup.types.d.ts.map +1 -0
- package/dist/client/types/lifecycle.interface.d.ts +72 -0
- package/dist/client/types/lifecycle.interface.d.ts.map +1 -0
- package/dist/client/types/producer.interface.d.ts +52 -0
- package/dist/client/types/producer.interface.d.ts.map +1 -0
- package/dist/client/types/producer.types.d.ts +90 -0
- package/dist/client/types/producer.types.d.ts.map +1 -0
- package/dist/client/types.d.ts +8 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/core.d.ts +10 -314
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1326 -74
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +39 -3
- package/dist/index.d.ts +7 -128
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1343 -74
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +56 -3
- package/dist/index.mjs.map +1 -1
- package/dist/nest/kafka.constants.d.ts +5 -0
- package/dist/nest/kafka.constants.d.ts.map +1 -0
- package/dist/nest/kafka.decorator.d.ts +49 -0
- package/dist/nest/kafka.decorator.d.ts.map +1 -0
- package/dist/nest/kafka.explorer.d.ts +17 -0
- package/dist/nest/kafka.explorer.d.ts.map +1 -0
- package/dist/nest/kafka.health.d.ts +7 -0
- package/dist/nest/kafka.health.d.ts.map +1 -0
- package/dist/nest/kafka.module.d.ts +61 -0
- package/dist/nest/kafka.module.d.ts.map +1 -0
- package/dist/otel.d.ts +83 -5
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +100 -6
- package/dist/otel.js.map +1 -1
- package/dist/otel.mjs +98 -5
- package/dist/otel.mjs.map +1 -1
- package/dist/testing/client.mock.d.ts +47 -0
- package/dist/testing/client.mock.d.ts.map +1 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/test.container.d.ts +63 -0
- package/dist/testing/test.container.d.ts.map +1 -0
- package/dist/{testing.d.mts → testing/transport.fake.d.ts} +7 -111
- package/dist/testing/transport.fake.d.ts.map +1 -0
- package/dist/testing.d.ts +2 -318
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +28 -2
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +28 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +22 -9
- package/dist/chunk-TPIP5VV7.mjs.map +0 -1
- package/dist/client-CBBUDDtu.d.ts +0 -751
- package/dist/client-D-SxYV2b.d.mts +0 -751
- package/dist/consumer.types-fFCag3VJ.d.ts +0 -958
- package/dist/core.d.mts +0 -314
- package/dist/index.d.mts +0 -128
- package/dist/otel.d.mts +0 -27
package/dist/otel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { BeforeConsumeResult, KafkaInstrumentation } from \"./client/types\";\nimport type { EventEnvelope } from \"./client/message/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n const activeSpans = new Map<string, ReturnType<typeof tracer.startSpan>>();\n\n return {\n beforeSend(_topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n const spanCtx = trace.setSpan(parentCtx, span);\n activeSpans.set(envelope.eventId, span);\n return {\n cleanup() {\n span.end();\n activeSpans.delete(envelope.eventId);\n },\n wrap(fn) {\n return context.with(spanCtx, fn);\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = activeSpans.get(envelope.eventId);\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAMO;AA0BA,SAAS,sBAA4C;AAC1D,QAAM,SAAS,iBAAM,UAAU,wBAAwB;AACvD,QAAM,cAAc,oBAAI,IAAiD;AAEzE,SAAO;AAAA,IACL,WAAW,QAAgB,SAAiC;AAC1D,6BAAY,OAAO,mBAAQ,OAAO,GAAG,OAAO;AAAA,IAC9C;AAAA,IAEA,UAAU,QAAgB;AAAA,IAI1B;AAAA,IAEA,cAAc,UAAmD;AAC/D,YAAM,YAAY,uBAAY,QAAQ,mBAAQ,OAAO,GAAG,SAAS,OAAO;AACxE,YAAM,OAAO,OAAO;AAAA,QAClB,iBAAiB,SAAS,KAAK;AAAA,QAC/B;AAAA,UACE,MAAM,oBAAS;AAAA,UACf,YAAY;AAAA,YACV,oBAAoB;AAAA,YACpB,8BAA8B,SAAS;AAAA,YACvC,wBAAwB,SAAS;AAAA,YACjC,6BAA6B,SAAS;AAAA,YACtC,0BAA0B,SAAS;AAAA,UACrC;AAAA,QACF;AAAA,QACA;AAAA,MACF;AACA,YAAM,UAAU,iBAAM,QAAQ,WAAW,IAAI;AAC7C,kBAAY,IAAI,SAAS,SAAS,IAAI;AACtC,aAAO;AAAA,QACL,UAAU;AACR,eAAK,IAAI;AACT,sBAAY,OAAO,SAAS,OAAO;AAAA,QACrC;AAAA,QACA,KAAK,IAAI;AACP,iBAAO,mBAAQ,KAAK,SAAS,EAAE;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAAA,IAEA,eAAe,UAA8B,OAAc;AACzD,YAAM,OAAO,YAAY,IAAI,SAAS,OAAO;AAC7C,UAAI,MAAM;AACR,aAAK,UAAU,EAAE,MAAM,0BAAe,OAAO,SAAS,MAAM,QAAQ,CAAC;AACrE,aAAK,gBAAgB,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n metrics,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { Meter, ObservableResult } from \"@opentelemetry/api\";\nimport type { BeforeConsumeResult, KafkaInstrumentation } from \"./client/types\";\nimport type { IKafkaAdmin } from \"./client/types/admin.interface\";\nimport type { TopicMapConstraint } from \"./client/types/common\";\nimport type { EventEnvelope } from \"./client/message/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n // Keyed by envelope object identity (not eventId) so two in-flight messages\n // that share an eventId cannot overwrite each other's span. WeakMap also\n // guarantees no leak if a cleanup path is ever missed.\n const activeSpans = new WeakMap<\n EventEnvelope<any>,\n ReturnType<typeof tracer.startSpan>\n >();\n\n return {\n beforeSend(_topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n const spanCtx = trace.setSpan(parentCtx, span);\n activeSpans.set(envelope, span);\n return {\n cleanup() {\n span.end();\n activeSpans.delete(envelope);\n },\n wrap(fn) {\n return context.with(spanCtx, fn);\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = activeSpans.get(envelope);\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n\n/**\n * Create a `KafkaInstrumentation` that records OpenTelemetry **metrics** for\n * both the send and consume paths.\n *\n * Requires `@opentelemetry/api` as a peer dependency. Instruments are created\n * once per instrumentation instance (not per message), so a single call to this\n * factory registers all counters/histograms exactly once.\n *\n * Recorded instruments (all under meter `@drarzter/kafka-client`):\n *\n * | Instrument | Type | Attributes | Recorded in |\n * |---|---|---|---|\n * | `kafka.client.messages.sent` | Counter | `topic` | `afterSend` |\n * | `kafka.client.messages.processed` | Counter | `topic` | `onMessage` |\n * | `kafka.client.messages.retried` | Counter | `topic` | `onRetry` |\n * | `kafka.client.messages.dlq` | Counter | `topic`, `reason` | `onDlq` |\n * | `kafka.client.messages.duplicate` | Counter | `topic`, `strategy` | `onDuplicate` |\n * | `kafka.client.consume.errors` | Counter | `topic` | `onConsumeError` |\n * | `kafka.client.consume.duration` | Histogram (ms) | `topic` | `beforeConsume` → `cleanup()` |\n *\n * Composes with `otelInstrumentation()` (traces): list both in\n * `instrumentation`. They share nothing and can be added in any order.\n *\n * @param options.meter - Override the meter used to create instruments.\n * Defaults to `metrics.getMeter(\"@drarzter/kafka-client\")`.\n *\n * @example\n * ```ts\n * import {\n * otelInstrumentation,\n * otelMetricsInstrumentation,\n * } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation(), otelMetricsInstrumentation()],\n * });\n * ```\n */\nexport function otelMetricsInstrumentation(options?: {\n meter?: Meter;\n}): KafkaInstrumentation {\n const meter = options?.meter ?? metrics.getMeter(\"@drarzter/kafka-client\");\n\n const sentCounter = meter.createCounter(\"kafka.client.messages.sent\", {\n description: \"Number of messages successfully sent to Kafka.\",\n });\n const processedCounter = meter.createCounter(\n \"kafka.client.messages.processed\",\n {\n description: \"Number of messages successfully processed by a consumer.\",\n },\n );\n const retriedCounter = meter.createCounter(\"kafka.client.messages.retried\", {\n description: \"Number of messages queued for retry.\",\n });\n const dlqCounter = meter.createCounter(\"kafka.client.messages.dlq\", {\n description: \"Number of messages routed to a DLQ topic.\",\n });\n const duplicateCounter = meter.createCounter(\n \"kafka.client.messages.duplicate\",\n {\n description: \"Number of Lamport-clock duplicate messages detected.\",\n },\n );\n const consumeErrorsCounter = meter.createCounter(\n \"kafka.client.consume.errors\",\n {\n description: \"Number of consumer handler errors.\",\n },\n );\n const consumeDuration = meter.createHistogram(\n \"kafka.client.consume.duration\",\n {\n description: \"Consumer handler duration.\",\n unit: \"ms\",\n },\n );\n\n return {\n afterSend(topic: string) {\n sentCounter.add(1, { topic });\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const topic = envelope.topic;\n const start = Date.now();\n return {\n cleanup() {\n consumeDuration.record(Date.now() - start, { topic });\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, _error: Error) {\n consumeErrorsCounter.add(1, { topic: envelope.topic });\n },\n\n onRetry(envelope: EventEnvelope<any>, _attempt: number, _max: number) {\n retriedCounter.add(1, { topic: envelope.topic });\n },\n\n onDlq(envelope: EventEnvelope<any>, reason: string) {\n dlqCounter.add(1, { topic: envelope.topic, reason });\n },\n\n onDuplicate(\n envelope: EventEnvelope<any>,\n strategy: \"drop\" | \"dlq\" | \"topic\",\n ) {\n duplicateCounter.add(1, { topic: envelope.topic, strategy });\n },\n\n onMessage(envelope: EventEnvelope<any>) {\n processedCounter.add(1, { topic: envelope.topic });\n },\n };\n}\n\n/**\n * Register an OpenTelemetry **ObservableGauge** `kafka.client.consumer.lag`\n * that reports per-partition consumer lag by polling `kafka.getConsumerLag()`\n * on each metric collection cycle.\n *\n * Requires `@opentelemetry/api` as a peer dependency. Accepts any object\n * implementing the `IKafkaAdmin` sub-interface (i.e. any `KafkaClient`).\n *\n * The async callback swallows errors silently — a broker query failure during\n * a collection cycle simply reports no lag samples for that cycle rather than\n * throwing inside the OTel metric reader.\n *\n * Gauge attributes: `topic`, `partition`, and `groupId` (empty string when the\n * client's default group is used).\n *\n * @param kafka - The client (or any `IKafkaAdmin`) to poll for lag.\n * @param options.meter - Override the meter. Defaults to\n * `metrics.getMeter(\"@drarzter/kafka-client\")`.\n * @param options.groupId - Consumer group to query. Defaults to the client's\n * constructor group.\n * @returns An unregister function that removes the observable callback. Call it\n * on shutdown to stop observing.\n *\n * @example\n * ```ts\n * import { otelLagGauge } from '@drarzter/kafka-client/otel';\n *\n * const unregister = otelLagGauge(kafka, { groupId: 'billing-service' });\n * // ...later, on shutdown:\n * unregister();\n * ```\n */\nexport function otelLagGauge<T extends TopicMapConstraint<T>>(\n kafka: IKafkaAdmin<T>,\n options?: { meter?: Meter; groupId?: string },\n): () => void {\n const meter = options?.meter ?? metrics.getMeter(\"@drarzter/kafka-client\");\n const groupId = options?.groupId;\n\n const gauge = meter.createObservableGauge(\"kafka.client.consumer.lag\", {\n description: \"Consumer group lag per topic partition.\",\n });\n\n const callback = async (result: ObservableResult) => {\n try {\n const lag = await kafka.getConsumerLag(groupId);\n for (const entry of lag) {\n result.observe(entry.lag, {\n topic: entry.topic,\n partition: entry.partition,\n groupId: groupId ?? \"\",\n });\n }\n } catch {\n // Swallow — a failed lag query should never break metric collection.\n }\n };\n\n gauge.addCallback(callback);\n\n return () => {\n gauge.removeCallback(callback);\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/otel.mjs
CHANGED
|
@@ -5,12 +5,13 @@ import {
|
|
|
5
5
|
trace,
|
|
6
6
|
context,
|
|
7
7
|
propagation,
|
|
8
|
+
metrics,
|
|
8
9
|
SpanKind,
|
|
9
10
|
SpanStatusCode
|
|
10
11
|
} from "@opentelemetry/api";
|
|
11
12
|
function otelInstrumentation() {
|
|
12
13
|
const tracer = trace.getTracer("@drarzter/kafka-client");
|
|
13
|
-
const activeSpans = /* @__PURE__ */ new
|
|
14
|
+
const activeSpans = /* @__PURE__ */ new WeakMap();
|
|
14
15
|
return {
|
|
15
16
|
beforeSend(_topic, headers) {
|
|
16
17
|
propagation.inject(context.active(), headers);
|
|
@@ -34,11 +35,11 @@ function otelInstrumentation() {
|
|
|
34
35
|
parentCtx
|
|
35
36
|
);
|
|
36
37
|
const spanCtx = trace.setSpan(parentCtx, span);
|
|
37
|
-
activeSpans.set(envelope
|
|
38
|
+
activeSpans.set(envelope, span);
|
|
38
39
|
return {
|
|
39
40
|
cleanup() {
|
|
40
41
|
span.end();
|
|
41
|
-
activeSpans.delete(envelope
|
|
42
|
+
activeSpans.delete(envelope);
|
|
42
43
|
},
|
|
43
44
|
wrap(fn) {
|
|
44
45
|
return context.with(spanCtx, fn);
|
|
@@ -46,7 +47,7 @@ function otelInstrumentation() {
|
|
|
46
47
|
};
|
|
47
48
|
},
|
|
48
49
|
onConsumeError(envelope, error) {
|
|
49
|
-
const span = activeSpans.get(envelope
|
|
50
|
+
const span = activeSpans.get(envelope);
|
|
50
51
|
if (span) {
|
|
51
52
|
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
52
53
|
span.recordException(error);
|
|
@@ -54,7 +55,99 @@ function otelInstrumentation() {
|
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
57
|
}
|
|
58
|
+
function otelMetricsInstrumentation(options) {
|
|
59
|
+
const meter = options?.meter ?? metrics.getMeter("@drarzter/kafka-client");
|
|
60
|
+
const sentCounter = meter.createCounter("kafka.client.messages.sent", {
|
|
61
|
+
description: "Number of messages successfully sent to Kafka."
|
|
62
|
+
});
|
|
63
|
+
const processedCounter = meter.createCounter(
|
|
64
|
+
"kafka.client.messages.processed",
|
|
65
|
+
{
|
|
66
|
+
description: "Number of messages successfully processed by a consumer."
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
const retriedCounter = meter.createCounter("kafka.client.messages.retried", {
|
|
70
|
+
description: "Number of messages queued for retry."
|
|
71
|
+
});
|
|
72
|
+
const dlqCounter = meter.createCounter("kafka.client.messages.dlq", {
|
|
73
|
+
description: "Number of messages routed to a DLQ topic."
|
|
74
|
+
});
|
|
75
|
+
const duplicateCounter = meter.createCounter(
|
|
76
|
+
"kafka.client.messages.duplicate",
|
|
77
|
+
{
|
|
78
|
+
description: "Number of Lamport-clock duplicate messages detected."
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
const consumeErrorsCounter = meter.createCounter(
|
|
82
|
+
"kafka.client.consume.errors",
|
|
83
|
+
{
|
|
84
|
+
description: "Number of consumer handler errors."
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
const consumeDuration = meter.createHistogram(
|
|
88
|
+
"kafka.client.consume.duration",
|
|
89
|
+
{
|
|
90
|
+
description: "Consumer handler duration.",
|
|
91
|
+
unit: "ms"
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
return {
|
|
95
|
+
afterSend(topic) {
|
|
96
|
+
sentCounter.add(1, { topic });
|
|
97
|
+
},
|
|
98
|
+
beforeConsume(envelope) {
|
|
99
|
+
const topic = envelope.topic;
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
return {
|
|
102
|
+
cleanup() {
|
|
103
|
+
consumeDuration.record(Date.now() - start, { topic });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
onConsumeError(envelope, _error) {
|
|
108
|
+
consumeErrorsCounter.add(1, { topic: envelope.topic });
|
|
109
|
+
},
|
|
110
|
+
onRetry(envelope, _attempt, _max) {
|
|
111
|
+
retriedCounter.add(1, { topic: envelope.topic });
|
|
112
|
+
},
|
|
113
|
+
onDlq(envelope, reason) {
|
|
114
|
+
dlqCounter.add(1, { topic: envelope.topic, reason });
|
|
115
|
+
},
|
|
116
|
+
onDuplicate(envelope, strategy) {
|
|
117
|
+
duplicateCounter.add(1, { topic: envelope.topic, strategy });
|
|
118
|
+
},
|
|
119
|
+
onMessage(envelope) {
|
|
120
|
+
processedCounter.add(1, { topic: envelope.topic });
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function otelLagGauge(kafka, options) {
|
|
125
|
+
const meter = options?.meter ?? metrics.getMeter("@drarzter/kafka-client");
|
|
126
|
+
const groupId = options?.groupId;
|
|
127
|
+
const gauge = meter.createObservableGauge("kafka.client.consumer.lag", {
|
|
128
|
+
description: "Consumer group lag per topic partition."
|
|
129
|
+
});
|
|
130
|
+
const callback = async (result) => {
|
|
131
|
+
try {
|
|
132
|
+
const lag = await kafka.getConsumerLag(groupId);
|
|
133
|
+
for (const entry of lag) {
|
|
134
|
+
result.observe(entry.lag, {
|
|
135
|
+
topic: entry.topic,
|
|
136
|
+
partition: entry.partition,
|
|
137
|
+
groupId: groupId ?? ""
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
gauge.addCallback(callback);
|
|
144
|
+
return () => {
|
|
145
|
+
gauge.removeCallback(callback);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
57
148
|
export {
|
|
58
|
-
otelInstrumentation
|
|
149
|
+
otelInstrumentation,
|
|
150
|
+
otelLagGauge,
|
|
151
|
+
otelMetricsInstrumentation
|
|
59
152
|
};
|
|
60
153
|
//# sourceMappingURL=otel.mjs.map
|
package/dist/otel.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { BeforeConsumeResult, KafkaInstrumentation } from \"./client/types\";\nimport type { EventEnvelope } from \"./client/message/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n const activeSpans = new Map<string, ReturnType<typeof tracer.startSpan>>();\n\n return {\n beforeSend(_topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n const spanCtx = trace.setSpan(parentCtx, span);\n activeSpans.set(envelope.eventId, span);\n return {\n cleanup() {\n span.end();\n activeSpans.delete(envelope.eventId);\n },\n wrap(fn) {\n return context.with(spanCtx, fn);\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = activeSpans.get(envelope.eventId);\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n"],"mappings":";;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA0BA,SAAS,sBAA4C;AAC1D,QAAM,SAAS,MAAM,UAAU,wBAAwB;AACvD,QAAM,cAAc,oBAAI,IAAiD;AAEzE,SAAO;AAAA,IACL,WAAW,QAAgB,SAAiC;AAC1D,kBAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;AAAA,IAC9C;AAAA,IAEA,UAAU,QAAgB;AAAA,IAI1B;AAAA,IAEA,cAAc,UAAmD;AAC/D,YAAM,YAAY,YAAY,QAAQ,QAAQ,OAAO,GAAG,SAAS,OAAO;AACxE,YAAM,OAAO,OAAO;AAAA,QAClB,iBAAiB,SAAS,KAAK;AAAA,QAC/B;AAAA,UACE,MAAM,SAAS;AAAA,UACf,YAAY;AAAA,YACV,oBAAoB;AAAA,YACpB,8BAA8B,SAAS;AAAA,YACvC,wBAAwB,SAAS;AAAA,YACjC,6BAA6B,SAAS;AAAA,YACtC,0BAA0B,SAAS;AAAA,UACrC;AAAA,QACF;AAAA,QACA;AAAA,MACF;AACA,YAAM,UAAU,MAAM,QAAQ,WAAW,IAAI;AAC7C,kBAAY,IAAI,SAAS,SAAS,IAAI;AACtC,aAAO;AAAA,QACL,UAAU;AACR,eAAK,IAAI;AACT,sBAAY,OAAO,SAAS,OAAO;AAAA,QACrC;AAAA,QACA,KAAK,IAAI;AACP,iBAAO,QAAQ,KAAK,SAAS,EAAE;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAAA,IAEA,eAAe,UAA8B,OAAc;AACzD,YAAM,OAAO,YAAY,IAAI,SAAS,OAAO;AAC7C,UAAI,MAAM;AACR,aAAK,UAAU,EAAE,MAAM,eAAe,OAAO,SAAS,MAAM,QAAQ,CAAC;AACrE,aAAK,gBAAgB,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n metrics,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { Meter, ObservableResult } from \"@opentelemetry/api\";\nimport type { BeforeConsumeResult, KafkaInstrumentation } from \"./client/types\";\nimport type { IKafkaAdmin } from \"./client/types/admin.interface\";\nimport type { TopicMapConstraint } from \"./client/types/common\";\nimport type { EventEnvelope } from \"./client/message/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n // Keyed by envelope object identity (not eventId) so two in-flight messages\n // that share an eventId cannot overwrite each other's span. WeakMap also\n // guarantees no leak if a cleanup path is ever missed.\n const activeSpans = new WeakMap<\n EventEnvelope<any>,\n ReturnType<typeof tracer.startSpan>\n >();\n\n return {\n beforeSend(_topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n const spanCtx = trace.setSpan(parentCtx, span);\n activeSpans.set(envelope, span);\n return {\n cleanup() {\n span.end();\n activeSpans.delete(envelope);\n },\n wrap(fn) {\n return context.with(spanCtx, fn);\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = activeSpans.get(envelope);\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n\n/**\n * Create a `KafkaInstrumentation` that records OpenTelemetry **metrics** for\n * both the send and consume paths.\n *\n * Requires `@opentelemetry/api` as a peer dependency. Instruments are created\n * once per instrumentation instance (not per message), so a single call to this\n * factory registers all counters/histograms exactly once.\n *\n * Recorded instruments (all under meter `@drarzter/kafka-client`):\n *\n * | Instrument | Type | Attributes | Recorded in |\n * |---|---|---|---|\n * | `kafka.client.messages.sent` | Counter | `topic` | `afterSend` |\n * | `kafka.client.messages.processed` | Counter | `topic` | `onMessage` |\n * | `kafka.client.messages.retried` | Counter | `topic` | `onRetry` |\n * | `kafka.client.messages.dlq` | Counter | `topic`, `reason` | `onDlq` |\n * | `kafka.client.messages.duplicate` | Counter | `topic`, `strategy` | `onDuplicate` |\n * | `kafka.client.consume.errors` | Counter | `topic` | `onConsumeError` |\n * | `kafka.client.consume.duration` | Histogram (ms) | `topic` | `beforeConsume` → `cleanup()` |\n *\n * Composes with `otelInstrumentation()` (traces): list both in\n * `instrumentation`. They share nothing and can be added in any order.\n *\n * @param options.meter - Override the meter used to create instruments.\n * Defaults to `metrics.getMeter(\"@drarzter/kafka-client\")`.\n *\n * @example\n * ```ts\n * import {\n * otelInstrumentation,\n * otelMetricsInstrumentation,\n * } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation(), otelMetricsInstrumentation()],\n * });\n * ```\n */\nexport function otelMetricsInstrumentation(options?: {\n meter?: Meter;\n}): KafkaInstrumentation {\n const meter = options?.meter ?? metrics.getMeter(\"@drarzter/kafka-client\");\n\n const sentCounter = meter.createCounter(\"kafka.client.messages.sent\", {\n description: \"Number of messages successfully sent to Kafka.\",\n });\n const processedCounter = meter.createCounter(\n \"kafka.client.messages.processed\",\n {\n description: \"Number of messages successfully processed by a consumer.\",\n },\n );\n const retriedCounter = meter.createCounter(\"kafka.client.messages.retried\", {\n description: \"Number of messages queued for retry.\",\n });\n const dlqCounter = meter.createCounter(\"kafka.client.messages.dlq\", {\n description: \"Number of messages routed to a DLQ topic.\",\n });\n const duplicateCounter = meter.createCounter(\n \"kafka.client.messages.duplicate\",\n {\n description: \"Number of Lamport-clock duplicate messages detected.\",\n },\n );\n const consumeErrorsCounter = meter.createCounter(\n \"kafka.client.consume.errors\",\n {\n description: \"Number of consumer handler errors.\",\n },\n );\n const consumeDuration = meter.createHistogram(\n \"kafka.client.consume.duration\",\n {\n description: \"Consumer handler duration.\",\n unit: \"ms\",\n },\n );\n\n return {\n afterSend(topic: string) {\n sentCounter.add(1, { topic });\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const topic = envelope.topic;\n const start = Date.now();\n return {\n cleanup() {\n consumeDuration.record(Date.now() - start, { topic });\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, _error: Error) {\n consumeErrorsCounter.add(1, { topic: envelope.topic });\n },\n\n onRetry(envelope: EventEnvelope<any>, _attempt: number, _max: number) {\n retriedCounter.add(1, { topic: envelope.topic });\n },\n\n onDlq(envelope: EventEnvelope<any>, reason: string) {\n dlqCounter.add(1, { topic: envelope.topic, reason });\n },\n\n onDuplicate(\n envelope: EventEnvelope<any>,\n strategy: \"drop\" | \"dlq\" | \"topic\",\n ) {\n duplicateCounter.add(1, { topic: envelope.topic, strategy });\n },\n\n onMessage(envelope: EventEnvelope<any>) {\n processedCounter.add(1, { topic: envelope.topic });\n },\n };\n}\n\n/**\n * Register an OpenTelemetry **ObservableGauge** `kafka.client.consumer.lag`\n * that reports per-partition consumer lag by polling `kafka.getConsumerLag()`\n * on each metric collection cycle.\n *\n * Requires `@opentelemetry/api` as a peer dependency. Accepts any object\n * implementing the `IKafkaAdmin` sub-interface (i.e. any `KafkaClient`).\n *\n * The async callback swallows errors silently — a broker query failure during\n * a collection cycle simply reports no lag samples for that cycle rather than\n * throwing inside the OTel metric reader.\n *\n * Gauge attributes: `topic`, `partition`, and `groupId` (empty string when the\n * client's default group is used).\n *\n * @param kafka - The client (or any `IKafkaAdmin`) to poll for lag.\n * @param options.meter - Override the meter. Defaults to\n * `metrics.getMeter(\"@drarzter/kafka-client\")`.\n * @param options.groupId - Consumer group to query. Defaults to the client's\n * constructor group.\n * @returns An unregister function that removes the observable callback. Call it\n * on shutdown to stop observing.\n *\n * @example\n * ```ts\n * import { otelLagGauge } from '@drarzter/kafka-client/otel';\n *\n * const unregister = otelLagGauge(kafka, { groupId: 'billing-service' });\n * // ...later, on shutdown:\n * unregister();\n * ```\n */\nexport function otelLagGauge<T extends TopicMapConstraint<T>>(\n kafka: IKafkaAdmin<T>,\n options?: { meter?: Meter; groupId?: string },\n): () => void {\n const meter = options?.meter ?? metrics.getMeter(\"@drarzter/kafka-client\");\n const groupId = options?.groupId;\n\n const gauge = meter.createObservableGauge(\"kafka.client.consumer.lag\", {\n description: \"Consumer group lag per topic partition.\",\n });\n\n const callback = async (result: ObservableResult) => {\n try {\n const lag = await kafka.getConsumerLag(groupId);\n for (const entry of lag) {\n result.observe(entry.lag, {\n topic: entry.topic,\n partition: entry.partition,\n groupId: groupId ?? \"\",\n });\n }\n } catch {\n // Swallow — a failed lag query should never break metric collection.\n }\n };\n\n gauge.addCallback(callback);\n\n return () => {\n gauge.removeCallback(callback);\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { IKafkaClient, TopicMapConstraint } from "../client/types";
|
|
2
|
+
/**
|
|
3
|
+
* Fully typed mock of `IKafkaClient<T>` where every method is a mock function.
|
|
4
|
+
* Compatible with Jest, Vitest, or any framework whose `fn()` returns
|
|
5
|
+
* an object with `.mock`, `.mockResolvedValue`, etc.
|
|
6
|
+
*/
|
|
7
|
+
export type MockKafkaClient<T extends TopicMapConstraint<T>> = {
|
|
8
|
+
[K in keyof IKafkaClient<T>]: IKafkaClient<T>[K] & Record<string, any>;
|
|
9
|
+
} & {
|
|
10
|
+
/**
|
|
11
|
+
* Present on the concrete `KafkaClient` but not part of `IKafkaClient` —
|
|
12
|
+
* included so services typed against the class can still use the mock.
|
|
13
|
+
*/
|
|
14
|
+
disconnectProducer: (() => Promise<void>) & Record<string, any>;
|
|
15
|
+
/** NestJS lifecycle hook on the concrete `KafkaClient`. */
|
|
16
|
+
onModuleDestroy: (() => Promise<void>) & Record<string, any>;
|
|
17
|
+
};
|
|
18
|
+
/** Factory that creates a no-op mock function (e.g. `() => jest.fn()`). */
|
|
19
|
+
export type MockFactory = () => (...args: any[]) => any;
|
|
20
|
+
/**
|
|
21
|
+
* Create a fully typed mock implementing every `IKafkaClient<T>` method.
|
|
22
|
+
* Useful for unit-testing services that depend on `KafkaClient` without
|
|
23
|
+
* touching a real broker.
|
|
24
|
+
*
|
|
25
|
+
* Auto-detects Jest (`jest.fn()`) or Vitest (`vi.fn()`). Pass a custom
|
|
26
|
+
* `mockFactory` for other frameworks.
|
|
27
|
+
*
|
|
28
|
+
* All methods resolve to sensible defaults:
|
|
29
|
+
* - `checkStatus()` → `{ status: 'up', clientId: 'mock-client', topics: [] }`
|
|
30
|
+
* - `getClientId()` → `"mock-client"`
|
|
31
|
+
* - void methods → `undefined`
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const kafka = createMockKafkaClient<MyTopics>();
|
|
36
|
+
*
|
|
37
|
+
* const service = new OrdersService(kafka);
|
|
38
|
+
* await service.createOrder();
|
|
39
|
+
*
|
|
40
|
+
* expect(kafka.sendMessage).toHaveBeenCalledWith(
|
|
41
|
+
* 'order.created',
|
|
42
|
+
* expect.objectContaining({ orderId: '123' }),
|
|
43
|
+
* );
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function createMockKafkaClient<T extends TopicMapConstraint<T>>(mockFactory?: MockFactory): MockKafkaClient<T>;
|
|
47
|
+
//# sourceMappingURL=client.mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.mock.d.ts","sourceRoot":"","sources":["../../src/testing/client.mock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAExE;;;;GAIG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,kBAAkB,CAAC,CAAC,CAAC,IAAI;KAC5D,CAAC,IAAI,MAAM,YAAY,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;CACvE,GAAG;IACF;;;OAGG;IACH,kBAAkB,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChE,2DAA2D;IAC3D,eAAe,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9D,CAAC;AAEF,2EAA2E;AAC3E,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;AA2BxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,kBAAkB,CAAC,CAAC,CAAC,EACnE,WAAW,CAAC,EAAE,WAAW,GACxB,eAAe,CAAC,CAAC,CAAC,CAkFpB"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createMockKafkaClient, type MockKafkaClient } from "./client.mock";
|
|
2
|
+
export { KafkaTestContainer, type KafkaTestContainerOptions, } from "./test.container";
|
|
3
|
+
export { FakeTransport, FakeProducer, FakeConsumer, FakeAdmin, FakeTransaction, } from "./transport.fake";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,eAAe,GAChB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Options for `KafkaTestContainer`. */
|
|
2
|
+
export interface KafkaTestContainerOptions {
|
|
3
|
+
/** Docker image. Default: `"confluentinc/cp-kafka:7.7.0"`. */
|
|
4
|
+
image?: string;
|
|
5
|
+
/** Warm up the transactional coordinator on start. Default: `true`. */
|
|
6
|
+
transactionWarmup?: boolean;
|
|
7
|
+
/** Topics to pre-create. Each entry can be a string (1 partition) or `{ topic, numPartitions }`. */
|
|
8
|
+
topics?: Array<string | {
|
|
9
|
+
topic: string;
|
|
10
|
+
numPartitions?: number;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Thin wrapper around `@testcontainers/kafka` that starts a single-node
|
|
15
|
+
* KRaft Kafka container and exposes `brokers` for use with `KafkaClient`.
|
|
16
|
+
*
|
|
17
|
+
* Handles common setup pain points:
|
|
18
|
+
* - Transaction coordinator warmup (avoids transactional producer hangs)
|
|
19
|
+
* - Topic pre-creation (avoids race conditions)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const container = new KafkaTestContainer({ topics: ['orders', 'payments'] });
|
|
24
|
+
* const brokers = await container.start();
|
|
25
|
+
*
|
|
26
|
+
* const kafka = new KafkaClient('test', 'test-group', brokers);
|
|
27
|
+
* // ... run tests ...
|
|
28
|
+
*
|
|
29
|
+
* await container.stop();
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example Jest lifecycle
|
|
33
|
+
* ```ts
|
|
34
|
+
* let container: KafkaTestContainer;
|
|
35
|
+
* let brokers: string[];
|
|
36
|
+
*
|
|
37
|
+
* beforeAll(async () => {
|
|
38
|
+
* container = new KafkaTestContainer({ topics: ['orders'] });
|
|
39
|
+
* brokers = await container.start();
|
|
40
|
+
* }, 120_000);
|
|
41
|
+
*
|
|
42
|
+
* afterAll(() => container.stop());
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare class KafkaTestContainer {
|
|
46
|
+
private container;
|
|
47
|
+
private readonly image;
|
|
48
|
+
private readonly transactionWarmup;
|
|
49
|
+
private readonly topics;
|
|
50
|
+
constructor(options?: KafkaTestContainerOptions);
|
|
51
|
+
/**
|
|
52
|
+
* Start the Kafka container, pre-create topics, and optionally warm up
|
|
53
|
+
* the transaction coordinator.
|
|
54
|
+
*
|
|
55
|
+
* @returns Broker connection strings, e.g. `["localhost:55123"]`.
|
|
56
|
+
*/
|
|
57
|
+
start(): Promise<string[]>;
|
|
58
|
+
/** Stop and remove the container. */
|
|
59
|
+
stop(): Promise<void>;
|
|
60
|
+
/** Broker connection strings. Throws if container is not started. */
|
|
61
|
+
get brokers(): string[];
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=test.container.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.container.d.ts","sourceRoot":"","sources":["../../src/testing/test.container.ts"],"names":[],"mappings":"AAOA,wCAAwC;AACxC,MAAM,WAAW,yBAAyB;IACxC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oGAAoG;IACpG,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,SAAS,CAAoC;IACrD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAErB;gBAEU,OAAO,CAAC,EAAE,yBAAyB;IAM/C;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IA2DhC,qCAAqC;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,qEAAqE;IACrE,IAAI,OAAO,IAAI,MAAM,EAAE,CAOtB;CACF"}
|
|
@@ -1,113 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { I as IKafkaClient, a as IAdmin, b as IPartitionWatermarks, c as IGroupTopicOffsets, d as IPartitionOffset, e as IGroupDescription, f as ITopicMetadata, g as IConsumer, h as IConsumerCreationOptions, i as IConsumerRunConfig, j as ITopicPartitions, k as ITopicPartitionOffset, l as ITopicPartition, m as IMessage, n as IProducer, o as IProducerRecord, p as ITransaction, q as IProducerCreationOptions, K as KafkaTransport } from './client-D-SxYV2b.mjs';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Fully typed mock of `IKafkaClient<T>` where every method is a mock function.
|
|
6
|
-
* Compatible with Jest, Vitest, or any framework whose `fn()` returns
|
|
7
|
-
* an object with `.mock`, `.mockResolvedValue`, etc.
|
|
8
|
-
*/
|
|
9
|
-
type MockKafkaClient<T extends TopicMapConstraint<T>> = {
|
|
10
|
-
[K in keyof IKafkaClient<T>]: IKafkaClient<T>[K] & Record<string, any>;
|
|
11
|
-
};
|
|
12
|
-
/** Factory that creates a no-op mock function (e.g. `() => jest.fn()`). */
|
|
13
|
-
type MockFactory = () => (...args: any[]) => any;
|
|
14
|
-
/**
|
|
15
|
-
* Create a fully typed mock implementing every `IKafkaClient<T>` method.
|
|
16
|
-
* Useful for unit-testing services that depend on `KafkaClient` without
|
|
17
|
-
* touching a real broker.
|
|
18
|
-
*
|
|
19
|
-
* Auto-detects Jest (`jest.fn()`) or Vitest (`vi.fn()`). Pass a custom
|
|
20
|
-
* `mockFactory` for other frameworks.
|
|
21
|
-
*
|
|
22
|
-
* All methods resolve to sensible defaults:
|
|
23
|
-
* - `checkStatus()` → `{ status: 'up', clientId: 'mock-client', topics: [] }`
|
|
24
|
-
* - `getClientId()` → `"mock-client"`
|
|
25
|
-
* - void methods → `undefined`
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* const kafka = createMockKafkaClient<MyTopics>();
|
|
30
|
-
*
|
|
31
|
-
* const service = new OrdersService(kafka);
|
|
32
|
-
* await service.createOrder();
|
|
33
|
-
*
|
|
34
|
-
* expect(kafka.sendMessage).toHaveBeenCalledWith(
|
|
35
|
-
* 'order.created',
|
|
36
|
-
* expect.objectContaining({ orderId: '123' }),
|
|
37
|
-
* );
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
declare function createMockKafkaClient<T extends TopicMapConstraint<T>>(mockFactory?: MockFactory): MockKafkaClient<T>;
|
|
41
|
-
|
|
42
|
-
/** Options for `KafkaTestContainer`. */
|
|
43
|
-
interface KafkaTestContainerOptions {
|
|
44
|
-
/** Docker image. Default: `"confluentinc/cp-kafka:7.7.0"`. */
|
|
45
|
-
image?: string;
|
|
46
|
-
/** Warm up the transactional coordinator on start. Default: `true`. */
|
|
47
|
-
transactionWarmup?: boolean;
|
|
48
|
-
/** Topics to pre-create. Each entry can be a string (1 partition) or `{ topic, numPartitions }`. */
|
|
49
|
-
topics?: Array<string | {
|
|
50
|
-
topic: string;
|
|
51
|
-
numPartitions?: number;
|
|
52
|
-
}>;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Thin wrapper around `@testcontainers/kafka` that starts a single-node
|
|
56
|
-
* KRaft Kafka container and exposes `brokers` for use with `KafkaClient`.
|
|
57
|
-
*
|
|
58
|
-
* Handles common setup pain points:
|
|
59
|
-
* - Transaction coordinator warmup (avoids transactional producer hangs)
|
|
60
|
-
* - Topic pre-creation (avoids race conditions)
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```ts
|
|
64
|
-
* const container = new KafkaTestContainer({ topics: ['orders', 'payments'] });
|
|
65
|
-
* const brokers = await container.start();
|
|
66
|
-
*
|
|
67
|
-
* const kafka = new KafkaClient('test', 'test-group', brokers);
|
|
68
|
-
* // ... run tests ...
|
|
69
|
-
*
|
|
70
|
-
* await container.stop();
|
|
71
|
-
* ```
|
|
72
|
-
*
|
|
73
|
-
* @example Jest lifecycle
|
|
74
|
-
* ```ts
|
|
75
|
-
* let container: KafkaTestContainer;
|
|
76
|
-
* let brokers: string[];
|
|
77
|
-
*
|
|
78
|
-
* beforeAll(async () => {
|
|
79
|
-
* container = new KafkaTestContainer({ topics: ['orders'] });
|
|
80
|
-
* brokers = await container.start();
|
|
81
|
-
* }, 120_000);
|
|
82
|
-
*
|
|
83
|
-
* afterAll(() => container.stop());
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
declare class KafkaTestContainer {
|
|
87
|
-
private container;
|
|
88
|
-
private readonly image;
|
|
89
|
-
private readonly transactionWarmup;
|
|
90
|
-
private readonly topics;
|
|
91
|
-
constructor(options?: KafkaTestContainerOptions);
|
|
92
|
-
/**
|
|
93
|
-
* Start the Kafka container, pre-create topics, and optionally warm up
|
|
94
|
-
* the transaction coordinator.
|
|
95
|
-
*
|
|
96
|
-
* @returns Broker connection strings, e.g. `["localhost:55123"]`.
|
|
97
|
-
*/
|
|
98
|
-
start(): Promise<string[]>;
|
|
99
|
-
/** Stop and remove the container. */
|
|
100
|
-
stop(): Promise<void>;
|
|
101
|
-
/** Broker connection strings. Throws if container is not started. */
|
|
102
|
-
get brokers(): string[];
|
|
103
|
-
}
|
|
104
|
-
|
|
1
|
+
import type { KafkaTransport, IProducer, IConsumer, IAdmin, ITransaction, IProducerRecord, IProducerCreationOptions, IConsumerCreationOptions, IConsumerRunConfig, ITopicPartition, ITopicPartitions, ITopicPartitionOffset, IPartitionWatermarks, IPartitionOffset, IGroupTopicOffsets, IGroupDescription, ITopicMetadata, IMessage } from "../client/transport/transport.interface";
|
|
105
2
|
/**
|
|
106
3
|
* An in-memory Kafka transaction.
|
|
107
4
|
* Staged sends are visible in `staged`; committed sends are flushed
|
|
108
5
|
* to the owning `FakeProducer.sent` on `commit()`.
|
|
109
6
|
*/
|
|
110
|
-
declare class FakeTransaction implements ITransaction {
|
|
7
|
+
export declare class FakeTransaction implements ITransaction {
|
|
111
8
|
private readonly producer;
|
|
112
9
|
/** Records staged within this transaction (not yet committed). */
|
|
113
10
|
readonly staged: IProducerRecord[];
|
|
@@ -142,7 +39,7 @@ declare class FakeTransaction implements ITransaction {
|
|
|
142
39
|
* In-memory producer. All `send()` calls are captured in `sent`.
|
|
143
40
|
* Transactions are backed by `FakeTransaction`.
|
|
144
41
|
*/
|
|
145
|
-
declare class FakeProducer implements IProducer {
|
|
42
|
+
export declare class FakeProducer implements IProducer {
|
|
146
43
|
/** All records delivered via `send()` (direct + committed transactions). */
|
|
147
44
|
readonly sent: IProducerRecord[];
|
|
148
45
|
/** All transactions opened via `transaction()`. */
|
|
@@ -166,7 +63,7 @@ declare class FakeProducer implements IProducer {
|
|
|
166
63
|
* Call `deliver(topic, message)` from your test to push messages through
|
|
167
64
|
* the `eachMessage` handler without a real broker.
|
|
168
65
|
*/
|
|
169
|
-
declare class FakeConsumer implements IConsumer {
|
|
66
|
+
export declare class FakeConsumer implements IConsumer {
|
|
170
67
|
readonly groupId: string;
|
|
171
68
|
readonly fromBeginning: boolean;
|
|
172
69
|
/** Topics subscribed via `subscribe()`. */
|
|
@@ -209,7 +106,7 @@ declare class FakeConsumer implements IConsumer {
|
|
|
209
106
|
* Pre-populate `topicOffsets`, `groupOffsets`, and `existingTopics`
|
|
210
107
|
* to control what admin queries return.
|
|
211
108
|
*/
|
|
212
|
-
declare class FakeAdmin implements IAdmin {
|
|
109
|
+
export declare class FakeAdmin implements IAdmin {
|
|
213
110
|
/** Topics returned by `listTopics()`. Add to this from your test. */
|
|
214
111
|
readonly existingTopics: string[];
|
|
215
112
|
/** Per-topic partition watermarks returned by `fetchTopicOffsets()`. */
|
|
@@ -280,7 +177,7 @@ declare class FakeAdmin implements IAdmin {
|
|
|
280
177
|
* expect(transport.mainProducer.sentTo('orders')).toHaveLength(1);
|
|
281
178
|
* ```
|
|
282
179
|
*/
|
|
283
|
-
declare class FakeTransport implements KafkaTransport {
|
|
180
|
+
export declare class FakeTransport implements KafkaTransport {
|
|
284
181
|
private readonly _producers;
|
|
285
182
|
private readonly _consumers;
|
|
286
183
|
private readonly _admin;
|
|
@@ -314,5 +211,4 @@ declare class FakeTransport implements KafkaTransport {
|
|
|
314
211
|
offset?: string;
|
|
315
212
|
}): Promise<void>;
|
|
316
213
|
}
|
|
317
|
-
|
|
318
|
-
export { FakeAdmin, FakeConsumer, FakeProducer, FakeTransaction, FakeTransport, KafkaTestContainer, type KafkaTestContainerOptions, type MockKafkaClient, createMockKafkaClient };
|
|
214
|
+
//# sourceMappingURL=transport.fake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.fake.d.ts","sourceRoot":"","sources":["../../src/testing/transport.fake.ts"],"names":[],"mappings":"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"}
|