@stacksjs/logging 0.75.15 → 0.75.17
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.
- package/README.md +31 -0
- package/dist/index.js +2 -2
- package/dist/loghq.d.ts +12 -0
- package/dist/loghq.js +1 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -33,6 +33,37 @@ echo('some echo message')
|
|
|
33
33
|
|
|
34
34
|
Learn more in the docs.
|
|
35
35
|
|
|
36
|
+
## LogHQ
|
|
37
|
+
|
|
38
|
+
Stacks includes a buffered LogHQ transport. Set the ingest key and the default
|
|
39
|
+
logging config registers it in production and staging:
|
|
40
|
+
|
|
41
|
+
```env
|
|
42
|
+
LOGHQ_KEY=loghq_your_project_key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`LOGHQ_BASE_URL` targets a self-hosted LogHQ instance. `LOGHQ_PROJECT` adds a
|
|
46
|
+
searchable project label to every entry and otherwise defaults to `APP_NAME`.
|
|
47
|
+
An empty key, or an environment outside the configured allowlist, creates no
|
|
48
|
+
transport, timer, queue, or request.
|
|
49
|
+
|
|
50
|
+
The transport buffers on the caller path and drains through `log.flush()`. Its
|
|
51
|
+
network failures are contained and reported only through the SDK's optional
|
|
52
|
+
console diagnostics, never back through `log`, so a delivery failure cannot
|
|
53
|
+
re-enter the transport.
|
|
54
|
+
|
|
55
|
+
For direct configuration, use the public adapter:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createLogHqTransport } from '@stacksjs/logging/loghq'
|
|
59
|
+
|
|
60
|
+
const transport = createLogHqTransport({
|
|
61
|
+
key: process.env.LOGHQ_KEY,
|
|
62
|
+
environment: 'production',
|
|
63
|
+
level: 'debug',
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
36
67
|
## 🧪 Testing
|
|
37
68
|
|
|
38
69
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AsyncLocalStorage}from"node:async_hooks";import process from"node:process";const EXIT_CODE_SUCCESS=0,EXIT_CODE_FATAL_ERROR=1;let _logger=null,_loggerInitPromise=null;const pendingWrites=new Set;function track(p){pendingWrites.add(p);p.finally(()=>pendingWrites.delete(p)).catch(()=>{});return p}let _flushOnExitRegistered=!1;function registerFlushOnExit(){if(_flushOnExitRegistered)return;_flushOnExitRegistered=!0;process.on("beforeExit",()=>{log.flush()})}const _transports=[],_brokenTransports=new Set,LEVEL_RANK={debug:0,info:1,success:1,warning:2,error:3};export function suppressedAtCurrentLevel(level){return LEVEL_RANK[level]<LEVEL_RANK[currentLevel()]}function addTransport(candidate){const t=candidate;if(!t||typeof t!=="object"||typeof t.log!=="function"){process.stderr.write(`[logging] Ignoring a transport without a log() function.
|
|
1
|
+
import{captureError,flushErrorReporters}from"@stacksjs/error-handling/reporters";import{AsyncLocalStorage}from"node:async_hooks";import process from"node:process";const EXIT_CODE_SUCCESS=0,EXIT_CODE_FATAL_ERROR=1;let _logger=null,_loggerInitPromise=null;const pendingWrites=new Set;function track(p){pendingWrites.add(p);p.finally(()=>pendingWrites.delete(p)).catch(()=>{});return p}let _flushOnExitRegistered=!1;function registerFlushOnExit(){if(_flushOnExitRegistered)return;_flushOnExitRegistered=!0;process.on("beforeExit",()=>{log.flush()})}const _transports=[],_brokenTransports=new Set,LEVEL_RANK={debug:0,info:1,success:1,warning:2,error:3};export function suppressedAtCurrentLevel(level){return LEVEL_RANK[level]<LEVEL_RANK[currentLevel()]}function addTransport(candidate){const t=candidate;if(!t||typeof t!=="object"||typeof t.log!=="function"){process.stderr.write(`[logging] Ignoring a transport without a log() function.
|
|
2
2
|
`);return null}if(typeof t.name!=="string"||!t.name){process.stderr.write(`[logging] Ignoring a transport without a name.
|
|
3
3
|
`);return null}_transports.push(t);return()=>{const at=_transports.indexOf(t);if(at!==-1)_transports.splice(at,1)}}export function registerTransport(transport){const detach=addTransport(transport);if(!detach)return()=>{};initLogger();return detach}export function transports(){return[..._transports]}function dispatch(level,message,args){if(_transports.length===0)return;let record;try{record={level,message,args,context:getLogContext(),timestamp:new Date().toISOString()}}catch{return}for(const transport of _transports){if(transport.level&&LEVEL_RANK[level]<LEVEL_RANK[transport.level])continue;try{transport.log(record)}catch(err){if(!_brokenTransports.has(transport.name)){_brokenTransports.add(transport.name);const reason=err instanceof Error?err.message:String(err);process.stderr.write(`[logging] Transport "${transport.name}" threw: ${reason}. Further throws from it are silent.
|
|
4
4
|
`)}}}}async function flushTransports(){const pending=_transports.filter((t)=>typeof t.flush==="function").map((t)=>t.flush().catch(()=>{}));if(pending.length>0)await Promise.allSettled(pending)}const VALID_LEVELS=new Set(["debug","info","success","warning","error"]);export function parseLogLevel(raw,fallback="info"){if(!raw)return fallback;const v=raw.toLowerCase();if(v==="warn")return"warning";if(VALID_LEVELS.has(v))return v;process.stderr.write(`[logging] Ignoring invalid LOG_LEVEL="${raw}" (expected: ${[...VALID_LEVELS].join(", ")}); using "${fallback}".
|
|
@@ -7,4 +7,4 @@ import{AsyncLocalStorage}from"node:async_hooks";import process from"node:process
|
|
|
7
7
|
caused by: ${cause.stack||`${cause.name}: ${cause.message}`}`;cause=cause.cause}return out}function normalizeContextValue(value,depth=0){if(value instanceof Error)return normalizeError(value);if(depth>=4||value===null||typeof value!=="object")return value;if(Array.isArray(value))return value.map((v)=>normalizeContextValue(v,depth+1));const proto=Object.getPrototypeOf(value);if(proto!==Object.prototype&&proto!==null)return value;const out={};for(const[k,v]of Object.entries(value))out[k]=normalizeContextValue(v,depth+1);return out}export function normalizeContext(ctx){return normalizeContextValue(ctx)}const logContextStorage=new AsyncLocalStorage;export function withLogContext(context,fn){return logContextStorage.run(context,fn)}export function getLogContext(){const store=logContextStorage.getStore(),trace=activeTraceId();if(!trace)return store;return{trace_id:trace,...store}}function activeTraceId(){try{const explicit=globalThis[Symbol.for("stacks.router.traceStorage")]?.getStore?.();if(explicit)return explicit;return globalThis[Symbol.for("stacks.router.requestStorage")]?.getStore?.()?._requestId}catch{return}}const CONFIG_READY_KEY=Symbol.for("@stacksjs/config:overridesReady");let _builtWith=null;async function readLoggingConfigNow(){try{return(await import("@stacksjs/config")).logging??null}catch{return null}}function configReadySignal(){const anchored=globalThis[CONFIG_READY_KEY];if(!anchored||typeof anchored.then!=="function")return null;return anchored}async function resolveSettings(logging,fallbackDirectory){const{level,format,writeToFile}=resolveLogSettings({envLevel:process.env.LOG_LEVEL,envFormat:process.env.LOG_FORMAT,cfgLevel:logging?.level,cfgFormat:logging?.format,cfgWriteToFile:typeof logging?.writeToFile==="boolean"?logging.writeToFile:void 0,isProduction:process.env.NODE_ENV==="production"});let logDirectory;if(logging?.logsPath)logDirectory=(await import("node:path")).dirname(logging.logsPath);if(!logDirectory)logDirectory=fallbackDirectory;if(!logDirectory)try{logDirectory=(await import("@stacksjs/path")).projectPath("storage/logs")}catch{logDirectory="storage/logs"}return{level,format,writeToFile,logDirectory}}function attachDeclaredTransports(logging){if(!Array.isArray(logging?.transports))return;for(const transport of logging.transports)if(!_transports.includes(transport))addTransport(transport)}let _configApplied=null;function applyConfigWhenReady(){if(_configApplied)return;const ready=configReadySignal();if(!ready)return;_configApplied=ready.then(async(resolved)=>{const logging=resolved?.logging;if(!logging)return;attachDeclaredTransports(logging);if(logging.level)_configLevel=parseLogLevel(logging.level);const built=_builtWith;if(!built||!_logger)return;const next=await resolveSettings(logging,built.logDirectory);if(built.level===next.level&&built.format===next.format&&built.writeToFile===next.writeToFile&&built.logDirectory===next.logDirectory)return;const previous=_logger,{Logger}=await import("@stacksjs/clarity");_logger=new Logger("stacks",{level:next.level,logDirectory:next.logDirectory,showTags:!1,fancy:next.format!=="json",format:next.format,writeToFile:next.writeToFile});_builtWith=next;try{await previous.flush?.();await previous.destroy?.()}catch{}}).catch(()=>{})}async function initLogger(){if(_logger)return;if(_loggerInitPromise)return _loggerInitPromise;registerFlushOnExit();_loggerInitPromise=(async()=>{const logging=await readLoggingConfigNow();attachDeclaredTransports(logging);if(logging?.level)_configLevel=parseLogLevel(logging.level);const settings=await resolveSettings(logging),{Logger}=await import("@stacksjs/clarity");_logger=new Logger("stacks",{level:settings.level,logDirectory:settings.logDirectory,showTags:!1,fancy:settings.format!=="json",format:settings.format,writeToFile:settings.writeToFile});_builtWith=settings})();_loggerInitPromise.then(applyConfigWhenReady);return _loggerInitPromise}async function getLogger(){await initLogger();return _logger}export async function logger(){await initLogger();applyConfigWhenReady();if(_configApplied)await _configApplied;return _logger}export function formatMessage(...args){const base=args.filter((arg)=>arg!==void 0).map((arg)=>{if(arg instanceof Error)return renderNormalizedError(normalizeError(arg));if(typeof arg==="object"&&arg!==null)return JSON.stringify(normalizeContextValue(arg),null,2);return String(arg)}).join(" "),ctx=logContextStorage.getStore();if(ctx?.requestId)return`[${ctx.requestId}] ${base}`;return base}const LEGACY_ERROR_OPTION_KEYS=new Set(["shouldExit","silent","message"]);function isLegacyErrorOptions(v){if(!v||typeof v!=="object"||v instanceof Error)return!1;if(!(("shouldExit"in v)||("silent"in v)))return!1;return Object.keys(v).every((k)=>LEGACY_ERROR_OPTION_KEYS.has(k))}export const log={info:async(...args)=>{const suppressed=suppressedAtCurrentLevel("info");if(suppressed&&_transports.length===0)return;const message=formatMessage(...args);if(suppressed){dispatch("info",message,args);return}const logger=await getLogger();dispatch("info",message,args);await logger.info(message)},success:async(message)=>{const suppressed=suppressedAtCurrentLevel("success");if(suppressed&&_transports.length===0)return;if(suppressed){dispatch("success",message,[message]);return}const logger=await getLogger();dispatch("success",message,[message]);await logger.success(message)},warn:async(message,context)=>{const logger=await getLogger();dispatch("warning",message,context===void 0?[message]:[message,context]);if(context==null){await logger.warn(message);return}const normalized=normalizeContextValue(context);await logger.warn(message,normalized)},warning:async(message)=>{const logger=await getLogger();dispatch("warning",message,[message]);await logger.warn(message)},error:async(message,error,context)=>{const legacyOptions=isLegacyErrorOptions(error)?error:void 0;let line;if(typeof message==="string")line=message;else line=renderNormalizedError(normalizeError(message));if(error!==void 0&&!legacyOptions)line=`${line} ${renderNormalizedError(normalizeError(error))}`;const mergedCtx={...getLogContext(),...context};if(Object.keys(mergedCtx).length>0)try{line=`${line} ${JSON.stringify(normalizeContext(mergedCtx))}`}catch{}const logger=await getLogger();dispatch("error",line,legacyOptions?[message]:[message,error,context].filter((a)=>a!==void 0));await logger.error(line);if(legacyOptions?.shouldExit){const{handleError}=await import("@stacksjs/error-handling/handler");handleError(message,legacyOptions)}},debug:async(...args)=>{const suppressed=currentLevel()!=="debug";if(suppressed&&_transports.length===0){if(configReadySignal()){if(!_logger&&!_loggerInitPromise)initLogger();else if(!_configApplied)applyConfigWhenReady()}return}const message=formatMessage(...args);if(suppressed){dispatch("debug",message,args);return}const logger=await getLogger();dispatch("debug",message,args);await logger.debug(message)},dump:async(...args)=>{const message=formatMessage(...args);await(await getLogger()).debug(`DUMP: ${message}`)},dd:async(...args)=>{const message=formatMessage(...args);await(await getLogger()).error(message);process.exit(EXIT_CODE_FATAL_ERROR)},echo:async(...args)=>{const message=formatMessage(...args);await(await getLogger()).info(`ECHO: ${message}`)},time:(label)=>{const start=performance.now();return async(metadata)=>{const duration=performance.now()-start,logger=await getLogger(),meta=metadata?` ${JSON.stringify(normalizeContext(metadata))}`:"";await logger.info(`${label}: ${duration.toFixed(2)}ms${meta}`)}},syncWarn:(msg)=>{process.stderr.write(`${msg}
|
|
8
8
|
`)},syncError:(msg)=>{process.stderr.write(`${msg}
|
|
9
9
|
`)},fatal:(msg,exitCode=EXIT_CODE_FATAL_ERROR)=>{process.stderr.write(`${msg}
|
|
10
|
-
`);process.exit(exitCode)},exit:async(msg,exitCode=EXIT_CODE_SUCCESS)=>{if(msg)await(exitCode===EXIT_CODE_SUCCESS?log.success(msg):log.error(msg));await log.flush();process.exit(exitCode)},flush:async()=>{if(pendingWrites.size>0)await Promise.allSettled([...pendingWrites]);await flushTransports();if(!_logger&&!_loggerInitPromise)return;try{const logger=await getLogger(),maybeFlush=logger.flush;if(typeof maybeFlush==="function")await maybeFlush.call(logger)}catch{}}};globalThis[Symbol.for("@stacksjs/logging:implementation-loaded")]=!0;export async function dump(...args){for(const arg of args)await log.debug(arg)}export async function dd(...args){const message=formatMessage(...args);console.log(message);process.exit(EXIT_CODE_FATAL_ERROR)}export async function echo(...args){await log.debug(...args)}function statusOf(error){if(error&&typeof error==="object"){const e=error;if(typeof e.status==="number")return e.status;if(typeof e.statusCode==="number")return e.statusCode}return}export function report(error,options={}){const status=options.status??statusOf(error),isClientError=typeof status==="number"&&status>=400&&status<500,message=options.label??"Unhandled error",context={...options.context,...status!=null?{status}:{}};if(isClientError){log.debug(`${message} (client error ${status}): ${normalizeError(error).message}`);return}log.error(message,error,context)}function emit(level,event,fields){const ctx=getLogContext(),payload={event,traceId:ctx?.requestId,...fields};if(level==="warn")track(log.warn(`[${event}]`,payload));else track(log[level](payload))}export const struct={request(method,path,status,durationMs,fields={}){const level=status>=500?"error":status>=400?"warn":"info";emit(level,"http.request",{method,path,status,durationMs,...fields})},query(sql,durationMs,fields={}){emit("debug","db.query",{sql,durationMs,...fields})},slowQuery(sql,durationMs,fields={}){emit("warn","db.slow_query",{sql,durationMs,...fields})},job(name,phase,fields={}){emit(phase==="failed"?"error":"info",`job.${phase}`,{jobName:name,...fields})},cache(op,key,fields={}){emit("debug",`cache.${op}`,{key,...fields})}};log.struct=struct;
|
|
10
|
+
`);process.exit(exitCode)},exit:async(msg,exitCode=EXIT_CODE_SUCCESS)=>{if(msg)await(exitCode===EXIT_CODE_SUCCESS?log.success(msg):log.error(msg));await log.flush();process.exit(exitCode)},flush:async()=>{if(pendingWrites.size>0)await Promise.allSettled([...pendingWrites]);await flushTransports();await flushErrorReporters();if(!_logger&&!_loggerInitPromise)return;try{const logger=await getLogger(),maybeFlush=logger.flush;if(typeof maybeFlush==="function")await maybeFlush.call(logger)}catch{}}};globalThis[Symbol.for("@stacksjs/logging:implementation-loaded")]=!0;export async function dump(...args){for(const arg of args)await log.debug(arg)}export async function dd(...args){const message=formatMessage(...args);console.log(message);process.exit(EXIT_CODE_FATAL_ERROR)}export async function echo(...args){await log.debug(...args)}function statusOf(error){if(error&&typeof error==="object"){const e=error;if(typeof e.status==="number")return e.status;if(typeof e.statusCode==="number")return e.statusCode}return}export function report(error,options={}){const status=options.status??statusOf(error),isClientError=typeof status==="number"&&status>=400&&status<500,message=options.label??"Unhandled error",context={...options.context,...status!=null?{status}:{}};if(isClientError){log.debug(`${message} (client error ${status}): ${normalizeError(error).message}`);return}captureError(error,{...context,...options.label?{label:options.label}:{}});log.error(message,error,context)}function emit(level,event,fields){const ctx=getLogContext(),payload={event,traceId:ctx?.requestId,...fields};if(level==="warn")track(log.warn(`[${event}]`,payload));else track(log[level](payload))}export const struct={request(method,path,status,durationMs,fields={}){const level=status>=500?"error":status>=400?"warn":"info";emit(level,"http.request",{method,path,status,durationMs,...fields})},query(sql,durationMs,fields={}){emit("debug","db.query",{sql,durationMs,...fields})},slowQuery(sql,durationMs,fields={}){emit("warn","db.slow_query",{sql,durationMs,...fields})},job(name,phase,fields={}){emit(phase==="failed"?"error":"info",`job.${phase}`,{jobName:name,...fields})},cache(op,key,fields={}){emit("debug",`cache.${op}`,{key,...fields})}};log.struct=struct;
|
package/dist/loghq.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LogHqLoggingOptions, LogTransport } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Build the declarative LogHQ transport used by `config/logging.ts`.
|
|
4
|
+
*
|
|
5
|
+
* No key means no SDK client, timer, queue, or request. Delivery behavior is
|
|
6
|
+
* delegated to `@loghq/stacks`, which buffers on the caller path and contains
|
|
7
|
+
* all network failures without routing diagnostics back through this logger.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createLogHqTransport(options: CreateLogHqTransportOptions): LogTransport | null;
|
|
10
|
+
export declare interface CreateLogHqTransportOptions extends LogHqLoggingOptions {
|
|
11
|
+
environment: string
|
|
12
|
+
}
|
package/dist/loghq.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{loghqTransport}from"@loghq/stacks";function logHqLevel(level){return level==="success"?"info":level}export function createLogHqTransport(options){const key=options.key?.trim();if(!key)return null;const project=options.project?.trim(),level=logHqLevel(options.level),transport=loghqTransport({key,host:options.baseUrl?.trim()||void 0,environment:options.environment,minLevel:level,enabled:options.enabled,batchSize:options.batchSize,flushInterval:options.flushInterval,maxQueueSize:options.maxQueueSize,timeout:options.timeout,maxRetries:options.maxRetries,beforeSend:project?(entry)=>({...entry,context:{...entry.context,project}}):void 0});transport.level=options.level;return transport}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/logging",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.17",
|
|
6
6
|
"description": "The Stacks logging system.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -66,15 +66,15 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@loghq/stacks": "^0.2.3",
|
|
68
68
|
"@stacksjs/clarity": "^0.3.33",
|
|
69
|
-
"@stacksjs/config": "0.75.
|
|
70
|
-
"@stacksjs/error-handling": "0.75.
|
|
71
|
-
"@stacksjs/path": "0.75.
|
|
72
|
-
"@stacksjs/types": "0.75.
|
|
69
|
+
"@stacksjs/config": "0.75.17",
|
|
70
|
+
"@stacksjs/error-handling": "0.75.17",
|
|
71
|
+
"@stacksjs/path": "0.75.17",
|
|
72
|
+
"@stacksjs/types": "0.75.17"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@stacksjs/cli": "0.75.
|
|
76
|
-
"@stacksjs/storage": "0.75.
|
|
77
|
-
"@stacksjs/validation": "0.75.
|
|
75
|
+
"@stacksjs/cli": "0.75.17",
|
|
76
|
+
"@stacksjs/storage": "0.75.17",
|
|
77
|
+
"@stacksjs/validation": "0.75.17",
|
|
78
78
|
"better-dx": "^0.2.24"
|
|
79
79
|
}
|
|
80
80
|
}
|