h1v3 0.6.4 → 0.7.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h1v3",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -3,7 +3,7 @@ import { all, assertNewDataDoesNotHave, assertNewDataHasString, any, assertNewDa
3
3
 
4
4
  export const RAW_STORE_META = Symbol("Raw database metadata");
5
5
 
6
- export function configureConfigStore(paths, _onValueWritten, _logger) {
6
+ export function configureConfigStore(paths, _onValueWritten, _observe) {
7
7
 
8
8
  if (!paths?.config?.path) throw new Error("Missing: paths.config.path");
9
9
  return {
@@ -17,10 +17,12 @@ export function configureConfigStore(paths, _onValueWritten, _logger) {
17
17
 
18
18
  }
19
19
 
20
+ const googleLogLevels = ["DEBUG", "INFO", "NOTICE", "WARNING", "ERROR", "CRITICAL", "ALERT", "EMERGENCY"];
21
+
20
22
  function decideSeverity(newData) {
21
23
 
22
- const text = newData.severity_text?.toLowerCase();
23
- return ["trace", "fatal", "info", "debug", "error", "warn"].includes(text) ? text : "info";
24
+ const text = newData.severity_text?.toUpperCase();
25
+ return googleLogLevels.includes(text) ? text : "info";
24
26
 
25
27
  }
26
28
 
@@ -30,7 +32,7 @@ function decideMessage(newData) {
30
32
 
31
33
  }
32
34
 
33
- function handleLogValueWritten(logger, e) {
35
+ function handleO11yEventWritten(observe, e) {
34
36
 
35
37
  const { after } = e.data;
36
38
  const newData = after.val();
@@ -39,7 +41,7 @@ function handleLogValueWritten(logger, e) {
39
41
  const now = new Date();
40
42
  const time_server_ms = now.valueOf();
41
43
  const timestamp = now.toISOString();
42
- const payload = {
44
+ observe({
43
45
  "logging.googleapis.com/trace": `projects/${process.env.GCLOUD_PROJECT}/traces/${newData.trace_id}`,
44
46
  "logging.googleapis.com/spanId": newData.span_id,
45
47
  "message": decideMessage(newData),
@@ -49,31 +51,30 @@ function handleLogValueWritten(logger, e) {
49
51
  time_server_ms
50
52
  },
51
53
  timestamp
52
- };
54
+ });
53
55
  after.ref.update({ time_server_ms });
54
- logger.write(payload);
55
56
 
56
57
  }
57
58
 
58
59
  }
59
60
 
60
- export function configureLoggingStore({ paths, onValueWritten, logger, region, instance }) {
61
+ export function configureLoggingStore({ paths, onValueWritten, observe, region, instance }) {
61
62
 
62
63
  if (!paths) throw new Error("Missing paths");
63
64
  if (!onValueWritten) throw new Error("Missing onValueWritten");
64
- if (!logger) throw new Error("Missing logger");
65
+ if (!observe) throw new Error("Missing observe");
65
66
  if (!region) throw new Error("Missing region");
66
67
  if (!instance) throw new Error("Missing instance");
67
68
 
68
- const ref = paths?.logs?.path;
69
- if (!ref) throw new Error("Missing: paths.logs.path");
69
+ const ref = paths?.o11y?.path;
70
+ if (!ref) throw new Error("Missing: paths.o11y.path");
70
71
  const triggerPath = refPathToTriggerPath(ref);
71
72
  return Object.assign(
72
73
  onValueWritten({
73
74
  ref: triggerPath,
74
75
  region,
75
76
  instance
76
- }, handleLogValueWritten.bind(this, logger)),
77
+ }, handleO11yEventWritten.bind(this, observe)),
77
78
  {
78
79
  [RAW_STORE_META]: {
79
80
  raw: true,
@@ -82,7 +83,7 @@ export function configureLoggingStore({ paths, onValueWritten, logger, region, i
82
83
  write: all(
83
84
  // required
84
85
  assertNewDataHasString("body", 500, 1),
85
- assertNewDataHasOneOf("severity_text", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"),
86
+ assertNewDataHasOneOf("severity_text", ...googleLogLevels),
86
87
  // trace context
87
88
  all(
88
89
  assertNewDataHasLowercaseHexCharacters("trace_id", 32),
@@ -4,10 +4,10 @@ import { refPathToTriggerPath } from "../paths.js";
4
4
 
5
5
  export const EVENT_STORE_META = Symbol("Event store configuration metadata");
6
6
 
7
- export function configureEventStore({ ref, projections, ...rest }, { onValueWritten, logger, region, instance }) {
7
+ export function configureEventStore({ ref, projections, ...rest }, { onValueWritten, observe, region, instance }) {
8
8
 
9
9
  if (!onValueWritten) throw new Error("Missing onValueWritten");
10
- if (!logger) throw new Error("Missing logger");
10
+ if (!observe) throw new Error("Missing observe");
11
11
  if (!region) throw new Error("Missing region");
12
12
  if (!instance) throw new Error("Missing instance");
13
13
 
@@ -18,7 +18,7 @@ export function configureEventStore({ ref, projections, ...rest }, { onValueWrit
18
18
  const writeRefPath = `${eventStorePath}/${EVENTS}/{eid}`;
19
19
 
20
20
  // handle an incoming write event
21
- const handler = e => updateProjections(e.data.after, projections, logger, rest.debug);
21
+ const handler = e => updateProjections(e.data.after, projections, observe, rest.debug);
22
22
 
23
23
  const metadata = {
24
24
  ...rest,
@@ -35,10 +35,10 @@ export function configureEventStore({ ref, projections, ...rest }, { onValueWrit
35
35
 
36
36
  }
37
37
 
38
- configureEventStore.inject = ({ onValueWritten, logger, paths, region, instance }) =>
38
+ configureEventStore.inject = ({ onValueWritten, observe, paths, region, instance }) =>
39
39
 
40
40
  (eventStoreFactory, ...args) =>
41
41
 
42
- configureEventStore(eventStoreFactory(paths, ...args), { onValueWritten, logger, region, instance });
42
+ configureEventStore(eventStoreFactory(paths, ...args), { onValueWritten, observe, region, instance });
43
43
 
44
44
 
@@ -1,7 +1,7 @@
1
1
  import { PROJECTIONS } from "../../dist/browser/client/event-store.js";
2
2
  import { inspect } from "node:util";
3
3
 
4
- export async function updateProjections(incomingEventSnap, projectionTransformationPerEvent, logger, debug = false) {
4
+ export async function updateProjections(incomingEventSnap, projectionTransformationPerEvent, observe, debug = false) {
5
5
 
6
6
  // order the events
7
7
  const eventsSnap = await incomingEventSnap.ref.parent.get();
@@ -35,7 +35,11 @@ export async function updateProjections(incomingEventSnap, projectionTransformat
35
35
 
36
36
  return (agg, e) => {
37
37
 
38
- logger.warn("Missing transform for event", { projection, event: e });
38
+ observe({
39
+ severity: "WARNING",
40
+ message: "Missing transform for event",
41
+ logData: { projection, event: e }
42
+ });
39
43
  return agg;
40
44
 
41
45
  };
@@ -46,9 +50,13 @@ export async function updateProjections(incomingEventSnap, projectionTransformat
46
50
 
47
51
  const projectionRef = incomingEventSnap.ref.parent.parent.child(PROJECTIONS).child(projection);
48
52
  const incomingEvent = incomingEventSnap.val();
49
- logger.info("Writing projection after event", {
50
- path: projectionRef,
51
- incomingEvent
53
+ observe({
54
+ severity: "INFO",
55
+ message: "Writing projection after event",
56
+ logData: {
57
+ path: projectionRef,
58
+ incomingEvent
59
+ }
52
60
  });
53
61
  try {
54
62
 
@@ -58,7 +66,11 @@ export async function updateProjections(incomingEventSnap, projectionTransformat
58
66
 
59
67
  const data = inspect(view).replace(/\n */g, " ");
60
68
  const path = projectionRef.toString();
61
- logger.error(`Error writing projection: ${err?.message}`, { projection, path, data });
69
+ observe({
70
+ severity: "ERROR",
71
+ message: `Error writing projection: ${err?.message}`,
72
+ logData: { projection, path, data }
73
+ });
62
74
 
63
75
  }
64
76
 
package/src/index.js CHANGED
@@ -31,4 +31,6 @@ export function explodePathVars(expr) {
31
31
  if (typeof expr !== "string") return expr;
32
32
  return expr.replaceAll(/\/(\$[^/]*)/g, "/' + $1 + '");
33
33
 
34
- }
34
+ }
35
+
36
+ export * as o11y from "./observability/index.js";
@@ -0,0 +1,123 @@
1
+ export async function provider({ trace, projectId, serviceName, log }) {
2
+
3
+ if (!serviceName) throw new Error("Missing serviceName");
4
+ if (!trace) throw new Error("Missing trace");
5
+ if (!projectId) throw new Error("Missing projectId");
6
+ if (!log) throw new Error("Missing log");
7
+
8
+ return async ({
9
+ message, severity, // required
10
+ traceId, spanId, timestamp, // advised
11
+ logData, // optional log line
12
+ spanStart, spanEnd, spanAttributes // optional trace attributes
13
+ }) => {
14
+
15
+ if (!message) throw new Error("Missing message");
16
+ if (!severity) throw new Error("Missing severity");
17
+
18
+ traceId = traceId || console.warn("No trace id provided - one will be generated") || generateLowerHex(16);
19
+ spanId = spanId || console.warn("No span id provided - one will be generated") || generateLowerHex(8);
20
+ timestamp = timestamp || console.warn("No timestamp provided - one will be generated") || new Date().toISOString();
21
+ if (!spanStart) {
22
+
23
+ spanStart = spanEnd = new Date(timestamp);
24
+
25
+ }
26
+
27
+ const tracePath = `projects/${projectId}/traces/${traceId}`;
28
+ const projectTraceSpanName = `${tracePath}/spans/${spanId}`;
29
+
30
+ const startTime = asISOString(spanStart);
31
+ const endTime = asISOString(spanEnd);
32
+
33
+ await createSpan({ trace }, { serviceName, projectTraceSpanName, message, spanId, startTime, endTime, spanAttributes });
34
+ await writeLogLine({ log, projectId }, { tracePath, spanId, severity, timestamp, message, logData });
35
+
36
+ };
37
+
38
+ }
39
+
40
+ function generateLowerHex(byteLength) {
41
+
42
+ const bytes = crypto.getRandomValues(new Uint8Array(byteLength));
43
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
44
+
45
+ }
46
+
47
+ export function toTraceAttributes(obj) {
48
+
49
+ const attributeMap = {};
50
+ for (const [key, value] of Object.entries(obj)) {
51
+ if (value === null || value === undefined) continue;
52
+
53
+ switch (typeof value) {
54
+ case 'string':
55
+ attributeMap[key] = { stringValue: { value } };
56
+ break;
57
+ case 'number':
58
+ // Cloud Trace expects integers as strings
59
+ attributeMap[key] = { intValue: String(Math.trunc(value)) };
60
+ break;
61
+ case 'boolean':
62
+ attributeMap[key] = { boolValue: value };
63
+ break;
64
+ default:
65
+ // fallback to JSON string
66
+ attributeMap[key] = { stringValue: { value: JSON.stringify(value) } };
67
+ break;
68
+ }
69
+ }
70
+ return { attributeMap };
71
+
72
+ }
73
+
74
+ async function writeLogLine({ log, projectId }, { message, tracePath, spanId, severity, timestamp, logData }) {
75
+
76
+ const logMeta = {
77
+ resource: { type: "global", labels: { "project_id": projectId } },
78
+ trace: tracePath,
79
+ spanId,
80
+ severity,
81
+ traceSampled: "true",
82
+ timestamp
83
+ };
84
+ console.log("Log meta", logMeta);
85
+
86
+ const entry = log.entry(
87
+ logMeta,
88
+ {
89
+ message,
90
+ ...logData
91
+ }
92
+ );
93
+ await log.write(entry);
94
+
95
+ }
96
+
97
+ async function createSpan({ trace }, { serviceName, projectTraceSpanName, spanId, startTime, endTime, spanAttributes, message }) {
98
+
99
+ const attributes = toTraceAttributes({
100
+ "service.name": serviceName,
101
+ message,
102
+ ...spanAttributes
103
+ });
104
+ await trace.projects.traces.spans.createSpan({
105
+ name: projectTraceSpanName,
106
+ requestBody: {
107
+ attributes,
108
+ name: projectTraceSpanName,
109
+ spanId,
110
+ displayName: { value: serviceName },
111
+ startTime,
112
+ endTime
113
+ }
114
+ });
115
+
116
+ }
117
+
118
+ function asISOString(x) {
119
+
120
+ if (typeof x === "string") return x;
121
+ if (typeof x?.toISOString !== "function") throw new Error("Missing toISOString function");
122
+ return x.toISOString();
123
+ }