@adobe-commerce/aio-toolkit 1.2.6 → 1.2.7

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/CHANGELOG.md CHANGED
@@ -5,6 +5,122 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.7] - 2026-06-18
9
+
10
+ ### ✨ Features
11
+
12
+ - **feat(telemetry): add Grafana LGTM telemetry provider**
13
+
14
+ New `GrafanaTelemetry` class in `src/framework/telemetry/grafana/` forwards OpenTelemetry
15
+ signals (traces, metrics, logs) to a Grafana LGTM collector via OTLP/HTTP. Three operating
16
+ modes:
17
+
18
+ - **Dev** (`GRAFANA_DEV=true`) — targets `http://localhost:4318`, the default OTLP port of the
19
+ [Grafana LGTM Docker image](https://github.com/grafana/docker-otel-lgtm); no credentials
20
+ required. Start the stack with:
21
+ ```bash
22
+ docker run --rm -p 3000:3000 -p 4317:4317 -p 4318:4318 --name otel-lgtm grafana/otel-lgtm:latest
23
+ ```
24
+ - **Tunnel / deployed** — targets `GRAFANA_ENDPOINT` without authentication; use a Cloudflare
25
+ tunnel to expose a local stack to deployed Runtime actions:
26
+ ```bash
27
+ npx cloudflared tunnel --url http://localhost:4318
28
+ ```
29
+ - **Grafana Cloud** (`GRAFANA_CLOUD=true`) — targets `GRAFANA_ENDPOINT` with HTTP Basic auth
30
+ derived from `GRAFANA_INSTANCE_ID` and `GRAFANA_API_KEY`.
31
+
32
+ Added to the provider fallback chain after New Relic:
33
+ 1. New Relic — `NEW_RELIC_TELEMETRY=true`
34
+ 2. Grafana LGTM — `GRAFANA_TELEMETRY=true`
35
+ 3. No telemetry — original action runs uninstrumented
36
+
37
+ **Configuration reference:**
38
+
39
+ | Variable | Required | Default | Description |
40
+ |---|---|---|---|
41
+ | `ENABLE_TELEMETRY` | ✅ | — | Must be `true` to enable any telemetry |
42
+ | `GRAFANA_TELEMETRY` | ✅ | — | Must be `true` to select this provider |
43
+ | `GRAFANA_DEV` | — | `false` | `true` for localhost dev mode |
44
+ | `GRAFANA_ENDPOINT` | ✅ tunnel & Cloud / — dev | — | Collector base URL |
45
+ | `GRAFANA_SERVICE_NAME` | — | `app-builder-app` | Service name tag |
46
+ | `GRAFANA_CLOUD` | — | `false` | `true` for Grafana Cloud Basic auth |
47
+ | `GRAFANA_INSTANCE_ID` | ✅ when `GRAFANA_CLOUD=true` | — | Grafana Cloud stack instance ID |
48
+ | `GRAFANA_API_KEY` | ✅ when `GRAFANA_CLOUD=true` | — | Grafana Cloud API key / token |
49
+
50
+ - **feat(log-sanitizer): add `LogSanitizer` class and `SENSITIVE_KEYS` param support**
51
+
52
+ New `LogSanitizer` class in `src/framework/telemetry/helpers/log-sanitizer/` deep-clones a
53
+ value and replaces sensitive field values with `"[REDACTED]"` before they reach the log stream.
54
+
55
+ **Built-in sensitive key detection (case-insensitive):**
56
+ - Exact matches: `authorization`, `x-api-key`, `cookie`, `set-cookie`
57
+ - Suffix patterns: `*_api_key`, `*_secret`, `*_token`, `*_password`, `*_key`, `*-token`, `*-secret`, `*-key`
58
+
59
+ All action classes (`RuntimeAction`, `EventConsumerAction`, `OpenwhiskAction`) automatically
60
+ sanitize headers, parameters, and results in their debug log calls. `Telemetry.createLogger()`
61
+ also sanitizes all object-type arguments passed to the returned logger. `LogSanitizer` is
62
+ exported from the main package index for direct use.
63
+
64
+ **Project-specific keys via `SENSITIVE_KEYS` param:**
65
+
66
+ New `static parseSensitiveKeys(raw: unknown): ReadonlySet<string>` parses the optional
67
+ `SENSITIVE_KEYS` action param into additional keys to redact. All action classes and
68
+ `Telemetry.createLogger()` call it automatically. Accepts:
69
+
70
+ | Input type | Example | Result |
71
+ |---|---|---|
72
+ | Comma-separated string | `"x-merchant-token,internal_id"` | `Set { "x-merchant-token", "internal_id" }` |
73
+ | Array | `["x-merchant-token", "INTERNAL_ID"]` | `Set { "x-merchant-token", "internal_id" }` |
74
+ | Single string | `"x-merchant-token"` | `Set { "x-merchant-token" }` |
75
+ | Anything else (incl. `undefined`) | — | `Set {}` (no additional keys) |
76
+
77
+ Add project-specific keys in `app.config.yaml`:
78
+ ```yaml
79
+ inputs:
80
+ SENSITIVE_KEYS: "x-merchant-token,internal_order_id"
81
+ ```
82
+
83
+ ### 🛠️ Internal
84
+
85
+ - **chore(telemetry): replace `@ts-expect-error` directives with ambient OTel type declaration**
86
+
87
+ Three stale `@ts-expect-error` directives in the telemetry source are replaced by a new
88
+ ambient module declaration in `src/global-types/adobe-aio-lib-telemetry-otel.d.ts` that
89
+ correctly types the `@adobe/aio-lib-telemetry/otel` sub-path export.
90
+
91
+ - **refactor(log-sanitizer): extract constants to `types.ts`**
92
+
93
+ `EXACT_SENSITIVE_KEYS` and `SENSITIVE_KEY_SUFFIXES` moved from inline module-level constants
94
+ to exported constants in a dedicated `types.ts`, making them importable and independently
95
+ testable.
96
+
97
+ - **feat(telemetry): add `GrafanaTelemetryValidator`**
98
+
99
+ New `GrafanaTelemetryValidator` implementing `BaseTelemetryValidator`:
100
+ - `isConfigured()` — `true` when both `ENABLE_TELEMETRY=true` and `GRAFANA_TELEMETRY=true`
101
+ - `validateConfiguration()` — no-op in dev mode; throws `TelemetryInputError` when
102
+ `GRAFANA_ENDPOINT` or `GRAFANA_SERVICE_NAME` is missing/empty in tunnel or Cloud mode
103
+
104
+ - **chore(test): add `test/tsconfig.json`**
105
+
106
+ Gives VS Code's TypeScript language server the correct context for test files (jest/node
107
+ types, relaxed `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`).
108
+
109
+ - **chore(test): fix missing platform mock in Linux `isCursorIde()` test**
110
+
111
+ The Linux `execPath` test case was not mocking `os.platform()` to return `'linux'`,
112
+ causing it to fall through to the macOS branch on macOS developer machines.
113
+
114
+ ### 📚 Documentation
115
+
116
+ - **README** — new Grafana LGTM section covering all three operating modes, `app.config.yaml`
117
+ snippets, `.env` variables, usage example, and full env-var reference table.
118
+ - **README** — `LogSanitizer` section documenting the class API, `parseSensitiveKeys()`,
119
+ `SENSITIVE_KEYS` param usage, sensitive key detection table, constructor options, and
120
+ behavioural notes.
121
+
122
+ ---
123
+
8
124
  ## [1.2.6] - 2026-05-10
