@hpcc-js/comms
Version:
hpcc-js - Communications
271 lines (242 loc) • 10.5 kB
text/typescript
import { scopedLogger } from "@hpcc-js/util";
import { LogaccessServiceBase, WsLogaccess } from "./wsdl/ws_logaccess/v1.08/ws_logaccess.ts";
const logger = scopedLogger("@hpcc-js/comms/services/wsLogaccess.ts");
export {
WsLogaccess
};
export interface GetLogsExRequest {
audience?: string;
class?: string[];
workunits?: string;
message?: string;
processid?: string;
logid?: string;
threadid?: string;
timestamp?: string;
components?: string;
instance?: string;
StartDate?: Date;
EndDate?: Date;
LogLineStartFrom: number,
LogLineLimit: number
}
export const enum LogType {
Disaster = "DIS",
Error = "ERR",
Warning = "WRN",
Information = "INF",
Progress = "PRO",
Metric = "MET"
}
export const enum TargetAudience {
Operator = "OPR",
User = "USR",
Programmer = "PRO",
Audit = "ADT"
}
// properties here are "LogType" values in Ws_logaccess.GetLogAccessInfo
export interface LogLine {
audience?: string;
class?: string;
workunits?: string;
message?: string;
processid?: number;
logid?: string;
threadid?: number;
timestamp?: string;
components?: string;
instance?: string;
}
export interface GetLogsExResponse {
lines: LogLine[],
total: number,
}
const knownLogManagerTypes = new Set(["azureloganalyticscurl", "elasticstack", "grafanacurl"]);
const logColumnTypeValues = new Set(Object.values(WsLogaccess.LogColumnType));
function getLogCategory(searchField: string): WsLogaccess.LogAccessType {
switch (searchField) {
case WsLogaccess.LogColumnType.workunits:
case "hpcc.log.jobid":
return WsLogaccess.LogAccessType.ByJobID;
case WsLogaccess.LogColumnType.audience:
case "hpcc.log.audience":
return WsLogaccess.LogAccessType.ByTargetAudience;
case WsLogaccess.LogColumnType.class:
case "hpcc.log.class":
return WsLogaccess.LogAccessType.ByLogType;
case WsLogaccess.LogColumnType.components:
case "kubernetes.container.name":
return WsLogaccess.LogAccessType.ByComponent;
default:
return WsLogaccess.LogAccessType.ByFieldName;
}
}
// Explicit list of filter-bearing keys on GetLogsExRequest.
// Using an allowlist avoids accidentally treating control fields (StartDate, LogLineLimit, etc.)
// as log filters if the server ever returns a column whose name collides with them.
const FILTER_KEYS = ["audience", "class", "workunits", "message", "processid", "logid", "threadid", "timestamp", "components", "instance"] as const;
function buildFilters(request: GetLogsExRequest, columnMap: Record<string, string>): WsLogaccess.leftFilter[] {
const filters: WsLogaccess.leftFilter[] = [];
for (const key of FILTER_KEYS) {
const value = request[key];
if (value == null || value === "" || (Array.isArray(value) && value.length === 0)) {
continue;
}
if (!(key in columnMap)) continue;
const isKnownLogType = logColumnTypeValues.has(key as WsLogaccess.LogColumnType);
let searchField: string = isKnownLogType ? key : columnMap[key];
const logCategory = getLogCategory(searchField);
if (logCategory === WsLogaccess.LogAccessType.ByFieldName) {
searchField = columnMap[key];
}
const appendWildcard = logCategory === WsLogaccess.LogAccessType.ByComponent;
const rawValues: string[] = Array.isArray(value) ? value : [value as string];
for (const raw of rawValues) {
filters.push({
LogCategory: logCategory,
SearchField: searchField,
// append wildcard to end of search value to include ephemeral
// containers that aren't listed in ECL Watch's filters
SearchByValue: appendWildcard ? raw + "*" : raw
});
}
}
return filters;
}
// Builds a left-leaning OR chain from filters that share the same SearchField.
function buildOrGroup(group: WsLogaccess.leftFilter[]): WsLogaccess.BinaryLogFilter {
const root: WsLogaccess.BinaryLogFilter = { leftFilter: group[0] } as WsLogaccess.BinaryLogFilter;
let node = root;
for (let i = 1; i < group.length; i++) {
node.Operator = WsLogaccess.LogAccessFilterOperator.OR;
if (i === group.length - 1) {
node.rightFilter = group[i] as WsLogaccess.rightFilter;
} else {
node.rightBinaryFilter = { BinaryLogFilter: [{ leftFilter: group[i] } as WsLogaccess.BinaryLogFilter] };
node = node.rightBinaryFilter.BinaryLogFilter[0];
}
}
return root;
}
// Recursively AND-chains two or more groups into a BinaryLogFilter (used for nesting beyond depth 1).
function buildAndChain(groups: WsLogaccess.leftFilter[][]): WsLogaccess.BinaryLogFilter {
const [firstGroup, ...remainingGroups] = groups;
const node: WsLogaccess.BinaryLogFilter = {} as WsLogaccess.BinaryLogFilter;
if (firstGroup.length === 1) {
node.leftFilter = firstGroup[0];
} else {
node.leftBinaryFilter = { BinaryLogFilter: [buildOrGroup(firstGroup)] };
}
if (remainingGroups.length === 0) return node;
node.Operator = WsLogaccess.LogAccessFilterOperator.AND;
if (remainingGroups.length === 1) {
const [secondGroup] = remainingGroups;
if (secondGroup.length === 1) {
node.rightFilter = secondGroup[0] as WsLogaccess.rightFilter;
} else {
node.rightBinaryFilter = { BinaryLogFilter: [buildOrGroup(secondGroup)] };
}
} else {
node.rightBinaryFilter = { BinaryLogFilter: [buildAndChain(remainingGroups)] };
}
return node;
}
// Groups filters by SearchField, OR-chains each group, then AND-chains the groups together.
// This ensures e.g. [class_INF, class_ERR, audience_USR] always produces
// (class_INF OR class_ERR) AND audience_USR regardless of input order.
function buildFilterTree(filters: WsLogaccess.leftFilter[]): WsLogaccess.Filter {
const groupMap = new Map<string, WsLogaccess.leftFilter[]>();
for (const f of filters) {
const existing = groupMap.get(f.SearchField);
if (existing) existing.push(f); else groupMap.set(f.SearchField, [f]);
}
const groups = [...groupMap.values()];
if (groups.length === 0) {
return { leftFilter: { LogCategory: WsLogaccess.LogAccessType.All } as WsLogaccess.leftFilter };
}
const [firstGroup, ...remainingGroups] = groups;
const filter: WsLogaccess.Filter = {};
if (firstGroup.length === 1) {
filter.leftFilter = firstGroup[0];
} else {
filter.leftBinaryFilter = { BinaryLogFilter: [buildOrGroup(firstGroup)] };
}
if (remainingGroups.length === 0) return filter;
filter.Operator = WsLogaccess.LogAccessFilterOperator.AND;
if (remainingGroups.length === 1) {
const [secondGroup] = remainingGroups;
if (secondGroup.length === 1) {
filter.rightFilter = secondGroup[0] as WsLogaccess.rightFilter;
} else {
filter.rightBinaryFilter = { BinaryLogFilter: [buildOrGroup(secondGroup)] };
}
} else {
filter.rightBinaryFilter = { BinaryLogFilter: [buildAndChain(remainingGroups)] };
}
return filter;
}
export class LogaccessService extends LogaccessServiceBase {
protected _logAccessInfo: Promise<WsLogaccess.GetLogAccessInfoResponse>;
GetLogAccessInfo(request: WsLogaccess.GetLogAccessInfoRequest = {}): Promise<WsLogaccess.GetLogAccessInfoResponse> {
if (!this._logAccessInfo) {
this._logAccessInfo = super.GetLogAccessInfo(request);
}
return this._logAccessInfo;
}
GetLogs(request: WsLogaccess.GetLogsRequest): Promise<WsLogaccess.GetLogsResponse> {
return super.GetLogs(request);
}
private convertLogLine(columnMap: Record<string, string>, line: any): LogLine {
const retVal: LogLine = {};
const fields = line?.fields ? Object.assign({}, ...line.fields) : null;
for (const key in columnMap) {
retVal[key] = fields ? fields[columnMap[key]] ?? "" : "";
}
return retVal;
}
async GetLogsEx(request: GetLogsExRequest): Promise<GetLogsExResponse> {
const logInfo = await this.GetLogAccessInfo();
const columnMap: Record<string, string> = {};
logInfo.Columns.Column.forEach(column => columnMap[column.LogType] = column.Name);
const filters = buildFilters(request, columnMap);
const range: Record<string, string> = {
StartDate: request.StartDate instanceof Date ? request.StartDate.toISOString() : new Date(0).toISOString()
};
if (request.EndDate instanceof Date) {
range.EndDate = request.EndDate.toISOString();
}
const getLogsRequest: WsLogaccess.GetLogsRequest = {
Filter: buildFilterTree(filters),
Range: range,
LogLineStartFrom: request.LogLineStartFrom ?? 0,
LogLineLimit: request.LogLineLimit ?? 100,
SelectColumnMode: WsLogaccess.LogSelectColumnMode.DEFAULT,
Format: "JSON",
SortBy: {
SortCondition: [{
BySortType: WsLogaccess.SortColumType.ByDate,
ColumnName: "",
Direction: 0
}]
}
};
return this.GetLogs(getLogsRequest).then(response => {
try {
const logLines = JSON.parse(response.LogLines);
const lines = knownLogManagerTypes.has(logInfo.RemoteLogManagerType)
? (logLines.lines?.map((line: any) => this.convertLogLine(columnMap, line)) ?? [])
: (logger.warning(`Unknown RemoteLogManagerType: ${logInfo.RemoteLogManagerType}`), []);
return {
lines,
total: response.TotalLogLinesAvailable ?? 10000
};
} catch (e: any) {
logger.error(e.message ?? e);
}
return {
lines: [],
total: 0
};
});
}
}