@nifrajs/otel 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.
@@ -0,0 +1,24 @@
1
+ /** Token-only OpenTelemetry spans for capability admission, execution, compensation, and reconciliation. */
2
+ import type { EffectLifecycleObserver } from "@nifrajs/core/effect-lifecycle";
3
+ import type { IdentityPlugin } from "@nifrajs/core/server";
4
+ import type { ObservationAdapter } from "./span.js";
5
+ export interface EffectTracingOptions {
6
+ readonly exporter?: ObservationAdapter;
7
+ readonly adapters?: readonly ObservationAdapter[];
8
+ /** Maximum unmatched started events retained at once. Default 10,000. */
9
+ readonly maxActive?: number;
10
+ /** Opportunistically expire unmatched events older than this many milliseconds. Default 5 min. */
11
+ readonly maxActiveAgeMs?: number;
12
+ /** Injectable wall clock for deterministic retention tests. */
13
+ readonly now?: () => number;
14
+ }
15
+ export interface EffectTracingPlugin extends IdentityPlugin {
16
+ /** Pass the same observer to `createSagaEngine({ observer })` and reconciliation helpers. */
17
+ readonly observer: EffectLifecycleObserver;
18
+ }
19
+ /**
20
+ * Installs child effect spans on subsequent routes. The observer consumes only the constrained
21
+ * `EffectLifecycleEvent` contract; request/business payloads and error text cannot enter an export.
22
+ */
23
+ export declare function effectTracing(options?: EffectTracingOptions): EffectTracingPlugin;
24
+ //# sourceMappingURL=effects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effects.d.ts","sourceRoot":"","sources":["../src/effects.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAE5G,OAAO,KAAK,EAAwB,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AACnG,OAAO,KAAK,EAAa,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErE,OAAO,KAAK,EAAkB,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAEnE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAA;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACjD,yEAAyE;IACzE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,kGAAkG;IAClG,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IACzD,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAA;CAC3C;AAoBD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,mBAAmB,CAoGrF"}
@@ -0,0 +1,129 @@
1
+ /** Token-only OpenTelemetry spans for capability admission, execution, compensation, and reconciliation. */
2
+ import { createObservationLifecycle } from "./lifecycle.js";
3
+ function attributesOf(event) {
4
+ const attributes = {
5
+ "nifra.effect.id": event.effectId,
6
+ "nifra.effect.capability": event.capability,
7
+ "nifra.effect.stage": event.stage,
8
+ "nifra.effect.phase": event.phase,
9
+ };
10
+ if (event.target !== undefined)
11
+ attributes["nifra.effect.target"] = event.target;
12
+ if (event.digest !== undefined)
13
+ attributes["nifra.effect.digest"] = event.digest;
14
+ if (event.attempt !== undefined)
15
+ attributes["nifra.effect.attempt"] = event.attempt;
16
+ if (event.errorCode !== undefined)
17
+ attributes["nifra.effect.error_code"] = event.errorCode;
18
+ if (event.durationMs !== undefined)
19
+ attributes["nifra.effect.duration_ms"] = event.durationMs;
20
+ for (const [axis, value] of Object.entries(event.cost ?? {})) {
21
+ attributes[`nifra.effect.cost.${axis}`] = value;
22
+ }
23
+ return attributes;
24
+ }
25
+ /**
26
+ * Installs child effect spans on subsequent routes. The observer consumes only the constrained
27
+ * `EffectLifecycleEvent` contract; request/business payloads and error text cannot enter an export.
28
+ */
29
+ export function effectTracing(options = {}) {
30
+ const adapters = [
31
+ ...(options.exporter === undefined ? [] : [options.exporter]),
32
+ ...(options.adapters ?? []),
33
+ ];
34
+ const maxActive = options.maxActive ?? 10_000;
35
+ const maxActiveAgeMs = options.maxActiveAgeMs ?? 5 * 60_000;
36
+ if (!Number.isSafeInteger(maxActive) || maxActive < 1) {
37
+ throw new RangeError("effect tracing maxActive must be a positive safe integer");
38
+ }
39
+ if (!Number.isSafeInteger(maxActiveAgeMs) || maxActiveAgeMs < 1) {
40
+ throw new RangeError("effect tracing maxActiveAgeMs must be a positive safe integer");
41
+ }
42
+ const now = options.now ?? Date.now;
43
+ const lifecycle = createObservationLifecycle({ adapters });
44
+ const active = new Map();
45
+ let nextSweepAt = Number.POSITIVE_INFINITY;
46
+ const keyOf = (event) => `${event.effectId}\n${event.stage}`;
47
+ const evict = (key, errorCode) => {
48
+ const entry = active.get(key);
49
+ if (entry === undefined)
50
+ return;
51
+ active.delete(key);
52
+ entry.observation.setAttributes({
53
+ "nifra.effect.phase": "ambiguous",
54
+ "nifra.effect.error_code": errorCode,
55
+ });
56
+ entry.observation.end({ status: "error" });
57
+ };
58
+ const sweep = () => {
59
+ const at = now();
60
+ if (!Number.isFinite(at) || at < nextSweepAt)
61
+ return at;
62
+ // `active` iterates in insertion order and entries carry a monotonic `at`, so the front is the
63
+ // oldest / earliest-expiring. Evict the expired prefix and stop at the first live entry: its expiry
64
+ // is the earliest remaining, i.e. the next sweep deadline. A sweep is therefore O(evicted), not
65
+ // O(active.size). (A backwards clock only defers an eviction to the size cap - the hard bound; age
66
+ // eviction is opportunistic.)
67
+ nextSweepAt = Number.POSITIVE_INFINITY;
68
+ for (const [key, entry] of active) {
69
+ const expiresAt = entry.at + maxActiveAgeMs;
70
+ if (at >= expiresAt) {
71
+ evict(key, "observation_expired");
72
+ }
73
+ else {
74
+ nextSweepAt = expiresAt;
75
+ break;
76
+ }
77
+ }
78
+ return at;
79
+ };
80
+ const observer = (event) => {
81
+ const at = sweep();
82
+ const key = keyOf(event);
83
+ if (event.phase === "started") {
84
+ if (active.has(key))
85
+ return;
86
+ while (active.size >= maxActive) {
87
+ const oldest = active.keys().next().value;
88
+ if (oldest === undefined)
89
+ break;
90
+ evict(oldest, "observation_evicted");
91
+ }
92
+ const startedAt = Number.isFinite(at) ? at : event.at;
93
+ active.set(key, Object.freeze({
94
+ observation: lifecycle.start({
95
+ name: `nifra.effect.${event.stage}`,
96
+ parent: event.trace ?? null,
97
+ attributes: attributesOf(event),
98
+ }),
99
+ at: startedAt,
100
+ }));
101
+ nextSweepAt = Math.min(nextSweepAt, startedAt + maxActiveAgeMs);
102
+ return;
103
+ }
104
+ const entry = active.get(key);
105
+ let observation;
106
+ if (entry === undefined) {
107
+ observation = lifecycle.start({
108
+ name: `nifra.effect.${event.stage}`,
109
+ parent: event.trace ?? null,
110
+ attributes: attributesOf(event),
111
+ });
112
+ }
113
+ else {
114
+ active.delete(key);
115
+ observation = entry.observation;
116
+ observation.setAttributes(attributesOf(event));
117
+ }
118
+ observation.end({ status: event.phase === "succeeded" ? "ok" : "error" });
119
+ };
120
+ const apply = (app) => {
121
+ app.observeCapability(observer);
122
+ return app;
123
+ };
124
+ return Object.assign(apply, {
125
+ pluginName: "nifra:effect-tracing",
126
+ observer,
127
+ });
128
+ }
129
+ //# sourceMappingURL=effects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effects.js","sourceRoot":"","sources":["../src/effects.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
10
10
  */
