rian 0.2.5 → 0.3.0-next.10
Sign up to get free protection for your applications and to get access to all the features.
- package/async.d.ts +59 -0
- package/async.js +1 -0
- package/async.mjs +1 -0
- package/exporter.otel.http.d.ts +3 -0
- package/exporter.otel.http.js +1 -0
- package/exporter.otel.http.mjs +1 -0
- package/exporter.zipkin.d.ts +3 -0
- package/exporter.zipkin.js +1 -0
- package/exporter.zipkin.mjs +1 -0
- package/index.d.ts +120 -80
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +16 -10
- package/readme.md +130 -130
- package/utils.d.ts +25 -0
- package/utils.js +1 -0
- package/utils.mjs +1 -0
package/async.d.ts
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
import type { CallableScope, Options, Scope } from 'rian';
|
2
|
+
|
3
|
+
export { report, configure } from 'rian';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Returns the current span in the current execution context.
|
7
|
+
*
|
8
|
+
* This will throw an error if there is no current span.
|
9
|
+
*
|
10
|
+
* @example
|
11
|
+
*
|
12
|
+
* ```ts
|
13
|
+
* function doWork() {
|
14
|
+
* const span = currentSpan();
|
15
|
+
* span.set_context({ foo: 'bar' });
|
16
|
+
* }
|
17
|
+
*
|
18
|
+
* span('some-name')(() => {
|
19
|
+
* doWork(); // will guarantee `currentSpan` returns this span
|
20
|
+
* });
|
21
|
+
* ```
|
22
|
+
*/
|
23
|
+
export function currentSpan(): Scope;
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Creates a new span for the currently active tracer.
|
27
|
+
*
|
28
|
+
* @example
|
29
|
+
*
|
30
|
+
* ```ts
|
31
|
+
* tracer('some-name')(() => {
|
32
|
+
* // some deeply nested moments later
|
33
|
+
* const s = span('my-span');
|
34
|
+
* });
|
35
|
+
* ```
|
36
|
+
*/
|
37
|
+
export function span(name: string): CallableScope;
|
38
|
+
|
39
|
+
export type Tracer<T> = (cb: T) => ReturnType<T>;
|
40
|
+
|
41
|
+
/**
|
42
|
+
* A tracer is a logical unit in your application. This alleviates the need to pass around a tracer instance.
|
43
|
+
*
|
44
|
+
* All spans produced by a tracer will all collect into a single span collection that is given to {@link report}.
|
45
|
+
*
|
46
|
+
* @example
|
47
|
+
*
|
48
|
+
* ```ts
|
49
|
+
* const trace = tracer('server');
|
50
|
+
*
|
51
|
+
* trace(() => {
|
52
|
+
* // application logic
|
53
|
+
* });
|
54
|
+
* ```
|
55
|
+
*/
|
56
|
+
export function tracer<T extends () => any>(
|
57
|
+
name: string,
|
58
|
+
options?: Options,
|
59
|
+
): Tracer<T>;
|
package/async.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
const e = require('node:async_hooks');const { measure:t } = require('rian/utils');const { make:n, parse:r, SAMPLED_FLAG:o } = require('tctx');const { is_sampled:a } = require('tctx');var s={};function i(e,t={}){s={...t,"service.name":e,"telemetry.sdk.name":"rian","telemetry.sdk.version":"0.3.0-next.9"}}var l=new Set,c=new WeakMap;async function p(e){let t=[],n=new Map;for(let[e,r]of l){let o;n.has(r)?o=n.get(r).spans:n.set(r,{scope:r,spans:o=[]}),o.push(e),c.has(r)&&(t.push(...c.get(r)),c.delete(r))}return l.clear(),t.length&&await Promise.all(t),e({resource:s,scopeSpans:n.values()})}function u(e,t){return a(t)}var d=new e.AsyncLocalStorage;function m(){let e=d.getStore()?.[1];if(null==e)throw new Error("no current span");return e}function f(e){let r=d.getStore();if(!r)throw Error("TODO");let o=r[0],a=o.scope,s=r[1],i=o.sampler,p=s?.traceparent??o.root_id,u=p?p.child():n(),m="boolean"!=typeof i?i(e,u,a):i;u.flags;let w={id:u,parent:p,start:Date.now(),name:e,events:[],context:{}};m&&l.add([w,a]);let g=e=>d.run([o,g],t,g,e);g.traceparent=u,g.span=f,g.set_context=e=>{"function"!=typeof e?Object.assign(w.context,e):w.context=e(w.context)},g.add_event=(e,t)=>{w.events.push({name:e,timestamp:Date.now(),attributes:t||{}})},g.end=()=>{null==w.end&&(w.end=Date.now())};let h=c.get(a);return g.__add_promise=e=>{h.add(e),e.then((()=>h.delete(e)))},g}function w(e,t){let n=t?.sampler??u,o={name:e},a={root_id:"string"==typeof t?.traceparent?r(t.traceparent):void 0,scope:o,sampler:n};return c.set(o,new Set),function(e){let t=d.getStore();return a.root_id||(a.root_id=t?.[0].root_id),d.run([a,t?.[1]||null],e)}}exports.configure=i;exports.currentSpan=m;exports.report=p;exports.span=f;exports.tracer=w;
|
package/async.mjs
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
import*as e from"node:async_hooks";import{measure as t}from"rian/utils";import{make as n,parse as r,SAMPLED_FLAG as o}from"tctx";import{is_sampled as a}from"tctx";var s={};function i(e,t={}){s={...t,"service.name":e,"telemetry.sdk.name":"rian","telemetry.sdk.version":"0.3.0-next.9"}}var l=new Set,c=new WeakMap;async function p(e){let t=[],n=new Map;for(let[e,r]of l){let o;n.has(r)?o=n.get(r).spans:n.set(r,{scope:r,spans:o=[]}),o.push(e),c.has(r)&&(t.push(...c.get(r)),c.delete(r))}return l.clear(),t.length&&await Promise.all(t),e({resource:s,scopeSpans:n.values()})}function u(e,t){return a(t)}var d=new e.AsyncLocalStorage;function m(){let e=d.getStore()?.[1];if(null==e)throw new Error("no current span");return e}function f(e){let r=d.getStore();if(!r)throw Error("TODO");let o=r[0],a=o.scope,s=r[1],i=o.sampler,p=s?.traceparent??o.root_id,u=p?p.child():n(),m="boolean"!=typeof i?i(e,u,a):i;u.flags;let w={id:u,parent:p,start:Date.now(),name:e,events:[],context:{}};m&&l.add([w,a]);let g=e=>d.run([o,g],t,g,e);g.traceparent=u,g.span=f,g.set_context=e=>{"function"!=typeof e?Object.assign(w.context,e):w.context=e(w.context)},g.add_event=(e,t)=>{w.events.push({name:e,timestamp:Date.now(),attributes:t||{}})},g.end=()=>{null==w.end&&(w.end=Date.now())};let h=c.get(a);return g.__add_promise=e=>{h.add(e),e.then((()=>h.delete(e)))},g}function w(e,t){let n=t?.sampler??u,o={name:e},a={root_id:"string"==typeof t?.traceparent?r(t.traceparent):void 0,scope:o,sampler:n};return c.set(o,new Set),function(e){let t=d.getStore();return a.root_id||(a.root_id=t?.[0].root_id),d.run([a,t?.[1]||null],e)}}export{i as configure,m as currentSpan,p as report,f as span,w as tracer};
|
@@ -0,0 +1 @@
|
|
1
|
+
var e=r=>{let a=typeof r,s={};return"string"===a?s.stringValue=r:"number"===a?Number.isInteger(r)?s.intValue=r:s.doubleValue=r:"boolean"===a?s.boolValue=r:Array.isArray(r)?s.arrayValue={values:r.map((t=>e(t)))}:r&&(s.kvlistValue={values:t(r)}),s},t=t=>{let r=[];for(let a of Object.keys(t))r.push({key:a,value:e(t[a])});return r},r=e=>{switch(e){default:case"INTERNAL":return 1;case"SERVER":return 2;case"CLIENT":return 3;case"PRODUCER":return 4;case"CONSUMER":return 5}},a=e=>a=>{let s=[];for(let e of a.scopeSpans){let a=[];s.push({scope:e.scope,spans:a});for(let s of e.spans){let e,{kind:n,error:u,...o}=s.context;u&&(e={code:2},"message"in u&&(e.message=u.message)),a.push({traceId:s.id.trace_id,spanId:s.id.parent_id,parentSpanId:s.parent?.parent_id,name:s.name,kind:r(n||"INTERNAL"),startTimeUnixNano:1e6*s.start,endTimeUnixNano:s.end?1e6*s.end:void 0,droppedAttributesCount:0,droppedEventsCount:0,droppedLinksCount:0,attributes:t(o),status:e||{code:0},events:s.events.map((e=>({name:e.name,attributes:t(e.attributes),droppedAttributesCount:0,timeUnixNano:1e6*e.timestamp})))})}}return e({resourceSpans:[{resource:{attributes:t(a.resource),droppedAttributesCount:0},scopeSpans:s}]})};exports.exporter=a;
|
@@ -0,0 +1 @@
|
|
1
|
+
var e=r=>{let a=typeof r,s={};return"string"===a?s.stringValue=r:"number"===a?Number.isInteger(r)?s.intValue=r:s.doubleValue=r:"boolean"===a?s.boolValue=r:Array.isArray(r)?s.arrayValue={values:r.map((t=>e(t)))}:r&&(s.kvlistValue={values:t(r)}),s},t=t=>{let r=[];for(let a of Object.keys(t))r.push({key:a,value:e(t[a])});return r},r=e=>{switch(e){default:case"INTERNAL":return 1;case"SERVER":return 2;case"CLIENT":return 3;case"PRODUCER":return 4;case"CONSUMER":return 5}},a=e=>a=>{let s=[];for(let e of a.scopeSpans){let a=[];s.push({scope:e.scope,spans:a});for(let s of e.spans){let e,{kind:n,error:u,...o}=s.context;u&&(e={code:2},"message"in u&&(e.message=u.message)),a.push({traceId:s.id.trace_id,spanId:s.id.parent_id,parentSpanId:s.parent?.parent_id,name:s.name,kind:r(n||"INTERNAL"),startTimeUnixNano:1e6*s.start,endTimeUnixNano:s.end?1e6*s.end:void 0,droppedAttributesCount:0,droppedEventsCount:0,droppedLinksCount:0,attributes:t(o),status:e||{code:0},events:s.events.map((e=>({name:e.name,attributes:t(e.attributes),droppedAttributesCount:0,timeUnixNano:1e6*e.timestamp})))})}}return e({resourceSpans:[{resource:{attributes:t(a.resource),droppedAttributesCount:0},scopeSpans:s}]})};export{a as exporter};
|
@@ -0,0 +1 @@
|
|
1
|
+
const { flattie:e } = require('flattie');var t=t=>a=>{let r=[];for(let t of a.scopeSpans)for(let n of t.spans){let{kind:s,error:i,...o}=n.context;i&&(o.error=!("message"in i)||{name:i.name,message:i.message,stack:i.stack}),r.push({id:n.id.parent_id,traceId:n.id.trace_id,parentId:n.parent?n.parent.parent_id:void 0,name:n.name,kind:"INTERNAL"===s?void 0:s,timestamp:1e3*n.start,duration:n.end?1e3*(n.end-n.start):void 0,localEndpoint:{serviceName:`${a.resource["service.name"]}@${t.scope.name}`},tags:e({...a.resource,...o},".",!0),annotations:n.events.map((e=>({value:`${e.name} :: ${JSON.stringify(e.attributes)}`,timestamp:1e3*e.timestamp})))})}return t(r)};exports.exporter=t;
|
@@ -0,0 +1 @@
|
|
1
|
+
import{flattie as e}from"flattie";var t=t=>a=>{let r=[];for(let t of a.scopeSpans)for(let n of t.spans){let{kind:s,error:i,...o}=n.context;i&&(o.error=!("message"in i)||{name:i.name,message:i.message,stack:i.stack}),r.push({id:n.id.parent_id,traceId:n.id.trace_id,parentId:n.parent?n.parent.parent_id:void 0,name:n.name,kind:"INTERNAL"===s?void 0:s,timestamp:1e3*n.start,duration:n.end?1e3*(n.end-n.start):void 0,localEndpoint:{serviceName:`${a.resource["service.name"]}@${t.scope.name}`},tags:e({...a.resource,...o},".",!0),annotations:n.events.map((e=>({value:`${e.name} :: ${JSON.stringify(e.attributes)}`,timestamp:1e3*e.timestamp})))})}return t(r)};export{t as exporter};
|
package/index.d.ts
CHANGED
@@ -1,31 +1,89 @@
|
|
1
1
|
import type { Traceparent } from 'tctx';
|
2
2
|
|
3
|
+
// --- tracer
|
4
|
+
|
5
|
+
/**
|
6
|
+
* The exporter is called when the {@link report} method is called.
|
7
|
+
*/
|
8
|
+
export type Exporter = (trace: {
|
9
|
+
resource: Context;
|
10
|
+
scopeSpans: IterableIterator<ScopedSpans>;
|
11
|
+
}) => any;
|
12
|
+
|
13
|
+
export type ScopedSpans = {
|
14
|
+
readonly scope: { readonly name: string };
|
15
|
+
readonly spans: ReadonlyArray<Readonly<Span>>;
|
16
|
+
};
|
17
|
+
|
18
|
+
export type Options = {
|
19
|
+
/**
|
20
|
+
* @borrows {@link Sampler}
|
21
|
+
*/
|
22
|
+
sampler?: Sampler | boolean;
|
23
|
+
|
24
|
+
/**
|
25
|
+
* A root, or extracted w3c traceparent string header.
|
26
|
+
*
|
27
|
+
* If the id is malformed, the {@link create} method will throw an exception. If no root is
|
28
|
+
* provided then one will be created obeying the {@link Options.sampler|sampling} rules on each span.
|
29
|
+
*/
|
30
|
+
traceparent?: string | null;
|
31
|
+
};
|
32
|
+
|
33
|
+
export type Tracer = Pick<Scope, 'span'>;
|
34
|
+
|
35
|
+
/**
|
36
|
+
* @borrows {@link Span.context}
|
37
|
+
*/
|
38
|
+
export type Context = {
|
39
|
+
[property: string]: any;
|
40
|
+
};
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Allows a sampling decision to be made. This method will influence the {@link Span.id|traceparent} sampling flag.
|
44
|
+
*
|
45
|
+
* Return true if the span should be sampled, and reported to the {@link Exporter}.
|
46
|
+
* Return false if the span should not be sampled, and not reported to the {@link Exporter}.
|
47
|
+
*/
|
48
|
+
export type Sampler = (
|
49
|
+
/**
|
50
|
+
* The name of the span.
|
51
|
+
*/
|
52
|
+
readonly name: string,
|
53
|
+
/**
|
54
|
+
* The traceparent id of the span.
|
55
|
+
*/
|
56
|
+
readonly id: Traceparent,
|
57
|
+
/**
|
58
|
+
* The tracer this span belongs to.
|
59
|
+
*/
|
60
|
+
readonly tracer: { readonly name: string },
|
61
|
+
) => boolean;
|
62
|
+
|
63
|
+
// --- spans
|
64
|
+
|
3
65
|
/**
|
4
66
|
* Spans are units within a distributed trace. Spans encapsulate mainly 3 pieces of information, a
|
5
67
|
* {@link Span.name|name}, and a {@link Span.start|start} and {@link Span.end|end} time.
|
6
68
|
*
|
7
69
|
* Each span should be named, not too vague, and not too precise. For example, "resolve_user_ids"
|
8
|
-
* and not "resolver_user_ids[1,2,3]" nor "
|
70
|
+
* and not "resolver_user_ids[1,2,3]" nor "resolver".
|
9
71
|
*
|
10
72
|
* A span forms part of a wider trace, and can be visualized like:
|
11
73
|
*
|
12
74
|
* ```plain
|
13
75
|
* [Span A················································(2ms)]
|
14
76
|
* [Span B·········································(1.7ms)]
|
15
|
-
* [Span D···············(0.8ms)]
|
77
|
+
* [Span D···············(0.8ms)] [Span C......(0.6ms)]
|
16
78
|
* ```
|
17
|
-
*
|
18
|
-
* ---
|
19
|
-
*
|
20
|
-
* Spans are aimed to interoperate with
|
21
|
-
* {@link https://github.com/opentracing/specification/blob/master/specification.md|OpenTracing's Spans}, albeit not entirely api compatible — they do share principles.
|
22
79
|
*/
|
23
|
-
export
|
80
|
+
export type Span = {
|
24
81
|
/**
|
25
82
|
* A human-readable name for this span. For example the function name, the name of a subtask,
|
26
83
|
* or stage of the larger stack.
|
27
84
|
*
|
28
85
|
* @example
|
86
|
+
*
|
29
87
|
* "resolve_user_ids"
|
30
88
|
* "[POST] /api"
|
31
89
|
*/
|
@@ -49,7 +107,7 @@ export interface Span {
|
|
49
107
|
/**
|
50
108
|
* The time represented as a UNIX epoch timestamp in milliseconds when this span was created.
|
51
109
|
* Typically, via
|
52
|
-
* {@link Scope.
|
110
|
+
* {@link Scope.span|scope.span()}.
|
53
111
|
*/
|
54
112
|
start: number;
|
55
113
|
|
@@ -63,7 +121,7 @@ export interface Span {
|
|
63
121
|
* An arbitrary context object useful for storing information during a trace.
|
64
122
|
*
|
65
123
|
* Usually following a convention such as `tag.*`, `http.*` or any of the
|
66
|
-
* {@link https://
|
124
|
+
* {@link https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/|OpenTelemetry Trace Semantic Conventions}.
|
67
125
|
*
|
68
126
|
* ### Note!
|
69
127
|
*
|
@@ -84,9 +142,11 @@ export interface Span {
|
|
84
142
|
* new span.
|
85
143
|
*/
|
86
144
|
events: { name: string; timestamp: number; attributes: Context }[];
|
87
|
-
}
|
145
|
+
};
|
146
|
+
|
147
|
+
// --- scopes
|
88
148
|
|
89
|
-
export
|
149
|
+
export type Scope = {
|
90
150
|
/**
|
91
151
|
* A W3C traceparent. One can .toString() this if you want to cross a network.
|
92
152
|
*/
|
@@ -95,7 +155,12 @@ export interface Scope {
|
|
95
155
|
/**
|
96
156
|
* Forks the span into a new child span.
|
97
157
|
*/
|
98
|
-
|
158
|
+
span(
|
159
|
+
/**
|
160
|
+
* @borrows {@link Span.name}
|
161
|
+
*/
|
162
|
+
name: string,
|
163
|
+
): CallableScope;
|
99
164
|
|
100
165
|
/**
|
101
166
|
* Allows the span's context to be set. Passing an object will be `Object.assign`ed into the
|
@@ -116,82 +181,57 @@ export interface Scope {
|
|
116
181
|
* timestamp nulled out — when the tracer ends.
|
117
182
|
*/
|
118
183
|
end(): void;
|
119
|
-
}
|
184
|
+
};
|
185
|
+
|
186
|
+
export type CallableScope = Scope & {
|
187
|
+
<Fn extends (scope: Omit<Scope, 'end'>) => any>(cb: Fn): ReturnType<Fn>;
|
188
|
+
};
|
120
189
|
|
121
|
-
|
122
|
-
end(): ReturnType<Exporter>;
|
123
|
-
}
|
190
|
+
// --- main api
|
124
191
|
|
125
192
|
/**
|
126
|
-
*
|
127
|
-
*
|
193
|
+
* A tracer is a logical unit in your application. This alleviates the need to pass around a tracer instance.
|
194
|
+
*
|
195
|
+
* All spans produced by a tracer will all collect into a single span collection that is given to {@link report}.
|
196
|
+
*
|
197
|
+
* @example
|
198
|
+
*
|
199
|
+
* ```ts
|
200
|
+
* // file: server.ts
|
201
|
+
* const trace = tracer('server');
|
202
|
+
*
|
203
|
+
* // file: orm.ts
|
204
|
+
* const trace = tracer('orm');
|
205
|
+
*
|
206
|
+
* // file: api.ts
|
207
|
+
* const trace = tracer('api');
|
208
|
+
* ```
|
128
209
|
*/
|
129
|
-
export
|
130
|
-
|
131
|
-
|
132
|
-
) => any;
|
210
|
+
export function tracer(name: string, options?: Options): Tracer;
|
211
|
+
|
212
|
+
// -- general api
|
133
213
|
|
134
214
|
/**
|
135
|
-
*
|
215
|
+
* Awaits all active promises, and then calls the {@link Options.exporter|exporter}. Passing all collected spans.
|
136
216
|
*/
|
137
|
-
export
|
138
|
-
|
139
|
-
|
217
|
+
export async function report<T extends Exporter>(
|
218
|
+
exporter: T,
|
219
|
+
): Promise<ReturnType<T>>;
|
140
220
|
|
141
221
|
/**
|
142
|
-
*
|
143
|
-
*
|
222
|
+
* Calling this method will set the resource attributes for this runtime. This is useful for things like:
|
223
|
+
* - setting the deployment environment of the application
|
224
|
+
* - setting the k8s namespace
|
225
|
+
* - ...
|
144
226
|
*
|
145
|
-
* The
|
146
|
-
* filtered out there.
|
227
|
+
* The `name` argument will set the `service.name` attribute. And is required.
|
147
228
|
*
|
148
|
-
*
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
/**
|
157
|
-
* Provinding a clock allows you to control the time of the span.
|
229
|
+
* The fields can be whatever you want, but it is recommended to follow the {@link https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/|OpenTelemetry Resource Semantic Conventions}.
|
230
|
+
*
|
231
|
+
* @example
|
232
|
+
*
|
233
|
+
* ```ts
|
234
|
+
* configure('my-service', { 'deployment.environment': 'production', 'k8s.namespace.name': 'default' });
|
235
|
+
* ```
|
158
236
|
*/
|
159
|
-
export
|
160
|
-
/**
|
161
|
-
* Must return the number of milliseconds since the epoch.
|
162
|
-
*/
|
163
|
-
now(): number;
|
164
|
-
};
|
165
|
-
|
166
|
-
export interface Options {
|
167
|
-
/**
|
168
|
-
* @borrows {@link Exporter}
|
169
|
-
*/
|
170
|
-
exporter: Exporter;
|
171
|
-
|
172
|
-
/**
|
173
|
-
* @borrows {@link Sampler}
|
174
|
-
*/
|
175
|
-
sampler?: Sampler | boolean;
|
176
|
-
|
177
|
-
context?: Context;
|
178
|
-
|
179
|
-
/**
|
180
|
-
* A root, or extracted w3c traceparent stringed header.
|
181
|
-
*
|
182
|
-
* If the id is malformed, the {@link create} method will throw an exception. If no root is
|
183
|
-
* provided then one will be created obeying the {@link Options.sampler|sampling} rules.
|
184
|
-
*/
|
185
|
-
traceparent?: string | null;
|
186
|
-
|
187
|
-
clock?: ClockLike;
|
188
|
-
}
|
189
|
-
|
190
|
-
export const create: (name: string, options: Options) => Tracer;
|
191
|
-
|
192
|
-
// ==> internals
|
193
|
-
|
194
|
-
/** @internal */
|
195
|
-
export interface CallableScope extends Scope {
|
196
|
-
(cb: (scope: Omit<Scope, 'end'>) => void): ReturnType<typeof cb>;
|
197
|
-
}
|
237
|
+
export function configure(name: string, attributes: Context = {}): void;
|
package/index.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
const {
|
1
|
+
const { measure:e } = require('rian/utils');const { make:t, parse:n, SAMPLED_FLAG:a } = require('tctx');const { is_sampled:r } = require('tctx');var s={};function o(e,t={}){s={...t,"service.name":e,"telemetry.sdk.name":"rian","telemetry.sdk.version":"0.3.0-next.9"}}var p=new Set,i=new WeakMap;async function l(e){let t=[],n=new Map;for(let[e,a]of p){let r;n.has(a)?r=n.get(a).spans:n.set(a,{scope:a,spans:r=[]}),r.push(e),i.has(a)&&(t.push(...i.get(a)),i.delete(a))}return p.clear(),t.length&&await Promise.all(t),e({resource:s,scopeSpans:n.values()})}function c(e,t){return r(t)}function d(a,r){let s=r?.sampler??c,o={name:a},l=new Set;i.set(o,l);let d="string"==typeof r?.traceparent?n(r.traceparent):void 0,m=(n,a)=>{let r=a?a.child():t(),i="boolean"!=typeof s?s(n,r,o):s;r.flags;let c={id:r,parent:a,start:Date.now(),name:n,events:[],context:{}};i&&p.add([c,o]);let d=t=>e(d,t);return d.traceparent=r,d.span=e=>m(e,r),d.set_context=e=>"function"==typeof e?void(c.context=e(c.context)):void Object.assign(c.context,e),d.add_event=(e,t)=>{c.events.push({name:e,timestamp:Date.now(),attributes:t||{}})},d.end=()=>{null==c.end&&(c.end=Date.now())},d.__add_promise=e=>{l.add(e),e.then((()=>l.delete(e)))},d};return{span:e=>m(e,d)}}exports.configure=o;exports.report=l;exports.tracer=d;
|
package/index.mjs
CHANGED
@@ -1 +1 @@
|
|
1
|
-
import{
|
1
|
+
import{measure as e}from"rian/utils";import{make as t,parse as n,SAMPLED_FLAG as a}from"tctx";import{is_sampled as r}from"tctx";var s={};function o(e,t={}){s={...t,"service.name":e,"telemetry.sdk.name":"rian","telemetry.sdk.version":"0.3.0-next.9"}}var p=new Set,i=new WeakMap;async function l(e){let t=[],n=new Map;for(let[e,a]of p){let r;n.has(a)?r=n.get(a).spans:n.set(a,{scope:a,spans:r=[]}),r.push(e),i.has(a)&&(t.push(...i.get(a)),i.delete(a))}return p.clear(),t.length&&await Promise.all(t),e({resource:s,scopeSpans:n.values()})}function c(e,t){return r(t)}function d(a,r){let s=r?.sampler??c,o={name:a},l=new Set;i.set(o,l);let d="string"==typeof r?.traceparent?n(r.traceparent):void 0,m=(n,a)=>{let r=a?a.child():t(),i="boolean"!=typeof s?s(n,r,o):s;r.flags;let c={id:r,parent:a,start:Date.now(),name:n,events:[],context:{}};i&&p.add([c,o]);let d=t=>e(d,t);return d.traceparent=r,d.span=e=>m(e,r),d.set_context=e=>"function"==typeof e?void(c.context=e(c.context)):void Object.assign(c.context,e),d.add_event=(e,t)=>{c.events.push({name:e,timestamp:Date.now(),attributes:t||{}})},d.end=()=>{null==c.end&&(c.end=Date.now())},d.__add_promise=e=>{l.add(e),e.then((()=>l.delete(e)))},d};return{span:e=>m(e,d)}}export{o as configure,l as report,d as tracer};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "rian",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.3.0-next.10",
|
4
4
|
"description": "Effective tracing for the edge and origins",
|
5
5
|
"keywords": [
|
6
6
|
"opentelemetry",
|
@@ -25,20 +25,25 @@
|
|
25
25
|
"import": "./index.mjs",
|
26
26
|
"require": "./index.js"
|
27
27
|
},
|
28
|
+
"./async": {
|
29
|
+
"types": "./async.d.ts",
|
30
|
+
"import": "./async.mjs",
|
31
|
+
"require": "./async.js"
|
32
|
+
},
|
28
33
|
"./exporter.otel.http": {
|
29
|
-
"types": "./exporter.otel.http
|
30
|
-
"import": "./exporter.otel.http
|
31
|
-
"require": "./exporter.otel.http
|
34
|
+
"types": "./exporter.otel.http.d.ts",
|
35
|
+
"import": "./exporter.otel.http.mjs",
|
36
|
+
"require": "./exporter.otel.http.js"
|
32
37
|
},
|
33
38
|
"./exporter.zipkin": {
|
34
|
-
"types": "./exporter.zipkin
|
35
|
-
"import": "./exporter.zipkin
|
36
|
-
"require": "./exporter.zipkin
|
39
|
+
"types": "./exporter.zipkin.d.ts",
|
40
|
+
"import": "./exporter.zipkin.mjs",
|
41
|
+
"require": "./exporter.zipkin.js"
|
37
42
|
},
|
38
43
|
"./utils": {
|
39
|
-
"types": "./utils
|
40
|
-
"import": "./utils
|
41
|
-
"require": "./utils
|
44
|
+
"types": "./utils.d.ts",
|
45
|
+
"import": "./utils.mjs",
|
46
|
+
"require": "./utils.js"
|
42
47
|
},
|
43
48
|
"./package.json": "./package.json"
|
44
49
|
},
|
@@ -49,6 +54,7 @@
|
|
49
54
|
"*.mjs",
|
50
55
|
"*.js",
|
51
56
|
"*.d.ts",
|
57
|
+
"!global.d.ts",
|
52
58
|
"exporter.*/*",
|
53
59
|
"utils/*"
|
54
60
|
],
|
package/readme.md
CHANGED
@@ -6,9 +6,6 @@
|
|
6
6
|
|
7
7
|
<p><code>npm add rian</code> doesn't overcomplicate tracing</p>
|
8
8
|
<span>
|
9
|
-
<a href="https://github.com/maraisr/rian/actions/workflows/ci.yml">
|
10
|
-
<img src="https://github.com/maraisr/rian/actions/workflows/ci.yml/badge.svg"/>
|
11
|
-
</a>
|
12
9
|
<a href="https://npm-stat.com/charts.html?package=rian">
|
13
10
|
<img src="https://badgen.net/npm/dw/rian?labelColor=black&color=black&cache=600" alt="downloads"/>
|
14
11
|
</a>
|
@@ -26,131 +23,151 @@
|
|
26
23
|
|
27
24
|
## ⚡ Features
|
28
25
|
|
29
|
-
- 🤔 **Familiar** — looks very much like
|
26
|
+
- 🤔 **Familiar** — looks very much like opentelemetry.
|
30
27
|
|
31
|
-
- ✅ **Simple** — `
|
28
|
+
- ✅ **Simple** — `configure()` an environment, create a `tracer()`, `report()` and done.
|
32
29
|
|
33
30
|
- 🏎 **Performant** — check the [benchmarks](#-benchmark).
|
34
31
|
|
35
|
-
- 🪶 **Lightweight** — a mere
|
32
|
+
- 🪶 **Lightweight** — a mere 1KB and next to no [dependencies](https://npm.anvaka.com/#/view/2d/rian/).
|
36
33
|
|
37
34
|
## 🚀 Usage
|
38
35
|
|
39
|
-
> Visit [/examples](/examples) for more
|
36
|
+
> Visit [/examples](/examples) for more!
|
40
37
|
|
41
38
|
```ts
|
42
|
-
import {
|
43
|
-
import { measure } from 'rian/utils';
|
39
|
+
import { configure, tracer, report } from 'rian';
|
44
40
|
import { exporter } from 'rian/exporter.otel.http';
|
45
41
|
|
46
|
-
// ~>
|
47
|
-
|
48
|
-
|
49
|
-
method: 'POST',
|
50
|
-
body: JSON.stringify(payload),
|
51
|
-
}),
|
52
|
-
);
|
53
|
-
|
54
|
-
// ~> Create a tracer — typically "per request" or "per operation"
|
55
|
-
const tracer = create('GET ~> /data', {
|
56
|
-
exporter: otel_endpoint,
|
42
|
+
// ~> configure the environment, all tracers will inherit this
|
43
|
+
configure('my-service' {
|
44
|
+
'service.version': 'DEV'
|
57
45
|
});
|
58
46
|
|
59
|
-
//
|
47
|
+
// ~> create a tracer — typically "per request" or "per operation".
|
48
|
+
const trace = tracer('request');
|
60
49
|
|
61
|
-
|
62
|
-
|
63
|
-
})
|
50
|
+
function handler(req) {
|
51
|
+
// ~> start a span
|
52
|
+
return trace.span(`${req.method} ${req.path}`)(async (s) => {
|
53
|
+
// set some fields on this span's context
|
54
|
+
s.set_context({ user_id: req.params.user_id });
|
55
|
+
|
56
|
+
// ~> span again for `db::read`
|
57
|
+
const data = await s.span('db::read')(() => db_execute('SELECT * FROM users'));
|
58
|
+
|
59
|
+
// ~> maybe have some manual spanning
|
60
|
+
const processing_span = s.span('process records');
|
64
61
|
|
65
|
-
|
66
|
-
|
62
|
+
for (let row of data) {
|
63
|
+
processing_span.add_event('doing stuff', { id: row.id });
|
64
|
+
do_stuff(row);
|
65
|
+
}
|
67
66
|
|
68
|
-
//
|
69
|
-
|
67
|
+
// don't forget to end
|
68
|
+
processing_span.end();
|
70
69
|
|
71
|
-
|
72
|
-
|
73
|
-
do_stuff(row);
|
70
|
+
return reply(200, { data });
|
71
|
+
});
|
74
72
|
}
|
75
73
|
|
76
|
-
|
74
|
+
const otel_exporter = exporter((payload) =>
|
75
|
+
fetch('/traces/otlp', {
|
76
|
+
method: 'POST',
|
77
|
+
body: JSON.stringify(payload),
|
78
|
+
}),
|
79
|
+
);
|
77
80
|
|
78
|
-
|
79
|
-
|
81
|
+
http.listen((req, executionCtx) => {
|
82
|
+
// ~> report all the spans once the response is sent
|
83
|
+
executionCtx.defer(() => report(otel_exporter));
|
84
|
+
return handler(req);
|
85
|
+
});
|
80
86
|
|
81
87
|
/*
|
82
88
|
And we end up with something like this in our reporting tool:
|
83
89
|
|
84
|
-
[ GET
|
85
|
-
[ db::read .... (0.5ms) ]
|
86
|
-
|
90
|
+
[ GET /data .................,,...................... (1.2ms) ] { request }
|
91
|
+
[ db::read .... (0.5ms) ] [ process records .... (0.5ms) ]
|
92
|
+
^ ^ ^ ^
|
93
|
+
{ user_id } ev { id: 1 } | |
|
94
|
+
ev { id: 2 } |
|
95
|
+
ev { id: 3 }
|
87
96
|
*/
|
88
97
|
```
|
89
98
|
|
99
|
+
You only need to `report` in your application once somewhere. All spans are collected into the same "bucket".
|
100
|
+
|
90
101
|
## 🔎 API
|
91
102
|
|
92
103
|
#### Module: [`rian`](./packages/rian/src/index.ts)
|
93
104
|
|
94
105
|
The main and _default_ module responsible for creating and provisioning spans.
|
95
106
|
|
96
|
-
> 💡 Note ~> when providing span context values,
|
97
|
-
> [Semantic Conventions](https://
|
98
|
-
> enforced.
|
107
|
+
> 💡 Note ~> when providing span context values, you can use
|
108
|
+
> [Semantic Conventions](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/), but won't
|
109
|
+
> be enforced.
|
110
|
+
|
111
|
+
#### Module: [`rian/async`](./packages/rian/src/async.ts)
|
112
|
+
|
113
|
+
A module that utilizes the `async_hooks` API to provide a `tracer` and `spans` that can be used where the current span
|
114
|
+
isnt accessable.
|
115
|
+
|
116
|
+
> 💡 Note ~> this module should be used mutually exclusively with the main `rian` module.
|
117
|
+
|
118
|
+
<detials>
|
119
|
+
|
120
|
+
<summary>Example</summary>
|
121
|
+
|
122
|
+
```ts
|
123
|
+
import { configure, tracer, span, currentSpan, report } from 'rian/async';
|
124
|
+
import { exporter } from 'rian/exporter.otel.http';
|
125
|
+
|
126
|
+
function handler(req) {
|
127
|
+
return span(`${req.method} ${req.path}`)(async () => {
|
128
|
+
const s = currentSpan();
|
129
|
+
|
130
|
+
s.set_context({ user_id: req.params.user_id });
|
131
|
+
|
132
|
+
const data = await s.span('db::read')(() => db_execute('SELECT * FROM users'));
|
133
|
+
|
134
|
+
const processing_span = s.span('process records');
|
135
|
+
|
136
|
+
for (let row of data) {
|
137
|
+
processing_span.add_event('doing stuff', { id: row.id });
|
138
|
+
do_stuff(row);
|
139
|
+
}
|
140
|
+
|
141
|
+
processing_span.end();
|
142
|
+
|
143
|
+
return reply(200, { data });
|
144
|
+
});
|
145
|
+
}
|
146
|
+
|
147
|
+
const httpTrace = tracer('http');
|
148
|
+
|
149
|
+
http.listen((req, executionCtx) => {
|
150
|
+
executionCtx.defer(() => report(exporter));
|
151
|
+
return httpTrace(() => handler(req));
|
152
|
+
});
|
153
|
+
```
|
154
|
+
|
155
|
+
</details>
|
99
156
|
|
100
157
|
#### Module: [`rian/exporter.zipkin`](./packages/rian/src/exporter.zipkin.ts)
|
101
158
|
|
102
159
|
Exports the spans created using the zipkin protocol and leaves the shipping up to you.
|
103
160
|
|
104
|
-
> 💡 Note ~> with the nature of zipkin, the `localEndpoint` must be set in your span context.
|
105
|
-
>
|
106
|
-
> <details><summary>Example</summary>
|
107
|
-
>
|
108
|
-
> ```ts
|
109
|
-
> const tracer = create('example', {
|
110
|
-
> context: {
|
111
|
-
> localEndpoint: {
|
112
|
-
> serviceName: 'my-service', // 👈 important part
|
113
|
-
> },
|
114
|
-
> },
|
115
|
-
> });
|
116
|
-
> ```
|
117
|
-
>
|
118
|
-
> Both of these are functionally equivalent. `service.name` will be used if no `localEndpoint.serviceName` is set.
|
119
|
-
>
|
120
|
-
> ```ts
|
121
|
-
> const tracer = create('example', {
|
122
|
-
> context: {
|
123
|
-
> 'service.name': 'my-service',
|
124
|
-
> },
|
125
|
-
> });
|
126
|
-
> ```
|
127
|
-
>
|
128
|
-
> </details>
|
129
|
-
|
130
161
|
#### Module: [`rian/exporter.otel.http`](./packages/rian/src/exporter.otel.http.ts)
|
131
162
|
|
132
163
|
Implements the OpenTelemetry protocol for use with http transports.
|
133
164
|
|
134
|
-
> 💡 Note ~> services require a `service.name` context value.
|
135
|
-
>
|
136
|
-
> <details><summary>Example</summary>
|
137
|
-
>
|
138
|
-
> ```ts
|
139
|
-
> const tracer = create('example', {
|
140
|
-
> context: {
|
141
|
-
> 'service.name': 'my-service', // 👈 important part
|
142
|
-
> },
|
143
|
-
> });
|
144
|
-
> ```
|
145
|
-
>
|
146
|
-
> </details>
|
147
|
-
|
148
165
|
## 🧑🍳 Exporter Recipes
|
149
166
|
|
150
167
|
<details><summary>NewRelic</summary>
|
151
168
|
|
152
169
|
```ts
|
153
|
-
import {
|
170
|
+
import { configure, tracer, report } from 'rian';
|
154
171
|
import { exporter } from 'rian/exporter.zipkin';
|
155
172
|
|
156
173
|
const newrelic = exporter((payload) =>
|
@@ -166,26 +183,25 @@ const newrelic = exporter((payload) =>
|
|
166
183
|
}),
|
167
184
|
);
|
168
185
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
});
|
186
|
+
configure('my-service');
|
187
|
+
|
188
|
+
const tracer = tracer('app');
|
189
|
+
|
190
|
+
await report(newrelic);
|
175
191
|
```
|
176
192
|
|
177
193
|
[learn more](https://docs.newrelic.com/docs/distributed-tracing/trace-api/introduction-trace-api/)
|
178
194
|
|
179
195
|
</details>
|
180
196
|
|
181
|
-
<details><summary>
|
197
|
+
<details><summary>Lightstep</summary>
|
182
198
|
|
183
199
|
```ts
|
184
|
-
import {
|
200
|
+
import { configure, tracer, report } from 'rian';
|
185
201
|
import { exporter } from 'rian/exporter.otel.http';
|
186
202
|
|
187
203
|
const lightstep = exporter((payload) =>
|
188
|
-
fetch('https://ingest.lightstep.com/traces/otlp/v0.
|
204
|
+
fetch('https://ingest.lightstep.com/traces/otlp/v0.9', {
|
189
205
|
method: 'POST',
|
190
206
|
headers: {
|
191
207
|
'lightstep-access-token': '<your api key>',
|
@@ -195,12 +211,11 @@ const lightstep = exporter((payload) =>
|
|
195
211
|
}),
|
196
212
|
);
|
197
213
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
});
|
214
|
+
configure('my-service');
|
215
|
+
|
216
|
+
const tracer = tracer('app');
|
217
|
+
|
218
|
+
await report(lightstep);
|
204
219
|
```
|
205
220
|
|
206
221
|
[learn more](https://opentelemetry.lightstep.com/tracing/)
|
@@ -209,34 +224,23 @@ const tracer = create('example', {
|
|
209
224
|
|
210
225
|
## 🤔 Motivation
|
211
226
|
|
212
|
-
|
213
|
-
|
214
|
-
In efforts to be better observant citizens, we generally reach for the — NewRelic, LightStep, DataDog's. Which, and in
|
215
|
-
no offence to them, is bloated and slow! Where they more often than not do way too much or and relatively speaking, ship
|
216
|
-
useless traces. Which ramp up your bill — see... every span you trace, costs.
|
217
|
-
|
218
|
-
And here we are, introducing **rian** — a lightweight, fast effective tracer. Inspired by the giants in the industry,
|
219
|
-
OpenTracing and OpenTelemetry.
|
220
|
-
|
221
|
-
You might have not heard of those before — and that is okay. It means the design goals from OpenTelemetry or OpenTracing
|
222
|
-
has been met. They are frameworks built to abstract the telemetry part from vendors. So folk like NewRelic can wrap
|
223
|
-
their layers on top of open telemetry — and have libraries instrument theirs without knowing about the vendor. Which
|
224
|
-
allows consumers to ship those spans to the vendor of their choosing. OpenTracing has a very similar design goal, so
|
225
|
-
please do go checkout their documentation's, to help decide.
|
227
|
+
To clarify, `rian` is the Irish word for "trace".
|
226
228
|
|
227
|
-
|
228
|
-
|
229
|
-
MongoDB ran", or how many network calls your ORM made. Cardinality will destroy you. Although rian can scale to support
|
230
|
-
those as well. But the reality is; there are profiler tools far more capable — "right tool for the job".
|
229
|
+
In our efforts to be observant citizens, we often rely on tools such as NewRelic, Lightstep, and Datadog. However, these
|
230
|
+
tools can be bloated and slow, often performing too many unnecessary tasks and driving up costs, as every span costs.
|
231
231
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
have that forwarded onto all sub-services.
|
232
|
+
This is where rian comes in as a lightweight, fast, and effective tracer inspired by industry giants OpenTracing and
|
233
|
+
OpenTelemetry. These frameworks were designed to abstract the telemetry part from vendors, allowing libraries to be
|
234
|
+
instrumented without needing to know about the vendor.
|
236
235
|
|
237
|
-
|
236
|
+
Rian does not intend to align or compete with them, slightly different goals. Rian aims to be used exclusively for
|
237
|
+
instrumenting your application, particularly critical business paths. While rian can scale to support more complex
|
238
|
+
constructs, there are profiler tools that are better suited for those jobs. Rian's primary design goal is to provide
|
239
|
+
better insights into your application's behavior, particularly for edge or service workers where a lean tracer is
|
240
|
+
favored.
|
238
241
|
|
239
|
-
Rian
|
242
|
+
Rian does not by design handle injecting [`w3c trace-context`](https://www.w3.org/TR/trace-context/), or
|
243
|
+
[propagating baggage](https://www.w3.org/TR/baggage/). But we do expose api's for achieving this.
|
240
244
|
|
241
245
|
## 💨 Benchmark
|
242
246
|
|
@@ -246,22 +250,18 @@ Rian is still in active development, but ready for production!
|
|
246
250
|
Validation :: single span
|
247
251
|
✔ rian
|
248
252
|
✔ opentelemetry
|
249
|
-
✔ opentracing
|
250
253
|
|
251
254
|
Benchmark :: single span
|
252
|
-
rian x
|
253
|
-
opentelemetry x
|
254
|
-
opentracing x 57,881 ops/sec ±38.08% (96 runs sampled)
|
255
|
+
rian x 339,110 ops/sec ±2.11% (89 runs sampled)
|
256
|
+
opentelemetry x 199,246 ops/sec ±14.78% (67 runs sampled)
|
255
257
|
|
256
258
|
Validation :: child span
|
257
259
|
✔ rian
|
258
260
|
✔ opentelemetry
|
259
|
-
✔ opentracing
|
260
261
|
|
261
262
|
Benchmark :: child span
|
262
|
-
rian x
|
263
|
-
opentelemetry x
|
264
|
-
opentracing x 36,181 ops/sec ±0.64% (97 runs sampled)
|
263
|
+
rian x 176,936 ops/sec ±2.30% (88 runs sampled)
|
264
|
+
opentelemetry x 124,447 ops/sec ±13.72% (70 runs sampled)
|
265
265
|
```
|
266
266
|
|
267
267
|
> And please... I know these results are anything but the full story. But it's a number and point on comparison.
|
@@ -273,5 +273,5 @@ MIT © [Marais Rossouw](https://marais.io)
|
|
273
273
|
##### Disclaimer
|
274
274
|
|
275
275
|
<sup>- NewRelic is a registered trademark of https://newrelic.com/ and not affiliated with this project.</sup><br />
|
276
|
-
<sup>-
|
277
|
-
<sup>-
|
276
|
+
<sup>- Datadog is a registered trademark of https://www.datadoghq.com/ and not affiliated with this project.</sup><br />
|
277
|
+
<sup>- Lightstep is a registered trademark of https://lightstep.com/ and not affiliated with this project.</sup>
|
package/utils.d.ts
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
import type { Scope } from 'rian';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* With a passed function, `measure` will run the function and once finishes, will end the span.
|
5
|
+
*
|
6
|
+
* The measure method will return whatever the function is, so if it's a promise, it returns a
|
7
|
+
* promise and so on. Any error is caught and re thrown, and automatically tracked in the
|
8
|
+
* context under the `error` property.
|
9
|
+
*
|
10
|
+
* All promises are tracked, and awaited on a `report`.
|
11
|
+
*
|
12
|
+
* This is a utility method, but is functionally equivalent to `scope.span('name')(fn)`.
|
13
|
+
*
|
14
|
+
* @example
|
15
|
+
*
|
16
|
+
* ```text
|
17
|
+
* const data = await measure(scope, get_data);
|
18
|
+
* // or with arguments:
|
19
|
+
* const data = await measure(scope, () => get_data('foo', 'bar'));
|
20
|
+
* ```
|
21
|
+
*/
|
22
|
+
export function measure<Fn extends (scope: Scope) => any>(
|
23
|
+
scope: Scope,
|
24
|
+
fn: Fn,
|
25
|
+
): ReturnType<Fn>;
|
package/utils.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
function r(r,t){try{var e=t(r),n=e instanceof Promise;return n&&r.__add_promise(e.catch((t=>{r.set_context({error:t})})).finally((()=>r.end()))),e}catch(t){throw t instanceof Error&&r.set_context({error:t}),t}finally{!0!==n&&r.end()}}exports.measure=r;
|
package/utils.mjs
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
function r(r,t){try{var e=t(r),n=e instanceof Promise;return n&&r.__add_promise(e.catch((t=>{r.set_context({error:t})})).finally((()=>r.end()))),e}catch(t){throw t instanceof Error&&r.set_context({error:t}),t}finally{!0!==n&&r.end()}}export{r as measure};
|