1- import fs from "node:fs/promises" ;
2- import path from "node:path" ;
3- import { getResolvedLoggerSettings } from "../../logging.js" ;
4- import { clamp } from "../../utils.js" ;
1+ import { readConfiguredLogTail } from "../../logging/log-tail.js" ;
52import {
63 ErrorCodes ,
74 errorShape ,
@@ -10,140 +7,6 @@ import {
107} from "../protocol/index.js" ;
118import type { GatewayRequestHandlers } from "./types.js" ;
129
13- const DEFAULT_LIMIT = 500 ;
14- const DEFAULT_MAX_BYTES = 250_000 ;
15- const MAX_LIMIT = 5000 ;
16- const MAX_BYTES = 1_000_000 ;
17- const ROLLING_LOG_RE = / ^ o p e n c l a w - \d { 4 } - \d { 2 } - \d { 2 } \. l o g $ / ;
18-
19- function isRollingLogFile ( file : string ) : boolean {
20- return ROLLING_LOG_RE . test ( path . basename ( file ) ) ;
21- }
22-
23- async function resolveLogFile ( file : string ) : Promise < string > {
24- const stat = await fs . stat ( file ) . catch ( ( ) => null ) ;
25- if ( stat ) {
26- return file ;
27- }
28- if ( ! isRollingLogFile ( file ) ) {
29- return file ;
30- }
31-
32- const dir = path . dirname ( file ) ;
33- const entries = await fs . readdir ( dir , { withFileTypes : true } ) . catch ( ( ) => null ) ;
34- if ( ! entries ) {
35- return file ;
36- }
37-
38- const candidates = await Promise . all (
39- entries
40- . filter ( ( entry ) => entry . isFile ( ) && ROLLING_LOG_RE . test ( entry . name ) )
41- . map ( async ( entry ) => {
42- const fullPath = path . join ( dir , entry . name ) ;
43- const fileStat = await fs . stat ( fullPath ) . catch ( ( ) => null ) ;
44- return fileStat ? { path : fullPath , mtimeMs : fileStat . mtimeMs } : null ;
45- } ) ,
46- ) ;
47- const sorted = candidates
48- . filter ( ( entry ) : entry is NonNullable < typeof entry > => Boolean ( entry ) )
49- . toSorted ( ( a , b ) => b . mtimeMs - a . mtimeMs ) ;
50- return sorted [ 0 ] ?. path ?? file ;
51- }
52-
53- async function readLogSlice ( params : {
54- file : string ;
55- cursor ?: number ;
56- limit : number ;
57- maxBytes : number ;
58- } ) {
59- const stat = await fs . stat ( params . file ) . catch ( ( ) => null ) ;
60- if ( ! stat ) {
61- return {
62- cursor : 0 ,
63- size : 0 ,
64- lines : [ ] as string [ ] ,
65- truncated : false ,
66- reset : false ,
67- } ;
68- }
69-
70- const size = stat . size ;
71- const maxBytes = clamp ( params . maxBytes , 1 , MAX_BYTES ) ;
72- const limit = clamp ( params . limit , 1 , MAX_LIMIT ) ;
73- let cursor =
74- typeof params . cursor === "number" && Number . isFinite ( params . cursor )
75- ? Math . max ( 0 , Math . floor ( params . cursor ) )
76- : undefined ;
77- let reset = false ;
78- let truncated = false ;
79- let start = 0 ;
80-
81- if ( cursor != null ) {
82- if ( cursor > size ) {
83- reset = true ;
84- start = Math . max ( 0 , size - maxBytes ) ;
85- truncated = start > 0 ;
86- } else {
87- start = cursor ;
88- if ( size - start > maxBytes ) {
89- reset = true ;
90- truncated = true ;
91- start = Math . max ( 0 , size - maxBytes ) ;
92- }
93- }
94- } else {
95- start = Math . max ( 0 , size - maxBytes ) ;
96- truncated = start > 0 ;
97- }
98-
99- if ( size === 0 || size <= start ) {
100- return {
101- cursor : size ,
102- size,
103- lines : [ ] as string [ ] ,
104- truncated,
105- reset,
106- } ;
107- }
108-
109- const handle = await fs . open ( params . file , "r" ) ;
110- try {
111- let prefix = "" ;
112- if ( start > 0 ) {
113- const prefixBuf = Buffer . alloc ( 1 ) ;
114- const prefixRead = await handle . read ( prefixBuf , 0 , 1 , start - 1 ) ;
115- prefix = prefixBuf . toString ( "utf8" , 0 , prefixRead . bytesRead ) ;
116- }
117-
118- const length = Math . max ( 0 , size - start ) ;
119- const buffer = Buffer . alloc ( length ) ;
120- const readResult = await handle . read ( buffer , 0 , length , start ) ;
121- const text = buffer . toString ( "utf8" , 0 , readResult . bytesRead ) ;
122- let lines = text . split ( "\n" ) ;
123- if ( start > 0 && prefix !== "\n" ) {
124- lines = lines . slice ( 1 ) ;
125- }
126- if ( lines . length > 0 && lines [ lines . length - 1 ] === "" ) {
127- lines = lines . slice ( 0 , - 1 ) ;
128- }
129- if ( lines . length > limit ) {
130- lines = lines . slice ( lines . length - limit ) ;
131- }
132-
133- cursor = size ;
134-
135- return {
136- cursor,
137- size,
138- lines,
139- truncated,
140- reset,
141- } ;
142- } finally {
143- await handle . close ( ) ;
144- }
145- }
146-
14710export const logsHandlers : GatewayRequestHandlers = {
14811 "logs.tail" : async ( { params, respond } ) => {
14912 if ( ! validateLogsTailParams ( params ) ) {
@@ -159,16 +22,13 @@ export const logsHandlers: GatewayRequestHandlers = {
15922 }
16023
16124 const p = params as { cursor ?: number ; limit ?: number ; maxBytes ?: number } ;
162- const configuredFile = getResolvedLoggerSettings ( ) . file ;
16325 try {
164- const file = await resolveLogFile ( configuredFile ) ;
165- const result = await readLogSlice ( {
166- file,
26+ const result = await readConfiguredLogTail ( {
16727 cursor : p . cursor ,
168- limit : p . limit ?? DEFAULT_LIMIT ,
169- maxBytes : p . maxBytes ?? DEFAULT_MAX_BYTES ,
28+ limit : p . limit ,
29+ maxBytes : p . maxBytes ,
17030 } ) ;
171- respond ( true , { file , ... result } , undefined ) ;
31+ respond ( true , result , undefined ) ;
17232 } catch ( err ) {
17333 respond (
17434 false ,
0 commit comments