@mondaydotcomorg/z2h-cli 0.25.7 → 0.25.9
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/handler-api/backend-runner/channels/channel-map.ts +6 -12
- package/handler-api/backend-runner/channels/channel-telemetry.ts +79 -0
- package/handler-api/backend-runner/channels/snowflake-channel/run-query.ts +18 -3
- package/handler-api/backend-runner/channels/snowflake-channel/snowflake.channel.ts +13 -1
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { logger
|
|
1
|
+
import { logger } from '@mondaydotcomorg/trident-backend-runtime';
|
|
2
2
|
|
|
3
|
+
import { withChannelTelemetry } from './channel-telemetry';
|
|
3
4
|
import { makeDbChannel } from './db-channel/db.channel';
|
|
4
5
|
import { makeLlmChannel } from './llm-channel/llm.channel';
|
|
5
6
|
import { makeMondayChannel } from './monday-channel/monday.channel';
|
|
@@ -56,8 +57,9 @@ const OAUTH_CHANNELS: Record<string, (caller: Caller) => ChannelMap> = {
|
|
|
56
57
|
* `monday_not_connected`, `monday_auth`, … A `4xx` means the handler (or the user's
|
|
57
58
|
* setup) is wrong and retrying won't help; `502` means the upstream failed.
|
|
58
59
|
*
|
|
59
|
-
* Each key
|
|
60
|
-
*
|
|
60
|
+
* Each key emits a unified call counter, duration distribution, and BI event tagged
|
|
61
|
+
* by app, channel, method, and outcome. Uncaught failures also reach BI at the
|
|
62
|
+
* invocation level — see `runner.service.ts`.
|
|
61
63
|
*/
|
|
62
64
|
export function buildChannels(appName: string, caller: Caller, declaredIntegrations: string[]): ChannelMap {
|
|
63
65
|
const channels: ChannelMap = {
|
|
@@ -78,13 +80,5 @@ export function buildChannels(appName: string, caller: Caller, declaredIntegrati
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
return Object.fromEntries(
|
|
82
|
-
Object.entries(channels).map(([key, fn]) => [
|
|
83
|
-
key,
|
|
84
|
-
metric.wrapFunctionWithMetrics(fn, {
|
|
85
|
-
customName: `z2h.channel.${key}`,
|
|
86
|
-
tags: { highCardinalityTags: { appName } },
|
|
87
|
-
}),
|
|
88
|
-
]),
|
|
89
|
-
);
|
|
83
|
+
return Object.fromEntries(Object.entries(channels).map(([key, fn]) => [key, withChannelTelemetry(appName, key, fn)]));
|
|
90
84
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { metric } from '@mondaydotcomorg/trident-backend-runtime';
|
|
2
|
+
import { BackendRunnerEvents } from '@mondaydotcomorg/z2h-shared-utils/observability';
|
|
3
|
+
import { trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability/trident';
|
|
4
|
+
|
|
5
|
+
import { ChannelError } from '../backend-runner.errors';
|
|
6
|
+
import type { ChannelHandler } from '../backend-runner.types';
|
|
7
|
+
import { BI_SOURCE } from '../constants';
|
|
8
|
+
|
|
9
|
+
type ChannelOutcome = 'success' | 'failure';
|
|
10
|
+
|
|
11
|
+
function parseChannelKey(key: string): { channel: string; method: string } {
|
|
12
|
+
const parts = key.split('.');
|
|
13
|
+
if (parts.length !== 2 || parts.some((part) => !part)) {
|
|
14
|
+
throw new Error(`channel key must use <channel>.<method>: ${key}`);
|
|
15
|
+
}
|
|
16
|
+
return { channel: parts[0], method: parts[1] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
20
|
+
return (
|
|
21
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
22
|
+
value !== null &&
|
|
23
|
+
typeof (value as PromiseLike<unknown>).then === 'function'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function recordChannelCall(
|
|
28
|
+
appName: string,
|
|
29
|
+
channel: string,
|
|
30
|
+
method: string,
|
|
31
|
+
outcome: ChannelOutcome,
|
|
32
|
+
startedAt: number,
|
|
33
|
+
error?: unknown,
|
|
34
|
+
): void {
|
|
35
|
+
const durationMs = performance.now() - startedAt;
|
|
36
|
+
const dimensions = { appName, channel, method };
|
|
37
|
+
|
|
38
|
+
metric.increment(BackendRunnerEvents.channel.callMetric, { ...dimensions, outcome });
|
|
39
|
+
metric.distribution(BackendRunnerEvents.channel.durationMetric, durationMs, dimensions);
|
|
40
|
+
void trackEvent(
|
|
41
|
+
BackendRunnerEvents.channel.biEvent,
|
|
42
|
+
{
|
|
43
|
+
...dimensions,
|
|
44
|
+
outcome,
|
|
45
|
+
duration_ms: durationMs,
|
|
46
|
+
...(error instanceof ChannelError ? { error_code: error.code } : {}),
|
|
47
|
+
},
|
|
48
|
+
BI_SOURCE,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function withChannelTelemetry(appName: string, key: string, handler: ChannelHandler): ChannelHandler {
|
|
53
|
+
const { channel, method } = parseChannelKey(key);
|
|
54
|
+
|
|
55
|
+
return (args) => {
|
|
56
|
+
const startedAt = performance.now();
|
|
57
|
+
try {
|
|
58
|
+
const result = handler(args);
|
|
59
|
+
if (isPromiseLike(result)) {
|
|
60
|
+
return Promise.resolve(result).then(
|
|
61
|
+
(value) => {
|
|
62
|
+
recordChannelCall(appName, channel, method, 'success', startedAt);
|
|
63
|
+
return value;
|
|
64
|
+
},
|
|
65
|
+
(error: unknown) => {
|
|
66
|
+
recordChannelCall(appName, channel, method, 'failure', startedAt, error);
|
|
67
|
+
throw error;
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
recordChannelCall(appName, channel, method, 'success', startedAt);
|
|
73
|
+
return result;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
recordChannelCall(appName, channel, method, 'failure', startedAt, error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
import { context, logger } from '@mondaydotcomorg/trident-backend-runtime';
|
|
3
|
+
import { context, logger, metric } from '@mondaydotcomorg/trident-backend-runtime';
|
|
4
4
|
import { BackendRunnerEvents } from '@mondaydotcomorg/z2h-shared-utils/observability';
|
|
5
5
|
import { trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability/trident';
|
|
6
6
|
import { getDefaultClient } from 'src/utils/elasticache';
|
|
@@ -26,6 +26,18 @@ export interface SnowflakeRow {
|
|
|
26
26
|
export interface RunQueryOptions {
|
|
27
27
|
/** Clamped to MAX_TIMEOUT_MS. Defaults to DEFAULT_TIMEOUT_MS. */
|
|
28
28
|
timeoutMs?: number;
|
|
29
|
+
/** Human-readable label for the pool identifier/logs, e.g. a widget name, instead of the default query-shape hash. Sanitized to `[a-z0-9-_]`, max 64 chars. */
|
|
30
|
+
identifier?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MAX_IDENTIFIER_LENGTH = 64;
|
|
34
|
+
|
|
35
|
+
function sanitizeIdentifier(raw: string): string {
|
|
36
|
+
return raw
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9-_]+/g, '-')
|
|
39
|
+
.replace(/^-+|-+$/g, '')
|
|
40
|
+
.slice(0, MAX_IDENTIFIER_LENGTH);
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
function hashQuery(boundSql: string, params: Record<string, SnowflakeParamValue>): string {
|
|
@@ -133,12 +145,14 @@ export async function runSnowflakeQuery(
|
|
|
133
145
|
|
|
134
146
|
// Cache key stays parameter-sensitive; the pool identifier deliberately is not.
|
|
135
147
|
const queryCacheKey = hashQuery(boundSql, params ?? {});
|
|
136
|
-
const
|
|
148
|
+
const sanitizedIdentifier = options?.identifier ? sanitizeIdentifier(options.identifier) : '';
|
|
149
|
+
const queryIdentifier = sanitizedIdentifier || hashQueryShape(sql);
|
|
137
150
|
const scope = cacheScope(useStateless);
|
|
138
151
|
const startMs = Date.now();
|
|
139
152
|
const cached = scope ? await readCache(queryCacheKey, scope) : null;
|
|
140
153
|
if (cached) {
|
|
141
154
|
const duration_ms = Date.now() - startMs;
|
|
155
|
+
metric.increment(BackendRunnerEvents.snowflake.cacheMetric, { appName, result: 'hit' });
|
|
142
156
|
void trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: true }, BI_SOURCE);
|
|
143
157
|
logger.info(
|
|
144
158
|
{
|
|
@@ -183,7 +197,7 @@ export async function runSnowflakeQuery(
|
|
|
183
197
|
);
|
|
184
198
|
} catch (err) {
|
|
185
199
|
// Failure BI tracking already happens generically at the invoke level
|
|
186
|
-
// (runner.service.ts's BackendRunnerEvents.invoke.
|
|
200
|
+
// (runner.service.ts's BackendRunnerEvents.invoke.biEvent, tagged with this
|
|
187
201
|
// ChannelError's `code`) — this stays a log only, for the queryHash/
|
|
188
202
|
// queryIdentifier that generic event can't know about.
|
|
189
203
|
const duration_ms = Date.now() - startMs;
|
|
@@ -194,6 +208,7 @@ export async function runSnowflakeQuery(
|
|
|
194
208
|
const rows = Array.isArray(data) ? data : [];
|
|
195
209
|
if (scope) {
|
|
196
210
|
await writeCache(queryCacheKey, scope, rows);
|
|
211
|
+
metric.increment(BackendRunnerEvents.snowflake.cacheMetric, { appName, result: 'miss' });
|
|
197
212
|
}
|
|
198
213
|
const duration_ms = Date.now() - startMs;
|
|
199
214
|
void trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: false }, BI_SOURCE);
|
|
@@ -33,7 +33,11 @@ export function makeSnowflakeChannel(appName: string) {
|
|
|
33
33
|
*
|
|
34
34
|
* @param sql - A SELECT statement. Non-SELECT is rejected with `400`.
|
|
35
35
|
* @param params - Optional map of `:name` → `string | number | boolean | null` bindings.
|
|
36
|
-
* @param options - Optional `{ timeoutMs?: number
|
|
36
|
+
* @param options - Optional `{ timeoutMs?: number, identifier?: string }`. `timeoutMs` is
|
|
37
|
+
* clamped to 5 min (`MAX_TIMEOUT_MS`); effective ceiling is ~5.5 min including the host
|
|
38
|
+
* backstop. `identifier` names this query in logs/metrics and the Snowflake pool identifier
|
|
39
|
+
* (e.g. a widget name) instead of the default query-shape hash — sanitized to `[a-z0-9-_]`,
|
|
40
|
+
* max 64 chars.
|
|
37
41
|
* @returns Array of row objects with **camelCase keys** — SQL aliases are normalized
|
|
38
42
|
* to camelCase regardless of their casing in the query
|
|
39
43
|
* (e.g. `AS current_user` → `rows[0].currentUser`, `AS total_count` → `rows[0].totalCount`).
|
|
@@ -63,6 +67,14 @@ export function makeSnowflakeChannel(appName: string) {
|
|
|
63
67
|
* { timeoutMs: 25_000 },
|
|
64
68
|
* );
|
|
65
69
|
*
|
|
70
|
+
* @example
|
|
71
|
+
* // Name this query in logs/metrics instead of the default hash
|
|
72
|
+
* const rows = await ctx.api.v1.snowflake.query(
|
|
73
|
+
* `SELECT COUNT(*) AS total FROM monday_items`,
|
|
74
|
+
* undefined,
|
|
75
|
+
* { identifier: 'total-items-kpi' },
|
|
76
|
+
* );
|
|
77
|
+
*
|
|
66
78
|
* @throws `400` `snowflake_invalid_argument` — `sql` was not a non-empty string.
|
|
67
79
|
* @throws `400` `snowflake_not_read_only` — the statement is not a SELECT.
|
|
68
80
|
* @throws `400` `snowflake_invalid_sql` — the statement could not be parsed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mondaydotcomorg/z2h-cli",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.9",
|
|
4
4
|
"bin": "./bin/z2h-cli.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@aws-sdk/client-s3": "^3.706.0",
|
|
54
|
-
"@mondaydotcomorg/z2h-shared-utils": "^0.4.
|
|
54
|
+
"@mondaydotcomorg/z2h-shared-utils": "^0.4.3",
|
|
55
55
|
"chokidar": "^3.6.0",
|
|
56
56
|
"commander": "^12.1.0",
|
|
57
57
|
"cors": "^2.8.6",
|