| 81 | } |
| 82 | |
| 83 | export class LokiLogAdapter implements WorkflowLogSink { |
| 84 | constructor( |
| 85 | private readonly client: LokiPushClient, |
| 86 | private readonly db?: NodePgDatabase<typeof schema>, |
| 87 | ) {} |
| 88 | private ensureIndexPromise?: Promise<void>; |
| 89 | |
| 90 | async append(entry: WorkflowLogEntry): Promise<void> { |
| 91 | if (!entry.message || entry.message.trim().length === 0) { |
| 92 | return; |
| 93 | } |
| 94 | |
| 95 | const timestamp = entry.timestamp ?? new Date(); |
| 96 | const labels = this.buildLabels(entry); |
| 97 | const lines = this.buildLines(entry.message, timestamp); |
| 98 | const lineCount = lines.length; |
| 99 | |
| 100 | try { |
| 101 | await this.client.push(labels, lines); |
| 102 | } catch (error) { |
| 103 | console.error('[LOKI] Failed to push log entry', error); |
| 104 | return; |
| 105 | } |
| 106 | |
| 107 | if (this.db) { |
| 108 | await this.persistMetadata({ |
| 109 | runId: entry.runId, |
| 110 | nodeRef: entry.nodeRef, |
| 111 | stream: entry.stream, |
| 112 | labels, |
| 113 | lineCount, |
| 114 | timestamp, |
| 115 | organizationId: entry.organizationId ?? null, |
| 116 | }); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | private buildLabels(entry: WorkflowLogEntry): Record<string, string> { |
| 121 | const labels: Record<string, string> = { |
| 122 | run_id: entry.runId, |
| 123 | node: entry.nodeRef, |
| 124 | stream: entry.stream, |
| 125 | }; |
| 126 | |
| 127 | if (entry.level) { |
| 128 | labels.level = entry.level; |
| 129 | } |
| 130 | |
| 131 | if (entry.metadata?.activityId) { |
| 132 | labels.activity_id = entry.metadata.activityId; |
| 133 | } |
| 134 | |
| 135 | if (entry.metadata?.attempt !== undefined) { |
| 136 | labels.attempt = String(entry.metadata.attempt); |
| 137 | } |
| 138 | |
| 139 | if (entry.metadata?.correlationId) { |
| 140 | labels.correlation_id = entry.metadata.correlationId; |
nothing calls this directly
no outgoing calls
no test coverage detected