@drarzter/kafka-client 0.9.4 → 0.11.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 +693 -8
- package/dist/chunk-OR7TPAAE.mjs +4760 -0
- package/dist/chunk-OR7TPAAE.mjs.map +1 -0
- package/dist/chunk-PQVBRDNV.mjs +149 -0
- package/dist/chunk-PQVBRDNV.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-SM4FZKAZ.mjs → cli/index.js} +1073 -309
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +356 -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 +163 -0
- package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/ops.d.ts +64 -0
- package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/pipeline.d.ts +168 -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 +68 -0
- package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/setup.d.ts +66 -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 +75 -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 +79 -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 +124 -0
- package/dist/client/message/schema-registry.d.ts.map +1 -0
- package/dist/client/message/serde.d.ts +68 -0
- package/dist/client/message/serde.d.ts.map +1 -0
- package/dist/client/message/topic.d.ts +159 -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 +221 -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 +167 -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 +13 -314
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1466 -123
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +45 -3
- package/dist/index.d.ts +7 -128
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1483 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -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/serde.d.ts +157 -0
- package/dist/serde.d.ts.map +1 -0
- package/dist/serde.js +308 -0
- package/dist/serde.js.map +1 -0
- package/dist/serde.mjs +158 -0
- package/dist/serde.mjs.map +1 -0
- 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 +26 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +26 -0
- package/dist/testing.mjs.map +1 -1
- package/package.json +40 -8
- package/dist/chunk-SM4FZKAZ.mjs.map +0 -1
- package/dist/client-1irhGEu0.d.mts +0 -751
- package/dist/client-BpFjkHhr.d.ts +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":[]}
|
package/dist/serde.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { MessageSerde, SerdeContext } from "./client/message/serde";
|
|
2
|
+
import type { SchemaRegistryClient } from "./client/message/schema-registry";
|
|
3
|
+
export { JsonSerde } from "./client/message/serde";
|
|
4
|
+
export type { MessageSerde, SerdeContext } from "./client/message/serde";
|
|
5
|
+
export { SchemaRegistryClient } from "./client/message/schema-registry";
|
|
6
|
+
/** Injectable dynamic-import function — overridable in tests. */
|
|
7
|
+
type ImportFn = (specifier: string) => Promise<any>;
|
|
8
|
+
/** Options common to the registry-backed serdes. */
|
|
9
|
+
interface RegistrySerdeCommonOptions {
|
|
10
|
+
/** Schema Registry client used to resolve/register schema ids. */
|
|
11
|
+
registry: SchemaRegistryClient;
|
|
12
|
+
/**
|
|
13
|
+
* Subject name. Defaults to Confluent `TopicNameStrategy`
|
|
14
|
+
* (`<topic>-value` / `<topic>-key`). Provide a literal string or a
|
|
15
|
+
* function of the {@link SerdeContext} to override.
|
|
16
|
+
*/
|
|
17
|
+
subject?: string | ((ctx: SerdeContext) => string);
|
|
18
|
+
/**
|
|
19
|
+
* Register `schema` on first serialize to obtain its id (dev-friendly).
|
|
20
|
+
* Default `false` → the id is resolved via `getLatestSchema(subject)`.
|
|
21
|
+
*/
|
|
22
|
+
autoRegister?: boolean;
|
|
23
|
+
/** @internal Injectable dynamic import for tests. */
|
|
24
|
+
importFn?: ImportFn;
|
|
25
|
+
}
|
|
26
|
+
/** Options for {@link avroSerde}. */
|
|
27
|
+
export interface AvroSerdeOptions extends RegistrySerdeCommonOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Avro schema (JSON string or object) used to serialize, and as the
|
|
30
|
+
* write-schema fallback. Required to serialize; deserialize resolves the
|
|
31
|
+
* writer schema from the registry via the wire-format id.
|
|
32
|
+
*/
|
|
33
|
+
schema?: string | object;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Confluent-wire-format **Avro** serde backed by a Schema Registry.
|
|
37
|
+
*
|
|
38
|
+
* Produces/consumes the exact byte layout Java/Go clients use, so this library
|
|
39
|
+
* interoperates with them through a shared registry:
|
|
40
|
+
*
|
|
41
|
+
* ```
|
|
42
|
+
* [magic 0x00][schema id: 4-byte big-endian][avro binary]
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Uses the optional peer dependency [`avsc`](https://www.npmjs.com/package/avsc)
|
|
46
|
+
* via dynamic import — install it to enable Avro:
|
|
47
|
+
*
|
|
48
|
+
* ```bash
|
|
49
|
+
* npm install avsc
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* - **serialize**: resolves the subject from the context, obtains the schema id
|
|
53
|
+
* (`registerSchema` when `autoRegister`, else `getLatestSchema`), Avro-encodes
|
|
54
|
+
* `value` against `schema`, and frames the bytes.
|
|
55
|
+
* - **deserialize**: reads the magic byte + big-endian id, resolves the writer
|
|
56
|
+
* schema via `registry.getSchemaById(id)` (cached forever), and Avro-decodes
|
|
57
|
+
* the remainder. The reader schema equals the writer schema in v1 — full
|
|
58
|
+
* reader-schema resolution / schema evolution is a future enhancement.
|
|
59
|
+
*
|
|
60
|
+
* The parsed `avsc` type is cached per schema string so repeated messages don't
|
|
61
|
+
* re-parse the schema.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { avroSerde } from '@drarzter/kafka-client/serde';
|
|
66
|
+
* import { SchemaRegistryClient } from '@drarzter/kafka-client';
|
|
67
|
+
*
|
|
68
|
+
* const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
|
|
69
|
+
* const orderSchema = {
|
|
70
|
+
* type: 'record',
|
|
71
|
+
* name: 'Order',
|
|
72
|
+
* fields: [{ name: 'orderId', type: 'string' }, { name: 'amount', type: 'double' }],
|
|
73
|
+
* };
|
|
74
|
+
*
|
|
75
|
+
* // Per-topic:
|
|
76
|
+
* const Orders = topic('orders')
|
|
77
|
+
* .serde(avroSerde({ registry, schema: orderSchema }))
|
|
78
|
+
* .type<Order>();
|
|
79
|
+
*
|
|
80
|
+
* // Client-wide:
|
|
81
|
+
* const kafka = new KafkaClient(id, group, brokers, {
|
|
82
|
+
* serde: avroSerde({ registry, schema: orderSchema }),
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function avroSerde(options: AvroSerdeOptions): MessageSerde;
|
|
87
|
+
/** Options for {@link protobufSerde}. */
|
|
88
|
+
export interface ProtobufSerdeOptions extends RegistrySerdeCommonOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Fully-qualified Protobuf message name to encode/decode,
|
|
91
|
+
* e.g. `"com.acme.orders.Order"`.
|
|
92
|
+
*/
|
|
93
|
+
messageType: string;
|
|
94
|
+
/**
|
|
95
|
+
* `.proto` source string defining {@link ProtobufSerdeOptions.messageType}.
|
|
96
|
+
* Required to serialize; deserialize resolves the writer `.proto` from the
|
|
97
|
+
* registry via the wire-format id.
|
|
98
|
+
*/
|
|
99
|
+
schema?: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Confluent-wire-format **Protobuf** serde backed by a Schema Registry.
|
|
103
|
+
*
|
|
104
|
+
* Produces/consumes the exact byte layout Java/Go clients use:
|
|
105
|
+
*
|
|
106
|
+
* ```
|
|
107
|
+
* [magic 0x00][schema id: 4-byte big-endian][message-index][protobuf binary]
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* The **message-index** identifies which message type within the `.proto` file
|
|
111
|
+
* was used. For the first/top-level message type (index `[0]`) Confluent writes
|
|
112
|
+
* the single byte `0x00`. This serde implements that top-level case only —
|
|
113
|
+
* multiple/nested message types are a documented v1 limitation and cause a
|
|
114
|
+
* clear error on deserialize.
|
|
115
|
+
*
|
|
116
|
+
* Uses the optional peer dependency
|
|
117
|
+
* [`protobufjs`](https://www.npmjs.com/package/protobufjs) via dynamic import —
|
|
118
|
+
* install it to enable Protobuf:
|
|
119
|
+
*
|
|
120
|
+
* ```bash
|
|
121
|
+
* npm install protobufjs
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* - **serialize**: obtains the schema id (`registerSchema` when `autoRegister`,
|
|
125
|
+
* else `getLatestSchema`), encodes `value` with the `protobufjs` `Type`, and
|
|
126
|
+
* frames it with the `0x00` message-index byte.
|
|
127
|
+
* - **deserialize**: reads the magic byte + big-endian id + message-index (which
|
|
128
|
+
* must be the single `0x00` byte), resolves the writer `.proto` via
|
|
129
|
+
* `registry.getSchemaById(id)` (cached forever), and decodes the remainder.
|
|
130
|
+
*
|
|
131
|
+
* The parsed `protobufjs` `Type` is cached per schema string so repeated
|
|
132
|
+
* messages don't re-parse the `.proto`.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { protobufSerde } from '@drarzter/kafka-client/serde';
|
|
137
|
+
* import { SchemaRegistryClient } from '@drarzter/kafka-client';
|
|
138
|
+
*
|
|
139
|
+
* const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
|
|
140
|
+
* const proto = `
|
|
141
|
+
* syntax = "proto3";
|
|
142
|
+
* message Order { string orderId = 1; double amount = 2; }
|
|
143
|
+
* `;
|
|
144
|
+
*
|
|
145
|
+
* // Per-topic:
|
|
146
|
+
* const Orders = topic('orders')
|
|
147
|
+
* .serde(protobufSerde({ registry, schema: proto, messageType: 'Order' }))
|
|
148
|
+
* .type<Order>();
|
|
149
|
+
*
|
|
150
|
+
* // Client-wide:
|
|
151
|
+
* const kafka = new KafkaClient(id, group, brokers, {
|
|
152
|
+
* serde: protobufSerde({ registry, schema: proto, messageType: 'Order' }),
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export declare function protobufSerde(options: ProtobufSerdeOptions): MessageSerde;
|
|
157
|
+
//# sourceMappingURL=serde.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serde.d.ts","sourceRoot":"","sources":["../src/serde.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAI7E,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAKxE,iEAAiE;AACjE,KAAK,QAAQ,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAqEpD,oDAAoD;AACpD,UAAU,0BAA0B;IAClC,kEAAkE;IAClE,QAAQ,EAAE,oBAAoB,CAAC;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,YAAY,KAAK,MAAM,CAAC,CAAC;IACnD;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAiB,SAAQ,0BAA0B;IAClE;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,CA4DjE;AAED,yCAAyC;AACzC,MAAM,WAAW,oBAAqB,SAAQ,0BAA0B;IACtE;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,YAAY,CAiFzE"}
|