9
125
 
10
126
  ### ✨ Features
package/README.md CHANGED
@@ -312,7 +312,7 @@ OpenTelemetry integration for enterprise-grade observability with distributed tr
312
312
  - Access to current OpenTelemetry span via `ctx.telemetry` for custom instrumentation
313
313
  - `telemetry.instrument()` method for wrapping functions with automatic span creation
314
314
  - Conditional span access based on `ENABLE_TELEMETRY` flag
315
- - Multi-provider support (New Relic implemented, Grafana ready)
315
+ - Multi-provider support (New Relic and Grafana LGTM implemented)
316
316
  - Graceful fallback when telemetry is not configured
317
317
  - Zero performance impact when disabled (opt-in via feature flags)
318
318
 
@@ -420,6 +420,195 @@ SELECT * FROM Log WHERE `x-adobe-commerce-request-id` = 'request-123' AND level
420
420
  SELECT count(*) FROM Log FACET `action.type` SINCE 1 hour ago
421
421
  ```
422
422
 
423
+ ---
424
+
425
+ #### Grafana LGTM Telemetry
426
+
427
+ The Grafana provider forwards traces, metrics, and logs to an OTLP/HTTP collector backed by the [Grafana LGTM](https://github.com/grafana/docker-otel-lgtm) all-in-one stack (**L**oki · **G**rafana · **T**empo · **M**imir). It works in two modes:
428
+
429
+ | Mode | When | Collector URL |
430
+ |---|---|---|
431
+ | **Dev** | `GRAFANA_DEV=true` | `http://localhost:4318` (Docker image) |
432
+ | **Deployed** | `GRAFANA_DEV` not set | `GRAFANA_ENDPOINT` (e.g. Cloudflare tunnel) |
433
+
434
+ ##### Dev mode — local Docker stack
435
+
436
+ Start the LGTM stack with a single command:
437
+
438
+ ```bash
439
+ docker run --rm -p 3000:3000 -p 4317:4317 -p 4318:4318 \
440
+ --name otel-lgtm \
441
+ grafana/otel-lgtm:latest
442
+ ```
443
+
444
+ This exposes:
445
+
446
+ | Port | Service |
447
+ |---|---|
448
+ | `3000` | Grafana UI — open `http://localhost:3000` (default credentials: `admin` / `admin`) |
449
+ | `4317` | OTLP gRPC endpoint |
450
+ | `4318` | OTLP HTTP endpoint (used by the toolkit) |
451
+
452
+ Configure `app.config.yaml`:
453
+
454
+ ```yaml
455
+ runtime-action:
456
+ function: actions/runtime-action/index.js
457
+ web: 'yes'
458
+ runtime: nodejs:22
459
+ inputs:
460
+ LOG_LEVEL: debug
461
+ ENABLE_TELEMETRY: true
462
+ GRAFANA_TELEMETRY: true
463
+ GRAFANA_DEV: true
464
+ GRAFANA_SERVICE_NAME: $GRAFANA_SERVICE_NAME
465
+ annotations:
466
+ require-adobe-auth: true
467
+ final: true
468
+ ```
469
+
470
+ `.env`:
471
+
472
+ ```bash
473
+ GRAFANA_SERVICE_NAME=my-app-builder-app
474
+ ```
475
+
476
+ ##### Deployed mode — Cloudflare tunnel
477
+
478
+ Adobe I/O Runtime actions run in the cloud and cannot reach `localhost`. Expose your local LGTM stack via a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/):
479
+
480
+ ```bash
481
+ npx cloudflared tunnel --url http://localhost:4318
482
+ # Outputs a URL like: https://abc123-def456.trycloudflare.com
483
+ ```
484
+
485
+ Configure `app.config.yaml`:
486
+
487
+ ```yaml
488
+ runtime-action:
489
+ function: actions/runtime-action/index.js
490
+ web: 'yes'
491
+ runtime: nodejs:22
492
+ inputs:
493
+ LOG_LEVEL: debug
494
+ ENABLE_TELEMETRY: true
495
+ GRAFANA_TELEMETRY: true
496
+ GRAFANA_ENDPOINT: $GRAFANA_ENDPOINT
497
+ GRAFANA_SERVICE_NAME: $GRAFANA_SERVICE_NAME
498
+ annotations:
499
+ require-adobe-auth: true
500
+ final: true
501
+ ```
502
+
503
+ `.env`:
504
+
505
+ ```bash
506
+ GRAFANA_ENDPOINT=https://abc123-def456.trycloudflare.com
507
+ GRAFANA_SERVICE_NAME=my-app-builder-app
508
+ ```
509
+
510
+ ##### Usage example
511
+
512
+ Telemetry is automatically initialized when you use framework action classes:
513
+
514
+ ```javascript
515
+ /*
516
+ * <license header>
517
+ */
518
+
519
+ const { RuntimeAction, RuntimeActionResponse, HttpMethod } = require('@adobe-commerce/aio-toolkit');
520
+ const name = 'runtime-action';
521
+
522
+ exports.main = RuntimeAction.execute(
523
+ name,
524
+ [HttpMethod.POST],
525
+ [],
526
+ ['Authorization'],
527
+ async (params, ctx) => {
528
+ const { logger, telemetry } = ctx;
529
+
530
+ logger.info({
531
+ message: `${name}-log`,
532
+ params: JSON.stringify(params),
533
+ });
534
+
535
+ const sampleInstrumental = telemetry.instrument(
536
+ `runtime.action.${name}.sampleInstrumental`,
537
+ async () => {
538
+ const span = telemetry.getCurrentSpan();
539
+
540
+ if (span) {
541
+ span.setAttribute('test', 'ABC');
542
+ }
543
+
544
+ logger.info({
545
+ message: `${name}-sampleInstrumental`,
546
+ test: 'ABC',
547
+ });
548
+
549
+ return 'Hello World';
550
+ }
551
+ );
552
+
553
+ const result = await sampleInstrumental();
554
+
555
+ return RuntimeActionResponse.success(result);
556
+ }
557
+ );
558
+ ```
559
+
560
+ ##### Grafana Cloud
561
+
562
+ Grafana Cloud provides a managed LGTM backend. It uses the same OTLP/HTTP protocol but requires
563
+ HTTP Basic authentication with your stack's **Instance ID** and an **API key**.
564
+
565
+ Find your OTLP endpoint and instance ID in **Grafana Cloud → Stack → OpenTelemetry**.
566
+
567
+ Configure `app.config.yaml`:
568
+
569
+ ```yaml
570
+ runtime-action:
571
+ inputs:
572
+ ENABLE_TELEMETRY: true
573
+ GRAFANA_TELEMETRY: true
574
+ GRAFANA_CLOUD: true
575
+ GRAFANA_ENDPOINT: $GRAFANA_ENDPOINT
576
+ GRAFANA_SERVICE_NAME: $GRAFANA_SERVICE_NAME
577
+ GRAFANA_INSTANCE_ID: $GRAFANA_INSTANCE_ID
578
+ GRAFANA_API_KEY: $GRAFANA_API_KEY
579
+ ```
580
+
581
+ `.env`:
582
+
583
+ ```bash
584
+ GRAFANA_ENDPOINT=https://otlp-gateway-prod-us-east-0.grafana.net/otlp
585
+ GRAFANA_SERVICE_NAME=my-app-builder-app
586
+ GRAFANA_INSTANCE_ID=123456
587
+ GRAFANA_API_KEY=glc_xxx
588
+ ```
589
+
590
+ ##### Environment variable reference
591
+
592
+ | Variable | Mode | Required | Default | Description |
593
+ |---|---|---|---|---|
594
+ | `ENABLE_TELEMETRY` | all | ✅ | — | Must be `true` to enable any telemetry |
595
+ | `GRAFANA_TELEMETRY` | all | ✅ | — | Must be `true` to select the Grafana provider |
596
+ | `GRAFANA_DEV` | dev | — | `false` | Set `true` to use `http://localhost:4318` |
597
+ | `GRAFANA_CLOUD` | cloud | — | `false` | Set `true` to enable Grafana Cloud Basic auth |
598
+ | `GRAFANA_ENDPOINT` | tunnel / cloud | ✅ | — | Collector base URL (not needed in dev mode) |
599
+ | `GRAFANA_SERVICE_NAME` | tunnel / cloud | ✅ | `app-builder-app` | Service name tag (not needed in dev mode) |
600
+ | `GRAFANA_INSTANCE_ID` | cloud | ✅ | — | Grafana Cloud stack instance ID |
601
+ | `GRAFANA_API_KEY` | cloud | ✅ | — | Grafana Cloud API key / token |
602
+
603
+ **Multi-provider fallback chain:**
604
+
605
+ The toolkit tries providers in this order:
606
+ 1. **New Relic** — when `NEW_RELIC_TELEMETRY=true`
607
+ 2. **Grafana LGTM** — when `GRAFANA_TELEMETRY=true`
608
+ 3. **No telemetry** — original action runs uninstrumented
609
+
610
+ ---
611
+
423
612
  **Supported Action Classes:**
