logroller 2.1.0 → 3.0.0
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 +406 -382
- package/dist/index.cjs +2 -1336
- package/dist/{shutdown.d.ts → index.d.cts} +13 -6
- package/dist/index.d.ts +36 -6
- package/dist/index.esm.js +2 -1331
- package/dist/pino.cjs +2 -0
- package/dist/pino.d.cts +6 -0
- package/dist/pino.d.ts +6 -0
- package/dist/pino.esm.js +2 -0
- package/dist/rotate-file-stream.cjs +2 -0
- package/dist/rotate-file-stream.d.cts +350 -0
- package/dist/rotate-file-stream.d.ts +198 -7
- package/dist/rotate-file-stream.esm.js +2 -0
- package/package.json +27 -10
- package/CHANGELOG.md +0 -151
- package/dist/fsio.d.ts +0 -11
- package/dist/parsers.d.ts +0 -18
- package/dist/time.d.ts +0 -13
- package/dist/types.d.ts +0 -159
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! logroller v3.0.0 | MIT License */
|
|
2
|
+
import{randomUUID as t}from"node:crypto";import e from"node:fs";import i from"node:path";import{Writable as s}from"node:stream";import a from"node:zlib";import{pipeline as n}from"node:stream/promises";const r=t=>{try{return"string"==typeof t?e.statSync(t).size:0}catch{return 0}},h=t=>{try{return e.existsSync(t)}catch{return!1}};async function o(t,i,s,r=6){try{return await n(e.createReadStream(t),a.createGzip({level:r}),e.createWriteStream(i)),await e.promises.unlink(t),!0}catch(t){return await e.promises.unlink(i).catch(()=>{}),s?.(t),!1}}const c=t=>t?new Promise(e=>t.end(()=>e())):Promise.resolve(),d={b:1,k:1024,kb:1024,m:1048576,mb:1048576,g:1073741824,gb:1073741824},l={ms:1,s:1e3,m:6e4,h:36e5,hour:36e5,hours:36e5};function u(t,e,i){if("number"==typeof t)return Math.round(t*l[e]);const s=/^\s*(-?\d+(?:\.\d+)?)\s*(h(?:our)?s?|ms|s|m)?\s*$/i.exec(t);if(!s)throw new Error(`Invalid ${i} "${t}"`);const a=(s[2]??e).toLowerCase(),n=l[a];if(void 0===n)throw new Error(`Invalid ${i} unit "${s[2]}" in "${t}"`);return Math.round(Number.parseFloat(s[1])*n)}function _(t){if(null==t||0===t)return 0;if("number"==typeof t)return t;const e=/^\s*(\d+(?:\.\d+)?)\s*(b|kb|k|mb|m|gb|g)?\s*$/i.exec(t);if(!e)throw new Error(`Invalid maxSize "${t}"`);return Math.round(Number.parseFloat(e[1])*(d[e[2]?.toLowerCase()]??1))}const m={ms:1,s:1e3,m:6e4,h:36e5};const p=new Map;function f(t){let e=p.get(t);if(!e){try{e=new Intl.DateTimeFormat("en-US",{timeZone:t,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch{throw new Error(`Invalid timeZone "${t}"`)}p.set(t,e)}return e}function w(t,e){const i={};for(const s of f(t).formatToParts(e))i[s.type]=s.value;return i}const v=t=>t.padStart(2,"0");function y(t){return t.replace(/[.*+?^${}()|[\]\\]/g,String.raw`\$&`)}function b(t,e){const i=w(t,e);return Date.UTC(+i.year,+i.month-1,+i.day,+i.hour,+i.minute,+i.second)-(e.getTime()-e.getTime()%1e3)}class g extends s{tz;template;dir;datePattern;zippedArchive;maxSize;maxFiles;freq;unrefTimers;maxTotalSize;_tL;_tMid;_tExt;_rxFor;_rxActive;_auditEnabled;_auditPath;_bootId=t();_onSeal;_sealHookTimeoutMs;_compressionLevel;_symlinkPath=null;_offsetMs;_stepMs;_bumpEvery;_curStamp;_index;_ws=null;_path=null;_bytes=0;_timer=null;_bumpTimer=null;_beatTimer;_ended=!1;_tail=Promise.resolve();_warnedBackward=!1;_stampSec=Number.NaN;_stampVal="";_audit;constructor(t={}){super(t.highWaterMark?{highWaterMark:t.highWaterMark}:{});const s=t.filename;if("string"!=typeof s||!s.includes("%DATE%"))throw new Error("opts.filename must contain '%DATE%' (e.g. 'logs/app-%DATE%.log')");var a;this.tz=t.tz||"UTC",f(this.tz),this.template=i.basename(s),this.dir=i.dirname(s),this.datePattern=t.datePattern||"YYYY-MM-DD",this.zippedArchive=!!t.zippedArchive,this.maxSize=_(t.maxSize??0),this.maxFiles=function(t){if(null==t||""===t||0===t)return null;if("number"==typeof t){if(t<0)throw new Error(`Invalid maxFiles "${t}"`);return{count:t}}const e=/^\s*(\d+)\s*d\s*$/i.exec(t);if(e)return 0===Number(e[1])?null:{days:Number.parseInt(e[1],10)};const i=/^\s*(\d+)\s*$/.exec(t);if(i)return 0===Number(i[1])?null:{count:Number.parseInt(i[1],10)};throw new Error(`Invalid maxFiles "${t}"`)}(t.maxFiles),this.maxTotalSize=_(t.maxTotalSize??0),this.freq=function(t){if(null==t)return{type:"daily"};if("daily"===t)return{type:"daily"};if("number"==typeof t){if(t<=0)throw new Error(`Invalid frequency "${t}"`);return{type:"interval",ms:t}}const e=/^\s*(\d+)\s*(ms|s|m|h)\s*$/i.exec(t);if(!e)throw new Error(`Invalid frequency "${t}"`);return{type:"interval",ms:Number.parseInt(e[1],10)*m[e[2].toLowerCase()]}}(t.frequency),this.unrefTimers=!!t.unrefTimers,this._offsetMs=null==(a=t.addHours)||""===a?0:u(a,"h","addHours"),this._stepMs=function(t){return null==t||""===t?0:u(t,"h","addHoursEveryMin")}(t.addHoursEveryMin),this._bumpEvery=Math.max(50,t.clockStepIntervalMs??6e4),this._compressionLevel=Math.min(9,Math.max(0,t.compressionLevel??6)),this._onSeal=t.onSeal,this._sealHookTimeoutMs=Math.max(0,t.sealHookTimeoutMs??3e4);const n=this.template.indexOf("%DATE%"),r=this.template.slice(n+6),h=r.lastIndexOf(".");this._tL=this.template.slice(0,n),this._tMid=h>=0?r.slice(0,h):"",this._tExt=h>=0?r.slice(h):"",this._rxFor=t=>new RegExp("^"+y(this._tL)+t+y(this._tMid)+"(?:\\.(\\d+))?"+y(this._tExt)+"(?:\\.gz)?$"),this._rxActive=this._rxFor("("+this.datePattern.split(/(YYYY|YY|MM|DD|HH|mm|ss)/).map((t,e)=>e%2?"YYYY"===t?"\\d{4}":"\\d{2}":y(t)).join("")+")"),this._auditEnabled=!1!==t.audit;const o=(this._tL.replace(/[^\w.-]+/g,"")||"log").replace(/^[-.]+|[-.]+$/g,"");var c,d,l,p;this._auditPath=i.join(this.dir,t.auditFile||`${o}_audit.json`),this._symlinkPath=t.symlink?i.join(this.dir,"string"==typeof t.symlink?t.symlink:`${o}-current.log`):null,e.mkdirSync(this.dir,{recursive:!0}),this._curStamp=this._stampNow(),this._audit=(c=this._tL,d=this._tExt,l=this.datePattern,p=this.tz,{version:1,prefix:c,ext:d,datePattern:l,tz:p,createdAt:null,updatedAt:null,activeStamp:null,activeIndex:null,writer:null,files:{},events:[]});const w=this._auditEnabled&&this._loadAudit();this._index=this._recoverIndexFor(this._curStamp);const v=this._audit.activeIndex;w&&null!==v&&this._audit.activeStamp===this._curStamp&&(this._index=Math.max(this._index,v)),0===this._offsetMs&&0===this._stepMs||process.nextTick(()=>this.emit("clock",{addHours:t.addHours??void 0,addHoursEveryMin:t.addHoursEveryMin??void 0,offsetMs:this._offsetMs})),process.nextTick(()=>{const e=t=>this.emit("warn",new Error(t));"interval"===this.freq.type?(this.freq.ms<864e5&&!this.datePattern.includes("HH")&&e(`frequency "${t.frequency}" needs HH in datePattern — segments would collapse onto one filename`),this.freq.ms<6e4&&!this.datePattern.includes("mm")&&e(`frequency "${t.frequency}" needs mm in datePattern`)):this.datePattern.includes("DD")||e("daily frequency needs DD in datePattern")}),this._job(async()=>{await this._sealOrphanedPastStamps(),this._auditEnabled&&(this._demoteZombieActives(),this._reconcileWithDisk(),await this._repairInterruptedGzips(),this._aevent("boot",{stamp:this._curStamp,index:this._index}),this._saveAudit()),await this._retire()}).catch(()=>{}),this._armTimer(),this._armBump(),this._auditEnabled&&(this._beatTimer=setInterval(()=>this._job(()=>this._saveAudit()).catch(()=>{}),1e4),this._beatTimer.unref()),this.once("close",()=>{this._stopTimer(),this._stopBump(),clearInterval(this._beatTimer)})}on(t,e){return super.on(t,e)}once(t,e){return super.once(t,e)}emit(t,...e){return super.emit(t,...e)}get filename(){return this._path}get currentIndex(){return this._index}rotateNow(){return this._job(()=>this._rotate("manual"))}advanceClock(){return this._job(()=>this._applyBump())}listSegments(){const t=this._rxActive,s=this._symlinkPath&&i.basename(this._symlinkPath),a=[];let n=[];try{n=e.readdirSync(this.dir)}catch{return a}for(const e of n){if(e===s)continue;const n=t.exec(e);if(!n)continue;const h=this._audit.files[e];a.push({file:i.join(this.dir,e),stamp:n[1],index:n[2]?Number.parseInt(n[2],10):0,gzipped:e.endsWith(".gz"),bytes:r(i.join(this.dir,e)),state:h?.state??"untracked",openedAt:h?.openedAt,archivedAt:h?.archivedAt})}return a.sort((t,e)=>t.stamp.localeCompare(e.stamp)||t.index-e.index),a}stats(){const t=this.listSegments();return{dir:this.dir,currentStamp:this._curStamp,activeFile:this._path,activeIndex:this._index,segments:t.length,archived:t.filter(t=>t.gzipped).length,totalBytes:t.reduce((t,e)=>t+e.bytes,0),clockOffsetMs:this._offsetMs,symlink:this._symlinkPath}}_write(t,e,i){this._writeBatch([t],i)}_writev(t,e){this._writeBatch(t.map(t=>t.chunk),e)}_writeBatch(t,e){this._ended?process.nextTick(e,new Error("write after end")):this._job(async()=>{try{this._stampAdvanced(this._stampNow())&&await this._rotate("time"),await this._ensureOpen();let i=[],s=0;for(const e of t){const t=this._bytes+s;this.maxSize>0&&t>0&&t+e.length>this.maxSize&&(await this._flush(i,s),i=[],s=0,await this._rotate("size"),await this._ensureOpen()),i.push(e),s+=e.length}await this._flush(i,s),e()}catch(t){e(t)}}).catch(()=>{})}async _flush(t,e){if(!e)return;const i=this._ws;if(!i)throw new Error("segment write stream not open");const s=1===t.length?t[0]:Buffer.concat(t,e);await new Promise((t,e)=>i.write(s,i=>i?e(i):t())),this._bytes+=e}_final(t){this._ended=!0,this._stopTimer(),this._stopBump(),clearInterval(this._beatTimer),this._job(async()=>{await c(this._ws);const t=this._path&&i.basename(this._path);if(t&&this._audit.files[t]){const e=this._audit.files[t];e.state="closed",e.closedAt=(new Date).toISOString(),e.bytes=r(this._path),this._aevent("close",{file:t}),this._saveAudit()}this._ws=null,this._path=null}).catch(()=>{}).finally(()=>t())}_destroy(t,e){this._ended=!0,this._stopTimer(),this._stopBump(),clearInterval(this._beatTimer),this._ws?.destroy(),this._ws=null,e(t)}async[Symbol.asyncDispose](){this.destroyed||this.writableFinished||await new Promise(t=>this.end(()=>t()))}_job(t){const e=this._tail.then(t,t);return this._tail=e.catch(()=>{}),e}_now(){return Date.now()+this._offsetMs}_stampNow(){const t=Math.floor(this._now()/1e3);var e,i;return t!==this._stampSec&&(this._stampSec=t,this._stampVal=(e=this.datePattern,i=w(this.tz,new Date(1e3*t)),e.replace(/YYYY|YY|MM|DD|HH|mm|ss/g,t=>{switch(t){case"YYYY":return i.year;case"YY":return i.year.slice(-2);case"MM":return v(i.month);case"DD":return v(i.day);case"HH":return v(i.hour);case"mm":return v(i.minute);case"ss":return v(i.second);default:return t}}))),this._stampVal}_resolveName(t,e){return i.join(this.dir,this._tL+t+this._tMid+(e?`.${e}`:"")+this._tExt)}_stampAdvanced(t){return t>this._curStamp||(t<this._curStamp&&!this._warnedBackward&&(this._warnedBackward=!0,this.emit("warn",new Error(`clock moved backward (stamp ${t} < ${this._curStamp}); keeping current segment until the clock catches up`))),!1)}_recoverIndexFor(t){const i=this._rxFor(y(t));let s=-1,a=-1,n=[];try{n=e.readdirSync(this.dir)}catch{}for(const t of n){const e=i.exec(t);if(!e)continue;const n=e[1]?Number.parseInt(e[1],10):0;n>s&&(s=n),!t.endsWith(".gz")&&n>a&&(a=n)}let r=a>=0?a:s>=0?s+1:0;const h=this._audit.activeIndex;return this._auditEnabled&&null!==h&&this._audit.activeStamp===t&&(r=Math.max(r,h)),r}_updateSymlink(){const t=this._symlinkPath;if(!t||!this._path)return;const s=`${t}.tmp`;try{try{e.unlinkSync(s)}catch{}e.symlinkSync(i.basename(this._path),s,"file"),e.renameSync(s,t)}catch(e){this._symlinkPath=null,this.emit("warn",new Error(`symlink unavailable ("${t}"): ${e.message} — continuing without it`))}}async _runSealHook(t,e){const s=this._onSeal;if(!s)return;let a;try{await Promise.race([Promise.resolve().then(()=>s({raw:t,gz:e})),new Promise((s,n)=>{a=setTimeout(()=>n(new Error(`onSeal hook timed out after ${this._sealHookTimeoutMs}ms (${i.basename(e??t)})`)),this._sealHookTimeoutMs)})])}catch(t){this.emit("warn",t)}finally{a&&clearTimeout(a)}}async _ensureOpen(){if(this._ws)return;const t=this._resolveName(this._curStamp,this._index);this._bytes=r(t);const s=e.createWriteStream(t,{flags:"a"});if(s.on("error",t=>this.destroy(t)),this._ws=s,this._path=t,this._updateSymlink(),this._auditEnabled){const e=i.basename(t);this._audit.files[e]={index:this._index,state:"active",gzipPending:!1,openedAt:(new Date).toISOString()},this._setActivePointer(this._index),this._aevent("open",{file:e,index:this._index,resumedBytes:this._bytes}),this._saveAudit()}this.emit("open",t)}async _rotate(t){const s=this._path,a=this._ws;this._ws=null,this._path=null,this._bytes=0;let n=null;if(a&&s){await c(a);const t=r(s),d=i.basename(s),l=this._auditEnabled?this._audit.files[d]:void 0;if(this.zippedArchive&&t>0){n=`${s}.gz`,l&&(l.gzipPending=!0,l.bytes=t,this._saveAudit());await o(s,n,t=>this.emit("warn",t),this._compressionLevel)?(this._finishArchivedRecord(d),await this._runSealHook(s,n)):(l&&(l.gzipPending=!1,l.state="kept-raw"),this._aevent("gzip-failed",{file:d}),n=null,await this._runSealHook(s,null))}else if(0===t){const t=`${s}.gz`;if(h(t))this._finishArchivedRecord(d),n=t,await this._runSealHook(s,t);else{try{e.unlinkSync(s)}catch{}l&&delete this._audit.files[d],this._aevent("drop-empty",{file:d})}}else t>0&&(l&&(l.state="sealed",l.bytes=t),await this._runSealHook(s,null))}let d=this._stampNow();d<this._curStamp&&(this._warnedBackward||(this._warnedBackward=!0,this.emit("warn",new Error(`clock moved backward (stamp ${d} < ${this._curStamp}); rotation clamped to the current period`))),d=this._curStamp);const l=d!==this._curStamp;this._curStamp=d,this._index=l||"time"===t?this._recoverIndexFor(d):this._index+1,this._bytes=0,this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("rotate",{reason:t,nextIndex:this._index,stamp:d}),this._saveAudit()),"time"!==t&&"manual"!==t||this._armTimer(),s&&this.emit("rotated",{reason:t,oldFile:s,archive:n,newFile:this._resolveName(d,this._index)}),await this._retire()}_hasContent(){return this._ws?this._bytes>0||r(this._path)>0:!!(this._path&&r(this._path)>0)}_armTimer(){if(this._stopTimer(),this._ended)return;const t=this._now();var e,i;const s=("daily"===this.freq.type?function(t,e){const i=w(t,new Date(e)),s=Date.UTC(+i.year,+i.month-1,+i.day+1);let a=s-b(t,new Date(s));for(let e=0;e<3;e++){const e=s-b(t,new Date(a));if(e===a)break;a=e}return a<=e&&(a+=864e5),a}(this.tz,t):(e=t,i=this.freq.ms,Math.floor(e/i)*i+i))-this._now(),a=s<0?1e3:Math.max(100,s);this._timer=setTimeout(()=>{this._job(async()=>{const t=this._stampNow();"daily"!==this.freq.type?this._hasContent()&&await this._rotate("time"):await this._crossPeriod(t),this._armTimer()}).catch(()=>{})},a),this.unrefTimers&&this._timer.unref()}async _crossPeriod(t,e=!1){this._stampAdvanced(t)&&(this._hasContent()?await this._rotate("time"):(this._curStamp=t,this._index=this._recoverIndexFor(t),this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("period",e?{stamp:t,virtual:e}:{stamp:t}),this._saveAudit()),this.emit("period",t)))}_armBump(){this._stopBump(),this._stepMs&&!this._ended&&(this._bumpTimer=setInterval(()=>this._job(()=>this._applyBump()).catch(()=>{}),this._bumpEvery),this.unrefTimers&&this._bumpTimer.unref())}_stopBump(){this._bumpTimer&&(clearInterval(this._bumpTimer),this._bumpTimer=null)}async _applyBump(){if(this._ended)return;this._offsetMs+=this._stepMs,this._armTimer();const t=this._stampNow();await this._crossPeriod(t,!0),this._auditEnabled&&(this._aevent("clock-step",{addedMsPerMin:this._stepMs,offsetMs:this._offsetMs,stamp:t}),this._saveAudit())}_stopTimer(){this._timer&&(clearTimeout(this._timer),this._timer=null)}_loadAudit(){let i;try{i=JSON.parse(e.readFileSync(this._auditPath,"utf8"))}catch{try{e.renameSync(this._auditPath,`${this._auditPath}.corrupt`)}catch{}return!1}const s=i;if(!s||1!==s.version||s.prefix!==this._tL||s.ext!==this._tExt){try{e.renameSync(this._auditPath,`${this._auditPath}.corrupt`)}catch{}return!1}this._audit.createdAt=s.createdAt??(new Date).toISOString(),this._audit.activeStamp="string"==typeof s.activeStamp?s.activeStamp:null,this._audit.activeIndex=Number.isInteger(s.activeIndex)?s.activeIndex:null,this._audit.files=s.files&&"object"==typeof s.files?s.files:{},this._audit.events=Array.isArray(s.events)?s.events.slice(-200):[];const a=s.writer;if(a&&"string"==typeof a.bootId&&a.bootId!==this._bootId&&"number"==typeof a.heartbeat&&Date.now()-a.heartbeat<3e4){const e=new Error(`concurrent writer detected (pid ${a.pid}, boot ${a.bootId}); two processes share this log family and will race. If running 'node --watch', exclude the logs dir via --watch-path.`);this.emit("warn",e),this._audit.events.push({id:t(),t:(new Date).toISOString(),type:"concurrent-writer",otherPid:a.pid,otherBoot:a.bootId})}return!0}_saveAudit(){if(this._auditEnabled)try{this._pruneAuditFiles(),this._audit.updatedAt=(new Date).toISOString(),this._audit.writer={pid:process.pid,bootId:this._bootId,heartbeat:Date.now()};const t=`${this._auditPath}.tmp`;e.writeFileSync(t,JSON.stringify(this._audit,null,1)),e.renameSync(t,this._auditPath)}catch(t){this.emit("warn",t)}}_pruneAuditFiles(){const t=this._audit.files,e=Object.keys(t);if(e.length<=600)return;const s=this._path&&i.basename(this._path),a=t=>t.archivedAt??t.closedAt??t.abandonedAt??t.openedAt??"",n=e.filter(e=>e!==s&&"active"!==t[e].state).sort((e,i)=>a(t[e]).localeCompare(a(t[i]))),r=e.length-600+50;for(const e of n.slice(0,r))delete t[e]}_aevent(e,i={}){if(!this._auditEnabled)return;const s=this._audit.events;s.push({id:t(),t:(new Date).toISOString(),type:e,...i}),s.length>200&&s.splice(0,s.length-200)}_setActivePointer(t){this._audit.activeStamp=this._curStamp,this._audit.activeIndex=t}_demoteZombieActives(){const t=this._path&&i.basename(this._path);for(const[e,i]of Object.entries(this._audit.files))"active"===i.state&&e!==t&&(i.state="abandoned",i.abandonedAt=(new Date).toISOString(),this._aevent("abandon",{file:e}))}_reconcileWithDisk(){for(const t of Object.keys(this._audit.files)){const e=this._audit.files[t];if(e.gzipPending)continue;if(t.endsWith(".gz")){const s=h(i.join(this.dir,t)),a=t.slice(0,-3),n=h(i.join(this.dir,a));if(s)continue;if(n){delete this._audit.files[t],this._audit.files[a]={...e,state:"sealed"},this._aevent("archive-regressed",{file:t,backTo:a}),this.emit("warn",new Error(`archive ${t} missing but raw ${a} exists`));continue}this._aevent("vanished",{file:t}),delete this._audit.files[t];continue}const s=h(i.join(this.dir,t)),a=h(i.join(this.dir,`${t}.gz`));s||(a?this._finishArchivedRecord(t,!0):(this._aevent("vanished",{file:t}),delete this._audit.files[t]))}}async _sealOrphanedPastStamps(){if(!this.zippedArchive)return;const t=this._rxActive;let s=[];try{s=e.readdirSync(this.dir)}catch{return}for(const e of s){if(e.endsWith(".gz"))continue;if(e===(this._path&&i.basename(this._path)))continue;const s=t.exec(e)?.[1];if(!s||s>=this._curStamp)continue;const a=i.join(this.dir,e);if(!h(a)||0===r(a))continue;const n=`${a}.gz`;await o(a,n,t=>this.emit("warn",t),this._compressionLevel)&&(this._auditEnabled&&this._audit.files[e]?this._finishArchivedRecord(e,!0):(this._auditEnabled&&(this._audit.files[n]={state:"archived",gzipPending:!1,archivedAt:(new Date).toISOString()},this._aevent("seal-past",{file:n})),this.emit("archive",n)),await this._runSealHook(a,n))}}async _repairInterruptedGzips(){for(const[t,s]of Object.entries(this._audit.files)){if(!s.gzipPending)continue;const a=i.join(this.dir,t),n=`${a}.gz`;if(!h(a)){s.gzipPending=!1,s.state="lost",this._aevent("gzip-lost",{file:t});continue}if(h(n)){try{e.unlinkSync(a)}catch{}this._finishArchivedRecord(t,!0),await this._runSealHook(a,n);continue}await o(a,n,t=>this.emit("warn",t),this._compressionLevel)?(this._finishArchivedRecord(t,!0),await this._runSealHook(a,n)):(s.gzipPending=!1,s.state="kept-raw",this._aevent("gzip-failed",{file:t}))}}_finishArchivedRecord(t,e=!1){const s=this._audit.files[t];if(!s)return;if(s.gzipPending=!1,s.state="archived",s.archivedAt=(new Date).toISOString(),t.endsWith(".gz"))return this.emit("archive",i.join(this.dir,t)),void this._aevent("archive",{file:t,...e&&{repaired:e}});const a=`${t}.gz`;delete this._audit.files[t],this._audit.files[a]=s,this.emit("archive",i.join(this.dir,a)),this._aevent("archive",{file:a,...e&&{repaired:e}})}async _retire(){const t=this.maxFiles;if(!t&&this.maxTotalSize<=0)return;const s=i.basename(this._resolveName(this._curStamp,this._index)),a=[i.basename(this._auditPath),`${i.basename(this._auditPath)}.tmp`,`${i.basename(this._auditPath)}.corrupt`,...this._symlinkPath?[i.basename(this._symlinkPath),`${i.basename(this._symlinkPath)}.tmp`]:[]];try{const n=(await e.promises.readdir(this.dir)).filter(t=>this._rxActive.test(t)&&t!==s&&!a.includes(t)),r=[];for(const t of n){const s=i.join(this.dir,t);try{const t=await e.promises.stat(s);r.push({full:s,mtimeMs:t.mtimeMs,size:t.size})}catch{}}r.sort((t,e)=>t.mtimeMs-e.mtimeMs);const h=new Set;if(null!=t?.days){const e=this._now()-864e5*t.days;for(const t of r)t.mtimeMs<e&&h.add(t.full)}if(null!=t?.count){const e=r.filter(t=>!h.has(t.full)),i=e.length-t.count;for(let t=0;t<i;t++)h.add(e[t].full)}if(this.maxTotalSize>0){const t=r.filter(t=>!h.has(t.full));let e=t.reduce((t,e)=>t+e.size,0),i=0;for(;e>this.maxTotalSize&&i<t.length;)h.add(t[i].full),e-=t[i].size,i++}if(0===h.size)return;for(const t of h)await e.promises.unlink(t).catch(()=>{}),this._auditEnabled&&(delete this._audit.files[i.basename(t)],this._aevent("deleted",{file:i.basename(t)})),this.emit("deleted",t);this._saveAudit()}catch(t){this.emit("warn",t)}}}function S(t){return new g(t)}export{g as R,S as c};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "logroller",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Zero-dependency rotating file stream for Node.js — daily/interval/size rotation, gzip archives, retention, timezone-aware, crash-safe resume",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vinau",
|
|
@@ -39,36 +39,53 @@
|
|
|
39
39
|
"types": "./dist/index.d.ts",
|
|
40
40
|
"exports": {
|
|
41
41
|
".": {
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
"import": {
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"default": "./dist/index.esm.js"
|
|
45
|
+
},
|
|
46
|
+
"require": {
|
|
47
|
+
"types": "./dist/index.d.cts",
|
|
48
|
+
"default": "./dist/index.cjs"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"./pino": {
|
|
52
|
+
"import": {
|
|
53
|
+
"types": "./dist/pino.d.ts",
|
|
54
|
+
"default": "./dist/pino.esm.js"
|
|
55
|
+
},
|
|
56
|
+
"require": {
|
|
57
|
+
"types": "./dist/pino.d.cts",
|
|
58
|
+
"default": "./dist/pino.cjs"
|
|
59
|
+
}
|
|
45
60
|
},
|
|
46
61
|
"./package.json": "./package.json"
|
|
47
62
|
},
|
|
48
63
|
"files": [
|
|
49
64
|
"dist",
|
|
50
65
|
"README.md",
|
|
51
|
-
"LICENSE"
|
|
52
|
-
"CHANGELOG.md"
|
|
66
|
+
"LICENSE"
|
|
53
67
|
],
|
|
54
68
|
"engines": {
|
|
55
69
|
"node": ">=16.14"
|
|
56
70
|
},
|
|
57
71
|
"sideEffects": false,
|
|
58
72
|
"scripts": {
|
|
59
|
-
"build": "
|
|
73
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && rollup -c",
|
|
60
74
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
61
|
-
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.
|
|
62
|
-
"test": "npm run build && node --test --experimental-strip-types test/rotate.test.ts test/features.test.ts",
|
|
75
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
76
|
+
"test": "npm run build && node --test --experimental-strip-types test/rotate.test.ts test/features.test.ts test/fixes.test.ts && node test/smoke.cjs",
|
|
63
77
|
"verify": "npm run typecheck && npm test",
|
|
64
78
|
"prepublishOnly": "npm run verify",
|
|
65
|
-
"release": "npm run verify && npm pack --dry-run"
|
|
79
|
+
"release": "npm run verify && npm pack --dry-run",
|
|
80
|
+
"check:types": "npx --yes @arethetypeswrong/cli --pack ."
|
|
66
81
|
},
|
|
67
82
|
"devDependencies": {
|
|
83
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
68
84
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
69
85
|
"@types/node": "^26.4.0",
|
|
70
86
|
"prettier": "^3.4.2",
|
|
71
87
|
"rollup": "^4.28.1",
|
|
88
|
+
"rollup-plugin-dts": "^6.5.1",
|
|
72
89
|
"tslib": "^2.8.1",
|
|
73
90
|
"typescript": "^5.7.2"
|
|
74
91
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project are documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [2.1.0] — 2026-08-28
|
|
9
|
-
|
|
10
|
-
### Added
|
|
11
|
-
|
|
12
|
-
- `symlink: true | string` — stable "current" pointer (e.g. `app-current.log`)
|
|
13
|
-
for `tail -f`/Filebeat/promtail; atomically flipped on every segment open;
|
|
14
|
-
degrades gracefully on symlink-denying filesystems; excluded from retention.
|
|
15
|
-
- `maxTotalSize: "5g"` — byte-budget retention: deletes OLDEST archives until
|
|
16
|
-
under budget; composes with `maxFiles`.
|
|
17
|
-
- `onSeal` hook (+ `sealHookTimeoutMs`) — awaited after a segment is archived
|
|
18
|
-
and BEFORE retention can delete it: the S3/GCS upload extension point.
|
|
19
|
-
Errors/timeouts degrade to `warn`; rotation never stalls.
|
|
20
|
-
- `installShutdown(streams)` / `flushAll()` — graceful SIGINT/SIGTERM
|
|
21
|
-
handling that flushes buffered writes before exit (fixes the classic
|
|
22
|
-
"last log lines missing" support case).
|
|
23
|
-
- `stats()` and `listSegments()` — health snapshot and per-file listing.
|
|
24
|
-
- `compressionLevel` (0–9, default 6) — passes through to gzip.
|
|
25
|
-
|
|
26
|
-
## [2.0.2] — 2026-08-28
|
|
27
|
-
|
|
28
|
-
### Fixed
|
|
29
|
-
|
|
30
|
-
- **Corrupt audit manifest quarantine**: an unparseable `<stem>_audit.json`
|
|
31
|
-
(partial write, bad edit) is now renamed to `.corrupt` and preserved,
|
|
32
|
-
matching existing behaviour for version/family mismatches. Previously it
|
|
33
|
-
was silently overwritten by the first manifest save, destroying the
|
|
34
|
-
event journal.
|
|
35
|
-
- **Backward-clock safety**: a clock moving backward (NTP correction,
|
|
36
|
-
manual change) no longer reopens a past log family or forks the segment
|
|
37
|
-
series. The stream keeps the current segment, warns once, and resumes
|
|
38
|
-
normal rotation when the clock catches up.
|
|
39
|
-
- **Timer anti-spin**: with the rotation boundary in the past (backward
|
|
40
|
-
clock), the timer now polls at 1 s instead of firing every 100 ms,
|
|
41
|
-
which previously could keep processes from exiting cleanly.
|
|
42
|
-
|
|
43
|
-
### Added
|
|
44
|
-
|
|
45
|
-
- **Construction-time pattern warning**: warns when `frequency` is finer
|
|
46
|
-
than the `datePattern` tokens allow (`daily`→`DD`, sub-daily→`HH`,
|
|
47
|
-
sub-minute→`mm`), preventing silent filename collisions where many
|
|
48
|
-
rotations overwrite a single file.
|
|
49
|
-
|
|
50
|
-
### Tests
|
|
51
|
-
|
|
52
|
-
- Suite expanded to 23 tests: interrupted-gzip repair, manifest divergence
|
|
53
|
-
healing (`archive-regressed`, `vanished`), manifest quarantine,
|
|
54
|
-
concurrent-writer detection, IST-midnight boundary, idle `period` slide,
|
|
55
|
-
`rotateNow()`, over-max resume, backward-clock guard, and coarse-pattern
|
|
56
|
-
warning.
|
|
57
|
-
|
|
58
|
-
## [2.0.1] — 2026-08-27
|
|
59
|
-
|
|
60
|
-
### Fixed
|
|
61
|
-
|
|
62
|
-
- Reactivated segments no longer inherit stale lifecycle timestamps
|
|
63
|
-
(`abandonedAt` could predate `openedAt`).
|
|
64
|
-
- Raw tails of already-passed periods are now gzipped at boot
|
|
65
|
-
(crash-near-midnight no longer leaves yesterday's file uncompressed).
|
|
66
|
-
- Rotation adopts a `.gz` produced by a (warned-about) concurrent writer
|
|
67
|
-
instead of recording a false `drop-empty` that orphaned the archive.
|
|
68
|
-
|
|
69
|
-
## [2.0.0] — 2026-08-28
|
|
70
|
-
|
|
71
|
-
Architecture release: the library moved from a single-file JavaScript SDK to a
|
|
72
|
-
modular **TypeScript** codebase with strict typings, first-class DX tooling,
|
|
73
|
-
and CI-gated releases. The public runtime API (`createStream`,
|
|
74
|
-
`RotateFileStream`, all options and events) remains compatible with 1.x;
|
|
75
|
-
TypeScript consumers gain full type coverage.
|
|
76
|
-
|
|
77
|
-
> ⚠️ Consumers on 1.x should re-run their smoke/integration tests after
|
|
78
|
-
> upgrading — mostly due to the deterministic boot-order fix below.
|
|
79
|
-
|
|
80
|
-
### Added
|
|
81
|
-
|
|
82
|
-
- **Full TypeScript source with shipped declarations** (`dist/index.d.ts`);
|
|
83
|
-
strict-mode compile (`noImplicitOverride`, `noUnusedLocals`,
|
|
84
|
-
`verbatimModuleSyntax`, …).
|
|
85
|
-
- **Tuple-typed events** via `RotateFileStreamEventMap` — `on("rotated", …)`
|
|
86
|
-
autocompletes and payload types are checked (`RotatedInfo`, `ClockInfo`),
|
|
87
|
-
while Node-core fallback overloads keep `.pipe()` structurally valid.
|
|
88
|
-
- `advanceClock()` — apply one virtual-clock tick immediately (previously the
|
|
89
|
-
step timer fired only every minute); makes day-crossing tests fully
|
|
90
|
-
deterministic.
|
|
91
|
-
- `clockStepIntervalMs` option — control how often `addHoursEveryMin` ticks.
|
|
92
|
-
- Modular source layout (`src/types.ts`, `parsers.ts`, `time.ts`, `fsio.ts`,
|
|
93
|
-
`rotate-file-stream.ts`) with `.js` extension imports so emitted
|
|
94
|
-
declarations resolve under `nodenext` consumers.
|
|
95
|
-
- Developer toolchain: Prettier, `npm run verify` gate (typecheck + build + tests).
|
|
96
|
-
- GitHub Actions: CI matrix on Node 18/20/22, release-triggered publish with
|
|
97
|
-
npm provenance (`id-token: write`, `--provenance`).
|
|
98
|
-
- Expanded README: comparison table, FAQ, restart-resume matrix, pino recipe.
|
|
99
|
-
|
|
100
|
-
### Fixed
|
|
101
|
-
|
|
102
|
-
- **Deterministic boot sequencing**: housekeeping (zombie demotion, disk
|
|
103
|
-
reconciliation, interrupted-gzip repair) is chained as job #1 of the
|
|
104
|
-
serial operation queue in the constructor instead of `process.nextTick`,
|
|
105
|
-
so a rapid first `write()` can no longer race a boot-time reopen or skip
|
|
106
|
-
zombie cleanup.
|
|
107
|
-
- Retention sweep skips `<audit>.corrupt` artifacts alongside `.tmp`.
|
|
108
|
-
|
|
109
|
-
### Changed
|
|
110
|
-
|
|
111
|
-
- Internals restructured behind the same public exports; barrels re-export
|
|
112
|
-
types explicitly (`import type { RotatedInfo } from "logroller"`).
|
|
113
|
-
- Stricter package hygiene metadata: `sideEffects: false`, explicit
|
|
114
|
-
`./package.json` export, `files` whitelist (`dist`, docs, license,
|
|
115
|
-
changelog).
|
|
116
|
-
- Test suite runs against the built bundle and covers: base writing +
|
|
117
|
-
manifest creation, size rotation/gzip/indexing, restart resume,
|
|
118
|
-
`addHours` naming, virtual midnight crossing, count-retention ordering
|
|
119
|
-
(mtime-deterministic), zombie-record lifecycle (`abandon → active →
|
|
120
|
-
closed`), and CJS loadability through the exports map.
|
|
121
|
-
|
|
122
|
-
## [1.0.0] — 2026-08-26
|
|
123
|
-
|
|
124
|
-
Initial release. Single-file zero-dependency JavaScript SDK
|
|
125
|
-
(`file-rotation-sdk.js`) providing a Writable stream with scheduled file
|
|
126
|
-
rotation.
|
|
127
|
-
|
|
128
|
-
### Added
|
|
129
|
-
|
|
130
|
-
- Time-based rotation: `frequency: "daily"` (local midnight in any IANA
|
|
131
|
-
timezone via `Intl`) or epoch-aligned intervals (`"30s"`, `"5m"`, `"2h"`).
|
|
132
|
-
- Size-based rotation: `maxSize` (`"20m"` / `"512k"` / raw bytes) producing
|
|
133
|
-
numbered segments — `app-D.log`, `app-D.1.log`, … — inserted between the
|
|
134
|
-
date stamp and the extension.
|
|
135
|
-
- Optional gzip archiving of sealed segments (`zippedArchive`), atomic tmp+
|
|
136
|
-
rename writes, source deleted only after verified compression success.
|
|
137
|
-
- Retention via `maxFiles`: age policy (`"90d"`) or count policy (`500`);
|
|
138
|
-
unset means files are never auto-deleted.
|
|
139
|
-
- Crash-safe restart resume from disk scan: raw tails are appended to,
|
|
140
|
-
sealed chains continue at the next free index; lazy period-roll catches
|
|
141
|
-
boundaries missed while suspended or shut down.
|
|
142
|
-
- Audit manifest (`<stem>_audit.json`): lifecycle records per file, capped
|
|
143
|
-
uuid-tagged event journal, `gzipPending` intent markers enabling automatic
|
|
144
|
-
repair of gzips interrupted by a crash, zombie active-record demotion,
|
|
145
|
-
disk-divergence healing (`archive-regressed`, `vanished`),
|
|
146
|
-
concurrent-writer detection via pid/bootId heartbeat.
|
|
147
|
-
- Virtual test clock: static shift (`addHours`) and per-minute stepping
|
|
148
|
-
(`addHoursEveryMin`).
|
|
149
|
-
- Dual-format distribution (ESM + CJS) bundled with Rollup; named exports
|
|
150
|
-
`createStream` and `RotateFileStream`; factory options mirroring common
|
|
151
|
-
rotator APIs; streams pipe-ready for pino multistreams.
|
package/dist/fsio.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
/** `stat().size` guarded to 0; string paths only. */
|
|
3
|
-
export declare const statSizeSafe: (p: string | null | undefined) => number;
|
|
4
|
-
export declare const existsSafe: (p: string) => boolean;
|
|
5
|
-
/**
|
|
6
|
-
* Compress `src` → `dst` at gzip `level`, deleting `src` ONLY on verified
|
|
7
|
-
* success. On failure removes the partial `dst` and reports via callback —
|
|
8
|
-
* logs degrade to uncompressed rather than losing data.
|
|
9
|
-
*/
|
|
10
|
-
export declare function gzipMove(src: string, dst: string, onWarn?: (err: Error) => void, level?: number): Promise<boolean>;
|
|
11
|
-
export declare const endStream: (ws: fs.WriteStream | null) => Promise<void>;
|
package/dist/parsers.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { RotateFileStreamOptions } from "./types.js";
|
|
2
|
-
export interface ParsedMaxFiles {
|
|
3
|
-
days?: number;
|
|
4
|
-
count?: number;
|
|
5
|
-
}
|
|
6
|
-
export type ParsedFrequency = {
|
|
7
|
-
readonly type: "daily";
|
|
8
|
-
} | {
|
|
9
|
-
readonly type: "interval";
|
|
10
|
-
ms: number;
|
|
11
|
-
};
|
|
12
|
-
export declare function parseSize(v: RotateFileStreamOptions["maxSize"]): number;
|
|
13
|
-
export declare function parseMaxFiles(v: RotateFileStreamOptions["maxFiles"]): ParsedMaxFiles | null;
|
|
14
|
-
export declare function parseFrequency(v: RotateFileStreamOptions["frequency"]): ParsedFrequency;
|
|
15
|
-
/** Static virtual-clock shift. Accepts hours by default; h/hours/m/s/ms. */
|
|
16
|
-
export declare function parseClockOffset(v: RotateFileStreamOptions["addHours"]): number;
|
|
17
|
-
/** Per-tick virtual-clock step. Accepts hours by default; h/hours/m/s/ms. */
|
|
18
|
-
export declare function parseClockStep(v: RotateFileStreamOptions["addHoursEveryMin"]): number;
|
package/dist/time.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** Cached `Intl.DateTimeFormat`; throws synchronously for invalid zones. */
|
|
2
|
-
export declare function getDtf(tz: string): Intl.DateTimeFormat;
|
|
3
|
-
export declare function partsIn(tz: string, date: Date): Record<string, string>;
|
|
4
|
-
/** Escape a literal string for embedding inside a RegExp. */
|
|
5
|
-
export declare function escRe(s: string): string;
|
|
6
|
-
/** Substitute YYYY|YY|MM|DD|HH|mm|ss tokens in `pattern`. */
|
|
7
|
-
export declare function formatStamp(pattern: string, parts: Record<string, string>): string;
|
|
8
|
-
/** Offset of `tz` vs UTC at instant `date`, milliseconds. */
|
|
9
|
-
export declare function tzOffsetMs(tz: string, date: Date): number;
|
|
10
|
-
/** Next local midnight in `tz`, DST-corrected by fixed-point iteration. */
|
|
11
|
-
export declare function nextMidnight(tz: string, fromMs: number): number;
|
|
12
|
-
/** Next epoch-aligned boundary (e.g. hh:00 for step = 1h). */
|
|
13
|
-
export declare const nextAligned: (from: number, step: number) => number;
|
package/dist/types.d.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* logroller — public types.
|
|
3
|
-
*
|
|
4
|
-
* Everything exported here is part of the stable API surface.
|
|
5
|
-
*/
|
|
6
|
-
export type RotationReason = "time" | "size" | "manual";
|
|
7
|
-
/** Options accepted by {@linkcode RotateFileStream} and `createStream`. */
|
|
8
|
-
export interface RotateFileStreamOptions {
|
|
9
|
-
/**
|
|
10
|
-
* Destination filename pattern. REQUIRED and must contain the `%DATE%`
|
|
11
|
-
* token.
|
|
12
|
-
* @example "logs/app-%DATE%.log"
|
|
13
|
-
*/
|
|
14
|
-
filename: string;
|
|
15
|
-
/** Tokens: `YYYY YY MM DD HH mm ss`. Must be coarse-or-equal to your
|
|
16
|
-
* rotation frequency, or segments collapse onto one name.
|
|
17
|
-
* @default "YYYY-MM-DD" */
|
|
18
|
-
datePattern?: string;
|
|
19
|
-
/** Gzip closed segments (`name.log` → `name.log.gz`).
|
|
20
|
-
* @default false */
|
|
21
|
-
zippedArchive?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* gzip level 0–9. 0 = stored (fastest, no ratio), 9 = best ratio,
|
|
24
|
-
* 6 = default library default. Ignored when `zippedArchive` is false.
|
|
25
|
-
* @default 6
|
|
26
|
-
*/
|
|
27
|
-
compressionLevel?: number;
|
|
28
|
-
/**
|
|
29
|
-
* Roll when the next write would exceed this size per segment.
|
|
30
|
-
* `"20m"`, `"512k"`, `"2g"`, or raw bytes. `0` disables.
|
|
31
|
-
* @default 0 (unlimited)
|
|
32
|
-
*/
|
|
33
|
-
maxSize?: number | string;
|
|
34
|
-
/**
|
|
35
|
-
* Retention by age or count:
|
|
36
|
-
* - `"90d"` — delete archived files older than 90 days (mtime)
|
|
37
|
-
* - `500` — keep at most the newest 500 archived files
|
|
38
|
-
* Unset means never auto-delete.
|
|
39
|
-
*/
|
|
40
|
-
maxFiles?: number | string;
|
|
41
|
-
/**
|
|
42
|
-
* Retention by total byte budget across archived files (the active
|
|
43
|
-
* segment is never counted). When archives exceed the budget, the
|
|
44
|
-
* OLDEST are deleted until under budget. Composes with `maxFiles`
|
|
45
|
-
* (either policy may delete). `"5g"`, `"500m"`, or raw bytes.
|
|
46
|
-
* Unset/0 = no byte budget.
|
|
47
|
-
*/
|
|
48
|
-
maxTotalSize?: number | string;
|
|
49
|
-
/** IANA timezone for stamps and the daily boundary.
|
|
50
|
-
* @default "UTC" */
|
|
51
|
-
tz?: string;
|
|
52
|
-
/** `"daily"` (local midnight in `tz`) or interval `"30s"`/`"5m"`/`"2h"`
|
|
53
|
-
* /ms. Interval boundaries align to epoch multiples.
|
|
54
|
-
* @default "daily" */
|
|
55
|
-
frequency?: number | string;
|
|
56
|
-
/**
|
|
57
|
-
* TEST ONLY. Static shift of the internal clock. `"-6h"`, `"24h"`,
|
|
58
|
-
* plain/fractional number, or `"30m"`/`"90s"`.
|
|
59
|
-
*/
|
|
60
|
-
addHours?: number | string;
|
|
61
|
-
/**
|
|
62
|
-
* TEST ONLY. Virtual time added per tick (see `clockStepIntervalMs`).
|
|
63
|
-
* Negative values exercise the backward-clock guard (no rotations).
|
|
64
|
-
*/
|
|
65
|
-
addHoursEveryMin?: number | string;
|
|
66
|
-
/** Real interval between virtual-clock ticks.
|
|
67
|
-
* @default 60_000 */
|
|
68
|
-
clockStepIntervalMs?: number;
|
|
69
|
-
/**
|
|
70
|
-
* Maintain a stable "current" pointer for log collectors
|
|
71
|
-
* (`tail -f`, Filebeat, promtail). `true` derives the name from the
|
|
72
|
-
* prefix (`"app-"` → `app-current.log`); a string names it exactly.
|
|
73
|
-
* The link is updated atomically each time a segment opens. Best-effort:
|
|
74
|
-
* on filesystems denying symlinks (Windows without Developer Mode) it
|
|
75
|
-
* warns once and disables itself.
|
|
76
|
-
* @default false */
|
|
77
|
-
symlink?: boolean | string;
|
|
78
|
-
/**
|
|
79
|
-
* Awaited after a segment is sealed/archived and BEFORE retention can
|
|
80
|
-
* delete it — the extension point for uploading to S3/GCS. Errors and
|
|
81
|
-
* timeouts degrade to a `warn`; they never break rotation.
|
|
82
|
-
* @default undefined */
|
|
83
|
-
onSeal?: (info: SealInfo) => Promise<void> | void;
|
|
84
|
-
/** Hard deadline for one `onSeal` invocation.
|
|
85
|
-
* @default 30_000 */
|
|
86
|
-
sealHookTimeoutMs?: number;
|
|
87
|
-
/** Detach rotation timers from the event loop.
|
|
88
|
-
* @default false */
|
|
89
|
-
unrefTimers?: boolean;
|
|
90
|
-
/** Maintain `<stem>_audit.json` (library-owned; treat as read-only).
|
|
91
|
-
* @default true */
|
|
92
|
-
audit?: boolean;
|
|
93
|
-
/** Override the audit manifest filename. */
|
|
94
|
-
auditFile?: string;
|
|
95
|
-
/** Passed through to the underlying `Writable`. */
|
|
96
|
-
highWaterMark?: number;
|
|
97
|
-
}
|
|
98
|
-
/** Payload delivered to `onSeal` when a segment completes its lifecycle. */
|
|
99
|
-
export interface SealInfo {
|
|
100
|
-
/** Closed segment path. May already be unlinked when `gz` exists. */
|
|
101
|
-
raw: string;
|
|
102
|
-
/** Compressed artifact; `null` when compression is off or failed. */
|
|
103
|
-
gz: string | null;
|
|
104
|
-
}
|
|
105
|
-
/** One file of the stream family, as reported by `listSegments()`. */
|
|
106
|
-
export interface SegmentInfo {
|
|
107
|
-
file: string;
|
|
108
|
-
/** Date stamp embedded in the filename. */
|
|
109
|
-
stamp: string;
|
|
110
|
-
/** Segment index within the stamp (0 = base file). */
|
|
111
|
-
index: number;
|
|
112
|
-
gzipped: boolean;
|
|
113
|
-
bytes: number;
|
|
114
|
-
/** Audit state, or `"untracked"` for files unknown to the manifest. */
|
|
115
|
-
state: string;
|
|
116
|
-
openedAt?: string;
|
|
117
|
-
archivedAt?: string;
|
|
118
|
-
}
|
|
119
|
-
/** Snapshot for dashboards/health checks — see `stats()`. */
|
|
120
|
-
export interface StreamStats {
|
|
121
|
-
dir: string;
|
|
122
|
-
currentStamp: string;
|
|
123
|
-
activeFile: string | null;
|
|
124
|
-
activeIndex: number;
|
|
125
|
-
/** All family files on disk (active included). */
|
|
126
|
-
segments: number;
|
|
127
|
-
/** Of those, how many are compressed archives. */
|
|
128
|
-
archived: number;
|
|
129
|
-
totalBytes: number;
|
|
130
|
-
/** Active virtual-clock shift (non-zero only under test knobs). */
|
|
131
|
-
clockOffsetMs: number;
|
|
132
|
-
/** Active symlink path, or `null`. */
|
|
133
|
-
symlink: string | null;
|
|
134
|
-
}
|
|
135
|
-
/** Payload for the `rotated` event. */
|
|
136
|
-
export interface RotatedInfo {
|
|
137
|
-
reason: RotationReason;
|
|
138
|
-
oldFile: string | null;
|
|
139
|
-
archive: string | null;
|
|
140
|
-
newFile: string;
|
|
141
|
-
}
|
|
142
|
-
/** Payload for the `clock` event. */
|
|
143
|
-
export interface ClockInfo {
|
|
144
|
-
addHours?: number | string;
|
|
145
|
-
addHoursEveryMin?: number | string;
|
|
146
|
-
offsetMs: number;
|
|
147
|
-
}
|
|
148
|
-
/** Tuple-style event map typing `on` / `once` / `emit`. */
|
|
149
|
-
export interface RotateFileStreamEventMap {
|
|
150
|
-
open: [file: string];
|
|
151
|
-
rotated: [info: RotatedInfo];
|
|
152
|
-
archive: [gzFile: string];
|
|
153
|
-
deleted: [file: string];
|
|
154
|
-
period: [stamp: string];
|
|
155
|
-
clock: [info: ClockInfo];
|
|
156
|
-
warn: [err: Error];
|
|
157
|
-
error: [err: Error];
|
|
158
|
-
close: [];
|
|
159
|
-
}
|