@effectstream/log 0.100.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/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@effectstream/log",
3
+ "version": "0.100.0",
4
+ "license": "MIT",
5
+ "exports": "./src/mod.ts",
6
+ "dependencies": {
7
+ "@effectstream/utils": "0.100.0",
8
+ "@opentelemetry/api": "^1.9.0",
9
+ "@opentelemetry/api-logs": "0.56.0",
10
+ "@opentelemetry/exporter-logs-otlp-http": "0.56.0",
11
+ "@opentelemetry/exporter-metrics-otlp-http": "0.56.0",
12
+ "@opentelemetry/exporter-trace-otlp-http": "0.56.0",
13
+ "@opentelemetry/resources": "1.29.0",
14
+ "@opentelemetry/sdk-logs": "0.56.0",
15
+ "@opentelemetry/sdk-metrics": "1.29.0",
16
+ "@opentelemetry/sdk-trace-base": "1.29.0",
17
+ "@opentelemetry/semantic-conventions": "1.28.0",
18
+ "chalk": "5.4.1",
19
+ "effection": "3.5.0",
20
+ "material-chalk": "1.1.1",
21
+ "superjson": "^2.2.1",
22
+ "tslog": "^4.9.3"
23
+ }
24
+ }
package/src/brands.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { Format, matchColor, registerBrand } from "material-chalk";
2
+
3
+ const paimaBrandColor = matchColor("#1BB17B");
4
+ registerBrand("paima", paimaBrandColor.formatAs(Format.Hct));
5
+
6
+ const hardhatBrandColor = matchColor("#FFF100");
7
+ registerBrand("hardhat", hardhatBrandColor.formatAs(Format.Hct));
8
+
9
+ const dolosBrandColor = matchColor("#FF0065");
10
+ registerBrand("dolos", dolosBrandColor.formatAs(Format.Hct));
package/src/const.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { SeverityNumber } from "@opentelemetry/api-logs";
2
+ import { getEnv } from "@effectstream/utils/runtime";
3
+
4
+ /**
5
+ * Main components
6
+ * These components get a "paima" prefix in the log.
7
+ */
8
+ export const PaimaComponents = {
9
+ EFFECTSTREAM_SYNC: "sync",
10
+ EFFECTSTREAM_RUNTIME: "runtime",
11
+ EFFECTSTREAM_PGLITE: "pglite",
12
+ APPLY_MIGRATIONS: "apply-migrations",
13
+ };
14
+ /**
15
+ * External components
16
+ */
17
+ export const ExternalComponents = {
18
+ HARDHAT: "hardhat",
19
+ YACI_DEVKIT: "yaci-devkit",
20
+ DOLOS: "dolos",
21
+ MIDNIGHT_NODE: "midnight-node",
22
+ MIDNIGHT_INDEXER: "midnight-indexer",
23
+ MIDNIGHT_PROOF_SERVER: "midnight-proof-server",
24
+ AVAIL_NODE: "avail-node",
25
+ AVAIL_CLIENT: "avail-light-client",
26
+ BITCOIN_CORE: "bitcoin-core",
27
+ NEAR_SANDBOX: "near-sandbox",
28
+ };
29
+
30
+ /**
31
+ * Paima Tools, helper and services to support the main components.
32
+ * These components get a "paima" prefix in the log.
33
+ */
34
+ export const PaimaToolsComponents = {
35
+ CHECKER: "checker",
36
+ TUI: "tui",
37
+ TMUX: "tmux",
38
+ COLLECTOR: "collector",
39
+ EXPLORER: "explorer",
40
+ DEPLOY_EVM_CONTRACTS: "deploy-evm-contracts",
41
+ MIDNIGHT_CONTRACT: "midnight-contract",
42
+ };
43
+ /**
44
+ * Secondary components
45
+ * These components are launched automatically.
46
+ */
47
+ export const SecondaryComponents = {
48
+ // This is the entry point, so it gets launched by default.
49
+ ORCHESTRATOR: "orchestrator",
50
+ // Thses processes are launched by their counterpart.
51
+ DOLOS_FILL_TEMPLATE: "dolos-fill-template",
52
+ DOLOS_WAIT: "dolos-wait",
53
+ YACI_DEVKIT_WAIT: "yaci-devkit-wait",
54
+ MIDNIGHT_NODE_WAIT: "midnight-node-wait",
55
+ MIDNIGHT_INDEXER_WAIT: "midnight-indexer-wait",
56
+ MIDNIGHT_PROOF_SERVER_WAIT: "midnight-proof-server-wait",
57
+ BITCOIN_CORE_WAIT: "bitcoin-core-wait",
58
+ BITCOIN_GENERATE_BLOCKS: "bitcoin-mine-blocks",
59
+ BITCOIN_WAIT_FOR_BLOCK: "bitcoin-wait-for-block",
60
+ AVAIL_NODE_WAIT: "avail-node-wait",
61
+ AVAIL_CLIENT_WAIT: "avail-light-client-wait",
62
+ NEAR_SANDBOX_WAIT: "near-sandbox-wait",
63
+ HARDHAT_WAIT: "hardhat-wait",
64
+ COLLECTOR_WAIT: "collector-wait",
65
+ EFFECTSTREAM_DB_WAIT: "db-wait",
66
+ LOKI: "loki",
67
+ };
68
+
69
+ /** All the components that can be launched by the orchestrator */
70
+ export const LaunchableComponents = {
71
+ ...PaimaComponents,
72
+ ...PaimaToolsComponents,
73
+ ...ExternalComponents,
74
+ };
75
+ /** All the components */
76
+ export const ComponentNames = {
77
+ ...LaunchableComponents,
78
+ ...SecondaryComponents,
79
+ };
80
+
81
+ const parseSeverity = (): SeverityNumber => {
82
+ const level = getEnv("EFFECTSTREAM_LOG_LEVEL")?.toUpperCase();
83
+ if (level && level in SeverityNumber) {
84
+ return SeverityNumber[level as keyof typeof SeverityNumber];
85
+ }
86
+ return SeverityNumber.INFO;
87
+ };
88
+
89
+ export const defaultSeverity = parseSeverity();
90
+
91
+ /**
92
+ * extra step in the logger to avoid performance degradation.
93
+ * otherwise, the following can slow down the program:
94
+ * `log.local(LogLevels.DEBUG, JSON.stringify(largeObj))`
95
+ * since it runs `JSON.stringify` even if the default `LogLevel` excludes it
96
+ */
97
+ export type DeferredLog = (logger: typeof console.log) => void;
98
+
99
+ // TODO: should re-export a type built-into material-chalk instead of defining it here
100
+ // notably, https://github.com/SebastienGllmt/material-chalk/blob/master/src/core.ts#L175
101
+ export type Namespace = string | string[];
102
+
103
+ export type LogFunc = (
104
+ component: string,
105
+ namespace: Namespace,
106
+ level: SeverityNumber,
107
+ doLog: DeferredLog,
108
+ ) => void;
package/src/mod.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { SeverityNumber } from "@opentelemetry/api-logs";
2
+ import { tsLogFormatted, tsLogGetFormattedMessage, tsLogOrchestrator, type TslogLogFunc } from "./tslog.ts";
3
+ import { otelLog, type OtelLogFunc } from "./otel/logger.ts";
4
+ import "./brands.ts"; // register material-chalk brands
5
+ import { getEnv } from "@effectstream/utils/runtime";
6
+ export * from "./otel/setup.ts";
7
+ export {
8
+ ComponentNames,
9
+ LaunchableComponents,
10
+ type Namespace,
11
+ } from "./const.ts";
12
+ export { attachTransport } from "./tslog.ts";
13
+ export { DefaultLogLevels } from "./tslog.ts";
14
+
15
+ // re-exporting this
16
+ // so that we don't need to re-import opentelemetry in every component
17
+ export { SeverityNumber };
18
+
19
+ const orchestratorEnv = getEnv("EFFECTSTREAM_ORCHESTRATOR");
20
+ const localLogger = orchestratorEnv ? tsLogOrchestrator : tsLogFormatted;
21
+ const remoteLogger = orchestratorEnv ? tsLogOrchestrator : otelLog;
22
+
23
+ export const log: {
24
+ local: TslogLogFunc;
25
+ localForce: TslogLogFunc;
26
+ remote: TslogLogFunc;
27
+ remoteForce: OtelLogFunc;
28
+ formatMessage: typeof tsLogGetFormattedMessage
29
+ } = {
30
+ local: localLogger,
31
+ remote: remoteLogger,
32
+ localForce: tsLogFormatted,
33
+ remoteForce: otelLog,
34
+ formatMessage: tsLogGetFormattedMessage,
35
+ };
@@ -0,0 +1,32 @@
1
+ import * as otlApi from "@opentelemetry/api-logs";
2
+ import type { LogFunc } from "../const.ts";
3
+ import { defaultSeverity } from "../const.ts";
4
+ import { toString } from "../utils.ts";
5
+
6
+ export type OtelLogFunc = LogFunc;
7
+
8
+ export const otelLog: OtelLogFunc = (
9
+ component,
10
+ namespace,
11
+ level,
12
+ doLog,
13
+ ): void => {
14
+ if (level < defaultSeverity) return;
15
+ // note: we can't just use otel span events, or debugging gets really hard
16
+ // since span events only get sent to the collector after the span completed.
17
+ // there are some open questions about logs/span/events too, so we just don't use span events for now
18
+ // See https://github.com/open-telemetry/opentelemetry-specification/discussions/3732
19
+
20
+ // TODO: modify these constants
21
+ const logger = otlApi.logs.getLogger(component, "0.3.0");
22
+ doLog((...data: unknown[]) => {
23
+ return logger.emit({
24
+ severityNumber: level,
25
+ body: data.map(toString).join(" "),
26
+ attributes: {
27
+ component,
28
+ namespace: Array.isArray(namespace) ? namespace.join(".") : namespace,
29
+ },
30
+ })
31
+ });
32
+ };
@@ -0,0 +1,104 @@
1
+ // careful: only include opentelemetry libs that work in the browser too
2
+ import type opentelemetry from "@opentelemetry/api";
3
+ import { createContext, type Operation } from "effection";
4
+ import { type IResource, Resource } from "@opentelemetry/resources";
5
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
6
+ import {
7
+ BatchLogRecordProcessor,
8
+ type LogRecordProcessor,
9
+ SimpleLogRecordProcessor,
10
+ } from "@opentelemetry/sdk-logs";
11
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
12
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
13
+ import type { MetricReader } from "@opentelemetry/sdk-metrics";
14
+ import {
15
+ BatchSpanProcessor,
16
+ SimpleSpanProcessor,
17
+ type SpanProcessor,
18
+ } from "@opentelemetry/sdk-trace-base";
19
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
20
+ import {
21
+ ATTR_SERVICE_NAME,
22
+ ATTR_SERVICE_VERSION,
23
+ } from "@opentelemetry/semantic-conventions";
24
+ import { env } from "node:process";
25
+
26
+ /**
27
+ * Note: this is based on `NodeSDKConfiguration`.
28
+ * We just don't want to include a nodejs dependency here
29
+ */
30
+ type DefaultOtelSetup = {
31
+ resource: IResource;
32
+ logRecordProcessors: LogRecordProcessor[];
33
+ metricReader: MetricReader;
34
+ spanProcessors?: SpanProcessor[];
35
+ };
36
+ export function defaultOtelSetup(
37
+ name: string,
38
+ version: string,
39
+ ): DefaultOtelSetup {
40
+ const collectorOptions = {
41
+ // override default on dev builds so no logs disappear
42
+ concurrencyLimit: env.NODE_ENV === "development"
43
+ ? Number.MAX_SAFE_INTEGER
44
+ : undefined,
45
+ } as const;
46
+
47
+ const baseLog = new OTLPLogExporter(collectorOptions);
48
+ const logProcessor = env.NODE_ENV === "development"
49
+ ? new SimpleLogRecordProcessor(baseLog)
50
+ : new BatchLogRecordProcessor(baseLog);
51
+
52
+ const baseTrace = new OTLPTraceExporter(collectorOptions);
53
+ const spanProcessor = env.NODE_ENV === "development"
54
+ ? new SimpleSpanProcessor(baseTrace)
55
+ : new BatchSpanProcessor(baseTrace);
56
+
57
+ return {
58
+ resource: Resource.default().merge(
59
+ new Resource({
60
+ [ATTR_SERVICE_NAME]: name,
61
+ [ATTR_SERVICE_VERSION]: version,
62
+ }),
63
+ ),
64
+ spanProcessors: [spanProcessor],
65
+ logRecordProcessors: [logProcessor],
66
+ metricReader: new PeriodicExportingMetricReader({
67
+ exporter: new OTLPMetricExporter(collectorOptions),
68
+ }),
69
+ };
70
+ }
71
+
72
+ type TelemetryContext = {
73
+ tracer: opentelemetry.Tracer;
74
+ span: undefined | opentelemetry.Span;
75
+ };
76
+ export const PaimaTelemetryContext = createContext<TelemetryContext>(
77
+ "paima:telemetry",
78
+ );
79
+
80
+ export function* useTelemetryContext(): Operation<TelemetryContext> {
81
+ const otel = yield* PaimaTelemetryContext.get();
82
+ if (!otel) {
83
+ throw new Error("Paima telemetry is not initialized");
84
+ }
85
+ return otel;
86
+ }
87
+ export function* withSpan<T>(
88
+ name: string,
89
+ continuation: () => Operation<T>,
90
+ ): Operation<T> {
91
+ const otel = yield* useTelemetryContext();
92
+ return yield* otel.tracer.startActiveSpan(
93
+ name,
94
+ function* (span: opentelemetry.Span) {
95
+ yield* PaimaTelemetryContext.set(
96
+ {
97
+ tracer: otel.tracer,
98
+ span,
99
+ },
100
+ );
101
+ return yield* continuation();
102
+ },
103
+ );
104
+ }
package/src/tslog.ts ADDED
@@ -0,0 +1,194 @@
1
+ import { SeverityNumber } from "@opentelemetry/api-logs";
2
+ import { type ILogObj, type IMeta, Logger } from "tslog";
3
+ import {
4
+ defaultSeverity,
5
+ type LogFunc,
6
+ type Namespace,
7
+ PaimaComponents,
8
+ PaimaToolsComponents,
9
+ } from "./const.ts";
10
+ import { chainedMessage, Format, matchColor } from "material-chalk";
11
+ import chalk from "chalk";
12
+ import { toString } from "./utils.ts";
13
+ // TODO How to handle this in the frontend & backend?
14
+ const ENV = { NODE_ENV: 'development' } // TODO This should come from @effectstream/utils/node-env ENV
15
+
16
+ /**
17
+ * https://github.com/fullstack-build/tslog/pull/308
18
+ */
19
+ export enum DefaultLogLevels {
20
+ SILLY = 0,
21
+ TRACE = 1,
22
+ DEBUG = 2,
23
+ INFO = 3,
24
+ WARN = 4,
25
+ ERROR = 5,
26
+ FATAL = 6,
27
+ }
28
+
29
+ export function mapSeverity(level: SeverityNumber): DefaultLogLevels {
30
+ if (level === SeverityNumber.UNSPECIFIED) {
31
+ return DefaultLogLevels.INFO;
32
+ }
33
+ if (level < SeverityNumber.DEBUG) {
34
+ return DefaultLogLevels.TRACE;
35
+ }
36
+ if (level < SeverityNumber.INFO) {
37
+ return DefaultLogLevels.DEBUG;
38
+ }
39
+ if (level < SeverityNumber.WARN) {
40
+ return DefaultLogLevels.INFO;
41
+ }
42
+ if (level < SeverityNumber.ERROR) {
43
+ return DefaultLogLevels.WARN;
44
+ }
45
+ if (level < SeverityNumber.FATAL) {
46
+ return DefaultLogLevels.ERROR;
47
+ }
48
+ return DefaultLogLevels.FATAL;
49
+ }
50
+
51
+ const chalkFormat = Format.Chalk(chalk);
52
+ const logLevels = {
53
+ SILLY: matchColor("#FFFFFF").formatAs(chalkFormat).dim,
54
+ TRACE: matchColor("#FFFFFF").formatAs(chalkFormat).dim,
55
+ DEBUG: matchColor("#00FF00").formatAs(chalkFormat).dim,
56
+ INFO: matchColor("#0000FF").formatAs(chalkFormat).dim,
57
+ WARN: matchColor("#FFFF00").formatAs(chalkFormat),
58
+ ERROR: matchColor("#FF0000").formatAs(chalkFormat),
59
+ FATAL: matchColor("#FF0000").formatAs(chalkFormat),
60
+ };
61
+
62
+ function padLeft(
63
+ input: number | string,
64
+ targetLength: number,
65
+ padChar: string = "0",
66
+ ): string {
67
+ const str = input.toString();
68
+ return str.padStart(targetLength, padChar);
69
+ }
70
+
71
+ const log: Logger<ILogObj> = new Logger({
72
+ minLevel: mapSeverity(defaultSeverity),
73
+ overwrite: {
74
+ formatMeta: (meta?: IMeta) => {
75
+ if (meta == null) return "";
76
+ const timestamp = chalk.white.dim((() => {
77
+ if (ENV.NODE_ENV !== "development") {
78
+ return meta.date.toISOString();
79
+ }
80
+ // use a shorter timestamp locally so it takes less space on the console
81
+ return `${padLeft(meta.date.getHours(), 2)}:${
82
+ padLeft(meta.date.getMinutes(), 2)
83
+ }:${padLeft(meta.date.getSeconds(), 2)}`;
84
+ })());
85
+ const logLevelName = logLevels
86
+ [meta.logLevelName as keyof typeof logLevels](meta.logLevelName);
87
+ return [timestamp, logLevelName].join(" ") + "\t";
88
+ },
89
+ },
90
+ });
91
+
92
+ export function attachTransport(callback: (logObj: ILogObj) => void) {
93
+ log.attachTransport((logObj) => {
94
+ callback(logObj);
95
+ });
96
+ }
97
+
98
+ function getLogMethod(level: SeverityNumber) {
99
+ if (level === SeverityNumber.UNSPECIFIED) {
100
+ const level = mapSeverity(defaultSeverity);
101
+ return log.log.bind(
102
+ log,
103
+ level,
104
+ DefaultLogLevels[level],
105
+ );
106
+ }
107
+ if (level < SeverityNumber.DEBUG) {
108
+ return log.trace.bind(log);
109
+ }
110
+ if (level < SeverityNumber.INFO) {
111
+ return log.debug.bind(log);
112
+ }
113
+ if (level < SeverityNumber.WARN) {
114
+ return log.info.bind(log);
115
+ }
116
+ if (level < SeverityNumber.ERROR) {
117
+ return log.warn.bind(log);
118
+ }
119
+ if (level < SeverityNumber.FATAL) {
120
+ return log.error.bind(log);
121
+ }
122
+ return log.fatal.bind(log);
123
+ }
124
+
125
+ export type TslogLogFunc = LogFunc;
126
+
127
+ const formatMessage = (
128
+ namespaces: Namespace,
129
+ ...data: unknown[]
130
+ ): string => {
131
+ return chainedMessage(
132
+ chalk,
133
+ namespaces,
134
+ ...data.map(toString),
135
+ );
136
+ };
137
+
138
+
139
+ const namespaces = (component: string, namespace: Namespace) => {
140
+ const subNamespaces = Array.isArray(namespace) ? namespace : [namespace];
141
+ if (
142
+ Object.values(PaimaComponents).includes(component as any) ||
143
+ Object.values(PaimaToolsComponents).includes(component as any)
144
+ ) {
145
+ return ["effectstream", component, ...subNamespaces];
146
+ }
147
+ return [component, ...subNamespaces];
148
+ };
149
+
150
+ export const tsLogFormatted: TslogLogFunc = (
151
+ component,
152
+ namespace,
153
+ level,
154
+ doLog,
155
+ ): void => {
156
+ doLog((...data: unknown[]) =>
157
+ getLogMethod(level)(
158
+ formatMessage(namespaces(component, namespace), ...data),
159
+ )
160
+ );
161
+ };
162
+
163
+ export const tsLogOrchestrator: TslogLogFunc = (
164
+ component,
165
+ namespace,
166
+ level,
167
+ doLog,
168
+ ): void => {
169
+ if (level < defaultSeverity) return;
170
+ doLog((...data: unknown[]) =>
171
+ console.log(JSON.stringify({
172
+ "__ORCHESTRATOR__": true,
173
+ data,
174
+ namespaces: namespaces(component, namespace),
175
+ level,
176
+ date: new Date().getTime(),
177
+ },
178
+ (_, v) => typeof v === "bigint" ? v.toString() : v
179
+ ))
180
+ )
181
+ };
182
+
183
+ export const tsLogGetFormattedMessage = (
184
+ component: string,
185
+ namespace: Namespace,
186
+ level: SeverityNumber,
187
+ doLog: any,
188
+ ): string => {
189
+ let val = "";
190
+ doLog((...data: unknown[]) => {
191
+ val = formatMessage(namespaces(component, namespace), ...data);
192
+ });
193
+ return val;
194
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,20 @@
1
+ import superjson from "superjson";
2
+
3
+ export function toString(str: unknown): string {
4
+ if (typeof str !== "object") {
5
+ if (typeof str === "function") {
6
+ return str.name === "" ? "[Function]" : `[Function: ${str.name}]`;
7
+ }
8
+ return (String(str));
9
+ }
10
+ if (str instanceof Error) {
11
+ return (`${str.name}: ${str.message}\nStack: ${str.stack}`);
12
+ }
13
+
14
+ try {
15
+ return superjson.stringify(str);
16
+ } catch (e) {
17
+ return toString(e);
18
+ // should not happen, but maybe there is some type that fails for some reason
19
+ }
20
+ }