@@ -40,7 +40,7 @@ export const enum TargetAudience {
4040 Audit = "ADT"
4141}
4242
43- //properties here are "LogType" values in Ws_logaccess.GetLogAccessInfo
43+ // properties here are "LogType" values in Ws_logaccess.GetLogAccessInfo
4444export interface LogLine {
4545 audience ?: string ;
4646 class ?: string ;
@@ -59,6 +59,142 @@ export interface GetLogsExResponse {
5959 total : number ,
6060}
6161
62+ const knownLogManagerTypes = new Set ( [ "azureloganalyticscurl" , "elasticstack" , "grafanacurl" ] ) ;
63+ const logColumnTypeValues = new Set ( Object . values ( WsLogaccess . LogColumnType ) ) ;
64+
65+ function getLogCategory ( searchField : string ) : WsLogaccess . LogAccessType {
66+ switch ( searchField ) {
67+ case WsLogaccess . LogColumnType . workunits :
68+ case "hpcc.log.jobid" :
69+ return WsLogaccess . LogAccessType . ByJobID ;
70+ case WsLogaccess . LogColumnType . audience :
71+ case "hpcc.log.audience" :
72+ return WsLogaccess . LogAccessType . ByTargetAudience ;
73+ case WsLogaccess . LogColumnType . class :
74+ case "hpcc.log.class" :
75+ return WsLogaccess . LogAccessType . ByLogType ;
76+ case WsLogaccess . LogColumnType . components :
77+ case "kubernetes.container.name" :
78+ return WsLogaccess . LogAccessType . ByComponent ;
79+ default :
80+ return WsLogaccess . LogAccessType . ByFieldName ;
81+ }
82+ }
83+
84+ // Explicit list of filter-bearing keys on GetLogsExRequest.
85+ // Using an allowlist avoids accidentally treating control fields (StartDate, LogLineLimit, etc.)
86+ // as log filters if the server ever returns a column whose name collides with them.
87+ const FILTER_KEYS = [ "audience" , "class" , "workunits" , "message" , "processid" , "logid" , "threadid" , "timestamp" , "components" , "instance" ] ;
88+
89+ function buildFilters ( request : GetLogsExRequest , columnMap : Record < string , string > ) : WsLogaccess . leftFilter [ ] {
90+ const filters : WsLogaccess . leftFilter [ ] = [ ] ;
91+ for ( const key of FILTER_KEYS ) {
92+ const value = request [ key ] ;
93+ if ( value == null || value === "" || ( Array . isArray ( value ) && value . length === 0 ) ) {
94+ continue ;
95+ }
96+ if ( ! ( key in columnMap ) ) continue ;
97+
98+ const isKnownLogType = logColumnTypeValues . has ( key as WsLogaccess . LogColumnType ) ;
99+ let searchField : string = isKnownLogType ? key : columnMap [ key ] ;
100+ const logCategory = getLogCategory ( searchField ) ;
101+ if ( logCategory === WsLogaccess . LogAccessType . ByFieldName ) {
102+ searchField = columnMap [ key ] ;
103+ }
104+
105+ const appendWildcard = logCategory === WsLogaccess . LogAccessType . ByComponent ;
106+ const rawValues : string [ ] = Array . isArray ( value ) ? value : [ value as string ] ;
107+ for ( const raw of rawValues ) {
108+ filters . push ( {
109+ LogCategory : logCategory ,
110+ SearchField : searchField ,
111+ // append wildcard to end of search value to include ephemeral
112+ // containers that aren't listed in ECL Watch's filters
113+ SearchByValue : appendWildcard ? raw + "*" : raw
114+ } ) ;
115+ }
116+ }
117+ return filters ;
118+ }
119+
120+ // Builds a left-leaning OR chain from filters that share the same SearchField.
121+ function buildOrGroup ( group : WsLogaccess . leftFilter [ ] ) : WsLogaccess . BinaryLogFilter {
122+ const root : WsLogaccess . BinaryLogFilter = { leftFilter : group [ 0 ] } as WsLogaccess . BinaryLogFilter ;
123+ let node = root ;
124+ for ( let i = 1 ; i < group . length ; i ++ ) {
125+ node . Operator = WsLogaccess . LogAccessFilterOperator . OR ;
126+ if ( i === group . length - 1 ) {
127+ node . rightFilter = group [ i ] as WsLogaccess . rightFilter ;
128+ } else {
129+ node . rightBinaryFilter = { BinaryLogFilter : [ { leftFilter : group [ i ] } as WsLogaccess . BinaryLogFilter ] } ;
130+ node = node . rightBinaryFilter . BinaryLogFilter [ 0 ] ;
131+ }
132+ }
133+ return root ;
134+ }
135+
136+ // Recursively AND-chains two or more groups into a BinaryLogFilter (used for nesting beyond depth 1).
137+ function buildAndChain ( groups : WsLogaccess . leftFilter [ ] [ ] ) : WsLogaccess . BinaryLogFilter {
138+ const [ firstGroup , ...remainingGroups ] = groups ;
139+ const node : WsLogaccess . BinaryLogFilter = { } as WsLogaccess . BinaryLogFilter ;
140+ if ( firstGroup . length === 1 ) {
141+ node . leftFilter = firstGroup [ 0 ] ;
142+ } else {
143+ node . leftBinaryFilter = { BinaryLogFilter : [ buildOrGroup ( firstGroup ) ] } ;
144+ }
145+ if ( remainingGroups . length === 0 ) return node ;
146+ node . Operator = WsLogaccess . LogAccessFilterOperator . AND ;
147+ if ( remainingGroups . length === 1 ) {
148+ const [ secondGroup ] = remainingGroups ;
149+ if ( secondGroup . length === 1 ) {
150+ node . rightFilter = secondGroup [ 0 ] as WsLogaccess . rightFilter ;
151+ } else {
152+ node . rightBinaryFilter = { BinaryLogFilter : [ buildOrGroup ( secondGroup ) ] } ;
153+ }
154+ } else {
155+ node . rightBinaryFilter = { BinaryLogFilter : [ buildAndChain ( remainingGroups ) ] } ;
156+ }
157+ return node ;
158+ }
159+
160+ // Groups filters by SearchField, OR-chains each group, then AND-chains the groups together.
161+ // This ensures e.g. [class_INF, class_ERR, audience_USR] always produces
162+ // (class_INF OR class_ERR) AND audience_USR regardless of input order.
163+ function buildFilterTree ( filters : WsLogaccess . leftFilter [ ] ) : WsLogaccess . Filter {
164+ const groupMap = new Map < string , WsLogaccess . leftFilter [ ] > ( ) ;
165+ for ( const f of filters ) {
166+ const existing = groupMap . get ( f . SearchField ) ;
167+ if ( existing ) existing . push ( f ) ; else groupMap . set ( f . SearchField , [ f ] ) ;
168+ }
169+ const groups = [ ...groupMap . values ( ) ] ;
170+
171+ if ( groups . length === 0 ) {
172+ return { leftFilter : { LogCategory : WsLogaccess . LogAccessType . All } as WsLogaccess . leftFilter } ;
173+ }
174+
175+ const [ firstGroup , ...remainingGroups ] = groups ;
176+ const filter : WsLogaccess . Filter = { } ;
177+ if ( firstGroup . length === 1 ) {
178+ filter . leftFilter = firstGroup [ 0 ] ;
179+ } else {
180+ filter . leftBinaryFilter = { BinaryLogFilter : [ buildOrGroup ( firstGroup ) ] } ;
181+ }
182+
183+ if ( remainingGroups . length === 0 ) return filter ;
184+ filter . Operator = WsLogaccess . LogAccessFilterOperator . AND ;
185+ if ( remainingGroups . length === 1 ) {
186+ const [ secondGroup ] = remainingGroups ;
187+ if ( secondGroup . length === 1 ) {
188+ filter . rightFilter = secondGroup [ 0 ] as WsLogaccess . rightFilter ;
189+ } else {
190+ filter . rightBinaryFilter = { BinaryLogFilter : [ buildOrGroup ( secondGroup ) ] } ;
191+ }
192+ } else {
193+ filter . rightBinaryFilter = { BinaryLogFilter : [ buildAndChain ( remainingGroups ) ] } ;
194+ }
195+ return filter ;
196+ }
197+
62198export class LogaccessService extends LogaccessServiceBase {
63199
64200 protected _logAccessInfo : Promise < WsLogaccess . GetLogAccessInfoResponse > ;
@@ -74,35 +210,27 @@ export class LogaccessService extends LogaccessServiceBase {
74210 return super . GetLogs ( request ) ;
75211 }
76212
213+ private convertLogLine ( columnMap : Record < string , string > , line : any ) : LogLine {
214+ const retVal : LogLine = { } ;
215+ const fields = line ?. fields ? Object . assign ( { } , ...line . fields ) : null ;
216+ for ( const key in columnMap ) {
217+ retVal [ key ] = fields ? fields [ columnMap [ key ] ] ?? "" : "" ;
218+ }
219+ return retVal ;
220+ }
221+
77222 async GetLogsEx ( request : GetLogsExRequest ) : Promise < GetLogsExResponse > {
78223 const logInfo = await this . GetLogAccessInfo ( ) ;
79- const columnMap = { } ;
224+ const columnMap : Record < string , string > = { } ;
80225 logInfo . Columns . Column . forEach ( column => columnMap [ column . LogType ] = column . Name ) ;
81226
82- const convertLogLine = ( line : any ) => {
83- const retVal : LogLine = { } ;
84- for ( const key in columnMap ) {
85- if ( line ?. fields ) {
86- retVal [ key ] = Object . assign ( { } , ...line . fields ) [ columnMap [ key ] ] ?? "" ;
87- } else {
88- retVal [ key ] = "" ;
89- }
90- }
91- return retVal ;
92- } ;
227+ const filters = buildFilters ( request , columnMap ) ;
93228
94229 const getLogsRequest : WsLogaccess . GetLogsRequest = {
95- Filter : {
96- leftBinaryFilter : {
97- BinaryLogFilter : [ {
98- leftFilter : {
99- LogCategory : WsLogaccess . LogAccessType . All ,
100- } ,
101- } as WsLogaccess . BinaryLogFilter ]
102- }
103- } ,
230+ Filter : buildFilterTree ( filters ) ,
104231 Range : {
105- StartDate : new Date ( 0 ) . toISOString ( ) ,
232+ StartDate : request . StartDate instanceof Date ? request . StartDate . toISOString ( ) : new Date ( 0 ) . toISOString ( ) ,
233+ EndDate : request . EndDate instanceof Date ? request . EndDate . toISOString ( ) : undefined
106234 } ,
107235 LogLineStartFrom : request . LogLineStartFrom ?? 0 ,
108236 LogLineLimit : request . LogLineLimit ?? 100 ,
@@ -117,142 +245,14 @@ export class LogaccessService extends LogaccessServiceBase {
117245 }
118246 } ;
119247
120- const filters : WsLogaccess . leftFilter [ ] = [ ] ;
121- const logTypes = Object . values ( WsLogaccess . LogColumnType ) ;
122- for ( const key in request ) {
123- if ( request [ key ] == null || request [ key ] === "" || ( Array . isArray ( request [ key ] ) && request [ key ] . length === 0 ) ) {
124- continue ;
125- }
126- let searchField ;
127- if ( key in columnMap ) {
128- if ( logTypes . includes ( key as WsLogaccess . LogColumnType ) ) {
129- searchField = key ;
130- } else {
131- searchField = columnMap [ key ] ;
132- }
133- }
134- let logCategory ;
135- if ( searchField ) {
136- switch ( searchField ) {
137- case WsLogaccess . LogColumnType . workunits :
138- case "hpcc.log.jobid" :
139- logCategory = WsLogaccess . LogAccessType . ByJobID ;
140- break ;
141- case WsLogaccess . LogColumnType . audience :
142- case "hpcc.log.audience" :
143- logCategory = WsLogaccess . LogAccessType . ByTargetAudience ;
144- break ;
145- case WsLogaccess . LogColumnType . class :
146- case "hpcc.log.class" :
147- logCategory = WsLogaccess . LogAccessType . ByLogType ;
148- break ;
149- case WsLogaccess . LogColumnType . components :
150- case "kubernetes.container.name" :
151- logCategory = WsLogaccess . LogAccessType . ByComponent ;
152- break ;
153- default :
154- logCategory = WsLogaccess . LogAccessType . ByFieldName ;
155- searchField = columnMap [ key ] ;
156- }
157- if ( Array . isArray ( request [ key ] ) ) {
158- request [ key ] . forEach ( value => {
159- if ( logCategory === WsLogaccess . LogAccessType . ByComponent ) {
160- value += "*" ;
161- }
162- filters . push ( {
163- LogCategory : logCategory ,
164- SearchField : searchField ,
165- SearchByValue : value
166- } ) ;
167- } ) ;
168- } else {
169- let value = request [ key ] ;
170- if ( logCategory === WsLogaccess . LogAccessType . ByComponent ) {
171- // append wildcard to end of search value to include ephemeral
172- // containers that aren't listed in ECL Watch's filters
173- value += "*" ;
174- }
175- filters . push ( {
176- LogCategory : logCategory ,
177- SearchField : searchField ,
178- SearchByValue : value
179- } ) ;
180- }
181- }
182- }
183-
184- if ( filters . length > 2 ) {
185- let binaryLogFilter = getLogsRequest . Filter . leftBinaryFilter . BinaryLogFilter [ 0 ] ;
186- filters . forEach ( ( filter , i ) => {
187- let operator = WsLogaccess . LogAccessFilterOperator . AND ;
188- if ( i > 0 ) {
189- if ( filters [ i - 1 ] . SearchField === filter . SearchField ) {
190- operator = WsLogaccess . LogAccessFilterOperator . OR ;
191- }
192- if ( i === filters . length - 1 ) {
193- binaryLogFilter . Operator = operator ;
194- binaryLogFilter . rightFilter = filter as WsLogaccess . rightFilter ;
195- } else {
196- binaryLogFilter . Operator = operator ;
197- binaryLogFilter . rightBinaryFilter = {
198- BinaryLogFilter : [ {
199- leftFilter : filter
200- } as WsLogaccess . BinaryLogFilter ]
201- } ;
202- binaryLogFilter = binaryLogFilter . rightBinaryFilter . BinaryLogFilter [ 0 ] ;
203- }
204- } else {
205- binaryLogFilter . leftFilter = filter as WsLogaccess . leftFilter ;
206- }
207- } ) ;
208- } else {
209- delete getLogsRequest . Filter . leftBinaryFilter ;
210- getLogsRequest . Filter . leftFilter = {
211- LogCategory : WsLogaccess . LogAccessType . All
212- } as WsLogaccess . leftFilter ;
213- if ( filters [ 0 ] ?. SearchField ) {
214- getLogsRequest . Filter . leftFilter = {
215- LogCategory : filters [ 0 ] ?. LogCategory ,
216- SearchField : filters [ 0 ] ?. SearchField ,
217- SearchByValue : filters [ 0 ] ?. SearchByValue
218- } ;
219- }
220- if ( filters [ 1 ] ?. SearchField ) {
221- getLogsRequest . Filter . Operator = WsLogaccess . LogAccessFilterOperator . AND ;
222- if ( filters [ 0 ] . SearchField === filters [ 1 ] . SearchField ) {
223- getLogsRequest . Filter . Operator = WsLogaccess . LogAccessFilterOperator . OR ;
224- }
225- getLogsRequest . Filter . rightFilter = {
226- LogCategory : filters [ 1 ] ?. LogCategory ,
227- SearchField : filters [ 1 ] ?. SearchField ,
228- SearchByValue : filters [ 1 ] ?. SearchByValue
229- } ;
230- }
231- }
232-
233- if ( request . StartDate ) {
234- getLogsRequest . Range . StartDate = request . StartDate . toISOString ( ) ;
235- }
236- if ( request . EndDate ) {
237- getLogsRequest . Range . EndDate = request . EndDate . toISOString ( ) ;
238- }
239-
240248 return this . GetLogs ( getLogsRequest ) . then ( response => {
241249 try {
242250 const logLines = JSON . parse ( response . LogLines ) ;
243- let lines = [ ] ;
244- switch ( logInfo . RemoteLogManagerType ) {
245- case "azureloganalyticscurl" :
246- case "elasticstack" :
247- case "grafanacurl" :
248- lines = logLines . lines ?. map ( convertLogLine ) ?? [ ] ;
249- break ;
250- default :
251- logger . warning ( `Unknown RemoteLogManagerType: ${ logInfo . RemoteLogManagerType } ` ) ;
252- lines = [ ] ;
253- }
251+ const lines = knownLogManagerTypes . has ( logInfo . RemoteLogManagerType )
252+ ? ( logLines . lines ?. map ( ( line : any ) => this . convertLogLine ( columnMap , line ) ) ?? [ ] )
253+ : ( logger . warning ( `Unknown RemoteLogManagerType: ${ logInfo . RemoteLogManagerType } ` ) , [ ] ) ;
254254 return {
255- lines : lines ,
255+ lines,
256256 total : response . TotalLogLinesAvailable ?? 10000
257257 } ;
258258 } catch ( e : any ) {
0 commit comments