rian 0.2.5 → 0.3.0-next.10

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/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,3 @@
1
+ import type { Exporter } from 'rian';
2
+
3
+ export const exporter: (request: (payload: any) => any) => Exporter;
@@ -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,3 @@
1
+ import type { Exporter } from 'rian';
2
+
3
+ export const exporter: (request: (payload: any) => any) => 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 "resolve".
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)] [Span C......(0.6ms)]
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 interface Span {
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.fork|tracer.fork()}.
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://github.com/opentracing/specification/blob/master/semantic_conventions.md|Semantic Conventions outlined by OpenTracing}.
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 interface Scope {
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
- fork(name: string): CallableScope;
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
- export interface Tracer extends Omit<Scope, 'end'> {
122
- end(): ReturnType<Exporter>;
123
- }
190
+ // --- main api
124
191
 
125
192
  /**
126
- * An exporter is a method called when the parent scope ends, gets given a Set of all spans traced
127
- * during this execution.
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 type Exporter = (
130
- spans: ReadonlySet<Readonly<Span>>,
131
- context: Context,
132
- ) => any;
210
+ export function tracer(name: string, options?: Options): Tracer;
211
+
212
+ // -- general api
133
213
 
134
214
  /**
135
- * @borrows {@link Span.context}
215
+ * Awaits all active promises, and then calls the {@link Options.exporter|exporter}. Passing all collected spans.
136
216
  */
137
- export interface Context {
138
- [property: string]: any;
139
- }
217
+ export async function report<T extends Exporter>(
218
+ exporter: T,
219
+ ): Promise<ReturnType<T>>;
140
220
 
141
221
  /**
142
- * Should return true when you want to sample the span, this is ran before the span is traced — so
143
- * decisions is made preemptively.
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 Span itself will still be included in the {@link Options.exporter|exporter}, and can be
146
- * filtered out there.
227
+ * The `name` argument will set the `service.name` attribute. And is required.
147
228
  *
148
- * Sampling does impact the traceparent, for injection and is encoded there.
149
- */
150
- export type Sampler = (
151
- name: string,
152
- parentId?: Traceparent,
153
- context?: Context,
154
- ) => boolean;
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 type ClockLike = {
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 { measureFn:e } = require('rian/utils');const t = require('tctx');var n=(e,n)=>!n||t.is_sampled(n),a={"telemetry.sdk.name":"rian","telemetry.sdk.version":"0.2.5"},r=(r,o)=>{let s=new Set,d=new Set,i=o.sampler||n,c="boolean"!=typeof i,p=o.clock||Date,l=(n,a)=>{let r=c?i(n,a,o.context):i,m=a?a.child(r):t.make(r),x={id:m,parent:a,start:p.now(),name:n,events:[],context:{}};r&&s.add(x);let u=t=>e(u,t);return u.traceparent=m,u.fork=e=>l(e,m),u.set_context=e=>{"function"!=typeof e?Object.assign(x.context,e):x.context=e(x.context)},u.add_event=(e,t)=>{x.events.push({name:e,timestamp:p.now(),attributes:t||{}})},u.end=()=>{null==x.end&&(x.end=p.now())},u.__add_promise=d.add.bind(d),u},m=l(r,"string"==typeof o.traceparent?t.parse(o.traceparent):void 0),x=m.end.bind(m);return m.end=async()=>(x(),d.size>0&&await Promise.all([...d.values()]),o.exporter(s,{...o.context||{},...a})),m};exports.create=r;
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{measureFn as e}from"rian/utils";import*as t from"tctx";var n=(e,n)=>!n||t.is_sampled(n),a={"telemetry.sdk.name":"rian","telemetry.sdk.version":"0.2.5"},r=(r,o)=>{let s=new Set,d=new Set,i=o.sampler||n,c="boolean"!=typeof i,p=o.clock||Date,l=(n,a)=>{let r=c?i(n,a,o.context):i,m=a?a.child(r):t.make(r),x={id:m,parent:a,start:p.now(),name:n,events:[],context:{}};r&&s.add(x);let u=t=>e(u,t);return u.traceparent=m,u.fork=e=>l(e,m),u.set_context=e=>{"function"!=typeof e?Object.assign(x.context,e):x.context=e(x.context)},u.add_event=(e,t)=>{x.events.push({name:e,timestamp:p.now(),attributes:t||{}})},u.end=()=>{null==x.end&&(x.end=p.now())},u.__add_promise=d.add.bind(d),u},m=l(r,"string"==typeof o.traceparent?t.parse(o.traceparent):void 0),x=m.end.bind(m);return m.end=async()=>(x(),d.size>0&&await Promise.all([...d.values()]),o.exporter(s,{...o.context||{},...a})),m};export{r as create};
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.2.5",
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/index.d.ts",
30
- "import": "./exporter.otel.http/index.mjs",
31
- "require": "./exporter.otel.http/index.js"
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/index.d.ts",
35
- "import": "./exporter.zipkin/index.mjs",
36
- "require": "./exporter.zipkin/index.js"
39
+ "types": "./exporter.zipkin.d.ts",
40
+ "import": "./exporter.zipkin.mjs",
41
+ "require": "./exporter.zipkin.js"
37
42
  },
38
43
  "./utils": {
39
- "types": "./utils/index.d.ts",
40
- "import": "./utils/index.mjs",
41
- "require": "./utils/index.js"
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 OpenTracing.
26
+ - 🤔 **Familiar** — looks very much like opentelemetry.
30
27
 
31
- - ✅ **Simple** — `create` a tracer, and `.end()` a tracer, done.
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 1Kb and next to no [dependencies](https://npm.anvaka.com/#/view/2d/rian/).
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 info!
36
+ > Visit [/examples](/examples) for more!
40
37
 
41
38
  ```ts
42
- import { create } from 'rian';
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
- // ~> Where to send the spans.
47
- const otel_endpoint = exporter((payload) =>
48
- fetch('/traces/otlp', {
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
- // Let us trace
47
+ // ~> create a tracer — typically "per request" or "per operation".
48
+ const trace = tracer('request');
60
49
 
61
- tracer.set_context({
62
- user: request_context.user_id,
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
- // ~> Wrap any method and be timed 🕺🏻
66
- const data = await measure(tracer.fork('db::read'), get_data);
62
+ for (let row of data) {
63
+ processing_span.add_event('doing stuff', { id: row.id });
64
+ do_stuff(row);
65
+ }
67
66
 
68
- // ~> Maybe have some in-flow spanning
69
- const span = tracer.span('process records');
67
+ // don't forget to end
68
+ processing_span.end();
70
69
 
71
- for (let row of data) {
72
- span.add_event('doing stuff', { id: row.id });
73
- do_stuff(row);
70
+ return reply(200, { data });
71
+ });
74
72
  }
75
73
 
76
- span.end();
74
+ const otel_exporter = exporter((payload) =>
75
+ fetch('/traces/otlp', {
76
+ method: 'POST',
77
+ body: JSON.stringify(payload),
78
+ }),
79
+ );
77
80
 
78
- // ~> And finally let's export — will also end the root span.
79
- await tracer.end();
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 ~> /data .................................... (1.2ms) ]
85
- [ db::read .... (0.5ms) ]
86
- [ process records .... (0.5ms) ]
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, please stick to
97
- > [Semantic Conventions](https://github.com/opentracing/specification/blob/master/semantic_conventions.md), but won't be
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 { create } from 'rian';
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
- const tracer = create('example', {
170
- context: {
171
- 'service.name': 'my-service', // 👈 important part
172
- },
173
- exporter: newrelic,
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>LightStep</summary>
197
+ <details><summary>Lightstep</summary>
182
198
 
183
199
  ```ts
184
- import { create } from 'rian';
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.6', {
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
- const tracer = create('example', {
199
- context: {
200
- 'service.name': 'my-service', // 👈 important part
201
- },
202
- exporter: lightstep,
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
- Firstly, what is `rian`? _trace_ in Irish is `rian`.
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
- Rian does not intend to align or compete with them. rian's intent is to be used to instrument your application and
228
- **only** your application. Rian is primed in that critical business paths where you don't care " which handlers
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
- Rian is simply a tracer you can use to see what your application is doing, have better insight into why something failed
233
- and stitch it with your logs. It starts by capturing a [`w3c trace-context`](https://www.w3.org/TR/trace-context/),
234
- tracing some business steps. "inbound request /data", "getting data", "sending email", or as granular as you'd like. And
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
- You see, the primary design goal is targeted at edge or service workers where lean quick tracers is favoured.
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 is still in active development, but ready for production!
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 381,751 ops/sec ±4.17% (84 runs sampled)
253
- opentelemetry x 201,584 ops/sec ±13.97% (63 runs sampled)
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 204,952 ops/sec ±5.78% (82 runs sampled)
263
- opentelemetry x 128,768 ops/sec ±11.47% (68 runs sampled)
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>- 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>
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};