summaryrefslogtreecommitdiff
path: root/u/trace
diff options
context:
space:
mode:
authorElizabeth Hunt <lizhunt@amazon.com>2025-05-13 18:58:45 -0700
committerElizabeth Hunt <lizhunt@amazon.com>2025-05-13 18:58:54 -0700
commit1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11 (patch)
tree07073c060b61688e4635fd4658315cc683589d3d /u/trace
parent2543ac8b11af11f034836591046cdb52911f9403 (diff)
downloadci-1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11.tar.gz
ci-1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11.zip
snapshot!
Diffstat (limited to 'u/trace')
-rw-r--r--u/trace/itrace.ts72
-rw-r--r--u/trace/logger.ts86
-rw-r--r--u/trace/mod.ts3
-rw-r--r--u/trace/trace.ts34
4 files changed, 195 insertions, 0 deletions
diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts
new file mode 100644
index 0000000..b483067
--- /dev/null
+++ b/u/trace/itrace.ts
@@ -0,0 +1,72 @@
+import { Mapper, SideEffect } from "../fn/mod.ts";
+
+export interface ITrace<TracingW> {
+ addTrace: Mapper<TracingW, ITrace<TracingW>>;
+ trace: SideEffect<TracingW>;
+}
+
+export type ITraceableTuple<T, Trace> = [T, Trace];
+export type ITraceableMapper<T, Trace, U, W = ITraceable<T, Trace>> = (
+ w: W,
+) => U;
+
+export interface ITraceable<T, Trace> {
+ readonly item: T;
+ readonly trace: ITrace<Trace>;
+
+ move<U>(u: U): ITraceable<U, Trace>;
+ map: <U>(
+ mapper: ITraceableMapper<T, Trace, U>,
+ ) => ITraceable<U, Trace>;
+ bimap: <U>(
+ mapper: ITraceableMapper<T, Trace, ITraceableTuple<U, Trace>>,
+ ) => ITraceable<U, Trace>;
+ peek: (peek: ITraceableMapper<T, Trace, void>) => ITraceable<T, Trace>;
+ flatMap: <U>(
+ mapper: ITraceableMapper<T, Trace, ITraceable<U, Trace>>,
+ ) => ITraceable<U, Trace>;
+ flatMapAsync<U>(
+ mapper: ITraceableMapper<T, Trace, Promise<ITraceable<U, Trace>>>,
+ ): ITraceable<Promise<U>, Trace>;
+}
+
+export class TraceableImpl<T, L> implements ITraceable<T, L> {
+ protected constructor(
+ readonly item: T,
+ readonly trace: ITrace<L>,
+ ) {}
+
+ public map<U>(mapper: ITraceableMapper<T, L, U>) {
+ const result = mapper(this);
+ return new TraceableImpl(result, this.trace);
+ }
+
+ public flatMap<U>(
+ mapper: ITraceableMapper<T, L, ITraceable<U, L>>,
+ ): ITraceable<U, L> {
+ return mapper(this);
+ }
+
+ public flatMapAsync<U>(
+ mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>,
+ ): ITraceable<Promise<U>, L> {
+ return new TraceableImpl(
+ mapper(this).then(({ item }) => item),
+ this.trace,
+ );
+ }
+
+ public peek(peek: ITraceableMapper<T, L, void>) {
+ peek(this);
+ return this;
+ }
+
+ public move<Tt>(t: Tt): ITraceable<Tt, L> {
+ return this.map(() => t);
+ }
+
+ public bimap<U>(mapper: ITraceableMapper<T, L, ITraceableTuple<U, L>>) {
+ const [item, trace] = mapper(this);
+ return new TraceableImpl(item, this.trace.addTrace(trace));
+ }
+}
diff --git a/u/trace/logger.ts b/u/trace/logger.ts
new file mode 100644
index 0000000..79da367
--- /dev/null
+++ b/u/trace/logger.ts
@@ -0,0 +1,86 @@
+import {
+ isDebug,
+ isObject,
+ type ITrace,
+ type SideEffect,
+ type Supplier,
+} from "@emprespresso/utils";
+
+export interface ILogger {
+ log: (...args: unknown[]) => void;
+ debug: (...args: unknown[]) => void;
+ warn: (...args: unknown[]) => void;
+ error: (...args: unknown[]) => void;
+}
+export type ILoggerLevel = "UNKNOWN" | "INFO" | "WARN" | "DEBUG" | "ERROR";
+const logLevelOrder: Array<ILoggerLevel> = ["DEBUG", "INFO", "WARN", "ERROR"];
+const defaultAllowedLevels = () =>
+ [
+ "UNKNOWN",
+ ...(isDebug() ? ["DEBUG"] : []),
+ "INFO",
+ "WARN",
+ "ERROR",
+ ] as Array<ILoggerLevel>;
+
+export const logWithLevel = (
+ logger: ILogger,
+ level: ILoggerLevel,
+): SideEffect<unknown> => {
+ switch (level) {
+ case "UNKNOWN":
+ case "INFO":
+ return logger.log;
+ case "DEBUG":
+ return logger.debug;
+ case "WARN":
+ return logger.warn;
+ case "ERROR":
+ return logger.error;
+ }
+};
+
+export const LoggerImpl = console;
+
+export type LogTraceSupplier = string | Supplier<string> | {
+ level: ILoggerLevel;
+};
+
+const foldTraces = (traces: Array<LogTraceSupplier>) => {
+ const { line, level } = traces.reduce(
+ (acc: { line: string; level: number }, t) => {
+ if (isObject(t) && "level" in t) {
+ return {
+ ...acc,
+ level: Math.max(logLevelOrder.indexOf(t.level), acc.level),
+ };
+ }
+ const prefix = [
+ acc.line,
+ typeof t === "function" ? t() : t,
+ ].join(" ");
+ return { ...acc, prefix };
+ },
+ { line: "", level: -1 },
+ );
+ return { line, level: logLevelOrder[level] ?? "UNKNOWN" };
+};
+
+const defaultTrace = () => `[${new Date().toISOString()}]`;
+export const LogTrace = (
+ logger: ILogger,
+ traces: Array<LogTraceSupplier> = [defaultTrace],
+ allowedLevels: Supplier<Array<ILoggerLevel>> = defaultAllowedLevels,
+ defaultLevel: ILoggerLevel = "INFO",
+): ITrace<LogTraceSupplier> => {
+ return {
+ addTrace: (trace: LogTraceSupplier) =>
+ LogTrace(logger, traces.concat(trace)),
+ trace: (trace: LogTraceSupplier) => {
+ const { line, level: _level } = foldTraces(traces.concat(trace));
+ if (!allowedLevels().includes(_level)) return;
+ const level = _level === "UNKNOWN" ? defaultLevel : _level;
+ logWithLevel(logger, level)(`[${level}]${line}`);
+ },
+ };
+};
diff --git a/u/trace/mod.ts b/u/trace/mod.ts
new file mode 100644
index 0000000..9c42858
--- /dev/null
+++ b/u/trace/mod.ts
@@ -0,0 +1,3 @@
+export * from "./itrace.ts";
+export * from "./logger.ts";
+export * from "./trace.ts";
diff --git a/u/trace/trace.ts b/u/trace/trace.ts
new file mode 100644
index 0000000..5d5c59b
--- /dev/null
+++ b/u/trace/trace.ts
@@ -0,0 +1,34 @@
+import type { Callable } from "@emprespresso/utils";
+import {
+ type ITraceableMapper,
+ type ITraceableTuple,
+ TraceableImpl,
+ TraceableLogger,
+} from "./mod.ts";
+
+export class Traceable<T> extends TraceableImpl<T, TraceableLogger> {
+ static from<T>(t: T) {
+ return new Traceable(t, new TraceableLogger());
+ }
+
+ static withFunctionTrace<F extends Callable, T>(
+ f: F,
+ ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> {
+ return (t) => [t.item, f.name];
+ }
+
+ static withClassTrace<C extends object, T>(
+ c: C,
+ ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> {
+ return (t) => [t.item, c.constructor.name];
+ }
+
+ static promiseify<T, U>(
+ mapper: ITraceableMapper<T, TraceableLogger, U>,
+ ): ITraceableMapper<Promise<T>, TraceableLogger, Promise<U>> {
+ return (traceablePromise) =>
+ traceablePromise.flatMapAsync(async (t) =>
+ t.move(await t.item).map(mapper)
+ ).item;
+ }
+}