@pepys/source-telemetry 0.0.0-stage → 0.0.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.
Files changed (31) hide show
  1. package/README.md +33 -2
  2. package/package.json +30 -4
  3. package/src/consumers/products/find-claude-code-model-request.ts +41 -0
  4. package/src/consumers/products/find-codex-model-request.ts +34 -0
  5. package/src/consumers/products/find-codex-turn-cost.ts +21 -0
  6. package/src/consumers/products/find-record-number.ts +101 -0
  7. package/src/consumers/products/parse-fact-type.ts +21 -0
  8. package/src/consumers/products/types.ts +34 -0
  9. package/src/consumers/telemetry-model-request.ts +91 -0
  10. package/src/consumers/telemetry-turn-cost.ts +85 -0
  11. package/src/generated/telemetry-model-request-boundary.ts +368 -0
  12. package/src/generated/telemetry-model-request.sql +35 -0
  13. package/src/generated/telemetry-turn-cost-boundary.ts +361 -0
  14. package/src/generated/telemetry-turn-cost.sql +26 -0
  15. package/src/index.ts +25 -0
  16. package/src/log-record-family.ts +41 -0
  17. package/src/otlp/format-canonical-json.ts +24 -0
  18. package/src/otlp/parse-logs-export.ts +71 -0
  19. package/src/otlp/read-otlp-facts.ts +226 -0
  20. package/src/otlp/receive-otlp-logs.ts +130 -0
  21. package/src/otlp/to-attribute-map.ts +63 -0
  22. package/src/otlp/types.ts +36 -0
  23. package/src/products/claude-code/claude-code-product.ts +45 -0
  24. package/src/products/codex/codex-product.ts +37 -0
  25. package/src/products/define-telemetry-product.ts +7 -0
  26. package/src/products/find-attribute-text.ts +12 -0
  27. package/src/products/products.ts +8 -0
  28. package/src/products/types.ts +21 -0
  29. package/src/telemetry-model-request-consumer.ts +59 -0
  30. package/src/telemetry-plugin.ts +42 -0
  31. package/src/telemetry-turn-cost-consumer.ts +42 -0
