File size: 5,196 Bytes
aa89cb5
 
 
 
 
 
 
0ccd578
 
aa89cb5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
export type RuntimeOperationFamily =
  | 'query'
  | 'mutation'
  | 'relation_load'
  | 'provider'
  | 'cache'
  | 'tfp'
  | 'audit'
  | 'object_storage';

export type RuntimeAttributeValue = string | number | boolean;

export type RuntimeErrorCategory =
  | 'validation' | 'authorization' | 'conflict' | 'timeout'
  | 'transport' | 'provider' | 'internal';

/** Stable classification derived from a native error type, never its message. */
export function runtimeErrorCategory(error: unknown): RuntimeErrorCategory {
  const type = error instanceof Error && error.constructor?.name
    ? error.constructor.name.toLowerCase()
    : typeof error;
  if (/timeout|deadline/.test(type)) return 'timeout';
  if (/authentication|authorization|unauthorized|forbidden|permission/.test(type)) return 'authorization';
  if (/validation|invalidargument|valueerror|parse|format/.test(type)) return 'validation';
  if (/conflict|optimistic|version|duplicate|alreadyexists/.test(type)) return 'conflict';
  if (/transport|network|connection|socket|http|ioerror/.test(type)) return 'transport';
  if (/provider|sql|database|jdbc/.test(type)) return 'provider';
  return 'internal';
}

export interface RuntimeOperation {
  readonly family: RuntimeOperationFamily;
  readonly name: string;
  readonly attributes?: Readonly<Record<string, RuntimeAttributeValue>>;
}

export interface RuntimeOperationCompletion {
  readonly attributes?: Readonly<Record<string, RuntimeAttributeValue>>;
}

export interface RuntimeExecutionCorrelation {
  readonly traceId: string;
  readonly spanId: string;
}

/** Only runtime-projected, allow-listed diagnostics may enter this event. */
export interface RuntimeDiagnosticEvent {
  readonly body: 'TeaQL SQL execution';
  readonly correlation?: RuntimeExecutionCorrelation;
  readonly attributes: Readonly<Record<string, RuntimeAttributeValue>>;
}

export interface RuntimeTelemetryScope {
  success(completion?: RuntimeOperationCompletion): void;
  failure(error: unknown): void;
  run?<T>(work: () => Promise<T>): Promise<T>;
  correlation?(): RuntimeExecutionCorrelation | undefined;
}

/** Provider-neutral TeaQL lifecycle contract. Implementations must be fail open. */
export interface RuntimeTelemetry {
  recordsSQLDiagnostics?(): boolean;
  start(operation: RuntimeOperation): RuntimeTelemetryScope;
  inject?(carrier: Record<string, string>): void;
  flush?(): void | Promise<void>;
  shutdown?(): void | Promise<void>;
  recordDiagnostic?(event: RuntimeDiagnosticEvent): void;
}

/** Inject the active runtime trace into transport metadata without affecting business work. */
export function injectRuntimeContext(
  telemetry: RuntimeTelemetry | undefined,
  carrier: Record<string, string>,
): Record<string, string> {
  try { telemetry?.inject?.(carrier); } catch { /* telemetry is fail open */ }
  return carrier;
}

const noopScope: RuntimeTelemetryScope = Object.freeze({
  success: () => undefined,
  failure: () => undefined,
});

export const NOOP_RUNTIME_TELEMETRY: RuntimeTelemetry = Object.freeze({
  start: () => noopScope,
});

const forbiddenAttributes = new Set([
  'teaql.entity.id',
  'teaql.user.id',
  'teaql.tenant.id',
  'teaql.query.parameters',
  'teaql.field.values',
  'teaql.audit.reason',
  'db.query.parameter_values',
  'http.request.body',
  'url.full',
]);

export function safeRuntimeOperation(operation: RuntimeOperation): RuntimeOperation {
  const attributes: Record<string, RuntimeAttributeValue> = {
    'teaql.operation.family': operation.family,
    'teaql.operation.name': operation.name,
  };
  for (const [key, value] of Object.entries(operation.attributes || {})) {
    if (!forbiddenAttributes.has(key)) attributes[key] = value;
  }
  return Object.freeze({ ...operation, attributes: Object.freeze(attributes) });
}

export function startRuntimeOperation(
  telemetry: RuntimeTelemetry | undefined,
  operation: RuntimeOperation,
): RuntimeTelemetryScope {
  if (!telemetry) return noopScope;
  try {
    const delegate = telemetry.start(safeRuntimeOperation(operation));
    let ended = false;
    return {
      correlation: () => {
        try { return delegate.correlation?.(); } catch { return undefined; }
      },
      run: delegate.run
        ? <T>(work: () => Promise<T>) => delegate.run!(work)
        : <T>(work: () => Promise<T>) => work(),
      success(completion) {
        if (ended) return;
        ended = true;
        try { delegate?.success(completion); } catch { /* telemetry is fail open */ }
      },
      failure(error) {
        if (ended) return;
        ended = true;
        try { delegate?.failure(error); } catch { /* telemetry is fail open */ }
      },
    };
  } catch {
    return noopScope;
  }
}

export async function observeRuntimeOperation<T>(
  telemetry: RuntimeTelemetry | undefined,
  operation: RuntimeOperation,
  work: () => Promise<T>,
  completion?: (result: T) => RuntimeOperationCompletion,
): Promise<T> {
  const scope = startRuntimeOperation(telemetry, operation);
  try {
    const result = await (scope.run ? scope.run(work) : work());
    scope.success(completion?.(result));
    return result;
  } catch (error) {
    scope.failure(error);
    throw error;
  }
}