424
613
 
425
614
  Telemetry is automatically integrated with all framework action classes:
@@ -430,6 +619,97 @@ Telemetry is automatically integrated with all framework action classes:
430
619
 
431
620
  All action classes provide structured logging with automatic request ID and action type tracking.
432
621
 
622
+ #### `LogSanitizer`
623
+
624
+ Utility class that deep-clones a value and replaces sensitive field values with `"[REDACTED]"` before writing to logs. Used internally by all action classes and available for direct use.
625
+
626
+ **Sensitive key detection (case-insensitive):**
627
+
628
+ | Match type | Keys / patterns |
629
+ |---|---|
630
+ | Exact | `authorization`, `x-api-key`, `cookie`, `set-cookie` |
631
+ | Suffix | `*_api_key`, `*_secret`, `*_token`, `*_password`, `*_key`, `*-token`, `*-secret`, `*-key` |
632
+
633
+ **Constructor:**
634
+
635
+ ```typescript
636
+ new LogSanitizer(
637
+ additionalSensitiveKeys?: ReadonlySet<string>, // extra keys to redact (lowercase)
638
+ maxDepth?: number // recursion limit, default 10
639
+ )
640
+ ```
641
+
642
+ **`LogSanitizer.parseSensitiveKeys(raw: unknown): ReadonlySet<string>`**
643
+
644
+ Static helper that parses the `SENSITIVE_KEYS` runtime param into a `ReadonlySet<string>` suitable for the constructor. Accepts three input forms:
645
+
646
+ | Input type | Example value | Result |
647
+ |---|---|---|
648
+ | Comma-separated string | `"x-merchant-token,internal_id"` | `Set { "x-merchant-token", "internal_id" }` |
649
+ | Array | `["x-merchant-token", "INTERNAL_ID"]` | `Set { "x-merchant-token", "internal_id" }` |
650
+ | Single string | `"x-merchant-token"` | `Set { "x-merchant-token" }` |
651
+ | Anything else (incl. `undefined`) | — | `Set {}` (no-op) |
652
+
653
+ All keys are lowercased; whitespace around commas and in array elements is trimmed; non-string array elements are silently dropped.
654
+
655
+ **Automatic `SENSITIVE_KEYS` param support:**
656
+
657
+ All action classes (`RuntimeAction`, `EventConsumerAction`, `OpenwhiskAction`) and `Telemetry.createLogger()` automatically call `parseSensitiveKeys(params.SENSITIVE_KEYS)` when initializing `LogSanitizer`. To extend sanitization with project-specific keys, add them to `app.config.yaml`:
658
+
659
+ ```yaml
660
+ application:
661
+ actions:
662
+ my-action:
663
+ function: actions/my-action/index.js
664
+ inputs:
665
+ SENSITIVE_KEYS: "x-merchant-token,internal_order_id"
666
+ ```
667
+
668
+ Or pass an array when testing:
669
+
670
+ ```typescript
671
+ await myAction({ ...params, SENSITIVE_KEYS: ['x-merchant-token', 'internal_order_id'] });
672
+ ```
673
+
674
+ **Usage:**
675
+
676
+ ```typescript
677
+ const { LogSanitizer } = require('@adobe-commerce/aio-toolkit');
678
+
679
+ const sanitizer = new LogSanitizer();
680
+
681
+ // Sanitize HTTP headers before logging
682
+ logger.debug({
683
+ message: 'Headers received',
684
+ headers: sanitizer.execute(params.__ow_headers || {}),
685
+ });
686
+ // authorization, x-api-key, cookie → "[REDACTED]"
687
+
688
+ // Sanitize full action parameters (includes env-bound secrets)
689
+ logger.debug({
690
+ message: 'Parameters received',
691
+ parameters: sanitizer.execute(params),
692
+ });
693
+ // MY_API_KEY, NEW_RELIC_LICENSE_KEY, GRAFANA_API_KEY → "[REDACTED]"
694
+
695
+ // Extend with project-specific keys using parseSensitiveKeys
696
+ const customSanitizer = new LogSanitizer(
697
+ LogSanitizer.parseSensitiveKeys(params.SENSITIVE_KEYS)
698
+ );
699
+ customSanitizer.execute(data);
700
+
701
+ // Or construct a Set directly
702
+ const fixedSanitizer = new LogSanitizer(new Set(['x-merchant-token', 'my_internal_secret']));
703
+ fixedSanitizer.execute(data);
704
+ ```
705
+
706
+ **Behaviour:**
707
+ - Returns a deep copy — never mutates the input
708
+ - Handles nested objects and arrays recursively (configurable depth limit)
709
+ - Primitives (`string`, `number`, `boolean`, `null`, `undefined`) are returned as-is
710
+
711
+ ---
712
+
433
713
  #### `RuntimeApiGatewayService`
434
714
  Flexible Adobe I/O Runtime API Gateway client that accepts bearer tokens for authenticated requests.
435
715
 
@@ -216,7 +216,7 @@ var require_package = __commonJS({
216
216
  "package.json"(exports2, module2) {
217
217
  module2.exports = {
218
218
  name: "@adobe-commerce/aio-toolkit",
219
- version: "1.2.6",
219
+ version: "1.2.7",
220
220
  description: "A comprehensive TypeScript toolkit for Adobe App Builder applications providing standardized Adobe Commerce integrations, I/O Events orchestration, file storage utilities, authentication helpers, and robust backend development tools with 100% test coverage.",
221
221
  main: "./dist/index.js",
222
222
  module: "./dist/index.mjs",