1// Copyright 2023 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15import { 16 Field, 17 LogEntry, 18 LogSourceEvent, 19 SourceData, 20} from './shared/interfaces'; 21 22export abstract class LogSource { 23 private eventListeners: { 24 eventType: string; 25 listener: (event: LogSourceEvent) => void; 26 }[]; 27 28 protected sourceId: string; 29 30 protected sourceName: string; 31 32 constructor(sourceName: string) { 33 this.eventListeners = []; 34 this.sourceId = crypto.randomUUID(); 35 this.sourceName = sourceName; 36 } 37 38 abstract start(): void; 39 40 abstract stop(): void; 41 42 addEventListener( 43 eventType: string, 44 listener: (event: LogSourceEvent) => void, 45 ): void { 46 this.eventListeners.push({ eventType, listener }); 47 } 48 49 removeEventListener( 50 eventType: string, 51 listener: (event: LogSourceEvent) => void, 52 ): void { 53 this.eventListeners = this.eventListeners.filter( 54 (eventListener) => 55 eventListener.eventType !== eventType || 56 eventListener.listener !== listener, 57 ); 58 } 59 60 emitEvent(event: LogSourceEvent): void { 61 this.eventListeners.forEach((eventListener) => { 62 if (eventListener.eventType === event.type) { 63 eventListener.listener(event); 64 } 65 }); 66 } 67 68 publishLogEntry(logEntry: LogEntry): void { 69 // Validate the log entry 70 const validationResult = this.validateLogEntry(logEntry); 71 if (validationResult !== null) { 72 console.error('Validation error:', validationResult); 73 return; 74 } 75 76 const sourceData: SourceData = { id: this.sourceId, name: this.sourceName }; 77 logEntry.sourceData = sourceData; 78 79 // Add the name of the log source as a field in the log entry 80 const logSourceField: Field = { key: 'log_source', value: this.sourceName }; 81 logEntry.fields.splice(1, 0, logSourceField); 82 83 this.emitEvent({ type: 'log-entry', data: logEntry }); 84 } 85 86 validateLogEntry(logEntry: LogEntry): string | null { 87 try { 88 if (!logEntry.timestamp) { 89 return 'Log entry has no valid timestamp'; 90 } 91 if (!Array.isArray(logEntry.fields)) { 92 return 'Log entry fields must be an array'; 93 } 94 if (logEntry.fields.length === 0) { 95 return 'Log entry fields must not be empty'; 96 } 97 98 // Handle backwards compatibility 99 if (logEntry.severity) { 100 logEntry.level = logEntry.severity; 101 delete logEntry.severity; 102 } 103 104 for (const field of logEntry.fields) { 105 if (!field.key || typeof field.key !== 'string') { 106 return 'Invalid field key'; 107 } 108 109 // Handle backwards compatibility 110 if (field.key === 'severity') { 111 field.key = 'level'; 112 } 113 114 if ( 115 field.value === undefined || 116 (typeof field.value !== 'string' && 117 typeof field.value !== 'boolean' && 118 typeof field.value !== 'number' && 119 typeof field.value !== 'object') 120 ) { 121 return 'Invalid field value'; 122 } 123 } 124 125 if (logEntry.level !== undefined && typeof logEntry.level !== 'string') { 126 return 'Invalid level value'; 127 } 128 129 return null; 130 } catch (error) { 131 if (error instanceof Error) { 132 console.error('Validation error:', error.message); 133 } 134 return 'An unexpected error occurred during validation'; 135 } 136 } 137} 138