Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HPCC-32874 Add WsLogAcces Health Report Method #19307

Open
wants to merge 2 commits into
base: candidate-9.8.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion esp/scm/ws_logaccess.ecm
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,23 @@ ESPResponse GetLogsResponse
[min_ver("1.02")] unsigned int TotalLogLinesAvailable;
};

ESPservice [auth_feature("WsLogAccess:READ"), version("1.06"), default_client_version("1.06"), exceptions_inline("xslt/exceptions.xslt")] ws_logaccess
ESPRequest GetHealthReportRequest
{
bool IncludeServerInternals(true);
bool IncludePluginInternals(true);
bool IncludeSampleQuery(true);
};

ESPResponse GetHealthReportResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should have [exceptions_inline] also

{
string Report;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd much rather see a structured response that has some common subset of useful values for ECLWatch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, commented below for further discussion

};

ESPservice [auth_feature("WsLogAccess:READ"), version("1.07"), default_client_version("1.07"), exceptions_inline("xslt/exceptions.xslt")] ws_logaccess
{
ESPmethod GetLogAccessInfo(GetLogAccessInfoRequest, GetLogAccessInfoResponse);
ESPmethod GetLogs(GetLogsRequest, GetLogsResponse);
ESPmethod [min_ver("1.07")] GetHealthReport(GetHealthReportRequest, GetHealthReportResponse);
};

SCMexportdef(ws_logaccess);
Expand Down
27 changes: 27 additions & 0 deletions esp/services/ws_logaccess/WsLogAccessService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,30 @@ bool Cws_logaccessEx::onGetLogs(IEspContext &context, IEspGetLogsRequest &req, I

return true;
}

bool Cws_logaccessEx::onGetHealthReport(IEspContext &context, IEspGetHealthReportRequest &req, IEspGetHealthReportResponse &resp)
{
StringBuffer report;
//LogAccessHealthReportDetails reportDetails;
LogAccessHealthReportOptions options;
options.IncludeServerInternals = req.getIncludeServerInternals();
options.IncludePluginInternals = req.getIncludePluginInternals();
options.IncludeSampleQuery = req.getIncludeSampleQuery();

report.set("{ ");
bool success = true;
if (!queryRemoteLogAccessor())
{
report.append("\"Error\": \"LogAccess plugin not available, review logAccess configuration!\"");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be part of a structured response.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In agreement. what is the ESP standard for these types of messages? (ie, what's the expected structure format)

success = false;
}
else
{
//queryRemoteLogAccessor()->healthReport(report, reportDetails);
queryRemoteLogAccessor()->healthReport(report, options);
}
report.append(" }");
resp.setReport(report.str());

return success;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing final newline

1 change: 1 addition & 0 deletions esp/services/ws_logaccess/WsLogAccessService.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Cws_logaccessEx : public Cws_logaccess
virtual ~Cws_logaccessEx();
virtual bool onGetLogAccessInfo(IEspContext &context, IEspGetLogAccessInfoRequest &req, IEspGetLogAccessInfoResponse &resp);
virtual bool onGetLogs(IEspContext &context, IEspGetLogsRequest &req, IEspGetLogsResponse & resp);
virtual bool onGetHealthReport(IEspContext &context, IEspGetHealthReportRequest &req, IEspGetHealthReportResponse &resp);
};

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ global:
id: "1"
name: "Loki"
namespace:
name: "hpcc"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to revert this change

name: "default"
logFormat:
type: "json"
logMaps:
Expand Down
139 changes: 138 additions & 1 deletion system/jlib/jlog.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1500,9 +1500,44 @@ enum LogAccessMappedField
LOGACCESS_MAPPEDFIELD_host,
LOGACCESS_MAPPEDFIELD_traceid,
LOGACCESS_MAPPEDFIELD_spanid,
LOGACCESS_MAPPEDFIELD_global,
LOGACCESS_MAPPEDFIELD_container,
LOGACCESS_MAPPEDFIELD_message,
LOGACCESS_MAPPEDFIELD_unmapped
};