11
11
  export { causalitySpanLink } from "./causality.js";
12
+ export { type EffectTracingOptions, type EffectTracingPlugin, effectTracing, } from "./effects.js";
12
13
  export { type ActiveObservation, createObservationLifecycle, type EndObservation, type ObservationClock, type ObservationContext, type ObservationLifecycle, type ObservationLifecycleOptions, type ObservationParent, type StartObservation, } from "./lifecycle.js";
13
14
  export { type OtlpExporter, type OtlpExporterOptions, otlpExporter } from "./otlp.js";
14
15
  export { type AttributeValue, combineObservationAdapters, consoleSpanExporter, type NifraSpan, type ObservationAdapter, type ObservationLink, type SpanStatus, } from "./span.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EACL,KAAK,iBAAiB,EACtB,0BAA0B,EAC1B,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EACL,KAAK,cAAc,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,aAAa,GACd,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,iBAAiB,EACtB,0BAA0B,EAC1B,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EACL,KAAK,cAAc,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
10
10
  */
11
11
  export { causalitySpanLink } from "./causality.js";
12
+ export { effectTracing, } from "./effects.js";
12
13
  export { createObservationLifecycle, } from "./lifecycle.js";
13
14
  export { otlpExporter } from "./otlp.js";
14
15
  export { combineObservationAdapters, consoleSpanExporter, } from "./span.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EAEL,0BAA0B,GAQ3B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAA+C,YAAY,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EAEL,0BAA0B,EAC1B,mBAAmB,GAKpB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EAEf,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA0C,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EAGL,aAAa,GACd,MAAM,cAAc,CAAA;AACrB,OAAO,EAEL,0BAA0B,GAQ3B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAA+C,YAAY,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EAEL,0BAA0B,EAC1B,mBAAmB,GAKpB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EAEf,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA0C,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/otel",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Distributed tracing for nifra — W3C traceparent/tracestate propagation + OpenTelemetry-semantic-convention spans via a pluggable exporter. Bridge to the OpenTelemetry SDK or log spans directly; no SDK bundled. Edge-safe.",
