@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.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- 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
|
|
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
|
|
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
|
|
48
|
+
## What it solves
|
|
43
49
|
|
|
44
|
-
Without a schema registry, producers and consumers must agree on message
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
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
|
|
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:
|
|
155
|
+
## Alternative: manual serializers
|
|
187
156
|
|
|
188
|
-
Instead of passing the full registry
|
|
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
|
-
##
|
|
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
|
-
-
|
|
214
|
-
-
|
|
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`
|