@inference-net/otel-cf-workers 2.0.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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Erwin van der Koogh
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,426 @@
1
+ # otel-cf-workers
2
+
3
+ OpenTelemetry instrumentation for Cloudflare Workers with automatic **tracing** and **logging** for handlers, bindings, and distributed traces.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ yarn add @inference-net/otel-cf-workers @opentelemetry/api
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ Add the `nodejs_compat` compatibility flag to your `wrangler.toml`:
14
+
15
+ ```toml
16
+ compatibility_flags = ["nodejs_compat"]
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### Tracing Only
22
+
23
+ ```typescript
24
+ import { trace } from '@opentelemetry/api'
25
+ import { instrument, ResolveConfigFn } from '@inference-net/otel-cf-workers'
26
+
27
+ export interface Env {
28
+ SIGNOZ_ENDPOINT: string
29
+ SIGNOZ_ACCESS_TOKEN: string
30
+ MY_KV: KVNamespace
31
+ MY_D1: D1Database
32
+ }
33
+
34
+ const handler = {
35
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
36
+ // Auto-instrumented: HTTP handler
37
+ await fetch('https://api.example.com') // Auto-instrumented: outbound fetch
38
+
39
+ await env.MY_KV.get('key') // Auto-instrumented: KV operations
40
+ await env.MY_D1.prepare('SELECT * FROM users').all() // Auto-instrumented: D1 queries
41
+
42
+ // Manual instrumentation: add custom attributes
43
+ trace.getActiveSpan()?.setAttribute('user.id', '123')
44
+
45
+ return new Response('Hello World!')
46
+ },
47
+ }
48
+
49
+ const config: ResolveConfigFn = (env: Env, _trigger) => {
50
+ return {
51
+ service: { name: 'my-worker' },
52
+ trace: {
53
+ exporter: {
54
+ url: env.SIGNOZ_ENDPOINT,
55
+ headers: { 'signoz-access-token': env.SIGNOZ_ACCESS_TOKEN },
56
+ },
57
+ },
58
+ }
59
+ }
60
+
61
+ export default instrument(handler, config)
62
+ ```
63
+
64
+ ### Tracing + Logging
65
+
66
+ ```typescript
67
+ import { trace } from '@opentelemetry/api'
68
+ import { instrument, getLogger, OTLPTransport, ConsoleTransport } from '@inference-net/otel-cf-workers'
69
+
70
+ const handler = {
71
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
72
+ const logger = getLogger('my-app')
73
+
74
+ // Logs automatically include trace context (trace ID, span ID)
75
+ logger.info('Processing request', {
76
+ 'http.url': request.url,
77
+ 'user.id': '123',
78
+ })
79
+
80
+ try {
81
+ await env.MY_KV.get('key')
82
+ logger.debug('KV operation complete')
83
+
84
+ return new Response('OK')
85
+ } catch (error) {
86
+ // Error logs automatically extract exception info
87
+ logger.error(error as Error)
88
+ return new Response('Error', { status: 500 })
89
+ }
90
+ },
91
+ }
92
+
93
+ const config: ResolveConfigFn = (env: Env, _trigger) => ({
94
+ service: { name: 'my-worker' },
95
+ trace: {
96
+ exporter: {
97
+ url: `${env.OTEL_ENDPOINT}/v1/traces`,
98
+ headers: { 'x-api-key': env.API_KEY },
99
+ },
100
+ },
101
+ logs: {
102
+ transports: [
103
+ new OTLPTransport({
104
+ url: `${env.OTEL_ENDPOINT}/v1/logs`,
105
+ headers: { 'x-api-key': env.API_KEY },
106
+ }),
107
+ new ConsoleTransport({ pretty: true }), // Also log to console
108
+ ],
109
+ },
110
+ })
111
+
112
+ export default instrument(handler, config)
113
+ ```
114
+
115
+ ### Durable Objects
116
+
117
+ ```typescript
118
+ import { instrumentDO, ResolveConfigFn } from '@inference-net/otel-cf-workers'
119
+
120
+ class MyDurableObject implements DurableObject {
121
+ async fetch(request: Request): Promise<Response> {
122
+ // Auto-instrumented: DO fetch handler
123
+ await this.ctx.storage.get('key') // Auto-instrumented: DO storage
124
+ await this.ctx.storage.sql.exec('SELECT * FROM data') // Auto-instrumented: DO SQL
125
+ return new Response('Hello from DO!')
126
+ }
127
+
128
+ async alarm(): Promise<void> {
129
+ // Auto-instrumented: DO alarm handler
130
+ }
131
+ }
132
+
133
+ const config: ResolveConfigFn = (env, _trigger) => ({
134
+ exporter: { url: env.OTEL_ENDPOINT },
135
+ service: { name: 'my-durable-object' },
136
+ })
137
+
138
+ export const MyDO = instrumentDO(MyDurableObject, config)
139
+ ```
140
+
141
+ ## OpenTelemetry Features
142
+
143
+ ### ✅ Fully Supported
144
+
145
+ **Tracing:**
146
+
147
+ - **Distributed Tracing**: Automatic W3C Trace Context propagation across services
148
+ - **Semantic Conventions**: Full support for OpenTelemetry semantic conventions (v1.28.0+)
149
+ - `db.query.text` - Database queries and keys
150
+ - `db.system.name` - Database system identification
151
+ - `db.operation.name` - Operation types
152
+ - `db.operation.batch.size` - Batch operation tracking
153
+ - `http.*` - HTTP request/response attributes
154
+ - `faas.*` - FaaS trigger and execution attributes
155
+ - **Custom Spans**: Create manual spans with `trace.getTracer()`
156
+ - **Span Attributes**: Set custom attributes on active spans
157
+ - **Context Propagation**: Async context management across Workers runtime
158
+ - **Sampling**: Both head and tail sampling strategies
159
+ - **Exporters**: OTLP/HTTP (JSON) format
160
+ - **Span Processors**: Custom trace-based batch processing
161
+
162
+ **Logging:**
163
+
164
+ - **Structured Logging**: OpenTelemetry Logs API with convenience methods
165
+ - **Automatic Trace Correlation**: Logs include trace ID and span ID from active spans
166
+ - **Child Loggers**: Inherit attributes from parent loggers for context propagation
167
+ - **Multiple Transports**: Send logs to OTLP backends, console, or custom destinations
168
+ - **Batching Strategies**: Configurable batching (immediate or size-based)
169
+ - **Severity Levels**: Standard OpenTelemetry severity levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
170
+ - **Console Instrumentation**: Optional capture of `console.log()`, `console.error()`, etc.
171
+ - **Custom Transports**: Extensible transport interface for custom log destinations
172
+
173
+ 📖 **See [LOGS.md](./LOGS.md) for complete logging documentation**
174
+
175
+ ### Cloudflare-Specific Attributes
176
+
177
+ In addition to OpenTelemetry standard attributes, we capture Cloudflare-specific metadata:
178
+
179
+ - `cloudflare.*` - Platform-specific attributes (ray ID, colo, script version)
180
+ - `geo.*` - Request geolocation data
181
+ - Response metadata (TTL, cache status, rows read/written)
182
+ - Binding-specific attributes (KV keys, D1 query stats, R2 checksums)
183
+
184
+ ## Cloudflare Platform Support
185
+
186
+ ### Triggers & Handlers
187
+
188
+ | Feature | Status | Notes |
189
+ | ------------------------------- | ------ | -------------------------------------------------- |
190
+ | HTTP Handler (`fetch`) | ✅ | Full support with geo, headers, user-agent parsing |
191
+ | Scheduled Handler (`scheduled`) | ✅ | Cron trigger instrumentation |
192
+ | Queue Consumer (`queue`) | ✅ | Message batch processing with ack/retry tracking |
193
+ | Email Handler (`email`) | ✅ | Incoming email processing |
194
+ | Durable Object `fetch` | ✅ | DO HTTP requests |
195
+ | Durable Object `alarm` | ✅ | DO alarm triggers |
196
+ | `ctx.waitUntil` | ✅ | Background promise tracking |
197
+ | Tail Handler (`tail`) | ❌ | Not yet supported |
198
+ | DO Hibernated WebSocket | ❌ | Not yet supported |
199
+
200
+ ### Bindings
201
+
202
+ | Binding | Status | Operations Instrumented |
203
+ | --------------------- | ------ | ---------------------------------------------------------------------------------------- |
204
+ | **KV Namespace** | ✅ | `get`, `put`, `delete`, `list`, `getWithMetadata` |
205
+ | **R2 Bucket** | ✅ | `head`, `get`, `put`, `delete`, `list`, `createMultipartUpload`, `resumeMultipartUpload` |
206
+ | **D1 Database** | ✅ | `prepare`, `exec`, `batch`, `all`, `run`, `first`, `raw` |
207
+ | **Durable Objects** | ✅ | Stub `fetch` calls |
208
+ | **DO Storage (KV)** | ✅ | `get`, `put`, `delete`, `list`, `getAlarm`, `setAlarm`, `deleteAlarm` |
209
+ | **DO Storage (SQL)** | ✅ | `exec`, `execBatch` |
210
+ | **Queue Producer** | ✅ | `send`, `sendBatch` |
211
+ | **Service Bindings** | ✅ | Worker-to-worker calls |
212
+ | **Analytics Engine** | ✅ | `writeDataPoint` |
213
+ | **Images** | ✅ | `get`, `list`, `delete` |
214
+ | **Rate Limiting** | ✅ | `limit` |
215
+ | **Workers AI** | ❌ | Not yet supported |
216
+ | **Vectorize** | ❌ | Not yet supported |
217
+ | **Hyperdrive** | ❌ | Not yet supported |
218
+ | **Browser Rendering** | ❌ | Not yet supported |
219
+ | **Email Sending** | ❌ | Not yet supported |
220
+ | **mTLS** | ❌ | Not yet supported |
221
+
222
+ ### Global APIs
223
+
224
+ | API | Status | Notes |
225
+ | --------- | ------ | ----------------------------------------------- |
226
+ | `fetch()` | ✅ | Global fetch calls with trace context injection |
227
+ | `caches` | ✅ | Cache API operations |
228
+
229
+ ### Cloudflare Modules
230
+
231
+ | Module | Status |
232
+ | -------------------- | ------ |
233
+ | `cloudflare:email` | ❌ |
234
+ | `cloudflare:sockets` | ❌ |
235
+
236
+ ## Configuration
237
+
238
+ ### Basic Configuration
239
+
240
+ ```typescript
241
+ const config: ResolveConfigFn = (env: Env, trigger) => ({
242
+ service: {
243
+ name: 'my-service',
244
+ version: '1.0.0', // Optional
245
+ namespace: 'production', // Optional
246
+ },
247
+ trace: {
248
+ exporter: {
249
+ url: env.SIGNOZ_ENDPOINT,
250
+ headers: { 'signoz-access-token': env.SIGNOZ_ACCESS_TOKEN },
251
+ },
252
+ },
253
+ // Logs are optional
254
+ logs: {
255
+ transports: [new OTLPTransport({ url: env.LOGS_ENDPOINT })],
256
+ },
257
+ })
258
+ ```
259
+
260
+ **Note:** Both `trace` and `logs` are optional. You can configure:
261
+
262
+ - Tracing only
263
+ - Logging only
264
+ - Both tracing and logging
265
+ - Neither (no telemetry)
266
+
267
+ ### Sampling
268
+
269
+ ```typescript
270
+ const config: ResolveConfigFn = (env, trigger) => ({
271
+ // ... exporter config
272
+ sampling: {
273
+ // Head sampling: sample 10% of requests at start
274
+ headSampler: {
275
+ ratio: 0.1,
276
+ acceptRemote: true, // Accept parent trace decisions
277
+ },
278
+ // Tail sampling: always keep errors even if not head-sampled
279
+ tailSampler: (trace) => {
280
+ const rootSpan = trace.localRootSpan
281
+ return (
282
+ rootSpan.status.code === SpanStatusCode.ERROR || (rootSpan.spanContext().traceFlags & TraceFlags.SAMPLED) !== 0
283
+ )
284
+ },
285
+ },
286
+ })
287
+ ```
288
+
289
+ ### Trace Context Propagation
290
+
291
+ ```typescript
292
+ const config: ResolveConfigFn = (env, trigger) => ({
293
+ // ... exporter config
294
+
295
+ // Control outbound trace context
296
+ fetch: {
297
+ includeTraceContext: (request) => {
298
+ // Only propagate to same-origin requests
299
+ return new URL(request.url).hostname === 'api.example.com'
300
+ },
301
+ },
302
+
303
+ // Control inbound trace context
304
+ handlers: {
305
+ fetch: {
306
+ acceptTraceContext: (request) => {
307
+ // Accept trace context from trusted origins
308
+ return request.headers.get('x-trusted') === 'true'
309
+ },
310
+ },
311
+ },
312
+ })
313
+ ```
314
+
315
+ ### Post-Processing
316
+
317
+ Redact sensitive data before export:
318
+
319
+ ```typescript
320
+ const config: ResolveConfigFn = (env, trigger) => ({
321
+ // ... exporter config
322
+ postProcessor: (spans) => {
323
+ return spans.map((span) => {
324
+ // Redact URLs with tokens
325
+ if (span.attributes['http.url']) {
326
+ span.attributes['http.url'] = span.attributes['http.url'].replace(/token=[^&]+/, 'token=REDACTED')
327
+ }
328
+ // Remove sensitive headers
329
+ delete span.attributes['http.request.header.authorization']
330
+ return span
331
+ })
332
+ },
333
+ })
334
+ ```
335
+
336
+ ### Custom Propagator
337
+
338
+ ```typescript
339
+ const config: ResolveConfigFn = (env, trigger) => ({
340
+ // ... exporter config
341
+ propagator: new MyCustomPropagator(),
342
+ })
343
+ ```
344
+
345
+ ## Manual Instrumentation
346
+
347
+ ### Adding Attributes
348
+
349
+ ```typescript
350
+ import { trace } from '@opentelemetry/api'
351
+
352
+ const handler = {
353
+ async fetch(request: Request, env: Env) {
354
+ const span = trace.getActiveSpan()
355
+ if (span) {
356
+ span.setAttribute('user.id', '123')
357
+ span.setAttribute('user.role', 'admin')
358
+ }
359
+ return new Response('OK')
360
+ },
361
+ }
362
+ ```
363
+
364
+ ### Creating Custom Spans
365
+
366
+ ```typescript
367
+ import { trace, SpanStatusCode } from '@opentelemetry/api'
368
+
369
+ const handler = {
370
+ async fetch(request: Request, env: Env) {
371
+ const tracer = trace.getTracer('my-app')
372
+
373
+ return await tracer.startActiveSpan('process-request', async (span) => {
374
+ span.setAttribute('request.id', crypto.randomUUID())
375
+
376
+ try {
377
+ const result = await doWork()
378
+ span.setStatus({ code: SpanStatusCode.OK })
379
+ return new Response(result)
380
+ } catch (error) {
381
+ span.recordException(error)
382
+ span.setStatus({ code: SpanStatusCode.ERROR })
383
+ throw error
384
+ } finally {
385
+ span.end()
386
+ }
387
+ })
388
+ },
389
+ }
390
+ ```
391
+
392
+ ## Limitations
393
+
394
+ - **Timing Accuracy**: The Workers runtime does not expose accurate timing information to protect against Spectre attacks. CPU-bound work may show 0ms duration. The clock only updates on I/O operations.
395
+ - **RPC-Style DO Calls**: Direct RPC method calls to Durable Objects (e.g., `await stub.myMethod()`) are not auto-instrumented. Use fetch-style calls (`await stub.fetch(request)`) for automatic tracing.
396
+
397
+ ## Examples
398
+
399
+ See the [examples directory](./examples) for complete working examples:
400
+
401
+ **Tracing:**
402
+
403
+ - [Basic Worker](./examples/worker) - HTTP handler with KV and D1
404
+ - [Quickstart Guide](./examples/quickstart/QUICKSTART_GUIDE.md) - Step-by-step tutorial
405
+
406
+ **Logging:**
407
+
408
+ - [Basic Logging](./examples/logs-basic.ts) - Simple logging setup
409
+ - [Advanced Logging](./examples/logs-advanced.ts) - Traces + logs correlation
410
+ - [Logs Only](./examples/logs-only.ts) - Logging without tracing
411
+ - [Child Loggers](./examples/logs-child-loggers.ts) - Context inheritance with child loggers
412
+
413
+ ## Resources
414
+
415
+ - [Logging Documentation](./LOGS.md) - Complete guide to OpenTelemetry Logs
416
+ - [OpenTelemetry Documentation](https://opentelemetry.io/docs/)
417
+ - [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/)
418
+ - [Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/)
419
+
420
+ ## License
421
+
422
+ BSD-3-Clause
423
+
424
+ ## Contributing
425
+
426
+ Contributions welcome! This is a fork maintained by [@context-labs](https://github.com/context-labs), originally from [evanderkoogh/otel-cf-workers](https://github.com/evanderkoogh/otel-cf-workers).
@@ -0,0 +1,38 @@
1
+ import { getLogger, SEVERITY_NUMBERS } from './index.js';
2
+
3
+ const severityMap = {
4
+ debug: SEVERITY_NUMBERS.DEBUG,
5
+ log: SEVERITY_NUMBERS.INFO,
6
+ info: SEVERITY_NUMBERS.INFO,
7
+ warn: SEVERITY_NUMBERS.WARN,
8
+ error: SEVERITY_NUMBERS.ERROR
9
+ };
10
+ function instrumentConsole() {
11
+ const logger = getLogger("console");
12
+ for (const [method, severityNumber] of Object.entries(severityMap)) {
13
+ const consoleMethod = method;
14
+ const original = console[consoleMethod];
15
+ if (typeof original !== "function") {
16
+ continue;
17
+ }
18
+ console[consoleMethod] = function(...args) {
19
+ try {
20
+ logger.emit({
21
+ severityNumber,
22
+ severityText: method.toUpperCase(),
23
+ body: args.length === 1 ? String(args[0]) : args.map(String).join(" "),
24
+ attributes: {
25
+ "log.source": "console",
26
+ "log.method": method,
27
+ "log.args_count": args.length
28
+ }
29
+ });
30
+ } catch (error) {
31
+ }
32
+ return original.apply(console, args);
33
+ };
34
+ }
35
+ }
36
+
37
+ export { instrumentConsole };
38
+ //# sourceMappingURL=console-Cy5QdByD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-Cy5QdByD.js","sources":["../src/logs/console.ts"],"sourcesContent":["import { getLogger } from './provider.js'\nimport { SEVERITY_NUMBERS } from '../constants.js'\n\nconst severityMap = {\n\tdebug: SEVERITY_NUMBERS.DEBUG,\n\tlog: SEVERITY_NUMBERS.INFO,\n\tinfo: SEVERITY_NUMBERS.INFO,\n\twarn: SEVERITY_NUMBERS.WARN,\n\terror: SEVERITY_NUMBERS.ERROR,\n} as const\n\ntype ConsoleMethod = keyof typeof severityMap\n\n/**\n * Instrument console methods to emit OpenTelemetry log records\n * This is OPT-IN and must be explicitly enabled in configuration\n */\nexport function instrumentConsole() {\n\tconst logger = getLogger('console')\n\n\tfor (const [method, severityNumber] of Object.entries(severityMap)) {\n\t\tconst consoleMethod = method as ConsoleMethod\n\t\tconst original = console[consoleMethod]\n\n\t\tif (typeof original !== 'function') {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Replace console method with instrumented version\n\t\t;(console as any)[consoleMethod] = function (...args: any[]) {\n\t\t\ttry {\n\t\t\t\t// Emit log record\n\t\t\t\tlogger.emit({\n\t\t\t\t\tseverityNumber,\n\t\t\t\t\tseverityText: method.toUpperCase(),\n\t\t\t\t\tbody: args.length === 1 ? String(args[0]) : args.map(String).join(' '),\n\t\t\t\t\tattributes: {\n\t\t\t\t\t\t'log.source': 'console',\n\t\t\t\t\t\t'log.method': method,\n\t\t\t\t\t\t'log.args_count': args.length,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\t// Don't break console if logging fails - but can't use console.error here!\n\t\t\t}\n\n\t\t\t// Always call original console method\n\t\t\treturn original.apply(console, args)\n\t\t}\n\t}\n}\n"],"names":[],"mappings":";;AAGA,MAAM,WAAA,GAAc;AAAA,EACnB,OAAO,gBAAA,CAAiB,KAAA;AAAA,EACxB,KAAK,gBAAA,CAAiB,IAAA;AAAA,EACtB,MAAM,gBAAA,CAAiB,IAAA;AAAA,EACvB,MAAM,gBAAA,CAAiB,IAAA;AAAA,EACvB,OAAO,gBAAA,CAAiB;AACzB,CAAA;AAQO,SAAS,iBAAA,GAAoB;AACnC,EAAA,MAAM,MAAA,GAAS,UAAU,SAAS,CAAA;AAElC,EAAA,KAAA,MAAW,CAAC,MAAA,EAAQ,cAAc,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACnE,IAAA,MAAM,aAAA,GAAgB,MAAA;AACtB,IAAA,MAAM,QAAA,GAAW,QAAQ,aAAa,CAAA;AAEtC,IAAA,IAAI,OAAO,aAAa,UAAA,EAAY;AACnC,MAAA;AAAA,IACD;AAGC,IAAC,OAAA,CAAgB,aAAa,CAAA,GAAI,SAAA,GAAa,IAAA,EAAa;AAC5D,MAAA,IAAI;AAEH,QAAA,MAAA,CAAO,IAAA,CAAK;AAAA,UACX,cAAA;AAAA,UACA,YAAA,EAAc,OAAO,WAAA,EAAY;AAAA,UACjC,IAAA,EAAM,IAAA,CAAK,MAAA,KAAW,CAAA,GAAI,OAAO,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,CAAE,KAAK,GAAG,CAAA;AAAA,UACrE,UAAA,EAAY;AAAA,YACX,YAAA,EAAc,SAAA;AAAA,YACd,YAAA,EAAc,MAAA;AAAA,YACd,kBAAkB,IAAA,CAAK;AAAA;AACxB,SACA,CAAA;AAAA,MACF,SAAS,KAAA,EAAO;AAAA,MAEhB;AAGA,MAAA,OAAO,QAAA,CAAS,KAAA,CAAM,OAAA,EAAS,IAAI,CAAA;AAAA,IACpC,CAAA;AAAA,EACD;AACD;;;;"}