@synnaxlabs/alamos 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/log.ts ADDED
@@ -0,0 +1,104 @@
1
+ // Copyright 2023 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { type UnknownRecord } from "@synnaxlabs/x";
11
+
12
+ import { Meta } from "@/meta";
13
+
14
+ export const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
15
+ export type LogLevel = (typeof LOG_LEVELS)[number];
16
+
17
+ export interface LogLevelFilterProps {
18
+ key: string;
19
+ path: string;
20
+ level: LogLevel;
21
+ }
22
+
23
+ /**
24
+ * LogLevelFilter is a function that returns true if the log at the given
25
+ * level should be emitted.
26
+ */
27
+ export type LogLevelFilter = (props: LogLevelFilterProps) => boolean;
28
+
29
+ export const logThresholdFilter = (thresh: LogLevel): LogLevelFilter => {
30
+ const threshIdx = LOG_LEVELS.indexOf(thresh);
31
+ return ({ level }) => LOG_LEVELS.indexOf(level) >= threshIdx;
32
+ };
33
+
34
+ export interface LogLevelKeyFilterProps {
35
+ include?: string[];
36
+ exclude?: string[];
37
+ }
38
+ export const logLevelKeyFiler = (props: LogLevelKeyFilterProps): LogLevelFilter => {
39
+ const { include, exclude } = props;
40
+ return ({ key }) => {
41
+ if (include != null && !include.includes(key)) return false;
42
+ if (exclude != null && exclude.includes(key)) return false;
43
+ return true;
44
+ };
45
+ };
46
+
47
+ export interface LoggerProps {
48
+ filters?: LogLevelFilter[];
49
+ }
50
+
51
+ export class Logger {
52
+ meta: Meta = Meta.NOOP;
53
+ filters: LogLevelFilter[];
54
+
55
+ constructor(p: LoggerProps = {}) {
56
+ const { filters = [] } = p;
57
+ this.filters = filters;
58
+ }
59
+
60
+ private filter(level: LogLevel): boolean {
61
+ return (
62
+ !this.meta.noop &&
63
+ this.filters.every((f) =>
64
+ f({
65
+ key: this.meta.key,
66
+ path: this.meta.path,
67
+ level,
68
+ }),
69
+ )
70
+ );
71
+ }
72
+
73
+ child(meta: Meta): Logger {
74
+ const l = new Logger({ filters: this.filters });
75
+ l.meta = meta;
76
+ return l;
77
+ }
78
+
79
+ debug(msg: string, kv?: UnknownRecord): void {
80
+ if (!this.filter("debug")) return;
81
+ if (kv == null) console.log("%cDEBUG", "color: #8c00f0;", this.meta.path, msg);
82
+ else console.log("%cDEBUG", "color: #8c00f0;", this.meta.path, msg, kv);
83
+ }
84
+
85
+ info(msg: string, kv?: UnknownRecord): void {
86
+ if (!this.filter("info")) return;
87
+ if (kv == null) console.log("%cINFO", "color: #005eff;", this.meta.path, msg);
88
+ else console.log("%cINFO", "color: #005eff;", this.meta.path, msg, kv);
89
+ }
90
+
91
+ warn(msg: string, kv?: UnknownRecord): void {
92
+ if (!this.filter("warn")) return;
93
+ if (kv == null) console.warn("WARN", this.meta.path, msg);
94
+ else console.warn("WARN", this.meta.path, msg, kv);
95
+ }
96
+
97
+ error(msg: string, kv?: UnknownRecord): void {
98
+ if (!this.filter("error")) return;
99
+ if (kv == null) console.error("ERROR", this.meta.path, msg);
100
+ else console.error("ERROR", this.meta.path, msg, kv);
101
+ }
102
+
103
+ static readonly NOOP = new Logger();
104
+ }
package/src/meta.ts ADDED
@@ -0,0 +1,41 @@
1
+ // Copyright 2023 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ export class Meta {
11
+ private readonly _noop: boolean = false;
12
+ readonly key: string;
13
+ readonly path: string;
14
+ readonly serviceName: string;
15
+
16
+ constructor(
17
+ key: string,
18
+ path: string,
19
+ serviceName: string = "",
20
+ noop: boolean = false,
21
+ ) {
22
+ this.key = key;
23
+ this.path = path;
24
+ this.serviceName = serviceName;
25
+ this._noop = noop;
26
+ }
27
+
28
+ child(key: string): Meta {
29
+ return new Meta(key, this.extendPath(key), this.serviceName);
30
+ }
31
+
32
+ extendPath(key: string): string {
33
+ return `${this.path}.${key}`;
34
+ }
35
+
36
+ get noop(): boolean {
37
+ return this._noop;
38
+ }
39
+
40
+ static readonly NOOP = new Meta("", "", "");
41
+ }
@@ -0,0 +1,38 @@
1
+ // Copyright 2023 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { beforeAll, describe, expect, it } from "vitest";
11
+
12
+ import { instrumentation } from "@/dev";
13
+ import { Instrumentation } from "@/instrumentation";
14
+ import { Tracer } from "@/trace";
15
+
16
+ describe("Trace", () => {
17
+ let ins: Instrumentation;
18
+ beforeAll(() => {
19
+ ins = instrumentation();
20
+ });
21
+ describe("initialize", () => {
22
+ it("should correctly initialize a tracer", () => {
23
+ const t = new Tracer();
24
+ expect(t).toBeDefined();
25
+ });
26
+ });
27
+ describe("trace", () => {
28
+ it("should start a span with the given key", () => {
29
+ ins.T.prod("test", (span) => {
30
+ expect(span.key).toEqual("test");
31
+ });
32
+ });
33
+ it("should trace an async function correctly", async () => {
34
+ const result = await ins.T.prod("async-test", async () => "test");
35
+ expect(result).toEqual("test");
36
+ });
37
+ });
38
+ });
package/src/trace.ts ADDED
@@ -0,0 +1,188 @@
1
+ // Copyright 2023 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import {
11
+ type Span as OtelSpan,
12
+ type Tracer as OtelTracer,
13
+ context,
14
+ SpanStatusCode,
15
+ propagation,
16
+ type AttributeValue,
17
+ } from "@opentelemetry/api";
18
+
19
+ import {
20
+ type Environment,
21
+ type EnvironmentFilter,
22
+ envThresholdFilter,
23
+ } from "@/environment";
24
+ import { Meta } from "@/meta";
25
+
26
+ /** Carrier is an entitty that can carry trace metadata across process bounds */
27
+ export type Carrier = Record<string, string>;
28
+
29
+ /** Function that executes under the given span */
30
+ export type SpanF = (span: Span) => unknown;
31
+
32
+ /**
33
+ * Tracer wraps an opentelemetry tracer to provide an opinionated intreface
34
+ * for tracing within the Synnax stack.
35
+ */
36
+ export class Tracer {
37
+ private meta: Meta = Meta.NOOP;
38
+ private readonly tracer: OtelTracer;
39
+ private readonly filter: EnvironmentFilter;
40
+
41
+ constructor(
42
+ tracer?: OtelTracer,
43
+ filter: EnvironmentFilter = envThresholdFilter("debug"),
44
+ ) {
45
+ this.tracer = tracer as OtelTracer;
46
+ this.filter = filter;
47
+ }
48
+
49
+ child(meta: Meta): Tracer {
50
+ const t = new Tracer(this.tracer, this.filter);
51
+ t.meta = meta;
52
+ return t;
53
+ }
54
+
55
+ /**
56
+ * Starts a new span in the debug environment. If a span already exists in the
57
+ * current context, it will be used as the parent span.
58
+ *
59
+ * @param key - The name of the span.
60
+ * @param f - The function to run under the span.
61
+ * @returns A span that tracks program execution. If the Tracer's environment
62
+ * rejects the 'debug' environment or the Tracer is noop, a NoopSpan is returned.
63
+ */
64
+ debug<F extends SpanF>(key: string, f: F): ReturnType<F> {
65
+ return this.trace(key, "debug", f);
66
+ }
67
+
68
+ /**
69
+ * Starts a new span in the bench environment. If a span already exists in the
70
+ * current context, it will be used as the parent span.
71
+ *
72
+ * @param key - The name of the span.
73
+ * @param f - The function to run under the span.
74
+ * @returns A span that tracks program execution. If the Tracer's environment
75
+ * rejects the 'bench' environment or the Tracer is noop, a NoopSpan is returned.
76
+ */
77
+ bench<F extends SpanF>(key: string, f: F): ReturnType<F> {
78
+ return this.trace(key, "bench", f);
79
+ }
80
+
81
+ /**
82
+ * Starts a new span in the prod environment. If a span already exists in the
83
+ * current context, it will be used as the parent span.
84
+ *
85
+ * @param key - The name of the span.
86
+ * @param f - The function to run under the span.
87
+ * @returns A span that tracks program execution. If the Tracer's environment
88
+ * rejects the 'prod' environment or the Tracer is noop, a NoopSpan is returned.
89
+ */
90
+ prod<F extends SpanF>(key: string, f: F): ReturnType<F> {
91
+ return this.trace(key, "prod", f);
92
+ }
93
+
94
+ /**
95
+ * Stars a new span with the given key and environment. If a span already
96
+ * exists in the current context, it will be used as the parent span.
97
+ *
98
+ * @param key - The name of the span.
99
+ * @param env - The environment to run the span under.
100
+ * @param f - The function to run under the span.
101
+ * @returns A span that tracks program execution. If the Tracer's environment
102
+ * rejects the provided span or the Tracer is noop, a NoopSpan is returned.
103
+ */
104
+ trace<F extends SpanF>(key: string, env: Environment, f: F): ReturnType<F> {
105
+ if (this.meta.noop || !this.filter(env))
106
+ return f(new NoopSpan(key)) as ReturnType<F>;
107
+ return this.tracer.startActiveSpan(key, (otelSpan) => {
108
+ const span = new _Span(key, otelSpan);
109
+ const result = f(span);
110
+ otelSpan.end();
111
+ return result as ReturnType<F>;
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Injects metadata about the current trace into the provided carrier. This
117
+ * metadata can be paresed on teh other side of a network or IPC request to
118
+ * allow the trace to proapgate across services.
119
+ *
120
+ * @param carrier - The carrier to inject the metadata into.
121
+ */
122
+ propagate(carrier: Carrier): void {
123
+ if (this.meta.noop) return;
124
+ const ctx = context.active();
125
+ propagation.inject(ctx, carrier, {
126
+ set: (carrier, key, value) => {
127
+ carrier[key] = value;
128
+ },
129
+ });
130
+ }
131
+
132
+ /** Tracer implementation that does nothing */
133
+ static readonly NOOP = new Tracer();
134
+ }
135
+
136
+ /** A span in a trace that can be used to track function execution */
137
+ export interface Span {
138
+ /**
139
+ * The key identifying the span. This is the name of the key
140
+ * passed into the tracing method combined with the path of the
141
+ * instrumentation that started the span. For example, take the
142
+ * instrumentation titled 'synnax' and call to trace with 'test.
143
+ * The span key would be 'synnax.test'.
144
+ */
145
+ key: string;
146
+ /**
147
+ * If the error is not null, records the error in the span and sets
148
+ * its status to error.
149
+ */
150
+ recordError: (error?: Error | null) => void;
151
+ /**
152
+ * Sets the given key-value pair as an attribute on the span.
153
+ */
154
+ set: (key: string, value: AttributeValue) => void;
155
+ }
156
+
157
+ export class _Span implements Span {
158
+ key: string;
159
+ private readonly otel: OtelSpan;
160
+
161
+ constructor(key: string, span: OtelSpan) {
162
+ this.key = key;
163
+ this.otel = span;
164
+ }
165
+
166
+ set(key: string, value: AttributeValue): void {
167
+ this.otel.setAttribute(key, value);
168
+ }
169
+
170
+ recordError(error?: Error | null): void {
171
+ if (error == null) return;
172
+ this.otel.recordException(error);
173
+ this.otel.setStatus({ code: SpanStatusCode.ERROR });
174
+ }
175
+ }
176
+
177
+ /** Span implementation that does nothing */
178
+ export class NoopSpan implements Span {
179
+ key: string;
180
+
181
+ constructor(key: string) {
182
+ this.key = key;
183
+ }
184
+
185
+ set(key: string, value: AttributeValue): void {}
186
+
187
+ recordError(_?: Error | null): void {}
188
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@synnaxlabs/tsconfig/base.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "experimentalDecorators": true,
6
+ "paths": {
7
+ "@/*": ["src/*"]
8
+ }
9
+ },
10
+ "include": ["src/**/*.ts"],
11
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "@synnaxlabs/tsconfig/vite.json",
3
+ "include": ["vite.config.ts"]
4
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,26 @@
1
+ // Copyright 2023 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import path from "path";
11
+ import { defineConfig } from "vite";
12
+ import { lib } from "@synnaxlabs/vite-plugin";
13
+
14
+ export default defineConfig({
15
+ plugins: [lib({ name: "alamos" })],
16
+ build: {
17
+ sourcemap: true,
18
+ minify: false,
19
+ lib: {
20
+ entry: {
21
+ index: path.resolve(".", "src/index.ts"),
22
+ dev: path.resolve(".", "src/dev/index.ts"),
23
+ },
24
+ }
25
+ },
26
+ });