const { Logging } = require('@google-cloud/logging') const GoogleCloudAdapter = require('../GoogleCloudAdapter') const { FirestoreAdapter } = require('../firestore') const { ErrorUtils } = require('../../../utils') const $errorUtils = new ErrorUtils() const $fs = new FirestoreAdapter({ databaseId: 'settings' }) const LOG_LEVELS: Record< TLogLevel, { severity: TLogEntry['severity']; level: number } > = { debug: { severity: 'DEBUG', level: 0 }, info: { severity: 'INFO', level: 1 }, warn: { severity: 'WARNING', level: 2 }, error: { severity: 'ERROR', level: 3 }, } export class LoggingAdapter extends GoogleCloudAdapter { private static instance: LoggingAdapter | null = null private settingsPromise: Promise | void | null = null private logLevel: TLogLevel = 'info' private env: TEnvironment | null = 'development' private service: string | null = 'no-service' private executionContext: string | null = null private initialized = false // Keep a single Logging instance private logging: typeof Logging private constructor() { super() // Initialize Logging just once this.logging = new Logging() // Attempt to auto-detect resource (optional based on environment) // If you do not need this, you can remove or call it in an async init step. this.logging.setDetectedResource().catch((err) => { console.error('Error setting detected resource:', err) }) } public static getInstance(): LoggingAdapter { if (!LoggingAdapter.instance) { LoggingAdapter.instance = new LoggingAdapter() } return LoggingAdapter.instance } public getState() { return { initialized: this.initialized, logLevel: this.logLevel, env: this.env, service: this.service, executionContext: this.executionContext, } } async initializeSettings( env: TEnvironment, service: string, executionContext: string ): Promise { if (!env || !service || !executionContext) { throw new Error('Missing required parameters for initialization') } this.env = env this.service = service if (executionContext !== this.executionContext) { this.executionContext = executionContext this.settingsPromise = null this.initialized = false } if (this.initialized) { return Promise.resolve() } try { if (!this.settingsPromise) { this.settingsPromise = this.fetchLogSettings() } await this.settingsPromise this.initialized = true } catch (error) { console.error('initializeSettings - Error fetching log settings:', error) this.initialized = false this.settingsPromise = null throw $errorUtils.errorHandler(error) } } private async fetchLogSettings(): Promise { if (!this.service) { console.error('fetchLogSettings - Service not initialized') throw new Error('Service not initialized') } try { const settings = await $fs.getDoc({ collection: 'microservices', id: this.service, }) if (settings && settings.logLevel) { this.logLevel = settings.logLevel as TLogLevel } return Promise.resolve() } catch (error) { console.error('fetchLogSettings - Error fetching log settings:', error) throw $errorUtils.errorHandler(error) } } // Use log levels to decide whether to log private shouldLog(level: TLogLevel) { return LOG_LEVELS[level].level >= LOG_LEVELS[this.logLevel].level } private ensureInitialized() { if (!this.initialized) { throw new Error('LoggingAdapter not initialized') } } private writeEntryAsync(props: TLogEntry) { // Fire & forget: we don’t await the result ;(async () => { try { const { severity, env, log_name, labels } = props let { message } = props // Format message if (typeof message === 'object') { message = JSON.stringify(message) } message = `${log_name}: ${message}` if (env !== 'production') { message += ` (${env})` } // Use log(...), not logSync(...) const log = this.logging.log(log_name) const metadata = { resource: { type: 'global' }, labels: { ...labels, env, service: log_name, }, severity, } const entry = log.entry(metadata, message) // Because log.write() returns a Promise (with .log()), // you can do: log .write(entry) .then(() => { // optional: console.log('Log written') }) .catch((err) => { console.error('Logging write error:', err) }) } catch (err) { console.error('writeEntryAsync outer error:', err) } })() } // Log methods private log(level: TLogLevel, props: TProps) { this.ensureInitialized() if (!this.shouldLog(level)) return if (!this.env || !this.service) { throw new Error('LoggingAdapter not initialized properly') } try { const severity = LOG_LEVELS[level].severity // Fire and forget: this.writeEntryAsync({ severity, env: this.env, log_name: this.service, labels: props.labels ?? {}, message: props.message, }) } catch (error) { // If you're willing to swallow the error at this stage, do so // or if you'd rather bubble it up, re-throw console.error('log error:', error) } } debug = (props: TProps) => { this.log('debug', props) return true } info = (props: TProps) => { this.log('info', props) return true } warn = (props: TProps) => { this.log('warn', props) return true } error = (props: TProps) => { this.log('error', props) return true } }