@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.
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/dist/Functions.d.mts +339 -0
- package/dist/Functions.mjs +213 -0
- package/dist/Functions.mjs.map +1 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -0
- package/dist/internal/callable.mjs +104 -0
- package/dist/internal/callable.mjs.map +1 -0
- package/dist/internal/documentTrigger.mjs +110 -0
- package/dist/internal/documentTrigger.mjs.map +1 -0
- package/dist/internal/kinds.mjs +19 -0
- package/dist/internal/kinds.mjs.map +1 -0
- package/dist/internal/member.mjs +12 -0
- package/dist/internal/member.mjs.map +1 -0
- package/dist/internal/runtime.mjs +79 -0
- package/dist/internal/runtime.mjs.map +1 -0
- package/dist/internal/serverSpan.mjs +187 -0
- package/dist/internal/serverSpan.mjs.map +1 -0
- package/dist/logger/CloudLogging.d.mts +92 -0
- package/dist/logger/CloudLogging.mjs +146 -0
- package/dist/logger/CloudLogging.mjs.map +1 -0
- package/dist/logger/index.d.mts +2 -0
- package/dist/logger/index.mjs +1 -0
- package/package.json +64 -3
- package/src/Functions.ts +482 -0
- package/src/index.ts +8 -0
- package/src/internal/callable.ts +156 -0
- package/src/internal/documentTrigger.ts +188 -0
- package/src/internal/kinds.ts +36 -0
- package/src/internal/member.ts +21 -0
- package/src/internal/runtime.ts +110 -0
- package/src/internal/serverSpan.ts +203 -0
- package/src/logger/CloudLogging.ts +152 -0
- package/src/logger/index.ts +8 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { layer } from "../logger/CloudLogging.mjs";
|
|
2
|
+
import { DeferredTracer, externalParent } from "./serverSpan.mjs";
|
|
3
|
+
import { Context, Effect, Exit, Layer, Logger, Scope } from "effect";
|
|
4
|
+
// src/internal/runtime.ts
|
|
5
|
+
/**
|
|
6
|
+
* What every kind of Function runs an event or a call under: the lazy,
|
|
7
|
+
* memoized build of its Layer, its server span, its Tracer and its logs.
|
|
8
|
+
*
|
|
9
|
+
* A kind's runtime turns what `firebase-functions` hands over into the input
|
|
10
|
+
* of its core `serve`, and passes that to {@link make}'s run function, which
|
|
11
|
+
* does the rest.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The memo map and scope every Layer of this process is built in, so a Layer
|
|
17
|
+
* that several exports share is built once. The scope is never closed:
|
|
18
|
+
* functions-framework exits the process without waiting for finalizers.
|
|
19
|
+
*/
|
|
20
|
+
const memoMap = Layer.makeMemoMapUnsafe();
|
|
21
|
+
const scope = Scope.makeUnsafe();
|
|
22
|
+
/**
|
|
23
|
+
* Builds `live` on the first event, and hands every later event the same
|
|
24
|
+
* services. Concurrent first events share one build. The build runs under the
|
|
25
|
+
* span and Tracer of the event that starts it, so it nests under that event.
|
|
26
|
+
*
|
|
27
|
+
* A failed build fails the event and is tried again on the next one. A Layer
|
|
28
|
+
* that fails closes the scope it was built in, which drops its failed entries
|
|
29
|
+
* from the memo map, so the retry builds anew.
|
|
30
|
+
*/
|
|
31
|
+
const lazyContext = (live) => {
|
|
32
|
+
let pending;
|
|
33
|
+
return Effect.flatMap(Effect.context(), (caller) => {
|
|
34
|
+
pending ??= Effect.runPromiseExit(Layer.buildWithMemoMap(live, memoMap, scope).pipe(Effect.provideContext(caller))).then((exit) => {
|
|
35
|
+
if (Exit.isFailure(exit)) pending = void 0;
|
|
36
|
+
return exit;
|
|
37
|
+
});
|
|
38
|
+
const attempt = pending;
|
|
39
|
+
return Effect.flatten(Effect.promise(() => attempt));
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The run function of one export: it builds `live` on the export's first
|
|
44
|
+
* event or call, so a worker that serves one export builds nothing for the
|
|
45
|
+
* others, and runs each `body` with the services of `live` provided.
|
|
46
|
+
*
|
|
47
|
+
* The run happens in a `kind: "server"` span, parented on the trace context
|
|
48
|
+
* Firebase read from the event or request. Its spans go to the Tracer of
|
|
49
|
+
* `live`, its logs go through `CloudLogging.layer` unless `live` provides a
|
|
50
|
+
* logger, and logs from before `live` is built wait until the spans have
|
|
51
|
+
* their final ids.
|
|
52
|
+
*
|
|
53
|
+
* Read the trace context synchronously, at the start of the SDK's handler:
|
|
54
|
+
* call the run function there.
|
|
55
|
+
*
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
const make = (live, options) => {
|
|
59
|
+
const context = lazyContext(live);
|
|
60
|
+
const spanOptions = {
|
|
61
|
+
kind: "server",
|
|
62
|
+
attributes: options.attributes
|
|
63
|
+
};
|
|
64
|
+
return (body, signal) => {
|
|
65
|
+
const parent = externalParent();
|
|
66
|
+
const deferred = new DeferredTracer();
|
|
67
|
+
return Effect.runPromiseExit(Effect.gen(function* () {
|
|
68
|
+
const services = yield* context.pipe(Effect.onError(() => Effect.sync(() => deferred.attach(Context.empty()))), Effect.onError(options.onBuildFailure));
|
|
69
|
+
deferred.attach(services);
|
|
70
|
+
return yield* Effect.provideContext(body, services);
|
|
71
|
+
}).pipe(Effect.withSpan(options.span, {
|
|
72
|
+
...spanOptions,
|
|
73
|
+
parent
|
|
74
|
+
}), Effect.updateService(Logger.CurrentLoggers, (loggers) => deferred.holdLogs(loggers)), Effect.provide(layer), Effect.withTracer(deferred.tracer)), signal === void 0 ? void 0 : { signal });
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
export { make };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=runtime.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.mjs","names":["CloudLogging.layer"],"sources":["../../src/internal/runtime.ts"],"sourcesContent":["/**\n * What every kind of Function runs an event or a call under: the lazy,\n * memoized build of its Layer, its server span, its Tracer and its logs.\n *\n * A kind's runtime turns what `firebase-functions` hands over into the input\n * of its core `serve`, and passes that to {@link make}'s run function, which\n * does the rest.\n *\n * @internal\n */\nimport { type Cause, Context, Effect, Exit, Layer, Logger, Scope } from \"effect\";\nimport * as CloudLogging from \"../logger/CloudLogging.ts\";\nimport { DeferredTracer, externalParent } from \"./serverSpan.ts\";\n\n/**\n * The memo map and scope every Layer of this process is built in, so a Layer\n * that several exports share is built once. The scope is never closed:\n * functions-framework exits the process without waiting for finalizers.\n */\nconst memoMap = Layer.makeMemoMapUnsafe();\nconst scope = Scope.makeUnsafe();\n\n/**\n * Builds `live` on the first event, and hands every later event the same\n * services. Concurrent first events share one build. The build runs under the\n * span and Tracer of the event that starts it, so it nests under that event.\n *\n * A failed build fails the event and is tried again on the next one. A Layer\n * that fails closes the scope it was built in, which drops its failed entries\n * from the memo map, so the retry builds anew.\n */\nconst lazyContext = <R, E>(live: Layer.Layer<R, E>) => {\n let pending: Promise<Exit.Exit<Context.Context<R>, E>> | undefined;\n return Effect.flatMap(Effect.context<never>(), (caller) => {\n pending ??= Effect.runPromiseExit(\n Layer.buildWithMemoMap(live, memoMap, scope).pipe(Effect.provideContext(caller)),\n ).then((exit) => {\n if (Exit.isFailure(exit)) pending = undefined;\n return exit;\n });\n const attempt = pending;\n return Effect.flatten(Effect.promise(() => attempt));\n });\n};\n\n/**\n * How one export runs each event or call.\n *\n * @internal\n */\nexport interface Options {\n /** The name of the `kind: \"server\"` span each event or call runs in. */\n readonly span: string;\n /** The attributes of that span. */\n readonly attributes: Readonly<Record<string, unknown>>;\n /**\n * Logs the Cause of a Layer that failed to build, in the span of the event\n * or call that built it.\n */\n readonly onBuildFailure: (cause: Cause.Cause<unknown>) => Effect.Effect<void>;\n}\n\n/**\n * The run function of one export: it builds `live` on the export's first\n * event or call, so a worker that serves one export builds nothing for the\n * others, and runs each `body` with the services of `live` provided.\n *\n * The run happens in a `kind: \"server\"` span, parented on the trace context\n * Firebase read from the event or request. Its spans go to the Tracer of\n * `live`, its logs go through `CloudLogging.layer` unless `live` provides a\n * logger, and logs from before `live` is built wait until the spans have\n * their final ids.\n *\n * Read the trace context synchronously, at the start of the SDK's handler:\n * call the run function there.\n *\n * @internal\n */\nexport const make = <R, E>(live: Layer.Layer<R, E>, options: Options) => {\n const context = lazyContext(live);\n const spanOptions = { kind: \"server\", attributes: options.attributes } as const;\n return <A>(\n body: Effect.Effect<A, unknown, unknown>,\n signal?: AbortSignal,\n ): Promise<Exit.Exit<A, unknown>> => {\n const parent = externalParent();\n const deferred = new DeferredTracer();\n return Effect.runPromiseExit(\n Effect.gen(function* () {\n const services = yield* context.pipe(\n // With no services to take a Tracer from, the run's spans and held\n // logs go to the native Tracer, so the failure is logged in one trace.\n Effect.onError(() => Effect.sync(() => deferred.attach(Context.empty()))),\n Effect.onError(options.onBuildFailure),\n );\n deferred.attach(services as Context.Context<never>);\n // The public signatures check that `live` provides what `body` needs.\n return yield* Effect.provideContext(body, services as Context.Context<any>);\n }).pipe(\n Effect.withSpan(options.span, { ...spanOptions, parent }),\n // Logs from before `live` is built wait for the spans' real ids.\n Effect.updateService(Logger.CurrentLoggers, (loggers) => deferred.holdLogs(loggers)),\n // Beneath `live`: a logger that `live` provides replaces this one.\n Effect.provide(CloudLogging.layer),\n Effect.withTracer(deferred.tracer),\n ),\n signal === undefined ? undefined : { signal },\n );\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAM,UAAU,MAAM,kBAAkB;AACxC,MAAM,QAAQ,MAAM,WAAW;;;;;;;;;;AAW/B,MAAM,eAAqB,SAA4B;CACrD,IAAI;CACJ,OAAO,OAAO,QAAQ,OAAO,QAAe,IAAI,WAAW;EACzD,YAAY,OAAO,eACjB,MAAM,iBAAiB,MAAM,SAAS,KAAK,CAAC,CAAC,KAAK,OAAO,eAAe,MAAM,CAAC,CACjF,CAAC,CAAC,MAAM,SAAS;GACf,IAAI,KAAK,UAAU,IAAI,GAAG,UAAU,KAAA;GACpC,OAAO;EACT,CAAC;EACD,MAAM,UAAU;EAChB,OAAO,OAAO,QAAQ,OAAO,cAAc,OAAO,CAAC;CACrD,CAAC;AACH;;;;;;;;;;;;;;;;;AAmCA,MAAa,QAAc,MAAyB,YAAqB;CACvE,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,cAAc;EAAE,MAAM;EAAU,YAAY,QAAQ;CAAW;CACrE,QACE,MACA,WACmC;EACnC,MAAM,SAAS,eAAe;EAC9B,MAAM,WAAW,IAAI,eAAe;EACpC,OAAO,OAAO,eACZ,OAAO,IAAI,aAAa;GACtB,MAAM,WAAW,OAAO,QAAQ,KAG9B,OAAO,cAAc,OAAO,WAAW,SAAS,OAAO,QAAQ,MAAM,CAAC,CAAC,CAAC,GACxE,OAAO,QAAQ,QAAQ,cAAc,CACvC;GACA,SAAS,OAAO,QAAkC;GAElD,OAAO,OAAO,OAAO,eAAe,MAAM,QAAgC;EAC5E,CAAC,CAAC,CAAC,KACD,OAAO,SAAS,QAAQ,MAAM;GAAE,GAAG;GAAa;EAAO,CAAC,GAExD,OAAO,cAAc,OAAO,iBAAiB,YAAY,SAAS,SAAS,OAAO,CAAC,GAEnF,OAAO,QAAQA,KAAkB,GACjC,OAAO,WAAW,SAAS,MAAM,CACnC,GACA,WAAW,KAAA,IAAY,KAAA,IAAY,EAAE,OAAO,CAC9C;CACF;AACF"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Context, Logger, Option, Tracer } from "effect";
|
|
2
|
+
import { traceContext } from "firebase-functions/v2";
|
|
3
|
+
// src/internal/serverSpan.ts
|
|
4
|
+
/** @internal */
|
|
5
|
+
/**
|
|
6
|
+
* The parent of a call's server span: the trace context `firebase-functions`
|
|
7
|
+
* read from the request's headers, the `X-Cloud-Trace-Context` header winning
|
|
8
|
+
* over `traceparent`. Read it synchronously at the start of the call, before
|
|
9
|
+
* any fiber runs: Effect's scheduler may later run under another request's
|
|
10
|
+
* async store. `undefined` when the request carried none, as in the emulator.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
const externalParent = () => {
|
|
15
|
+
const store = traceContext.getStore();
|
|
16
|
+
if (store === void 0) return void 0;
|
|
17
|
+
return Tracer.externalSpan({
|
|
18
|
+
traceId: store.traceId,
|
|
19
|
+
spanId: store.parentId.padStart(16, "0"),
|
|
20
|
+
sampled: store.sample
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A span opened before the Tracer of `Live` is known. It records what happens
|
|
25
|
+
* to it, and replays that onto the real span once {@link DeferredTracer.attach}
|
|
26
|
+
* creates it. From then on it forwards everything.
|
|
27
|
+
*/
|
|
28
|
+
var DeferredSpan = class DeferredSpan {
|
|
29
|
+
_tag = "Span";
|
|
30
|
+
name;
|
|
31
|
+
parent;
|
|
32
|
+
annotations;
|
|
33
|
+
kind;
|
|
34
|
+
sampled;
|
|
35
|
+
options;
|
|
36
|
+
real;
|
|
37
|
+
calls = [];
|
|
38
|
+
pendingAttributes = /* @__PURE__ */ new Map();
|
|
39
|
+
pendingLinks;
|
|
40
|
+
pendingStatus;
|
|
41
|
+
provisional;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.name = options.name;
|
|
45
|
+
this.parent = options.parent;
|
|
46
|
+
this.annotations = options.annotations;
|
|
47
|
+
this.kind = options.kind;
|
|
48
|
+
this.sampled = options.sampled;
|
|
49
|
+
this.pendingLinks = [...options.links];
|
|
50
|
+
this.pendingStatus = {
|
|
51
|
+
_tag: "Started",
|
|
52
|
+
startTime: options.startTime
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
get ids() {
|
|
56
|
+
if (this.provisional === void 0) {
|
|
57
|
+
const native = new Tracer.NativeSpan({
|
|
58
|
+
...this.options,
|
|
59
|
+
links: []
|
|
60
|
+
});
|
|
61
|
+
this.provisional = {
|
|
62
|
+
traceId: native.traceId,
|
|
63
|
+
spanId: native.spanId
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return this.provisional;
|
|
67
|
+
}
|
|
68
|
+
get traceId() {
|
|
69
|
+
return this.real?.traceId ?? this.ids.traceId;
|
|
70
|
+
}
|
|
71
|
+
get spanId() {
|
|
72
|
+
return this.real?.spanId ?? this.ids.spanId;
|
|
73
|
+
}
|
|
74
|
+
get status() {
|
|
75
|
+
return this.real?.status ?? this.pendingStatus;
|
|
76
|
+
}
|
|
77
|
+
get attributes() {
|
|
78
|
+
return this.real?.attributes ?? this.pendingAttributes;
|
|
79
|
+
}
|
|
80
|
+
get links() {
|
|
81
|
+
return this.real?.links ?? this.pendingLinks;
|
|
82
|
+
}
|
|
83
|
+
end(endTime, exit) {
|
|
84
|
+
if (this.real !== void 0) return this.real.end(endTime, exit);
|
|
85
|
+
if (this.pendingStatus._tag === "Ended") return;
|
|
86
|
+
this.pendingStatus = {
|
|
87
|
+
_tag: "Ended",
|
|
88
|
+
startTime: this.pendingStatus.startTime,
|
|
89
|
+
endTime,
|
|
90
|
+
exit
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
attribute(key, value) {
|
|
94
|
+
if (this.real !== void 0) return this.real.attribute(key, value);
|
|
95
|
+
this.pendingAttributes.set(key, value);
|
|
96
|
+
}
|
|
97
|
+
event(name, startTime, attributes) {
|
|
98
|
+
if (this.real !== void 0) return this.real.event(name, startTime, attributes);
|
|
99
|
+
this.calls.push((span) => span.event(name, startTime, attributes));
|
|
100
|
+
}
|
|
101
|
+
addLinks(links) {
|
|
102
|
+
if (this.real !== void 0) return this.real.addLinks(links);
|
|
103
|
+
this.calls.push((span) => span.addLinks(links));
|
|
104
|
+
}
|
|
105
|
+
/** Opens the real span on `tracer` and replays onto it what happened so far. */
|
|
106
|
+
attach(tracer) {
|
|
107
|
+
const parent = Option.map(this.parent, (parent) => parent instanceof DeferredSpan ? parent.real ?? parent : parent);
|
|
108
|
+
const real = tracer.span({
|
|
109
|
+
...this.options,
|
|
110
|
+
parent,
|
|
111
|
+
links: this.pendingLinks
|
|
112
|
+
});
|
|
113
|
+
for (const [key, value] of this.pendingAttributes) real.attribute(key, value);
|
|
114
|
+
for (const call of this.calls) call(real);
|
|
115
|
+
if (this.pendingStatus._tag === "Ended") real.end(this.pendingStatus.endTime, this.pendingStatus.exit);
|
|
116
|
+
this.real = real;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The Tracer a call starts under. A call's server span opens before `Live` is
|
|
121
|
+
* built, and so before its Tracer is known, so that a first call's Layer
|
|
122
|
+
* build nests under it. This Tracer defers the spans opened until then, and
|
|
123
|
+
* {@link DeferredTracer.attach} hands them to the Tracer of `Live`, which
|
|
124
|
+
* exports them with their original times and parents.
|
|
125
|
+
*
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
var DeferredTracer = class {
|
|
129
|
+
deferred = [];
|
|
130
|
+
held = [];
|
|
131
|
+
real;
|
|
132
|
+
/**
|
|
133
|
+
* Wraps each of `loggers` so that an entry logged before {@link attach}
|
|
134
|
+
* is held, and logged on `attach`, once the spans it was logged under have
|
|
135
|
+
* their real ids. A logger reads the trace of the current span, and a
|
|
136
|
+
* deferred span's ids before `attach` are ones no exporter sees.
|
|
137
|
+
*
|
|
138
|
+
* The tracer logger isn't held: it writes onto the span itself, which a
|
|
139
|
+
* deferred span already replays.
|
|
140
|
+
*/
|
|
141
|
+
holdLogs(loggers) {
|
|
142
|
+
return new Set(Array.from(loggers, (logger) => logger === Logger.tracerLogger ? logger : Logger.make((options) => {
|
|
143
|
+
if (this.real !== void 0) return logger.log(options);
|
|
144
|
+
const held = {
|
|
145
|
+
...options,
|
|
146
|
+
fiber: snapshot(options.fiber)
|
|
147
|
+
};
|
|
148
|
+
this.held.push(() => logger.log(held));
|
|
149
|
+
})));
|
|
150
|
+
}
|
|
151
|
+
tracer = Tracer.make({ span: (options) => {
|
|
152
|
+
if (this.real !== void 0) return this.real.span(options);
|
|
153
|
+
const span = new DeferredSpan(options);
|
|
154
|
+
this.deferred.push(span);
|
|
155
|
+
return span;
|
|
156
|
+
} });
|
|
157
|
+
/**
|
|
158
|
+
* Hands every span opened so far to the Tracer that `services` provide, the
|
|
159
|
+
* native one when they provide none, in the order they were opened, so that
|
|
160
|
+
* each parent exists before its children. Then logs the entries
|
|
161
|
+
* {@link holdLogs} held, in the order they were logged.
|
|
162
|
+
*
|
|
163
|
+
* Attach with no services when `Live` failed to build, so the spans and
|
|
164
|
+
* logs of the failed call still get ids of one trace.
|
|
165
|
+
*/
|
|
166
|
+
attach(services) {
|
|
167
|
+
if (this.real !== void 0) return;
|
|
168
|
+
this.real = Context.get(services, Tracer.Tracer);
|
|
169
|
+
for (const span of this.deferred) span.attach(this.real);
|
|
170
|
+
this.deferred.length = 0;
|
|
171
|
+
for (const log of this.held) log();
|
|
172
|
+
this.held.length = 0;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* `fiber` as it is now: what a logger reads from it later, such as the log
|
|
177
|
+
* annotations and the current span, stays what it was when the entry was
|
|
178
|
+
* logged. Both are derived from the fiber's context, which never changes in
|
|
179
|
+
* place, only gets replaced.
|
|
180
|
+
*/
|
|
181
|
+
const snapshot = (fiber) => Object.create(fiber, {
|
|
182
|
+
context: { value: fiber.context },
|
|
183
|
+
cache: { value: fiber.cache }
|
|
184
|
+
});
|
|
185
|
+
export { DeferredTracer, externalParent };
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=serverSpan.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverSpan.mjs","names":[],"sources":["../../src/internal/serverSpan.ts"],"sourcesContent":["/** @internal */\nimport { Context, Logger, Option, Tracer, type Exit, type Fiber } from \"effect\";\nimport { traceContext } from \"firebase-functions/v2\";\n\n/**\n * The parent of a call's server span: the trace context `firebase-functions`\n * read from the request's headers, the `X-Cloud-Trace-Context` header winning\n * over `traceparent`. Read it synchronously at the start of the call, before\n * any fiber runs: Effect's scheduler may later run under another request's\n * async store. `undefined` when the request carried none, as in the emulator.\n *\n * @internal\n */\nexport const externalParent = (): Tracer.ExternalSpan | undefined => {\n const store = traceContext.getStore();\n if (store === undefined) return undefined;\n return Tracer.externalSpan({\n traceId: store.traceId,\n // The Cloud header's decimal span id reaches us as unpadded hex.\n spanId: store.parentId.padStart(16, \"0\"),\n sampled: store.sample,\n });\n};\n\ntype SpanOptions = Parameters<Tracer.Tracer[\"span\"]>[0];\n\n/**\n * A span opened before the Tracer of `Live` is known. It records what happens\n * to it, and replays that onto the real span once {@link DeferredTracer.attach}\n * creates it. From then on it forwards everything.\n */\nclass DeferredSpan implements Tracer.Span {\n readonly _tag = \"Span\";\n readonly name: string;\n readonly parent: Option.Option<Tracer.AnySpan>;\n readonly annotations: Context.Context<never>;\n readonly kind: Tracer.SpanKind;\n readonly sampled: boolean;\n readonly options: SpanOptions;\n real: Tracer.Span | undefined;\n // What happened before `real` existed, in order.\n private readonly calls: Array<(span: Tracer.Span) => void> = [];\n private readonly pendingAttributes = new Map<string, unknown>();\n private readonly pendingLinks: Array<Tracer.SpanLink>;\n private pendingStatus: Tracer.SpanStatus;\n private provisional: { traceId: string; spanId: string } | undefined;\n\n constructor(options: SpanOptions) {\n this.options = options;\n this.name = options.name;\n this.parent = options.parent;\n this.annotations = options.annotations;\n this.kind = options.kind;\n this.sampled = options.sampled;\n this.pendingLinks = [...options.links];\n this.pendingStatus = { _tag: \"Started\", startTime: options.startTime };\n }\n\n // Read before `attach` only by a Tracer that `Live` provides to its own\n // Layers: such a span joins the trace, but its parent's span id is one no\n // exporter sees.\n private get ids() {\n if (this.provisional === undefined) {\n const native = new Tracer.NativeSpan({ ...this.options, links: [] });\n this.provisional = { traceId: native.traceId, spanId: native.spanId };\n }\n return this.provisional;\n }\n\n get traceId() {\n return this.real?.traceId ?? this.ids.traceId;\n }\n\n get spanId() {\n return this.real?.spanId ?? this.ids.spanId;\n }\n\n get status() {\n return this.real?.status ?? this.pendingStatus;\n }\n\n get attributes() {\n return this.real?.attributes ?? this.pendingAttributes;\n }\n\n get links() {\n return this.real?.links ?? this.pendingLinks;\n }\n\n end(endTime: bigint, exit: Exit.Exit<unknown, unknown>) {\n if (this.real !== undefined) return this.real.end(endTime, exit);\n if (this.pendingStatus._tag === \"Ended\") return;\n this.pendingStatus = { _tag: \"Ended\", startTime: this.pendingStatus.startTime, endTime, exit };\n }\n\n attribute(key: string, value: unknown) {\n if (this.real !== undefined) return this.real.attribute(key, value);\n this.pendingAttributes.set(key, value);\n }\n\n event(name: string, startTime: bigint, attributes?: Record<string, unknown>) {\n if (this.real !== undefined) return this.real.event(name, startTime, attributes);\n this.calls.push((span) => span.event(name, startTime, attributes));\n }\n\n addLinks(links: ReadonlyArray<Tracer.SpanLink>) {\n if (this.real !== undefined) return this.real.addLinks(links);\n this.calls.push((span) => span.addLinks(links));\n }\n\n /** Opens the real span on `tracer` and replays onto it what happened so far. */\n attach(tracer: Tracer.Tracer) {\n const parent = Option.map(this.parent, (parent) =>\n parent instanceof DeferredSpan ? (parent.real ?? parent) : parent,\n );\n const real = tracer.span({ ...this.options, parent, links: this.pendingLinks });\n for (const [key, value] of this.pendingAttributes) real.attribute(key, value);\n for (const call of this.calls) call(real);\n if (this.pendingStatus._tag === \"Ended\") {\n real.end(this.pendingStatus.endTime, this.pendingStatus.exit);\n }\n this.real = real;\n }\n}\n\n/**\n * The Tracer a call starts under. A call's server span opens before `Live` is\n * built, and so before its Tracer is known, so that a first call's Layer\n * build nests under it. This Tracer defers the spans opened until then, and\n * {@link DeferredTracer.attach} hands them to the Tracer of `Live`, which\n * exports them with their original times and parents.\n *\n * @internal\n */\nexport class DeferredTracer {\n private readonly deferred: Array<DeferredSpan> = [];\n private readonly held: Array<() => void> = [];\n private real: Tracer.Tracer | undefined;\n\n /**\n * Wraps each of `loggers` so that an entry logged before {@link attach}\n * is held, and logged on `attach`, once the spans it was logged under have\n * their real ids. A logger reads the trace of the current span, and a\n * deferred span's ids before `attach` are ones no exporter sees.\n *\n * The tracer logger isn't held: it writes onto the span itself, which a\n * deferred span already replays.\n */\n holdLogs(\n loggers: ReadonlySet<Logger.Logger<unknown, any>>,\n ): ReadonlySet<Logger.Logger<unknown, any>> {\n return new Set(\n Array.from(loggers, (logger) =>\n logger === Logger.tracerLogger\n ? logger\n : Logger.make<unknown, unknown>((options) => {\n if (this.real !== undefined) return logger.log(options);\n const held = { ...options, fiber: snapshot(options.fiber) };\n this.held.push(() => logger.log(held));\n }),\n ),\n );\n }\n\n readonly tracer: Tracer.Tracer = Tracer.make({\n span: (options) => {\n if (this.real !== undefined) return this.real.span(options);\n const span = new DeferredSpan(options);\n this.deferred.push(span);\n return span;\n },\n });\n\n /**\n * Hands every span opened so far to the Tracer that `services` provide, the\n * native one when they provide none, in the order they were opened, so that\n * each parent exists before its children. Then logs the entries\n * {@link holdLogs} held, in the order they were logged.\n *\n * Attach with no services when `Live` failed to build, so the spans and\n * logs of the failed call still get ids of one trace.\n */\n attach(services: Context.Context<never>) {\n if (this.real !== undefined) return;\n this.real = Context.get(services, Tracer.Tracer);\n for (const span of this.deferred) span.attach(this.real);\n this.deferred.length = 0;\n for (const log of this.held) log();\n this.held.length = 0;\n }\n}\n\n/**\n * `fiber` as it is now: what a logger reads from it later, such as the log\n * annotations and the current span, stays what it was when the entry was\n * logged. Both are derived from the fiber's context, which never changes in\n * place, only gets replaced.\n */\nconst snapshot = <A, E>(fiber: Fiber.Fiber<A, E>): Fiber.Fiber<A, E> =>\n Object.create(fiber, {\n context: { value: fiber.context },\n cache: { value: fiber.cache },\n }) as Fiber.Fiber<A, E>;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Layer, Logger } from "effect";
|
|
2
|
+
declare namespace CloudLogging_d_exports {
|
|
3
|
+
export { layer, logger };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The Cloud Logging logger: writes each entry as one JSON line, the format
|
|
7
|
+
* Firebase's own `logger` writes, to `console.error` for Error and Fatal and
|
|
8
|
+
* to `console.log` for everything else.
|
|
9
|
+
*
|
|
10
|
+
* **When to use**
|
|
11
|
+
*
|
|
12
|
+
* Use with `Logger.layer` to combine it with other loggers. To replace
|
|
13
|
+
* Effect's default logger with it, use {@link layer}.
|
|
14
|
+
*
|
|
15
|
+
* **Details**
|
|
16
|
+
*
|
|
17
|
+
* Each entry is a JSON object with these fields:
|
|
18
|
+
*
|
|
19
|
+
* - `severity`: Trace and Debug are `DEBUG`, Info is `INFO`, Warn is
|
|
20
|
+
* `WARNING`, Error is `ERROR` and Fatal is `CRITICAL`.
|
|
21
|
+
* - `message`: the first value logged. A non-empty Cause is appended as
|
|
22
|
+
* `Cause.pretty` output, with its stacks, so Error Reporting picks up
|
|
23
|
+
* defects.
|
|
24
|
+
* - `args`: the other values logged, if any.
|
|
25
|
+
* - `spans`: the log spans, as `{ label: milliseconds }`, if any.
|
|
26
|
+
* - `logging.googleapis.com/trace`, `logging.googleapis.com/spanId` and
|
|
27
|
+
* `logging.googleapis.com/trace_sampled`: the trace of the fiber's current
|
|
28
|
+
* span, with the project from `GCLOUD_PROJECT`, else `GOOGLE_CLOUD_PROJECT`.
|
|
29
|
+
* Left out when there is no span, and when neither variable is set, since a
|
|
30
|
+
* span id means nothing to Cloud Logging without its trace. Cloud Functions
|
|
31
|
+
* sets `GCLOUD_PROJECT`.
|
|
32
|
+
* - Every log annotation, as a field of its own. `severity`, `message` and the
|
|
33
|
+
* `logging.googleapis.com/` fields win over an annotation of the same name.
|
|
34
|
+
*
|
|
35
|
+
* The trace comes from the fiber, never from Firebase's `traceContext`, so
|
|
36
|
+
* concurrent requests on one instance each log under their own trace. Lines go
|
|
37
|
+
* to the `Console` service, which is the global `console` unless you provide
|
|
38
|
+
* another. Entries carry no uid: add one with `Effect.annotateLogs`.
|
|
39
|
+
*
|
|
40
|
+
* @example Logging under a uid
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { Effect } from "effect";
|
|
43
|
+
* import { CloudLogging } from "@visiolab/effect-firebase-functions/logger";
|
|
44
|
+
*
|
|
45
|
+
* const program = Effect.log("Order placed").pipe(
|
|
46
|
+
* Effect.annotateLogs({ uid: "user-1" }),
|
|
47
|
+
* Effect.provide(CloudLogging.layer),
|
|
48
|
+
* );
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @see {@link layer} for replacing Effect's default logger with it
|
|
52
|
+
*
|
|
53
|
+
* @stability unstable
|
|
54
|
+
* @category loggers
|
|
55
|
+
* @since 0.3.0
|
|
56
|
+
*/
|
|
57
|
+
export declare const logger: Logger.Logger<unknown, void>;
|
|
58
|
+
/**
|
|
59
|
+
* Replaces the current loggers with the Cloud Logging {@link logger} and
|
|
60
|
+
* Effect's `Logger.tracerLogger`, which records logs as span events.
|
|
61
|
+
*
|
|
62
|
+
* **When to use**
|
|
63
|
+
*
|
|
64
|
+
* Use to log as Cloud Logging entries outside a Function deployed with
|
|
65
|
+
* `Functions`, in any Effect program that runs on Google Cloud. Deployed
|
|
66
|
+
* Functions already log this way.
|
|
67
|
+
*
|
|
68
|
+
* **Details**
|
|
69
|
+
*
|
|
70
|
+
* Provided in the Layer of a deployed Function, it changes nothing. To log
|
|
71
|
+
* differently there, provide your own `Logger.layer`, which wins over this one.
|
|
72
|
+
*
|
|
73
|
+
* @example Logging from a scheduled job
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { Effect } from "effect";
|
|
76
|
+
* import { CloudLogging } from "@visiolab/effect-firebase-functions/logger";
|
|
77
|
+
*
|
|
78
|
+
* const job = Effect.log("Cleaning up").pipe(
|
|
79
|
+
* Effect.withSpan("cleanup"),
|
|
80
|
+
* Effect.provide(CloudLogging.layer),
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @see {@link logger} for the logger on its own
|
|
85
|
+
*
|
|
86
|
+
* @stability unstable
|
|
87
|
+
* @category layers
|
|
88
|
+
* @since 0.3.0
|
|
89
|
+
*/
|
|
90
|
+
export declare const layer: Layer.Layer<never>;
|
|
91
|
+
export { CloudLogging_d_exports };
|
|
92
|
+
//# sourceMappingURL=CloudLogging.d.mts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { __exportAll } from "../_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { Cause, Console, Formatter, Inspectable, Logger, References } from "effect";
|
|
3
|
+
// src/logger/CloudLogging.ts
|
|
4
|
+
/**
|
|
5
|
+
* A Logger that writes Effect logs as Cloud Logging structured entries, one
|
|
6
|
+
* JSON line each, tied to the trace of the fiber's current span.
|
|
7
|
+
*
|
|
8
|
+
* `Functions` installs it for every call or event, beneath the Layer you
|
|
9
|
+
* serve, so you need this module only to log the same way elsewhere, or to
|
|
10
|
+
* change how a deployed Function logs.
|
|
11
|
+
*
|
|
12
|
+
* @stability unstable
|
|
13
|
+
* @since 0.3.0
|
|
14
|
+
*/
|
|
15
|
+
var CloudLogging_exports = /* @__PURE__ */ __exportAll({
|
|
16
|
+
layer: () => layer,
|
|
17
|
+
logger: () => logger
|
|
18
|
+
});
|
|
19
|
+
const severities = {
|
|
20
|
+
All: "DEFAULT",
|
|
21
|
+
Trace: "DEBUG",
|
|
22
|
+
Debug: "DEBUG",
|
|
23
|
+
Info: "INFO",
|
|
24
|
+
Warn: "WARNING",
|
|
25
|
+
Error: "ERROR",
|
|
26
|
+
Fatal: "CRITICAL",
|
|
27
|
+
None: "DEFAULT"
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The Cloud Logging logger: writes each entry as one JSON line, the format
|
|
31
|
+
* Firebase's own `logger` writes, to `console.error` for Error and Fatal and
|
|
32
|
+
* to `console.log` for everything else.
|
|
33
|
+
*
|
|
34
|
+
* **When to use**
|
|
35
|
+
*
|
|
36
|
+
* Use with `Logger.layer` to combine it with other loggers. To replace
|
|
37
|
+
* Effect's default logger with it, use {@link layer}.
|
|
38
|
+
*
|
|
39
|
+
* **Details**
|
|
40
|
+
*
|
|
41
|
+
* Each entry is a JSON object with these fields:
|
|
42
|
+
*
|
|
43
|
+
* - `severity`: Trace and Debug are `DEBUG`, Info is `INFO`, Warn is
|
|
44
|
+
* `WARNING`, Error is `ERROR` and Fatal is `CRITICAL`.
|
|
45
|
+
* - `message`: the first value logged. A non-empty Cause is appended as
|
|
46
|
+
* `Cause.pretty` output, with its stacks, so Error Reporting picks up
|
|
47
|
+
* defects.
|
|
48
|
+
* - `args`: the other values logged, if any.
|
|
49
|
+
* - `spans`: the log spans, as `{ label: milliseconds }`, if any.
|
|
50
|
+
* - `logging.googleapis.com/trace`, `logging.googleapis.com/spanId` and
|
|
51
|
+
* `logging.googleapis.com/trace_sampled`: the trace of the fiber's current
|
|
52
|
+
* span, with the project from `GCLOUD_PROJECT`, else `GOOGLE_CLOUD_PROJECT`.
|
|
53
|
+
* Left out when there is no span, and when neither variable is set, since a
|
|
54
|
+
* span id means nothing to Cloud Logging without its trace. Cloud Functions
|
|
55
|
+
* sets `GCLOUD_PROJECT`.
|
|
56
|
+
* - Every log annotation, as a field of its own. `severity`, `message` and the
|
|
57
|
+
* `logging.googleapis.com/` fields win over an annotation of the same name.
|
|
58
|
+
*
|
|
59
|
+
* The trace comes from the fiber, never from Firebase's `traceContext`, so
|
|
60
|
+
* concurrent requests on one instance each log under their own trace. Lines go
|
|
61
|
+
* to the `Console` service, which is the global `console` unless you provide
|
|
62
|
+
* another. Entries carry no uid: add one with `Effect.annotateLogs`.
|
|
63
|
+
*
|
|
64
|
+
* @example Logging under a uid
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { Effect } from "effect";
|
|
67
|
+
* import { CloudLogging } from "@visiolab/effect-firebase-functions/logger";
|
|
68
|
+
*
|
|
69
|
+
* const program = Effect.log("Order placed").pipe(
|
|
70
|
+
* Effect.annotateLogs({ uid: "user-1" }),
|
|
71
|
+
* Effect.provide(CloudLogging.layer),
|
|
72
|
+
* );
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @see {@link layer} for replacing Effect's default logger with it
|
|
76
|
+
*
|
|
77
|
+
* @stability unstable
|
|
78
|
+
* @category loggers
|
|
79
|
+
* @since 0.3.0
|
|
80
|
+
*/
|
|
81
|
+
const logger = Logger.make(({ cause, date, fiber, logLevel, message }) => {
|
|
82
|
+
const values = Array.isArray(message) ? message : [message];
|
|
83
|
+
const [first, ...args] = values;
|
|
84
|
+
let text = values.length === 0 ? "" : typeof first === "string" ? first : Inspectable.toStringUnknown(first);
|
|
85
|
+
if (cause.reasons.length > 0) {
|
|
86
|
+
const pretty = Cause.pretty(cause);
|
|
87
|
+
text = text === "" ? pretty : `${text}\n${pretty}`;
|
|
88
|
+
}
|
|
89
|
+
const entry = { ...fiber.getRef(References.CurrentLogAnnotations) };
|
|
90
|
+
if (args.length > 0) entry["args"] = args;
|
|
91
|
+
const logSpans = fiber.getRef(References.CurrentLogSpans);
|
|
92
|
+
if (logSpans.length > 0) {
|
|
93
|
+
const now = date.getTime();
|
|
94
|
+
entry["spans"] = Object.fromEntries(logSpans.map(([label, start]) => [label, now - start]));
|
|
95
|
+
}
|
|
96
|
+
for (const key of Object.keys(entry)) if (key.startsWith("logging.googleapis.com/")) delete entry[key];
|
|
97
|
+
entry["severity"] = severities[logLevel];
|
|
98
|
+
entry["message"] = text;
|
|
99
|
+
const span = fiber.cache.span;
|
|
100
|
+
const project = process.env["GCLOUD_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"];
|
|
101
|
+
if (span !== void 0 && project !== void 0 && project !== "") {
|
|
102
|
+
entry["logging.googleapis.com/trace"] = `projects/${project}/traces/${span.traceId}`;
|
|
103
|
+
entry["logging.googleapis.com/spanId"] = span.spanId;
|
|
104
|
+
entry["logging.googleapis.com/trace_sampled"] = span.sampled;
|
|
105
|
+
}
|
|
106
|
+
const console = fiber.getRef(Console.Console);
|
|
107
|
+
const line = Formatter.formatJson(entry);
|
|
108
|
+
if (logLevel === "Error" || logLevel === "Fatal") console.error(line);
|
|
109
|
+
else console.log(line);
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* Replaces the current loggers with the Cloud Logging {@link logger} and
|
|
113
|
+
* Effect's `Logger.tracerLogger`, which records logs as span events.
|
|
114
|
+
*
|
|
115
|
+
* **When to use**
|
|
116
|
+
*
|
|
117
|
+
* Use to log as Cloud Logging entries outside a Function deployed with
|
|
118
|
+
* `Functions`, in any Effect program that runs on Google Cloud. Deployed
|
|
119
|
+
* Functions already log this way.
|
|
120
|
+
*
|
|
121
|
+
* **Details**
|
|
122
|
+
*
|
|
123
|
+
* Provided in the Layer of a deployed Function, it changes nothing. To log
|
|
124
|
+
* differently there, provide your own `Logger.layer`, which wins over this one.
|
|
125
|
+
*
|
|
126
|
+
* @example Logging from a scheduled job
|
|
127
|
+
* ```ts
|
|
128
|
+
* import { Effect } from "effect";
|
|
129
|
+
* import { CloudLogging } from "@visiolab/effect-firebase-functions/logger";
|
|
130
|
+
*
|
|
131
|
+
* const job = Effect.log("Cleaning up").pipe(
|
|
132
|
+
* Effect.withSpan("cleanup"),
|
|
133
|
+
* Effect.provide(CloudLogging.layer),
|
|
134
|
+
* );
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @see {@link logger} for the logger on its own
|
|
138
|
+
*
|
|
139
|
+
* @stability unstable
|
|
140
|
+
* @category layers
|
|
141
|
+
* @since 0.3.0
|
|
142
|
+
*/
|
|
143
|
+
const layer = Logger.layer([logger, Logger.tracerLogger]);
|
|
144
|
+
export { CloudLogging_exports, layer, logger };
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=CloudLogging.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloudLogging.mjs","names":[],"sources":["../../src/logger/CloudLogging.ts"],"sourcesContent":["/**\n * A Logger that writes Effect logs as Cloud Logging structured entries, one\n * JSON line each, tied to the trace of the fiber's current span.\n *\n * `Functions` installs it for every call or event, beneath the Layer you\n * serve, so you need this module only to log the same way elsewhere, or to\n * change how a deployed Function logs.\n *\n * @stability unstable\n * @since 0.3.0\n */\nimport { Cause, Console, Formatter, Inspectable, type Layer, Logger, References } from \"effect\";\nimport type * as LogLevel from \"effect/LogLevel\";\n\nconst severities: Record<LogLevel.LogLevel, string> = {\n All: \"DEFAULT\",\n Trace: \"DEBUG\",\n Debug: \"DEBUG\",\n Info: \"INFO\",\n Warn: \"WARNING\",\n Error: \"ERROR\",\n Fatal: \"CRITICAL\",\n None: \"DEFAULT\",\n};\n\n/**\n * The Cloud Logging logger: writes each entry as one JSON line, the format\n * Firebase's own `logger` writes, to `console.error` for Error and Fatal and\n * to `console.log` for everything else.\n *\n * **When to use**\n *\n * Use with `Logger.layer` to combine it with other loggers. To replace\n * Effect's default logger with it, use {@link layer}.\n *\n * **Details**\n *\n * Each entry is a JSON object with these fields:\n *\n * - `severity`: Trace and Debug are `DEBUG`, Info is `INFO`, Warn is\n * `WARNING`, Error is `ERROR` and Fatal is `CRITICAL`.\n * - `message`: the first value logged. A non-empty Cause is appended as\n * `Cause.pretty` output, with its stacks, so Error Reporting picks up\n * defects.\n * - `args`: the other values logged, if any.\n * - `spans`: the log spans, as `{ label: milliseconds }`, if any.\n * - `logging.googleapis.com/trace`, `logging.googleapis.com/spanId` and\n * `logging.googleapis.com/trace_sampled`: the trace of the fiber's current\n * span, with the project from `GCLOUD_PROJECT`, else `GOOGLE_CLOUD_PROJECT`.\n * Left out when there is no span, and when neither variable is set, since a\n * span id means nothing to Cloud Logging without its trace. Cloud Functions\n * sets `GCLOUD_PROJECT`.\n * - Every log annotation, as a field of its own. `severity`, `message` and the\n * `logging.googleapis.com/` fields win over an annotation of the same name.\n *\n * The trace comes from the fiber, never from Firebase's `traceContext`, so\n * concurrent requests on one instance each log under their own trace. Lines go\n * to the `Console` service, which is the global `console` unless you provide\n * another. Entries carry no uid: add one with `Effect.annotateLogs`.\n *\n * @example Logging under a uid\n * ```ts\n * import { Effect } from \"effect\";\n * import { CloudLogging } from \"@visiolab/effect-firebase-functions/logger\";\n *\n * const program = Effect.log(\"Order placed\").pipe(\n * Effect.annotateLogs({ uid: \"user-1\" }),\n * Effect.provide(CloudLogging.layer),\n * );\n * ```\n *\n * @see {@link layer} for replacing Effect's default logger with it\n *\n * @stability unstable\n * @category loggers\n * @since 0.3.0\n */\nexport const logger: Logger.Logger<unknown, void> = Logger.make(\n ({ cause, date, fiber, logLevel, message }) => {\n const values = Array.isArray(message) ? message : [message];\n const [first, ...args] = values;\n let text =\n values.length === 0\n ? \"\"\n : typeof first === \"string\"\n ? first\n : Inspectable.toStringUnknown(first);\n if (cause.reasons.length > 0) {\n const pretty = Cause.pretty(cause);\n text = text === \"\" ? pretty : `${text}\\n${pretty}`;\n }\n\n const entry: Record<string, unknown> = { ...fiber.getRef(References.CurrentLogAnnotations) };\n if (args.length > 0) entry[\"args\"] = args;\n const logSpans = fiber.getRef(References.CurrentLogSpans);\n if (logSpans.length > 0) {\n const now = date.getTime();\n entry[\"spans\"] = Object.fromEntries(logSpans.map(([label, start]) => [label, now - start]));\n }\n for (const key of Object.keys(entry)) {\n if (key.startsWith(\"logging.googleapis.com/\")) delete entry[key];\n }\n entry[\"severity\"] = severities[logLevel];\n entry[\"message\"] = text;\n const span = fiber.cache.span;\n const project = process.env[\"GCLOUD_PROJECT\"] ?? process.env[\"GOOGLE_CLOUD_PROJECT\"];\n if (span !== undefined && project !== undefined && project !== \"\") {\n entry[\"logging.googleapis.com/trace\"] = `projects/${project}/traces/${span.traceId}`;\n entry[\"logging.googleapis.com/spanId\"] = span.spanId;\n entry[\"logging.googleapis.com/trace_sampled\"] = span.sampled;\n }\n\n const console = fiber.getRef(Console.Console);\n const line = Formatter.formatJson(entry);\n if (logLevel === \"Error\" || logLevel === \"Fatal\") console.error(line);\n else console.log(line);\n },\n);\n\n/**\n * Replaces the current loggers with the Cloud Logging {@link logger} and\n * Effect's `Logger.tracerLogger`, which records logs as span events.\n *\n * **When to use**\n *\n * Use to log as Cloud Logging entries outside a Function deployed with\n * `Functions`, in any Effect program that runs on Google Cloud. Deployed\n * Functions already log this way.\n *\n * **Details**\n *\n * Provided in the Layer of a deployed Function, it changes nothing. To log\n * differently there, provide your own `Logger.layer`, which wins over this one.\n *\n * @example Logging from a scheduled job\n * ```ts\n * import { Effect } from \"effect\";\n * import { CloudLogging } from \"@visiolab/effect-firebase-functions/logger\";\n *\n * const job = Effect.log(\"Cleaning up\").pipe(\n * Effect.withSpan(\"cleanup\"),\n * Effect.provide(CloudLogging.layer),\n * );\n * ```\n *\n * @see {@link logger} for the logger on its own\n *\n * @stability unstable\n * @category layers\n * @since 0.3.0\n */\nexport const layer: Layer.Layer<never> = Logger.layer([logger, Logger.tracerLogger]);\n"],"mappings":";;;;;;;;;;;;;;;;;;AAcA,MAAM,aAAgD;CACpD,KAAK;CACL,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACP,OAAO;CACP,MAAM;AACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAa,SAAuC,OAAO,MACxD,EAAE,OAAO,MAAM,OAAO,UAAU,cAAc;CAC7C,MAAM,SAAS,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;CAC1D,MAAM,CAAC,OAAO,GAAG,QAAQ;CACzB,IAAI,OACF,OAAO,WAAW,IACd,KACA,OAAO,UAAU,WACf,QACA,YAAY,gBAAgB,KAAK;CACzC,IAAI,MAAM,QAAQ,SAAS,GAAG;EAC5B,MAAM,SAAS,MAAM,OAAO,KAAK;EACjC,OAAO,SAAS,KAAK,SAAS,GAAG,KAAK,IAAI;CAC5C;CAEA,MAAM,QAAiC,EAAE,GAAG,MAAM,OAAO,WAAW,qBAAqB,EAAE;CAC3F,IAAI,KAAK,SAAS,GAAG,MAAM,UAAU;CACrC,MAAM,WAAW,MAAM,OAAO,WAAW,eAAe;CACxD,IAAI,SAAS,SAAS,GAAG;EACvB,MAAM,MAAM,KAAK,QAAQ;EACzB,MAAM,WAAW,OAAO,YAAY,SAAS,KAAK,CAAC,OAAO,WAAW,CAAC,OAAO,MAAM,KAAK,CAAC,CAAC;CAC5F;CACA,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GACjC,IAAI,IAAI,WAAW,yBAAyB,GAAG,OAAO,MAAM;CAE9D,MAAM,cAAc,WAAW;CAC/B,MAAM,aAAa;CACnB,MAAM,OAAO,MAAM,MAAM;CACzB,MAAM,UAAU,QAAQ,IAAI,qBAAqB,QAAQ,IAAI;CAC7D,IAAI,SAAS,KAAA,KAAa,YAAY,KAAA,KAAa,YAAY,IAAI;EACjE,MAAM,kCAAkC,YAAY,QAAQ,UAAU,KAAK;EAC3E,MAAM,mCAAmC,KAAK;EAC9C,MAAM,0CAA0C,KAAK;CACvD;CAEA,MAAM,UAAU,MAAM,OAAO,QAAQ,OAAO;CAC5C,MAAM,OAAO,UAAU,WAAW,KAAK;CACvC,IAAI,aAAa,WAAW,aAAa,SAAS,QAAQ,MAAM,IAAI;MAC/D,QAAQ,IAAI,IAAI;AACvB,CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,QAA4B,OAAO,MAAM,CAAC,QAAQ,OAAO,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CloudLogging_exports as CloudLogging } from "./CloudLogging.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visiolab/effect-firebase-functions",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Cloud Functions for Firebase deployment of the @visiolab/effect-firebase Functions.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"effect",
|
|
7
|
+
"firebase",
|
|
8
|
+
"firebase-functions",
|
|
9
|
+
"typescript"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/VisioLab/effect-firebase",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/VisioLab/effect-firebase/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/VisioLab/effect-firebase.git",
|
|
19
|
+
"directory": "packages/functions"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src/**/*.ts",
|
|
23
|
+
"dist/**/*.mjs",
|
|
24
|
+
"dist/**/*.mjs.map",
|
|
25
|
+
"dist/**/*.d.mts",
|
|
26
|
+
"dist/**/*.d.mts.map"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": [],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./dist/index.mjs",
|
|
32
|
+
"./Functions": "./dist/Functions.mjs",
|
|
33
|
+
"./logger": "./dist/logger/index.mjs",
|
|
34
|
+
"./logger/CloudLogging": "./dist/logger/CloudLogging.mjs",
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@effect/vitest": "^4.1.0",
|
|
42
|
+
"effect": "4.1.0",
|
|
43
|
+
"firebase": "^13.0.0",
|
|
44
|
+
"firebase-admin": "^14.5.0",
|
|
45
|
+
"firebase-functions": "^7.4.0",
|
|
46
|
+
"typescript": "^7.0.2",
|
|
47
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
|
|
48
|
+
"vite-plus": "1.4.0",
|
|
49
|
+
"vitest": "5.0.5",
|
|
50
|
+
"@visiolab/effect-firebase": "^0.3.0",
|
|
51
|
+
"@visiolab/effect-firebase-admin": "^0.3.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"effect": "~4.1.0",
|
|
55
|
+
"firebase-admin": "^14.5.0",
|
|
56
|
+
"firebase-functions": "^7.4.0",
|
|
57
|
+
"@visiolab/effect-firebase": "^0.3.0",
|
|
58
|
+
"@visiolab/effect-firebase-admin": "^0.3.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "vp pack",
|
|
62
|
+
"build-fixture": "vp -C tests/fixture pack",
|
|
63
|
+
"dev": "vp pack --watch",
|
|
64
|
+
"test": "vp test",
|
|
65
|
+
"check": "vp check"
|
|
66
|
+
}
|
|
6
67
|
}
|