@visiolab/effect-firebase-functions 0.0.0-stage → 0.3.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.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * A Logger that writes Effect logs as Cloud Logging structured entries, one
3
+ * JSON line each, tied to the trace of the fiber's current span.
4
+ *
5
+ * `Functions` installs it for every call or event, beneath the Layer you
6
+ * serve, so you need this module only to log the same way elsewhere, or to
7
+ * change how a deployed Function logs.
8
+ *
9
+ * @stability unstable
10
+ * @since 0.3.0
11
+ */
12
+ import { Cause, Console, Formatter, Inspectable, type Layer, Logger, References } from "effect";
13
+ import type * as LogLevel from "effect/LogLevel";
14
+
15
+ const severities: Record<LogLevel.LogLevel, string> = {
16
+ All: "DEFAULT",
17
+ Trace: "DEBUG",
18
+ Debug: "DEBUG",
19
+ Info: "INFO",
20
+ Warn: "WARNING",
21
+ Error: "ERROR",
22
+ Fatal: "CRITICAL",
23
+ None: "DEFAULT",
24
+ };
25
+
26
+ /**
27
+ * The Cloud Logging logger: writes each entry as one JSON line, the format
28
+ * Firebase's own `logger` writes, to `console.error` for Error and Fatal and
29
+ * to `console.log` for everything else.
30
+ *
31
+ * **When to use**
32
+ *
33
+ * Use with `Logger.layer` to combine it with other loggers. To replace
34
+ * Effect's default logger with it, use {@link layer}.
35
+ *
36
+ * **Details**
37
+ *
38
+ * Each entry is a JSON object with these fields:
39
+ *
40
+ * - `severity`: Trace and Debug are `DEBUG`, Info is `INFO`, Warn is
41
+ * `WARNING`, Error is `ERROR` and Fatal is `CRITICAL`.
42
+ * - `message`: the first value logged. A non-empty Cause is appended as
43
+ * `Cause.pretty` output, with its stacks, so Error Reporting picks up
44
+ * defects.
45
+ * - `args`: the other values logged, if any.
46
+ * - `spans`: the log spans, as `{ label: milliseconds }`, if any.
47
+ * - `logging.googleapis.com/trace`, `logging.googleapis.com/spanId` and
48
+ * `logging.googleapis.com/trace_sampled`: the trace of the fiber's current
49
+ * span, with the project from `GCLOUD_PROJECT`, else `GOOGLE_CLOUD_PROJECT`.
50
+ * Left out when there is no span, and when neither variable is set, since a
51
+ * span id means nothing to Cloud Logging without its trace. Cloud Functions
52
+ * sets `GCLOUD_PROJECT`.
53
+ * - Every log annotation, as a field of its own. `severity`, `message` and the
54
+ * `logging.googleapis.com/` fields win over an annotation of the same name.
55
+ *
56
+ * The trace comes from the fiber, never from Firebase's `traceContext`, so
57
+ * concurrent requests on one instance each log under their own trace. Lines go
58
+ * to the `Console` service, which is the global `console` unless you provide
59
+ * another. Entries carry no uid: add one with `Effect.annotateLogs`.
60
+ *
61
+ * @example Logging under a uid
62
+ * ```ts
63
+ * import { Effect } from "effect";
64
+ * import { CloudLogging } from "@visiolab/effect-firebase-functions/logger";
65
+ *
66
+ * const program = Effect.log("Order placed").pipe(
67
+ * Effect.annotateLogs({ uid: "user-1" }),
68
+ * Effect.provide(CloudLogging.layer),
69
+ * );
70
+ * ```
71
+ *
72
+ * @see {@link layer} for replacing Effect's default logger with it
73
+ *
74
+ * @stability unstable
75
+ * @category loggers
76
+ * @since 0.3.0
77
+ */
78
+ export const logger: Logger.Logger<unknown, void> = Logger.make(
79
+ ({ cause, date, fiber, logLevel, message }) => {
80
+ const values = Array.isArray(message) ? message : [message];
81
+ const [first, ...args] = values;
82
+ let text =
83
+ values.length === 0
84
+ ? ""
85
+ : typeof first === "string"
86
+ ? first
87
+ : Inspectable.toStringUnknown(first);
88
+ if (cause.reasons.length > 0) {
89
+ const pretty = Cause.pretty(cause);
90
+ text = text === "" ? pretty : `${text}\n${pretty}`;
91
+ }
92
+
93
+ const entry: Record<string, unknown> = { ...fiber.getRef(References.CurrentLogAnnotations) };
94
+ if (args.length > 0) entry["args"] = args;
95
+ const logSpans = fiber.getRef(References.CurrentLogSpans);
96
+ if (logSpans.length > 0) {
97
+ const now = date.getTime();
98
+ entry["spans"] = Object.fromEntries(logSpans.map(([label, start]) => [label, now - start]));
99
+ }
100
+ for (const key of Object.keys(entry)) {
101
+ if (key.startsWith("logging.googleapis.com/")) delete entry[key];
102
+ }
103
+ entry["severity"] = severities[logLevel];
104
+ entry["message"] = text;
105
+ const span = fiber.cache.span;
106
+ const project = process.env["GCLOUD_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"];
107
+ if (span !== undefined && project !== undefined && project !== "") {
108
+ entry["logging.googleapis.com/trace"] = `projects/${project}/traces/${span.traceId}`;
109
+ entry["logging.googleapis.com/spanId"] = span.spanId;
110
+ entry["logging.googleapis.com/trace_sampled"] = span.sampled;
111
+ }
112
+
113
+ const console = fiber.getRef(Console.Console);
114
+ const line = Formatter.formatJson(entry);
115
+ if (logLevel === "Error" || logLevel === "Fatal") console.error(line);
116
+ else console.log(line);
117
+ },
118
+ );
119
+
120
+ /**
121
+ * Replaces the current loggers with the Cloud Logging {@link logger} and
122
+ * Effect's `Logger.tracerLogger`, which records logs as span events.
123
+ *
124
+ * **When to use**
125
+ *
126
+ * Use to log as Cloud Logging entries outside a Function deployed with
127
+ * `Functions`, in any Effect program that runs on Google Cloud. Deployed
128
+ * Functions already log this way.
129
+ *
130
+ * **Details**
131
+ *
132
+ * Provided in the Layer of a deployed Function, it changes nothing. To log
133
+ * differently there, provide your own `Logger.layer`, which wins over this one.
134
+ *
135
+ * @example Logging from a scheduled job
136
+ * ```ts
137
+ * import { Effect } from "effect";
138
+ * import { CloudLogging } from "@visiolab/effect-firebase-functions/logger";
139
+ *
140
+ * const job = Effect.log("Cleaning up").pipe(
141
+ * Effect.withSpan("cleanup"),
142
+ * Effect.provide(CloudLogging.layer),
143
+ * );
144
+ * ```
145
+ *
146
+ * @see {@link logger} for the logger on its own
147
+ *
148
+ * @stability unstable
149
+ * @category layers
150
+ * @since 0.3.0
151
+ */
152
+ export const layer: Layer.Layer<never> = Logger.layer([logger, Logger.tracerLogger]);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Writes Effect logs as Cloud Logging structured entries, tied to the trace
3
+ * of the fiber's current span.
4
+ *
5
+ * @stability unstable
6
+ * @since 0.3.0
7
+ */
8
+ export * as CloudLogging from "./CloudLogging.ts";