@venizia/ignis-docs 0.2.0 → 0.2.1-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.
Files changed (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,3 +1,9 @@
1
+ ---
2
+ title: Kafka Producer
3
+ description: KafkaProducerHelper - connection, SASL, serialization, compression, and transactions
4
+ difficulty: intermediate
5
+ ---
6
+
1
7
  # Producer
2
8
 
3
9
  The `KafkaProducerHelper` wraps `@platformatic/kafka`'s `Producer` with health tracking, graceful shutdown, broker event callbacks, and a transaction helper.
@@ -36,7 +42,7 @@ interface IKafkaProducerOptions<KeyType, ValueType, HeaderKeyType, HeaderValueTy
36
42
  | `identifier` | `string` | `'kafka-producer'` | Scoped logging identifier |
37
43
  | `serializers` | `Partial<Serializers<K,V,HK,HV>>` | -- | Key/value/header serializers |
38
44
  | `compression` | `CompressionAlgorithmValue` | -- | `'none'`, `'gzip'`, `'snappy'`, `'lz4'`, `'zstd'` |
39
- | `acks` | `TKafkaAcks` | -- | Acknowledgment level: `0`, `1`, or `-1` |
45
+ | `acks` | `TKafkaAcks` | -- | Acknowledgment level: `0` (none), `1` (leader), `-1` (all) |
40
46
  | `idempotent` | `boolean` | -- | Enable idempotent producer (exactly-once within partition) |
41
47
  | `transactionalId` | `string` | -- | Transactional ID for exactly-once across partitions |
42
48
  | `strict` | `boolean` | `true` | Strict mode -- fail on unknown topics |
@@ -46,7 +52,49 @@ interface IKafkaProducerOptions<KeyType, ValueType, HeaderKeyType, HeaderValueTy
46
52
  | `onBrokerConnect` | `TKafkaBrokerEventCallback` | -- | Called when broker connects |
47
53
  | `onBrokerDisconnect` | `TKafkaBrokerEventCallback` | -- | Called when broker disconnects |
48
54
 
49
- Plus all [Connection Options](./#connection-options).
55
+ Plus the shared [Connection & Authentication](#connection--authentication) options below, all inherited from `IKafkaConnectionOptions`.
56
+
57
+ ## Connection & Authentication
58
+
59
+ `IKafkaProducerOptions`, `IKafkaConsumerOptions`, and `IKafkaAdminOptions` all extend `IKafkaConnectionOptions` - this section applies to all three.
60
+
61
+ | Option | Type | Default | Description |
62
+ |--------|------|---------|-------------|
63
+ | `bootstrapBrokers` | `string[]` | -- | Broker addresses (`host:port`). **Required** |
64
+ | `clientId` | `string` | -- | Unique client identifier. **Required** |
65
+ | `retries` | `number` | `3` | Connection retries before failing |
66
+ | `retryDelay` | `number` | `1000` | Delay between retries (ms) |
67
+ | `sasl` | `SASLOptions` | -- | SASL authentication |
68
+ | `tls` / `ssl` | `TLSConnectionOptions` | -- | TLS options (`ssl` is an alias for `tls`) |
69
+ | `connectTimeout` | `number` | -- | TCP connection timeout (ms) |
70
+ | `requestTimeout` | `number` | -- | Kafka request timeout (ms) |
71
+
72
+ **SASL mechanisms** (`@platformatic/kafka` supports five):
73
+
74
+ | Mechanism | Use case |
75
+ |-----------|----------|
76
+ | `PLAIN` | Username/password (pair with `tls` in production) |
77
+ | `SCRAM-SHA-256` / `SCRAM-SHA-512` | Challenge-response - password never sent in plaintext |
78
+ | `OAUTHBEARER` | Token-based (Azure Event Hubs, Confluent Cloud). `token` accepts a string or an async function returning one |
79
+ | `GSSAPI` | Kerberos authentication |
80
+
81
+ ```typescript
82
+ import fs from 'node:fs';
83
+ import { KafkaProducerHelper } from '@venizia/ignis-helpers/kafka';
84
+
85
+ const producer = KafkaProducerHelper.newInstance({
86
+ bootstrapBrokers: ['broker1:9093', 'broker2:9093'],
87
+ clientId: 'order-producer',
88
+ sasl: { mechanism: 'SCRAM-SHA-512', username: 'kafka-user', password: 'kafka-password' },
89
+ tls: {
90
+ ca: fs.readFileSync('/path/to/ca.pem'),
91
+ cert: fs.readFileSync('/path/to/client-cert.pem'),
92
+ key: fs.readFileSync('/path/to/client-key.pem'),
93
+ },
94
+ connectTimeout: 30_000,
95
+ requestTimeout: 30_000,
96
+ });
97
+ ```
50
98
 
51
99
  ## Basic Example
52
100
 
@@ -92,6 +140,74 @@ await helper.close();
92
140
  await helper.close({ isForce: true });
93
141
  ```
94
142
 
143
+ ## Serialization
144
+
145
+ `@platformatic/kafka`'s default wire format is `Buffer`. The helpers default the generic types to `string`, but you must pass serializers explicitly or messages travel as raw `Buffer`.
146
+
147
+ | Export | Type | Description |
148
+ |--------|------|-------------|
149
+ | `stringSerializer` / `stringDeserializer` | `Serializer<string>` / `Deserializer<string>` | UTF-8 string <-> `Buffer` |
150
+ | `jsonSerializer` / `jsonDeserializer` | `Serializer<T>` / `Deserializer<T>` | `JSON.stringify` / `JSON.parse` + UTF-8 |
151
+ | `stringSerializers` / `stringDeserializers` | `Serializers<string,string,string,string>` / `Deserializers<...>` | All four positions (key, value, header key, header value) as string |
152
+ | `serializersFrom(s)` / `deserializersFrom(d)` | `<T>(fn) => Serializers<T,T,T,T>` / `<T>(fn) => Deserializers<T,T,T,T>` | Build all four positions from a single serializer/deserializer |
153
+
154
+ ```typescript
155
+ import { stringSerializers } from '@platformatic/kafka';
156
+
157
+ const producer = KafkaProducerHelper.newInstance({
158
+ bootstrapBrokers: ['localhost:9092'],
159
+ clientId: 'my-producer',
160
+ serializers: stringSerializers,
161
+ });
162
+ ```
163
+
164
+ ```typescript
165
+ import { jsonSerializer, stringSerializer, serializersFrom } from '@platformatic/kafka';
166
+
167
+ const producer = KafkaProducerHelper.newInstance({
168
+ bootstrapBrokers: ['localhost:9092'],
169
+ clientId: 'my-producer',
170
+ serializers: { ...serializersFrom(jsonSerializer), key: stringSerializer },
171
+ });
172
+
173
+ await producer.getProducer().send({
174
+ messages: [{
175
+ topic: 'orders',
176
+ key: 'order-123',
177
+ value: { id: '123', status: 'created', amount: 99 },
178
+ }],
179
+ });
180
+ ```
181
+
182
+ For schema-validated serialization (Avro, Protobuf, JSON Schema), pass a `registry` instead of `serializers` - see [Schema Registry](./schema-registry).
183
+
184
+ ## Compression
185
+
186
+ `@platformatic/kafka` supports five compression algorithms:
187
+
188
+ | Algorithm | Value | Description |
189
+ |-----------|-------|-------------|
190
+ | None | `'none'` | No compression (default) |
191
+ | GZIP | `'gzip'` | Good compression ratio, moderate CPU |
192
+ | Snappy | `'snappy'` | Fast compression, moderate ratio |
193
+ | LZ4 | `'lz4'` | Very fast, good for high-throughput |
194
+ | Zstandard | `'zstd'` | Best ratio, moderate CPU |
195
+
196
+ ```typescript
197
+ const helper = KafkaProducerHelper.newInstance({
198
+ bootstrapBrokers: ['localhost:9092'],
199
+ clientId: 'my-producer',
200
+ serializers: stringSerializers,
201
+ compression: 'zstd',
202
+ });
203
+
204
+ // Override per-send
205
+ await helper.getProducer().send({
206
+ messages: [{ topic: 'logs', key: 'l1', value: largePayload }],
207
+ compression: 'lz4',
208
+ });
209
+ ```
210
+
95
211
  ## Transactions
96
212
 
97
213
  `runInTransaction()` wraps `beginTransaction()` -> callback -> `commit()` / `abort()` with automatic logging.
@@ -133,6 +249,8 @@ const result = await helper.runInTransaction(async ({ send, addConsumer, addOffs
133
249
  });
134
250
  ```
135
251
 
252
+ If the callback throws, the transaction is automatically aborted and the error re-thrown.
253
+
136
254
  ### Transaction Context
137
255
 
138
256
  The callback receives an `IKafkaTransactionContext`:
@@ -144,8 +262,6 @@ The callback receives an `IKafkaTransactionContext`:
144
262
  | `addConsumer(consumer)` | `(consumer: Consumer) => Promise<void>` | Add a consumer for exactly-once |
145
263
  | `addOffset(message)` | `(message: Message) => Promise<void>` | Add consumed message offset to transaction |
146
264
 
147
- If the callback throws, the transaction is automatically aborted and the error re-thrown.
148
-
149
265
  ## Graceful Shutdown
150
266
 
151
267
  `close()` implements a two-phase shutdown:
@@ -166,7 +282,7 @@ await helper.close({ isForce: true });
166
282
 
167
283
  ## API Reference (`@platformatic/kafka`)
168
284
 
169
- After calling `helper.getProducer()`, you have full access to the `Producer` class:
285
+ After calling `helper.getProducer()`, you have full access to the `Producer` class.
170
286
 
171
287
  ### `producer.send(options)`
172
288
 
@@ -272,3 +388,16 @@ await producer.send({
272
388
  },
273
389
  });
274
390
  ```
391
+
392
+ ## See also
393
+
394
+ - [Kafka Overview](./) - the four helpers, shared health/close API, and the compile-binary caveat
395
+ - [Consumer](./consumer) - the receiving side, including message callbacks and lag monitoring
396
+ - [Schema Registry](./schema-registry) - schema-validated serialization instead of manual `serializers`
397
+ - [Compiling to a Single Binary](./compile-binary) - required when this helper ships inside a `bun build --compile` binary
398
+
399
+ **Files:**
400
+
401
+ - [`packages/helpers/src/modules/queue/kafka/producer.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/queue/kafka/producer.ts) - `KafkaProducerHelper`
402
+ - [`packages/helpers/src/modules/queue/kafka/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/queue/kafka/base.ts) - `BaseKafkaHelper`, shared health tracking and shutdown
403
+ - [`packages/helpers/src/modules/queue/kafka/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/queue/kafka/common/types.ts) - `IKafkaProducerOptions`, `IKafkaConnectionOptions`, `IKafkaTransactionContext`
@@ -1,3 +1,9 @@
1
+ ---
2
+ title: Kafka Schema Registry
3
+ description: KafkaSchemaRegistryHelper - schema-validated serialization for Avro, Protobuf, and JSON Schema
4
+ difficulty: intermediate
5
+ ---
6
+
1
7
  # Schema Registry
2
8
 
3
9
  The `KafkaSchemaRegistryHelper` wraps `@platformatic/kafka`'s `ConfluentSchemaRegistry`. It provides a centralized schema registry that auto-serializes/deserializes messages using registered schemas (Avro, Protobuf, JSON Schema).
@@ -39,18 +45,25 @@ interface IKafkaSchemaRegistryOptions extends ConfluentSchemaRegistryOptions {
39
45
  | `jsonValidateSend` | `boolean` | -- | Validate JSON schema on produce |
40
46
  | `identifier` | `string` | `'kafka-schema-registry'` | Scoped logging identifier |
41
47
 
42
- ## What Schema Registry Solves
48
+ ## What it solves
43
49
 
44
- Without a schema registry, producers and consumers must agree on message format out-of-band. If the producer changes the shape of `value` (adds/removes fields), consumers break silently at runtime.
50
+ Without a schema registry, producers and consumers must agree on message shape out-of-band. If the producer changes the shape of `value` (adds/removes fields), consumers break silently at runtime.
45
51
 
46
- **Schema Registry** is a centralized server (Confluent Schema Registry) that stores and validates schemas (Avro, Protobuf, JSON Schema). It enforces a contract:
52
+ - **Schema Registry is a centralized server** (Confluent Schema Registry) that stores and validates schemas.
53
+ - **It enforces a contract:** the producer says "I want to send this shape", the registry validates it against the registered schema before the message reaches Kafka; the consumer asks "what shape is this?" and the registry tells it how to deserialize.
47
54
 
48
- ```
49
- Producer -> "I want to send this shape" -> Schema Registry validates -> Kafka
50
- Kafka -> Consumer -> "What shape is this?" -> Schema Registry tells -> Deserialize
51
- ```
55
+ | | Without registry | With registry |
56
+ |---|---|---|
57
+ | **Message format** | Raw string, manual `JSON.stringify`/`JSON.parse` | Typed object, auto ser/deser |
58
+ | **Validation** | None -- runtime crashes on shape drift | Schema validated before send |
59
+ | **Schema evolution** | Breaks consumers silently | Backward/forward compatibility enforced |
60
+ | **Where schemas live** | Nowhere (tribal knowledge) | Centralized server, e.g. `http://registry:8081` |
61
+
62
+ Use it when you need schema enforcement and compatibility checks across producers/consumers - especially in multi-team environments. Skip it for simple string/JSON messages where both sides are controlled by the same team and format changes are coordinated by hand.
63
+
64
+ ## Without vs. with the registry
52
65
 
53
- ### Without Schema Registry (raw strings)
66
+ Without a registry, serialization is entirely manual and unchecked:
54
67
 
55
68
  ```typescript
56
69
  // Producer -- manually serialize
@@ -79,22 +92,28 @@ const consumer = KafkaConsumerHelper.newInstance({
79
92
  });
80
93
  ```
81
94
 
82
- Problem: if producer adds `{ id: 1, total: 99.99, currency: 'USD' }` or removes `total`, consumer has no way to know until it crashes.
95
+ If the producer adds `{ id: 1, total: 99.99, currency: 'USD' }` or removes `total`, the consumer has no way to know until it crashes.
83
96
 
84
- ### With Schema Registry (auto serialize/deserialize)
97
+ With a registry, both sides pass `registry` instead of `serializers`/`deserializers`:
85
98
 
86
99
  ```typescript
100
+ import {
101
+ KafkaSchemaRegistryHelper,
102
+ KafkaProducerHelper,
103
+ KafkaConsumerHelper,
104
+ } from '@venizia/ignis-helpers/kafka';
105
+
87
106
  // 1. Create registry -- points to Confluent Schema Registry server
88
107
  const registry = KafkaSchemaRegistryHelper.newInstance({
89
108
  url: 'http://localhost:8081',
90
109
  // auth: { username: 'user', password: 'pass' }, // optional
91
110
  });
92
111
 
93
- // 2. Producer -- pass registry, it auto-serializes values using registered schema
112
+ // 2. Producer -- pass registry, it auto-serializes values using the registered schema
94
113
  const producer = KafkaProducerHelper.newInstance({
95
114
  bootstrapBrokers: ['127.0.0.1:29092'],
96
115
  clientId: 'order-producer',
97
- registry: registry.getRegistry(), // <- registry handles serialization
116
+ registry: registry.getRegistry(),
98
117
  });
99
118
 
100
119
  await producer.getProducer().send({
@@ -106,61 +125,11 @@ await producer.getProducer().send({
106
125
  });
107
126
  // If the value doesn't match the registered schema -> error BEFORE sending to Kafka
108
127
 
109
- // 3. Consumer -- pass same registry, it auto-deserializes
128
+ // 3. Consumer -- pass the same registry, it auto-deserializes
110
129
  const consumer = KafkaConsumerHelper.newInstance({
111
130
  bootstrapBrokers: ['127.0.0.1:29092'],
112
131
  clientId: 'order-consumer',
113
132
  groupId: 'order-group',
114
- registry: registry.getRegistry(), // <- registry handles deserialization
115
- onMessage: async ({ message }) => {
116
- // message.value is already a typed object, not a raw string
117
- console.log(message.value.id, message.value.total);
118
- },
119
- });
120
- ```
121
-
122
- ### Comparison
123
-
124
- | | Without Registry | With Registry |
125
- |---|---|---|
126
- | **Message format** | Raw string, manual `JSON.stringify/parse` | Typed object, auto ser/deser |
127
- | **Validation** | None -- runtime crashes | Schema validated before send |
128
- | **Schema evolution** | Break consumers silently | Backward/forward compatibility enforced |
129
- | **Where schemas live** | Nowhere (tribal knowledge) | Centralized server `http://registry:8081` |
130
-
131
- You only need it when you want **schema enforcement** across producers/consumers. For simple string messages, skip it entirely.
132
-
133
- ## Basic Usage
134
-
135
- ```typescript
136
- import { KafkaSchemaRegistryHelper, KafkaProducerHelper, KafkaConsumerHelper } from '@venizia/ignis-helpers/kafka';
137
-
138
- // 1. Create registry -- points to Confluent Schema Registry server
139
- const registry = KafkaSchemaRegistryHelper.newInstance({
140
- url: 'http://localhost:8081',
141
- });
142
-
143
- // 2. Producer -- registry auto-serializes values using registered schema
144
- const producer = KafkaProducerHelper.newInstance({
145
- bootstrapBrokers: ['localhost:9092'],
146
- clientId: 'order-producer',
147
- registry: registry.getRegistry(),
148
- });
149
-
150
- await producer.getProducer().send({
151
- messages: [{
152
- topic: 'orders',
153
- key: 'order-1',
154
- value: { id: 1, total: 99.99 }, // object, not string -- registry serializes
155
- }],
156
- });
157
- // If value doesn't match the registered schema -> error BEFORE sending to Kafka
158
-
159
- // 3. Consumer -- registry auto-deserializes
160
- const consumer = KafkaConsumerHelper.newInstance({
161
- bootstrapBrokers: ['localhost:9092'],
162
- clientId: 'order-consumer',
163
- groupId: 'order-group',
164
133
  registry: registry.getRegistry(),
165
134
  onMessage: async ({ message }) => {
166
135
  // message.value is already a typed object, not a raw string
@@ -171,7 +140,7 @@ const consumer = KafkaConsumerHelper.newInstance({
171
140
  await consumer.start({ topics: ['orders'] });
172
141
  ```
173
142
 
174
- ## With Authentication
143
+ ## With authentication
175
144
 
176
145
  ```typescript
177
146
  const registry = KafkaSchemaRegistryHelper.newInstance({
@@ -183,9 +152,9 @@ const registry = KafkaSchemaRegistryHelper.newInstance({
183
152
  });
184
153
  ```
185
154
 
186
- ## Alternative: Manual Serializers
155
+ ## Alternative: manual serializers
187
156
 
188
- Instead of passing the full registry, you can extract serializers/deserializers for manual use:
157
+ Instead of passing the full registry to `registry`, extract serializers/deserializers for manual use alongside `serializers`/`deserializers`:
189
158
 
190
159
  ```typescript
191
160
  const registry = KafkaSchemaRegistryHelper.newInstance({
@@ -204,11 +173,17 @@ const consumer = KafkaConsumerHelper.newInstance({
204
173
  clientId: 'my-consumer',
205
174
  groupId: 'my-group',
206
175
  deserializers: registry.getDeserializers(),
207
- onMessage: async ({ message }) => { ... },
176
+ onMessage: async ({ message }) => { /* ... */ },
208
177
  });
209
178
  ```
210
179
 
211
- ## When to Use
180
+ ## See also
181
+
182
+ - [Kafka Overview](./) - the four helpers, shared health/close API, and the compile-binary caveat
183
+ - [Producer](./producer) - `registry` as a producer option, plus the manual `serializers` alternative
184
+ - [Consumer](./consumer) - `registry` as a consumer option, plus the manual `deserializers` alternative
185
+
186
+ **Files:**
212
187
 
213
- - **Use schema registry** when you need schema enforcement, validation, and compatibility checks across producers/consumers -- especially in multi-team environments
214
- - **Skip schema registry** for simple string/JSON messages where both sides are controlled by the same team and format changes are coordinated
188
+ - [`packages/helpers/src/modules/queue/kafka/schema/registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/queue/kafka/schema/registry.ts) - `KafkaSchemaRegistryHelper`
189
+ - [`packages/helpers/src/modules/queue/kafka/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/queue/kafka/common/types.ts) - `IKafkaSchemaRegistryOptions`
@@ -0,0 +1,193 @@
1
+ ---
2
+ title: HfLogger - High-Frequency Logging Guide
3
+ description: How to use the ring-buffer HfLogger correctly - setup, the ILogger surface, hot-path cost model, flusher lifecycle, and the limitations you must design around.
4
+ difficulty: advanced
5
+ ---
6
+
7
+ # HfLogger - High-Frequency Logging Guide
8
+
9
+ `HfLogger` is a fixed-layout ring-buffer logger for hot paths where even the standard `Logger` is too expensive. A log call writes a 256-byte binary entry into a lazily-allocated buffer - no string formatting, no transport I/O, no per-call allocation on the fast path - and a separate `HfLogFlusher` drains entries later, off the hot path.
10
+
11
+ It implements `ILogger` (`AbstractLogger`), so it is a drop-in replacement anywhere an `ILogger` is expected - a direct method for every level (`debug`, `info`, `warn`, `error`, `emerg`), plus `log` and `for`, all work. It is still entirely separate from the Winston-backed `Logger` pipeline: no formatters, no transports, no `APP_ENV_LOGGER_*` variables apply to it. It trades that pipeline away for enqueue speed - measured at 59.4ns/op on the bytes path and 66.0ns/op on the string no-args path on a modern machine (Bun 1.3.14, 1M-iteration median), against 831ns/op for pino writing to `/dev/null` on the same machine - roughly 14x faster.
12
+
13
+ > [!IMPORTANT]
14
+ > Reach for `HfLogger` only when a profiler shows logging itself in your hot path - order engines, market-data ticks, per-packet paths at 100k+ events/sec. For everything else, the standard scoped `Logger` is the right tool: it formats, redacts, rotates files, and ships UDP. Most services never need this module.
15
+
16
+ ## Mental model
17
+
18
+ One ring buffer per process holds 65,536 entries of 256 bytes each (16MB), allocated lazily on the first `HfLogger.get()` call - not at module import. Writers stamp entries in; the ring never blocks and never grows. When the ring is full, the NEXT write overwrites the OLDEST entry - the buffer trades completeness for a bounded, allocation-free hot path, and the flusher now counts and reports every entry it overwrites before it could be read (see "Lap accounting" below).
19
+
20
+ **Entry layout (256 bytes):**
21
+
22
+ | Offset | Size | Field |
23
+ |--------|------|-------|
24
+ | 0-7 | 8 bytes | Timestamp (`float64` epoch milliseconds, sub-millisecond precision) |
25
+ | 8 | 1 byte | Level (`0`=debug, `1`=info, `2`=warn, `3`=error, `4`=emerg) |
26
+ | 9 | 1 byte | Scope length (0-32) |
27
+ | 10-41 | 32 bytes | Scope bytes |
28
+ | 42 | 1 byte | Message length (0-213) |
29
+ | 43-255 | 213 bytes | Message bytes |
30
+
31
+ The two length bytes are what make reads exact: the flusher decodes only the bytes a field actually holds, never the fixed-width remainder, so there is no NUL padding and no stale tail leaking from whatever a reused slot last held. Because entries are fixed-width binary, everything you log must fit the layout: scopes at most 32 bytes, messages at most 213 bytes. Anything longer is truncated, not rejected.
32
+
33
+ ## Setup - everything happens at initialization time
34
+
35
+ The hot path takes pre-encoded bytes, not strings. Encode once, at startup, and keep the references:
36
+
37
+ ```typescript
38
+ import { HfLogger, HfLogFlusher } from '@venizia/ignis-helpers';
39
+
40
+ // -- Initialization phase (once, before any hot path runs) --
41
+
42
+ // 1. Get the logger for a scope (cached; scope bytes are pre-computed)
43
+ const orderLogger = HfLogger.get('OrderEngine');
44
+
45
+ // 2. Pre-encode EVERY message the hot path will ever emit
46
+ const MSG_ORDER_SENT = HfLogger.encodeMessage('Order sent');
47
+ const MSG_ORDER_FILLED = HfLogger.encodeMessage('Order filled');
48
+ const MSG_ORDER_REJECTED = HfLogger.encodeMessage('Order rejected');
49
+
50
+ // 3. Start the background flusher
51
+ const flusher = new HfLogFlusher();
52
+ flusher.start(100); // drain every 100ms
53
+ ```
54
+
55
+ ```typescript
56
+ // -- Hot path (bytes path, ~59ns/call) --
57
+ orderLogger.log('info', MSG_ORDER_SENT);
58
+ orderLogger.log('info', MSG_ORDER_FILLED);
59
+ orderLogger.log('error', MSG_ORDER_REJECTED);
60
+ ```
61
+
62
+ ```typescript
63
+ // -- Or drain manually (e.g. at a batch boundary or before shutdown) --
64
+ await flusher.flush();
65
+
66
+ // -- And stop the interval when the logger is no longer needed --
67
+ flusher.stop();
68
+ ```
69
+
70
+ ## The ILogger surface - and its cost model
71
+
72
+ `HfLogger` implements the same `ILogger` methods as the standard `Logger`, so it can be typed and passed around as `ILogger`. But each method sits on a different point of the cost curve, and picking the right one is the whole point of this module:
73
+
74
+ ```typescript
75
+ import { HfLogger } from '@venizia/ignis-helpers';
76
+ import type { ILogger } from '@venizia/ignis-helpers';
77
+
78
+ const logger: ILogger = HfLogger.get('OrderEngine');
79
+
80
+ // Fast: no-args string call resolves through the same bounded encode cache as
81
+ // encodeMessage() - a Map.get() plus the bytes-path write. ~66ns/call on a cache hit.
82
+ logger.info('Order sent');
83
+
84
+ // Slow path: any args force formatLogMessage() (deep inspection + secret redaction)
85
+ // and an UNCACHED encode, because dynamic strings must never grow the cache.
86
+ // Correct, but this is not the hot path - use it for control-flow events, not per-tick data.
87
+ logger.info('Order sent: %s', orderId);
88
+
89
+ // debug() returns before any encoding when SHOULD_LOG_DEBUG is false (same gate as Logger).
90
+ logger.debug('Verbose diagnostic');
91
+
92
+ // .for() composes a sub-scope the same way BaseLogger does.
93
+ const fillLogger = logger.for('fill');
94
+ fillLogger.info('Order filled');
95
+
96
+ // log() also accepts pre-encoded bytes directly - the true hot path, unchanged from before.
97
+ const MSG_ORDER_SENT = HfLogger.encodeMessage('Order sent');
98
+ logger.log('info', MSG_ORDER_SENT);
99
+ ```
100
+
101
+ Rule of thumb: pre-encode and call the bytes overload for anything on a per-event, per-tick, per-packet path. Reach for the no-args string call when the message is a small fixed set of facts you did not think to pre-encode. Reach for the args form only off the true hot path - it is correct (nothing is ever silently dropped), just not free.
102
+
103
+ ## The rules that keep it fast and correct
104
+
105
+ **1. Never encode in the hot path.** `HfLogger.encodeMessage` (and the no-args string call, which shares the same cache) remembers every distinct string it sees. The cache is FIFO-bounded at 4096 entries - calling it with dynamic strings (`encodeMessage('order ' + id)`) still puts UTF-8 encoding on your hot path and now evicts the oldest cached message once you cross the cap, corrupting the fixed vocabulary you rely on elsewhere. If a value varies per event, it does not belong in an `HfLogger` message; log the static fact here and the variable detail through the standard `Logger` at a lower frequency, or use the args form off the hot path.
106
+
107
+ **2. A fixed vocabulary of messages.** The design point of the bytes path is a finite set of pre-encoded facts ("Order sent", "Tick received", "Risk check failed"). If you find yourself needing free-form text on the hot path, you are in the wrong module - or you want the args form, off the hot path.
108
+
109
+ **3. Size the flush interval against your write rate.** The ring holds 65,536 entries. If more entries than that are written between two flushes, the oldest unflushed entries are overwritten - and the flusher now reports exactly how many via `dropped` on the sink batch (see below) instead of silently emitting stale data. Pick the interval so that `writeRate x interval < 65,536` with comfortable margin: at 100k logs/sec, a 100ms interval accumulates ~10k entries per drain - safe; at 1M logs/sec you need ~30ms or faster.
110
+
111
+ **4. One process, one thread.** `HfLogger` is safe only on a single thread within a single process - the write index is a plain counter, not shared or atomic. Do not log to it from worker threads; each worker importing the module gets its own independent ring, and nothing coordinates them. This is a documented design point, not an accident.
112
+
113
+ **5. Flush before shutdown.** Entries live only in memory. An exiting process loses everything not yet flushed - call `await flusher.flush()` in your shutdown path, and `flusher.stop()` to clear the interval.
114
+
115
+ ## The flusher
116
+
117
+ ```typescript
118
+ import { HfLogFlusher } from '@venizia/ignis-helpers';
119
+ import type { IHfLogFlusherOptions } from '@venizia/ignis-helpers';
120
+
121
+ // Default: renders to stdout, one write() per batch of up to 1024 entries.
122
+ const flusher = new HfLogFlusher();
123
+
124
+ // Append to a file instead of stdout.
125
+ const fileFlusher = new HfLogFlusher({ filePath: './app_data/hf.log' });
126
+
127
+ // Full custom delivery - receives the rendered lines AND the drop count for this batch.
128
+ const customFlusher = new HfLogFlusher({
129
+ sink: batch => {
130
+ if (batch.dropped > 0) {
131
+ console.warn(`HfLogFlusher lapped: ${batch.dropped} entries overwritten`);
132
+ }
133
+ shipToAggregator(batch.lines);
134
+ },
135
+ batchSize: 512,
136
+ });
137
+
138
+ flusher.start(100); // interval-based draining, unref'd so it never blocks process exit
139
+ await flusher.flush(); // one-shot drain, e.g. before shutdown
140
+ flusher.stop(); // clears the interval; start() again to resume
141
+ ```
142
+
143
+ A rendered line looks like this:
144
+
145
+ ```
146
+ 2026-07-18T09:41:03.128Z [info] OrderEngine Order sent
147
+ ```
148
+
149
+ `<ISO timestamp> [<level name>] <scope> <message>` - readable, parseable, and free of NULs or stale bytes.
150
+
151
+ ### Lap accounting
152
+
153
+ Every batch the flusher hands to its sink carries `dropped: number` - the count of entries overwritten by the ring before the flusher could read them since the previous batch. The default sink emits a `warn` marker line ahead of the batch when `dropped > 0`:
154
+
155
+ ```
156
+ 2026-07-18T09:41:03.200Z [warn] HfLogFlusher ring lapped - 342 entries overwritten before they could be read
157
+ ```
158
+
159
+ A custom `sink` gets the same `dropped` count on `batch.dropped` and decides how to surface it. This replaces the old silent behavior where a lapped ring simply emitted whatever currently sat in each slot.
160
+
161
+ ## Current limitations
162
+
163
+ These are real behaviors of the current implementation - design around them:
164
+
165
+ - **Single-thread only.** The write index is not shared or atomic across threads; each worker thread importing the module gets an independent ring. Do not log to `HfLogger` from worker threads expecting a shared buffer.
166
+ - **The ring overwrites the oldest entry when lapped.** If the producer writes faster than the flusher drains (rule 3 above), unflushed entries are silently overwritten in memory - but the flusher now counts and reports every one of them via `dropped`, so the loss is visible rather than invisible.
167
+ - **213-byte message cap, 32-byte scope cap.** Both are truncation-only - a longer value is cut, not rejected. The truncation is a byte-boundary cut, not a character-boundary one, so it can split a multibyte UTF-8 character - the truncated tail then renders as the U+FFFD replacement character.
168
+ - **Run one flusher per process.** Each `HfLogFlusher` tracks its own read position from the start of the ring, not a shared cursor - a second flusher re-emits entries the first one already drained.
169
+ - **A fixed, pre-encoded vocabulary is still the right pattern for the bytes path.** `HfLogger.encodeMessage` / the no-args string call exist to make the ENCODE cost a one-time expense; the args form is correct but is the slow path by design.
170
+ - **The encode cache is FIFO-bounded at 4096 entries.** Well within a real fixed vocabulary, but a hot path that generates many distinct dynamic strings through the no-args string call will start evicting and re-encoding.
171
+
172
+ ## Performance characteristics
173
+
174
+ Measured on Bun 1.3.14, 1M-iteration median:
175
+
176
+ | Path | Cost | Notes |
177
+ |------|------|-------|
178
+ | Bytes (`log(level, preEncodedBytes)`) | 59.4ns/op | The true hot path - no formatting, no allocation |
179
+ | String, no args (`info('Order sent')`) | 66.0ns/op | Cache-hit encode lookup + bytes-path write |
180
+ | pino, sync, `/dev/null` | 831ns/op | ~14x slower than the `HfLogger` bytes path on the same machine |
181
+
182
+ Heap growth measured at 0.0MB over 1M bytes-path logs - the hot path does not allocate. That buys an ENQUEUE, not a durable log line; the flusher still pays rendering cost later, off the hot path.
183
+
184
+ ## See also
185
+
186
+ - [Logger overview](/extensions/helpers/logger/) - the standard scoped logger (start here)
187
+ - [Full reference](/extensions/helpers/logger/reference) - `HfLogger`/`HfLogFlusher` API tables and the ring-buffer entry format
188
+ - [Performance best practices](/best-practices/performance-optimization) - when high-frequency logging is and is not the answer
189
+
190
+ **Files:**
191
+
192
+ - [`packages/helpers/src/modules/logger/hf/logger.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/logger/hf/logger.ts) - `HfLogger`
193
+ - [`packages/helpers/src/modules/logger/hf/flusher.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/logger/hf/flusher.ts) - `HfLogFlusher`