@@ -0,0 +1,361 @@
1
+ // Generated by `pepys build`. Do not edit.
2
+
3
+ declare const idTag: unique symbol;
4
+ declare const kindTag: unique symbol;
5
+
6
+ export type EntityID = string & { readonly [idTag]: 'EntityID' };
7
+
8
+ type KindedID<Kind extends string> = EntityID & { readonly [kindTag]: Kind };
9
+
10
+ export type TelemetrySessionID = KindedID<'telemetry:session'>;
11
+
12
+ export function toTelemetrySessionID(id: string): TelemetrySessionID {
13
+ const text = 'telemetry:session:' + id;
14
+
15
+ if (!isTelemetrySessionID(text)) {
16
+ throw new Error('an ID of the kind telemetry:session is never empty');
17
+ }
18
+
19
+ return text;
20
+ }
21
+
22
+ export function parseTelemetrySessionID(text: string): TelemetrySessionID | undefined {
23
+ return isTelemetrySessionID(text) ? text : undefined;
24
+ }
25
+
26
+ function isTelemetrySessionID(text: string): text is TelemetrySessionID {
27
+ return text.length > 'telemetry:session:'.length && text.startsWith('telemetry:session:');
28
+ }
29
+
30
+ export type TelemetryTurnCostID = KindedID<'telemetry:turn_cost'>;
31
+
32
+ export function toTelemetryTurnCostID(id: string): TelemetryTurnCostID {
33
+ const text = 'telemetry:turn_cost:' + id;
34
+
35
+ if (!isTelemetryTurnCostID(text)) {
36
+ throw new Error('an ID of the kind telemetry:turn_cost is never empty');
37
+ }
38
+
39
+ return text;
40
+ }
41
+
42
+ export function parseTelemetryTurnCostID(text: string): TelemetryTurnCostID | undefined {
43
+ return isTelemetryTurnCostID(text) ? text : undefined;
44
+ }
45
+
46
+ function isTelemetryTurnCostID(text: string): text is TelemetryTurnCostID {
47
+ return text.length > 'telemetry:turn_cost:'.length && text.startsWith('telemetry:turn_cost:');
48
+ }
49
+
50
+ export interface TelemetryTurnCostModel {
51
+ input: string;
52
+ }
53
+
54
+ export type TelemetryTurnCostMessage = { kind: 'apply'; input: string } | { kind: 'reset' };
55
+
56
+ export function initTelemetryTurnCostModel(): TelemetryTurnCostModel {
57
+ return { input: '' };
58
+ }
59
+
60
+ export function updateTelemetryTurnCostModel(model: TelemetryTurnCostModel, message: TelemetryTurnCostMessage): TelemetryTurnCostModel {
61
+ return message.kind === 'reset' ? initTelemetryTurnCostModel() : { ...model, input: message.input };
62
+ }
63
+
64
+ export interface TelemetryPayload {
65
+ resource: TelemetryPayloadResource;
66
+ scope: TelemetryPayloadScope;
67
+ logRecord: TelemetryPayloadLogRecord;
68
+ origin: TelemetryPayloadOrigin;
69
+ }
70
+
71
+ export interface TelemetryPayloadResource {
72
+ attributes: TelemetryPayloadResourceAttributesItem[];
73
+ }
74
+
75
+ export interface TelemetryPayloadResourceAttributesItem {
76
+ key: string;
77
+ value: unknown;
78
+ }
79
+
80
+ export interface TelemetryPayloadScope {
81
+ name: string;
82
+ version?: string;
83
+ }
84
+
85
+ export interface TelemetryPayloadLogRecord {
86
+ attributes?: TelemetryPayloadLogRecordAttributesItem[];
87
+ }
88
+
89
+ export interface TelemetryPayloadLogRecordAttributesItem {
90
+ key: string;
91
+ value: unknown;
92
+ }
93
+
94
+ export interface TelemetryPayloadOrigin {
95
+ issuer: string;
96
+ subject: string;
97
+ }
98
+
99
+ export type TelemetryTurnCostFact =
100
+ | { id: string; type: 'telemetry.claude_code.api_request' | 'telemetry.claude_code.api_error' | 'telemetry.claude_code.api_refusal' | 'telemetry.claude_code.user_prompt' | 'telemetry.claude_code.assistant_response' | 'telemetry.claude_code.tool_result' | 'telemetry.claude_code.tool_decision' | 'telemetry.claude_code.unlisted' | 'telemetry.codex.sse_event' | 'telemetry.codex.api_request' | 'telemetry.codex.turn_cost' | 'telemetry.codex.conversation_starts' | 'telemetry.codex.user_prompt' | 'telemetry.codex.tool_decision' | 'telemetry.codex.tool_result' | 'telemetry.codex.unlisted'; entity: string; at: string; payload: TelemetryPayload };
101
+
102
+ export function parseTelemetryTurnCostFact(factJSON: string): TelemetryTurnCostFact | undefined {
103
+ const parsed: unknown = JSON.parse(factJSON);
104
+
105
+ return decodeTelemetryTurnCostFact(parsed);
106
+ }
107
+
108
+ function decodeTelemetryTurnCostFact(value: unknown): TelemetryTurnCostFact | undefined {
109
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
110
+ return undefined;
111
+ }
112
+
113
+ const id = 'id' in value ? value.id : undefined;
114
+ const type = 'type' in value ? value.type : undefined;
115
+ const entity = 'entity' in value ? value.entity : undefined;
116
+ const at = 'at' in value ? value.at : undefined;
117
+ const payload = 'payload' in value ? value.payload : undefined;
118
+
119
+ if (typeof id !== 'string' || typeof entity !== 'string' || typeof at !== 'string') {
120
+ return undefined;
121
+ }
122
+
123
+ if (type === 'telemetry.claude_code.api_request' || type === 'telemetry.claude_code.api_error' || type === 'telemetry.claude_code.api_refusal' || type === 'telemetry.claude_code.user_prompt' || type === 'telemetry.claude_code.assistant_response' || type === 'telemetry.claude_code.tool_result' || type === 'telemetry.claude_code.tool_decision' || type === 'telemetry.claude_code.unlisted' || type === 'telemetry.codex.sse_event' || type === 'telemetry.codex.api_request' || type === 'telemetry.codex.turn_cost' || type === 'telemetry.codex.conversation_starts' || type === 'telemetry.codex.user_prompt' || type === 'telemetry.codex.tool_decision' || type === 'telemetry.codex.tool_result' || type === 'telemetry.codex.unlisted') {
124
+ const decoded = decodeTelemetryPayload(payload);
125
+
126
+ if (decoded === undefined) {
127
+ return undefined;
128
+ }
129
+
130
+ return { id, type, entity, at, payload: decoded };
131
+ }
132
+
133
+ return undefined;
134
+ }
135
+
136
+ function decodeTelemetryPayload(value: unknown): TelemetryPayload | undefined {
137
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
138
+ return undefined;
139
+ }
140
+
141
+ const raw0 = 'resource' in value ? value.resource : undefined;
142
+ const field0 = decodeTelemetryPayloadResource(raw0);
143
+
144
+ if (field0 === undefined) {
145
+ return undefined;
146
+ }
147
+
148
+ const raw1 = 'scope' in value ? value.scope : undefined;
149
+ const field1 = decodeTelemetryPayloadScope(raw1);
150
+
151
+ if (field1 === undefined) {
152
+ return undefined;
153
+ }
154
+
155
+ const raw2 = 'logRecord' in value ? value.logRecord : undefined;
156
+ const field2 = decodeTelemetryPayloadLogRecord(raw2);
157
+
158
+ if (field2 === undefined) {
159
+ return undefined;
160
+ }
161
+
162
+ const raw3 = 'origin' in value ? value.origin : undefined;
163
+ const field3 = decodeTelemetryPayloadOrigin(raw3);
164
+
165
+ if (field3 === undefined) {
166
+ return undefined;
167
+ }
168
+
169
+ return {
170
+ resource: field0,
171
+ scope: field1,
172
+ logRecord: field2,
173
+ origin: field3,
174
+ };
175
+ }
176
+
177
+ function decodeTelemetryPayloadResource(value: unknown): TelemetryPayloadResource | undefined {
178
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
179
+ return undefined;
180
+ }
181
+
182
+ const raw0 = 'attributes' in value ? value.attributes : undefined;
183
+ const field0 = decodeTelemetryPayloadResourceAttributes(raw0);
184
+
185
+ if (field0 === undefined) {
186
+ return undefined;
187
+ }
188
+
189
+ return {
190
+ attributes: field0,
191
+ };
192
+ }
193
+
194
+ function decodeTelemetryPayloadResourceAttributes(value: unknown): TelemetryPayloadResourceAttributesItem[] | undefined {
195
+ if (!Array.isArray(value)) {
196
+ return undefined;
197
+ }
198
+
199
+ const items: TelemetryPayloadResourceAttributesItem[] = [];
200
+
201
+ for (let index = 0; index < value.length; index += 1) {
202
+ const element: unknown = value[index];
203
+ const item = decodeTelemetryPayloadResourceAttributesItem(element);
204
+
205
+ if (item === undefined) {
206
+ return undefined;
207
+ }
208
+
209
+ items.push(item);
210
+ }
211
+
212
+ return items;
213
+ }
214
+
215
+ function decodeTelemetryPayloadResourceAttributesItem(value: unknown): TelemetryPayloadResourceAttributesItem | undefined {
216
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
217
+ return undefined;
218
+ }
219
+
220
+ const raw0 = 'key' in value ? value.key : undefined;
221
+ const field0 = typeof raw0 === 'string' ? raw0 : undefined;
222
+
223
+ if (field0 === undefined) {
224
+ return undefined;
225
+ }
226
+
227
+ const field1 = 'value' in value ? value.value : undefined;
228
+
229
+ return {
230
+ key: field0,
231
+ value: field1,
232
+ };
233
+ }
234
+
235
+ function decodeTelemetryPayloadScope(value: unknown): TelemetryPayloadScope | undefined {
236
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
237
+ return undefined;
238
+ }
239
+
240
+ const raw0 = 'name' in value ? value.name : undefined;
241
+ const field0 = typeof raw0 === 'string' ? raw0 : undefined;
242
+
243
+ if (field0 === undefined) {
244
+ return undefined;
245
+ }
246
+
247
+ const raw1 = 'version' in value ? value.version : undefined;
248
+ const field1 = raw1 === undefined ? undefined : typeof raw1 === 'string' ? raw1 : undefined;
249
+
250
+ if (raw1 !== undefined && field1 === undefined) {
251
+ return undefined;
252
+ }
253
+
254
+ return {
255
+ name: field0,
256
+ ...(field1 === undefined ? {} : { version: field1 }),
257
+ };
258
+ }
259
+
260
+ function decodeTelemetryPayloadLogRecord(value: unknown): TelemetryPayloadLogRecord | undefined {
261
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
262
+ return undefined;
263
+ }
264
+
265
+ const raw0 = 'attributes' in value ? value.attributes : undefined;
266
+ const field0 = raw0 === undefined ? undefined : decodeTelemetryPayloadLogRecordAttributes(raw0);
267
+
268
+ if (raw0 !== undefined && field0 === undefined) {
269
+ return undefined;
270
+ }
271
+
272
+ return {
273
+ ...(field0 === undefined ? {} : { attributes: field0 }),
274
+ };
275
+ }
276
+
277
+ function decodeTelemetryPayloadLogRecordAttributes(value: unknown): TelemetryPayloadLogRecordAttributesItem[] | undefined {
278
+ if (!Array.isArray(value)) {
279
+ return undefined;
280
+ }
281
+
282
+ const items: TelemetryPayloadLogRecordAttributesItem[] = [];
283
+
284
+ for (let index = 0; index < value.length; index += 1) {
285
+ const element: unknown = value[index];
286
+ const item = decodeTelemetryPayloadLogRecordAttributesItem(element);
287
+
288
+ if (item === undefined) {
289
+ return undefined;
290
+ }
291
+
292
+ items.push(item);
293
+ }
294
+
295
+ return items;
296
+ }
297
+
298
+ function decodeTelemetryPayloadLogRecordAttributesItem(value: unknown): TelemetryPayloadLogRecordAttributesItem | undefined {
299
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
300
+ return undefined;
301
+ }
302
+
303
+ const raw0 = 'key' in value ? value.key : undefined;
304
+ const field0 = typeof raw0 === 'string' ? raw0 : undefined;
305
+
306
+ if (field0 === undefined) {
307
+ return undefined;
308
+ }
309
+
310
+ const field1 = 'value' in value ? value.value : undefined;
311
+
312
+ return {
313
+ key: field0,
314
+ value: field1,
315
+ };
316
+ }
317
+
318
+ function decodeTelemetryPayloadOrigin(value: unknown): TelemetryPayloadOrigin | undefined {
319
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
320
+ return undefined;
321
+ }
322
+
323
+ const raw0 = 'issuer' in value ? value.issuer : undefined;
324
+ const field0 = typeof raw0 === 'string' ? raw0 : undefined;
325
+
326
+ if (field0 === undefined) {
327
+ return undefined;
328
+ }
329
+
330
+ const raw1 = 'subject' in value ? value.subject : undefined;
331
+ const field1 = typeof raw1 === 'string' ? raw1 : undefined;
332
+
333
+ if (field1 === undefined) {
334
+ return undefined;
335
+ }
336
+
337
+ return {
338
+ issuer: field0,
339
+ subject: field1,
340
+ };
341
+ }
342
+
343
+ export interface TelemetryTurnCostRow {
344
+ id: TelemetryTurnCostID;
345
+ session_id: EntityID;
346
+ agent_session: string | null;
347
+ product: string;
348
+ model: string | null;
349
+ cost_usd: number;
350
+ origin_issuer: string;
351
+ origin_subject: string;
352
+ at: string;
353
+ }
354
+
355
+ export interface TelemetryTurnCostKey {
356
+ id: TelemetryTurnCostID;
357
+ }
358
+
359
+ export type TelemetryTurnCostOperation =
360
+ | { upsert: { telemetry_turn_cost: TelemetryTurnCostRow } }
361
+ | { delete: { telemetry_turn_cost: TelemetryTurnCostKey } };
@@ -0,0 +1,26 @@
1
+ -- Generated by `pepys build`. Do not edit.
2
+
3
+ CREATE TABLE "telemetry_turn_cost" (
4
+ "id" TEXT NOT NULL,
5
+ "session_id" TEXT NOT NULL,
6
+ "agent_session" TEXT,
7
+ "product" TEXT NOT NULL,
8
+ "model" TEXT,
9
+ "cost_usd" REAL NOT NULL,
10
+ "origin_issuer" TEXT NOT NULL,
11
+ "origin_subject" TEXT NOT NULL,
12
+ "at" TEXT NOT NULL,
13
+ PRIMARY KEY ("id")
14
+ );
15
+
16
+ CREATE INDEX "telemetry_turn_cost_session_id_idx" ON "telemetry_turn_cost" ("session_id");
17
+
18
+ CREATE VIEW "entity" AS
19
+ SELECT
20
+ "id" AS "id",
21
+ 'telemetry:turn_cost' AS "kind",
22
+ "product" AS "title",
23
+ NULL AS "url",
24
+ NULL AS "actor",
25
+ "at" AS "updated_at"
26
+ FROM "telemetry_turn_cost";
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export {
2
+ buildLogRecordTypes,
3
+ logRecordFamily,
4
+ logRecordPayloadSchema,
5
+ } from './log-record-family.ts';
6
+
7
+ export type { LogRecordPayload } from './log-record-family.ts';
8
+ export { readOtlpFacts } from './otlp/read-otlp-facts.ts';
9
+
10
+ export type {
11
+ DroppedRecord,
12
+ OtlpFacts,
13
+ OtlpReadOptions,
14
+ TelemetryFact,
15
+ } from './otlp/read-otlp-facts.ts';
16
+
17
+ export { receiveOtlpLogs } from './otlp/receive-otlp-logs.ts';
18
+ export type * from './otlp/types.ts';
19
+ export { defineTelemetryProduct } from './products/define-telemetry-product.ts';
20
+ export { products } from './products/products.ts';
21
+ export type { ProductName } from './products/products.ts';
22
+ export type { TelemetryProduct } from './products/types.ts';
23
+ export { telemetryModelRequestConsumer } from './telemetry-model-request-consumer.ts';
24
+ export { telemetryPlugin, telemetrySource } from './telemetry-plugin.ts';
25
+ export { telemetryTurnCostConsumer } from './telemetry-turn-cost-consumer.ts';
@@ -0,0 +1,41 @@
1
+ import { defineFactFamily } from '@pepys/kit';
2
+ import { z } from 'zod';
3
+ import { products } from './products/products.ts';
4
+ import type { TelemetryProduct } from './products/types.ts';
5
+
6
+ const keyValueSchema = z.object({ key: z.string(), value: z.unknown() });
7
+
8
+ // What the exporter wrote, whole, and the proven sender. The glue types the attribute lists and the
9
+ // origin; an attribute's value is an OTLP union, which a consumer narrows itself.
10
+ export const logRecordPayloadSchema = z.object({
11
+ resource: z.object({ attributes: z.array(keyValueSchema) }),
12
+ scope: z.object({ name: z.string(), version: z.string().optional() }),
13
+ logRecord: z.looseObject({ attributes: z.array(keyValueSchema).optional() }),
14
+
15
+ // The only part of the payload that the sender did not write: ingest copies it from the proof.
16
+ // It is not in the fact ID, which depends on the record alone.
17
+ origin: z.object({ issuer: z.string(), subject: z.string() }),
18
+ });
19
+
20
+ export type LogRecordPayload = z.infer<typeof logRecordPayloadSchema>;
21
+
22
+ // `telemetry.<product>.<event>` for each listed event, and `telemetry.<product>.unlisted`.
23
+ export function buildLogRecordTypes(
24
+ list: readonly TelemetryProduct[],
25
+ ): Readonly<Record<string, typeof logRecordPayloadSchema>> {
26
+ const entries = list.flatMap((product) =>
27
+ [...Object.keys(product.events), 'unlisted'].map(
28
+ (event) => [`telemetry.${product.name}.${event}`, logRecordPayloadSchema] as const,
29
+ ),
30
+ );
31
+
32
+ return Object.fromEntries(entries);
33
+ }
34
+
35
+ // One family and one stream: one log record of a known product is one fact, stored whole.
36
+ export const logRecordFamily = defineFactFamily({
37
+ stream: 'facts.telemetry.log_record',
38
+ api: 'otlp-logs/1',
39
+ entityKind: 'telemetry:session',
40
+ types: buildLogRecordTypes(products),
41
+ });
@@ -0,0 +1,24 @@
1
+ import { isRecord } from '@zgeoff/std';
2
+
3
+ // JSON with the keys of every object sorted and no white space, so one value has one text whatever
4
+ // the key order the exporter wrote.
5
+ export function formatCanonicalJSON(value: unknown): string {
6
+ return JSON.stringify(toSortedValue(value));
7
+ }
8
+
9
+ function toSortedValue(value: unknown): unknown {
10
+ if (Array.isArray(value)) {
11
+ return value.map((item: unknown) => toSortedValue(item));
12
+ }
13
+
14
+ if (!isRecord(value)) {
15
+ return value;
16
+ }
17
+
18
+ // `fromEntries` defines each key as an own property, so a `__proto__` key stays a key
19
+ return Object.fromEntries(
20
+ Object.keys(value)
21
+ .toSorted()
22
+ .map((key) => [key, toSortedValue(value[key])] as const),
23
+ );
24
+ }
@@ -0,0 +1,71 @@
1
+ import { isRecord } from '@zgeoff/std';
2
+ import type { LogsExport, OtlpKeyValue, OtlpLogRecord, ResourceLogs, ScopeLogs } from './types.ts';
3
+
4
+ // The export in a JSON body, or undefined for a value that is no logs export at all. Inside an
5
+ // export, a part of the wrong shape is skipped, so one odd record never refuses the others: an
6
+ // exporter drops an export that gets a 4xx answer.
7
+ export function parseLogsExport(value: unknown): LogsExport | undefined {
8
+ if (!isRecord(value)) {
9
+ return undefined;
10
+ }
11
+
12
+ const resourceLogs = value['resourceLogs'];
13
+
14
+ if (resourceLogs === undefined) {
15
+ return { resourceLogs: [] };
16
+ }
17
+
18
+ if (!Array.isArray(resourceLogs)) {
19
+ return undefined;
20
+ }
21
+
22
+ return { resourceLogs: collectObjects(resourceLogs).map((item) => parseResourceLogs(item)) };
23
+ }
24
+
25
+ type JSONObject = Readonly<Record<string, unknown>>;
26
+
27
+ function collectObjects(value: unknown): JSONObject[] {
28
+ const items: readonly unknown[] = Array.isArray(value) ? value : [];
29
+
30
+ return items.filter((item): item is JSONObject => isRecord(item));
31
+ }
32
+
33
+ function parseResourceLogs(value: JSONObject): ResourceLogs {
34
+ const resource = value['resource'];
35
+ const attributes = isRecord(resource) ? resource['attributes'] : undefined;
36
+
37
+ return {
38
+ resource: { attributes: parseAttributes(attributes) },
39
+ scopeLogs: collectObjects(value['scopeLogs']).map((item) => parseScopeLogs(item)),
40
+ };
41
+ }
42
+
43
+ function parseAttributes(value: unknown): OtlpKeyValue[] {
44
+ return collectObjects(value).flatMap((entry) => {
45
+ const key = entry['key'];
46
+
47
+ return typeof key === 'string' ? [{ key, value: entry['value'] }] : [];
48
+ });
49
+ }
50
+
51
+ function parseScopeLogs(value: JSONObject): ScopeLogs {
52
+ const scope = isRecord(value['scope']) ? value['scope'] : {};
53
+ const name = scope['name'];
54
+ const version = scope['version'];
55
+
56
+ return {
57
+ scope: {
58
+ name: typeof name === 'string' ? name : '',
59
+ ...(typeof version === 'string' ? { version } : {}),
60
+ },
61
+ logRecords: collectObjects(value['logRecords']).map((item) => parseLogRecord(item)),
62
+ };
63
+ }
64
+
65
+ function parseLogRecord(value: JSONObject): OtlpLogRecord {
66
+ if (value['attributes'] === undefined) {
67
+ return { ...value, attributes: undefined };
68
+ }
69
+
70
+ return { ...value, attributes: parseAttributes(value['attributes']) };
71
+ }