inline const char * MappedFieldTypeToString(LogAccessMappedField mappedField)
{
if (mappedField == LOGACCESS_MAPPEDFIELD_timestamp)
return "timestamp";
else if (mappedField == LOGACCESS_MAPPEDFIELD_jobid)
return "jobid";
else if (mappedField == LOGACCESS_MAPPEDFIELD_component)
return "component";
else if (mappedField == LOGACCESS_MAPPEDFIELD_class)
return "class";
else if (mappedField == LOGACCESS_MAPPEDFIELD_audience)
return "audience";
else if (mappedField == LOGACCESS_MAPPEDFIELD_instance)
return "instance";
else if (mappedField == LOGACCESS_MAPPEDFIELD_pod)
return "pod";
else if (mappedField == LOGACCESS_MAPPEDFIELD_host)
return "host";
else if (mappedField == LOGACCESS_MAPPEDFIELD_traceid)
return "traceID";
else if (mappedField == LOGACCESS_MAPPEDFIELD_spanid)
return "spanID";
else if (mappedField == LOGACCESS_MAPPEDFIELD_global)
return "global";
else if (mappedField == LOGACCESS_MAPPEDFIELD_container)
return "container";
else if (mappedField == LOGACCESS_MAPPEDFIELD_message)
return "message";
else
return "UNKNOWNFIELDTYPE";
}

enum SortByDirection
{
SORTBY_DIRECTION_none,
Expand Down Expand Up @@ -1675,6 +1710,106 @@ struct LogQueryResultDetails
unsigned int totalReceived;
unsigned int totalAvailable;
};
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing before you finalize you'll remove these comments and those in WsLogAccessService.cpp.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some, but kept the structure around as a mechanism to track structured responses for the ESP

typedef enum
{
LOGACCESS_STATUS_unknown,
LOGACCESS_STATUS_ok,
LOGACCESS_STATUS_fail
} LogAccessHealthStatus;

struct LogAccessConnectionDetails
{
StringAttr connectionString;
StringAttr connectionInfo;
StringAttr connectionStatus;
StringAttr sampleQueryStatus;

void toJSON(StringBuffer & out)
{
}
};

struct LogAccessConfigLogMap
{
LogAccessMappedField logFieldType;
StringAttr fieldName;
StringAttr sourceName;
LogAccessConfigLogMap(LogAccessMappedField type, const char * name, const char * source)
{
logFieldType = type;
fieldName.set(name);
sourceName.set(source);
}

void toJSON(StringBuffer & out)
{
out.appendf("\"%s\": { \"ColName\": \"%s\", \"Source\": \"%s\" }", MappedFieldTypeToString(logFieldType), fieldName.str(), sourceName.str());
}
};

struct LogAccessConfigDetails
{
StringAttr connectionInfo;
StringArray logMaps;
//IArrayOf<LogAccessConfigLogMap> logMaps;

void appendLogMap(LogAccessConfigLogMap logMap)
{
StringBuffer logMapJson;
logMap.toJSON(logMapJson);
logMaps.append(logMapJson.str());
//logMaps.append(logMap);
}

void toJSON(StringBuffer & out)
{
out.appendf("\"ConfigInfo\": { \"LogMaps\": {");

ForEachItemIn(i, logMaps)
//logMaps.item(i).toJSON(out);
out.append(logMaps.item(i));

//close out the logmaps
out.append(" }");
//close out the ConfigInfo
out.append(" }");
}
};

struct LogAccessHealthReportDetails
{
LogAccessConnectionDetails connectionInfo;
LogAccessConfigDetails configInfo;
StringBuffer JsonMessages;

void appendLogMap(LogAccessConfigLogMap logMap)
{
configInfo.appendLogMap(logMap);
}

void toJSON(StringBuffer & out)
{
StringBuffer scratch;

out.append("{ \"Connection\": ");
connectionInfo.toJSON(scratch);
out.append(scratch.str());

out.append(", \"ConfigInfo\": ");
configInfo.toJSON(scratch.clear());
out.append(scratch.str());

out.appendf(", \"Messages\": \"%s\"", JsonMessages.str());
}
};
*/
struct LogAccessHealthReportOptions
{
bool IncludeServerInternals = true;
bool IncludePluginInternals = true;
bool IncludeSampleQuery = true;
};

// Log Access Interface - Provides filtered access to persistent logging - independent of the log storage mechanism
// -- Declares method to retrieve log entries based on options set
Expand All @@ -1690,6 +1825,8 @@ interface IRemoteLogAccess : extends IInterface
virtual IPropertyTree * queryLogMap() const = 0;
virtual const char * fetchConnectionStr() const = 0;
virtual bool supportsResultPaging() const = 0;
//virtual bool healthReport(StringBuffer & messages, LogAccessHealthReportDetails & report) = 0;
virtual bool healthReport(StringBuffer & report, LogAccessHealthReportOptions options) = 0;
};