5
5
  "keywords": [
6
6
  "bun",
@@ -29,6 +29,11 @@
29
29
  "bun": "./src/metrics.ts",
30
30
  "types": "./dist/metrics.d.ts",
31
31
  "default": "./dist/metrics.js"
32
+ },
33
+ "./effects": {
34
+ "bun": "./src/effects.ts",
35
+ "types": "./dist/effects.d.ts",
36
+ "default": "./dist/effects.js"
32
37
  }
33
38
  },
34
39
  "files": [
@@ -44,7 +49,7 @@
44
49
  "access": "public"
45
50
  },
46
51
  "peerDependencies": {
47
- "@nifrajs/core": "^2.0.0"
52
+ "@nifrajs/core": "^2.1.0"
48
53
  },
49
54
  "devDependencies": {
50
55
  "@nifrajs/core": "workspace:*"
package/src/effects.ts ADDED
@@ -0,0 +1,146 @@
1
+ /** Token-only OpenTelemetry spans for capability admission, execution, compensation, and reconciliation. */
2
+
3
+ import type { EffectLifecycleEvent, EffectLifecycleObserver } from "@nifrajs/core/effect-lifecycle"
4
+ import type { AnyServer, IdentityPlugin } from "@nifrajs/core/server"
5
+ import { type ActiveObservation, createObservationLifecycle } from "./lifecycle.ts"
6
+ import type { AttributeValue, ObservationAdapter } from "./span.ts"
7
+
8
+ export interface EffectTracingOptions {
9
+ readonly exporter?: ObservationAdapter
10
+ readonly adapters?: readonly ObservationAdapter[]
11
+ /** Maximum unmatched started events retained at once. Default 10,000. */
12
+ readonly maxActive?: number
13
+ /** Opportunistically expire unmatched events older than this many milliseconds. Default 5 min. */
14
+ readonly maxActiveAgeMs?: number
15
+ /** Injectable wall clock for deterministic retention tests. */
16
+ readonly now?: () => number
17
+ }
18
+
19
+ export interface EffectTracingPlugin extends IdentityPlugin {
20
+ /** Pass the same observer to `createSagaEngine({ observer })` and reconciliation helpers. */
21
+ readonly observer: EffectLifecycleObserver
22
+ }
23
+
24
+ function attributesOf(event: EffectLifecycleEvent): Record<string, AttributeValue> {
25
+ const attributes: Record<string, AttributeValue> = {
26
+ "nifra.effect.id": event.effectId,
27
+ "nifra.effect.capability": event.capability,
28
+ "nifra.effect.stage": event.stage,
29
+ "nifra.effect.phase": event.phase,
30
+ }
31
+ if (event.target !== undefined) attributes["nifra.effect.target"] = event.target
32
+ if (event.digest !== undefined) attributes["nifra.effect.digest"] = event.digest
33
+ if (event.attempt !== undefined) attributes["nifra.effect.attempt"] = event.attempt
34
+ if (event.errorCode !== undefined) attributes["nifra.effect.error_code"] = event.errorCode
35
+ if (event.durationMs !== undefined) attributes["nifra.effect.duration_ms"] = event.durationMs
36
+ for (const [axis, value] of Object.entries(event.cost ?? {})) {
37
+ attributes[`nifra.effect.cost.${axis}`] = value
38
+ }
39
+ return attributes
40
+ }
41
+
42
+ /**
43
+ * Installs child effect spans on subsequent routes. The observer consumes only the constrained
44
+ * `EffectLifecycleEvent` contract; request/business payloads and error text cannot enter an export.
45
+ */
46
+ export function effectTracing(options: EffectTracingOptions = {}): EffectTracingPlugin {
47
+ const adapters = [
48
+ ...(options.exporter === undefined ? [] : [options.exporter]),
49
+ ...(options.adapters ?? []),
50
+ ]
51
+ const maxActive = options.maxActive ?? 10_000
52
+ const maxActiveAgeMs = options.maxActiveAgeMs ?? 5 * 60_000
53
+ if (!Number.isSafeInteger(maxActive) || maxActive < 1) {
54
+ throw new RangeError("effect tracing maxActive must be a positive safe integer")
55
+ }
56
+ if (!Number.isSafeInteger(maxActiveAgeMs) || maxActiveAgeMs < 1) {
57
+ throw new RangeError("effect tracing maxActiveAgeMs must be a positive safe integer")
58
+ }
59
+ const now = options.now ?? Date.now
60
+ const lifecycle = createObservationLifecycle({ adapters })
61
+ const active = new Map<string, { readonly observation: ActiveObservation; readonly at: number }>()
62
+ let nextSweepAt = Number.POSITIVE_INFINITY
63
+ const keyOf = (event: EffectLifecycleEvent): string => `${event.effectId}\n${event.stage}`
64
+
65
+ const evict = (key: string, errorCode: "observation_evicted" | "observation_expired"): void => {
66
+ const entry = active.get(key)
67
+ if (entry === undefined) return
68
+ active.delete(key)
69
+ entry.observation.setAttributes({
70
+ "nifra.effect.phase": "ambiguous",
71
+ "nifra.effect.error_code": errorCode,
72
+ })
73
+ entry.observation.end({ status: "error" })
74
+ }
75
+
76
+ const sweep = (): number => {
77
+ const at = now()
78
+ if (!Number.isFinite(at) || at < nextSweepAt) return at
79
+ // `active` iterates in insertion order and entries carry a monotonic `at`, so the front is the
80
+ // oldest / earliest-expiring. Evict the expired prefix and stop at the first live entry: its expiry
81
+ // is the earliest remaining, i.e. the next sweep deadline. A sweep is therefore O(evicted), not
82
+ // O(active.size). (A backwards clock only defers an eviction to the size cap - the hard bound; age
83
+ // eviction is opportunistic.)
84
+ nextSweepAt = Number.POSITIVE_INFINITY
85
+ for (const [key, entry] of active) {
86
+ const expiresAt = entry.at + maxActiveAgeMs
87
+ if (at >= expiresAt) {
88
+ evict(key, "observation_expired")
89
+ } else {
90
+ nextSweepAt = expiresAt
91
+ break
92
+ }
93
+ }
94
+ return at
95
+ }
96
+
97
+ const observer: EffectLifecycleObserver = (event) => {
98
+ const at = sweep()
99
+ const key = keyOf(event)
100
+ if (event.phase === "started") {
101
+ if (active.has(key)) return
102
+ while (active.size >= maxActive) {
103
+ const oldest = active.keys().next().value as string | undefined
104
+ if (oldest === undefined) break
105
+ evict(oldest, "observation_evicted")
106
+ }
107
+ const startedAt = Number.isFinite(at) ? at : event.at
108
+ active.set(
109
+ key,
110
+ Object.freeze({
111
+ observation: lifecycle.start({
112
+ name: `nifra.effect.${event.stage}`,
113
+ parent: event.trace ?? null,
114
+ attributes: attributesOf(event),
115
+ }),
116
+ at: startedAt,
117
+ }),
118
+ )
119
+ nextSweepAt = Math.min(nextSweepAt, startedAt + maxActiveAgeMs)
120
+ return
121
+ }
122
+ const entry = active.get(key)
123
+ let observation: ActiveObservation
124
+ if (entry === undefined) {
125
+ observation = lifecycle.start({
126
+ name: `nifra.effect.${event.stage}`,
127
+ parent: event.trace ?? null,
128
+ attributes: attributesOf(event),
129
+ })
130
+ } else {
131
+ active.delete(key)
132
+ observation = entry.observation
133
+ observation.setAttributes(attributesOf(event))
134
+ }
135
+ observation.end({ status: event.phase === "succeeded" ? "ok" : "error" })
136
+ }
137
+
138
+ const apply = <S extends AnyServer>(app: S): S => {
139
+ app.observeCapability(observer)
140
+ return app
141
+ }
142
+ return Object.assign(apply, {
143
+ pluginName: "nifra:effect-tracing",
144
+ observer,
145
+ }) as EffectTracingPlugin
146
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,11 @@
10
10
  */
11
11
 
12
12
  export { causalitySpanLink } from "./causality.ts"
13
+ export {
14
+ type EffectTracingOptions,
15
+ type EffectTracingPlugin,
16
+ effectTracing,
17
+ } from "./effects.ts"
13
18
  export {
14
19
  type ActiveObservation,
15
20
  createObservationLifecycle,