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 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};