// Helper functions to construct log access filters
Expand All @@ -1714,7 +1851,7 @@ extern jlib_decl bool fetchLog(LogQueryResultDetails & resultDetails, StringBuff
extern jlib_decl bool fetchJobIDLog(LogQueryResultDetails & resultDetails, StringBuffer & returnbuf, IRemoteLogAccess & logAccess, const char *jobid, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
extern jlib_decl bool fetchComponentLog(LogQueryResultDetails & resultDetails, StringBuffer & returnbuf, IRemoteLogAccess & logAccess, const char * component, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
extern jlib_decl bool fetchLogByAudience(LogQueryResultDetails & resultDetails, StringBuffer & returnbuf, IRemoteLogAccess & logAccess, MessageAudience audience, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
extern jlib_decl bool fetchLogByClass(LogQueryResultDetails & resultDetails, StringBuffer & returnbuf, IRemoteLogAccess & logAccess, LogMsgClass logclass, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
extern jlib_decl bool fetchLogByClass(LogQueryResultDetails & resultDetails, StringBuffer & returnbuf, IRemoteLogAccess & logAccess, LogMsgClass logclass, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
extern jlib_decl IRemoteLogAccess * queryRemoteLogAccessor();

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,151 @@ bool AzureLogAnalyticsCurlClient::processSearchJsonResp(LogQueryResultDetails &
return true;
}

bool AzureLogAnalyticsCurlClient::healthReport(StringBuffer & report, LogAccessHealthReportOptions options)
{
try
{
report.appendf("\"ConnectionInfo\": { \"TargetALAWorkspaceID\": \"%s\" ", m_logAnalyticsWorkspaceID.str());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this and the other report building code, maybe it would be better to use the JSON builder functions in jstring like appendJSONStringValue and others. They can handle encoding and quoting in a standardized way.

report.appendf(", \"TargetALATenantID\": \"%s\"", m_aadTenantID.str());
report.appendf(", \"TargetALAClientID\": \"%s\"", m_aadClientID.str());
report.appendf(", \"TargetALASecret\": \"%sempty\"", m_aadClientSecret.length()==0 ? "" : "not ");
report.appendf(", \"TargetsContainerLogV2\": \"%s\"", targetIsContainerLogV2 ? "true" : "false");
report.appendf(", \"ComponentsQueryJoins\": \"%sabled\"", m_disableComponentNameJoins ? "dis" : "en");
report.appendf(", \"BlobModeUnstructuredLogData\": \"%sabled\"", m_blobMode ? "en" : "dis");
report.append( "}"); //close conninfo

report.append(", \"ConfigurationInfo\": { ");

if (m_pluginCfg)
{
StringBuffer configJSON;
toJSON(m_pluginCfg, configJSON, 0);
report.appendf("\"ConfigurationTree\": %s", configJSON.str()); //json encode
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here as in ElasticStackLogAccess.cpp about JSON vs JSON in a string which needs encoding.

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any sensitive information in the configuration that shouldn't be exposed, like maybe tokens or passwords or internal URLs? On the other hand maybe this is a moot question since ECLWatch already gives access to component configs for roxie, esp etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair question, I don't think we support credentials in the config, but I'll make sure we don't, and if we do I'll mask it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HELM chart Schema doesn't call for any sensitive data, I think we're ok.

else
{
report.append("\"Error\": \"Configuration tree is empty!!!\"");
}
report.append(" }"); // close config info

report.append(", \"Internals\": { ");
if (options.IncludeServerInternals)
{
report.appendf("\"Plugin\": { \"LogMaps\": {");
report.appendf("\"Global\": { \"ColName\": \"%s\", \"Source\": \"%s\", \"TimeStampCol\": \"%s\"}", m_globalSearchColName.str(), m_globalIndexSearchPattern.str(), m_globalIndexTimestampField.str());
report.appendf(", \"Workunits\": { \"ColName\": \"%s\", \"Source\": \"%s\"}", m_workunitSearchColName.str(), m_workunitIndexSearchPattern.str());
report.appendf(", \"Components\": { \"ColName\": \"%s\", \"Source\": \"%s\", \"LookupKey\": \"%s\", \"TimeStampCol\": \"%s\"}", m_componentsSearchColName.str(), m_componentsIndexSearchPattern.str(), m_componentsLookupKeyColumn.str(), m_componentsTimestampField.str());
report.appendf(", \"Audience\": { \"ColName\": \"%s\", \"Source\": \"%s\" }", m_audienceSearchColName.str(), m_audienceIndexSearchPattern.str());
report.appendf(", \"Class\": { \"ColName\": \"%s\", \"Source\": \"%s\"}", m_classSearchColName.str(), m_classIndexSearchPattern.str());
report.appendf(", \"Instance\": { \"ColName\": \"%s\", \"Source\": \"%s\", \"LookupKey\": \"%s\"}", m_instanceSearchColName.str(), m_instanceIndexSearchPattern.str(), m_instanceLookupKeyColumn.str());
report.appendf(", \"Pod\": { \"ColName\": \"%s\", \"Source\": \"%s\"}", m_podSearchColName.str(), m_podIndexSearchPattern.str());
report.appendf(", \"TraceID\": { \"ColName\": \"%s\", \"Source\": \"%s\"}", m_traceSearchColName.str(), m_traceIndexSearchPattern.str());
report.appendf(", \"SpanID\": { \"ColName\": \"%s\", \"Source\": \"%s\"}", m_spanSearchColName.str(), m_spanIndexSearchPattern.str());
report.appendf(", \"Host\": { \"ColName\": \"%s\", \"Source\": \"%s\"}", m_hostSearchColName.str(), m_hostIndexSearchPattern.str());
report.append(" }"); //close logmaps
report.append(" }"); //close plugin
}

if (options.IncludeServerInternals)
{
report.append(", \"Server\": { }");
}

report.append(" }"); //close internals
if (options.IncludeSampleQuery)
{
report.append(", \"SampleTokenRequest\": { ");
try
{

StringBuffer token;
requestLogAnalyticsAccessToken(token, m_aadClientID, m_aadClientSecret, m_aadTenantID); //throws if issues encountered
rpastrana marked this conversation as resolved.
Show resolved Hide resolved

if (token.isEmpty())
report.append("\"Error\": \"Empty token received\"");

}
catch(IException * e)
{
StringBuffer description;
e->errorMessage(description);
report.appendf("\"Error\": \"Exception while requesting token (%d) - %s\"", e->errorCode(), description.str());
e->Release();
}
catch(...)
{
report.append("\"Error\": \"Unknown exception while requesting token\"");
}
report.append(" }"); //close sample token request

report.append(", \"SampleQuery\": { ");
try
{
report.appendf("\"Query\": { \"LogFormat\": \"JSON\",");
LogAccessLogFormat outputFormat = LOGACCESS_LOGFORMAT_json;
LogAccessConditions queryOptions;

report.appendf("\"Filter\": {\"type\": \"byWildcard\", \"value\": \"*\" },");
queryOptions.setFilter(getWildCardLogAccessFilter("*"));

struct LogAccessTimeRange range;
CDateTime endtt;
endtt.setNow();
range.setEnd(endtt);
StringBuffer endstr;
endtt.getString(endstr);

CDateTime startt;
startt.setNow();
startt.adjustTimeSecs(-60); //an hour ago
range.setStart(startt);

StringBuffer startstr;
startt.getString(startstr);
report.appendf("\"TimeRange\": {\"Start\": \"%s\", \"End\": \"%s\" },", startstr.str(), endstr.str());

queryOptions.setTimeRange(range);
queryOptions.setLimit(5);
report.appendf("\"Limit\": \"5\" }, ");

StringBuffer queryString, queryIndex;
populateKQLQueryString(queryString, queryIndex, queryOptions);

StringBuffer encodedValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only string that needs to be encoded, or are there other cases where user input could be part of any value returned in the report? Would it be a good idea to encode all strings returned?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, within the sample query section, yes. But point taken, I'll look around and see if anything else could/should be encoded.

encodeJSON(encodedValue, queryString.str());

report.appendf("\"KQLQuery\": \"%s\", ", encodedValue.str());
report.appendf("\"QueryIndex\": \"%s\", ", queryIndex.str());

StringBuffer logs;
LogQueryResultDetails resultDetails;
fetchLog(resultDetails, queryOptions, logs, outputFormat);
report.appendf("\"ResultCount\": \"%d\", ", resultDetails.totalReceived);
report.appendf("\"Results\": %s", logs.str());
}
catch(IException * e)
{
StringBuffer description;
e->errorMessage(description);
report.appendf("\"Error\": \"Exception while executing sample ALA query (%d) - %s\"", e->errorCode(), description.str());
e->Release();
}
catch(...)
{
report.append("\"Error\": \"Unknown exception while executing sample ALA query\"");
}
report.append(" }"); //close sample query
}
}
catch(...)
{
report.append("\"Error\": \"Encountered unexpected exception during health report\"");
return false;
}

return true;
}

bool AzureLogAnalyticsCurlClient::fetchLog(LogQueryResultDetails & resultDetails, const LogAccessConditions & options, StringBuffer & returnbuf, LogAccessLogFormat format)
{
StringBuffer token;
Expand Down
Loading
Loading