diff --git a/esp/scm/ws_logaccess.ecm b/esp/scm/ws_logaccess.ecm index 5232f983275..5d82e8ee9f6 100644 --- a/esp/scm/ws_logaccess.ecm +++ b/esp/scm/ws_logaccess.ecm @@ -229,10 +229,59 @@ 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 IncludeConfiguration(false); + bool IncludeDebugReport(false); + bool IncludeSampleQuery(false); +}; + +ESPenum LogAccessStatusCode : int +{ + Unknown(0, "Unknown"), + Green(1, "Green"), + Yellow(2, "Yellow"), + Red(3, "Red") +}; + +/* +ESPenum LogAccessStatusCode : int +{ + NotConfigured(0, "NotConfigured"), + Misconfigured(1, "Misconfigured"), + FailedToConnect(2, "FailedToConnect"), + FailedToAuth(3, "FailedToAuth"), + FailedToQuery(4, "FailedToQuery"), + EmptyQueryResult(5, "EmptyQueryResult"), + Warned(6, "Warned"), + Failed(7, "Failed") +};*/ + +ESPStruct LogAccessStatus +{ + LogAccessStatusCode Code; + LogAccessStatusCode Messages; +}; + +ESPStruct LogAccessDebugReport +{ + string SampleQueryReport; + string PluginDebugReport; + string ServerDebugReport; +}; + +ESPResponse [exceptions_inline] GetHealthReportResponse +{ + ESPStruct LogAccessStatus Status; + ESPStruct LogAccessDebugReport DebugReport; + string Configuration; +}; + +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); diff --git a/esp/services/ws_logaccess/WsLogAccessService.cpp b/esp/services/ws_logaccess/WsLogAccessService.cpp index 98e4a98ad90..3dde28f79a1 100644 --- a/esp/services/ws_logaccess/WsLogAccessService.cpp +++ b/esp/services/ws_logaccess/WsLogAccessService.cpp @@ -385,3 +385,52 @@ bool Cws_logaccessEx::onGetLogs(IEspContext &context, IEspGetLogsRequest &req, I return true; } + +bool Cws_logaccessEx::onGetHealthReport(IEspContext &context, IEspGetHealthReportRequest &req, IEspGetHealthReportResponse &resp) +{ + IEspLogAccessStatus * status = createLogAccessStatus("",""); + + StringBuffer report; + LogAccessHealthReportDetails reportDetails; + LogAccessHealthReportOptions options; + options.IncludeConfiguration = req.getIncludeConfiguration(); + options.IncludeDebugReport = req.getIncludeDebugReport(); + options.IncludeSampleQuery = req.getIncludeSampleQuery(); + + if (!queryRemoteLogAccessor()) + { + status->setCode("Red"); + status->setMessages("Configuration Error - LogAccess plugin not available, review logAccess configuration!"); + } + else + { + IEspLogAccessDebugReport * debugReport = createLogAccessDebugReport(); + queryRemoteLogAccessor()->healthReport(options, reportDetails); + status->setCode(LogAccessHealthStatusToString(reportDetails.status.code)); + VStringBuffer encapsulatedMessages("{%s}", reportDetails.status.message.str()); + status->setMessages(encapsulatedMessages.str()); + + if (options.IncludeConfiguration) + { + resp.setConfiguration(reportDetails.Configuration.str()); + DBGLOG("WsLogAccessHealth: configuration: %s", reportDetails.Configuration.str()); + } + + if (options.IncludeSampleQuery) + { + debugReport->setSampleQueryReport(reportDetails.DebugReport.SampleQueryReport.str()); + } + + if (options.IncludeDebugReport) + { + debugReport->setPluginDebugReport(reportDetails.DebugReport.PluginDebugReport.str()); + debugReport->setServerDebugReport(reportDetails.DebugReport.ServerDebugReport.str()); + } + + resp.setDebugReport(*debugReport); + } + + resp.setStatus(*status); + + return true; +} diff --git a/esp/services/ws_logaccess/WsLogAccessService.hpp b/esp/services/ws_logaccess/WsLogAccessService.hpp index a4f70dff896..d28c97b3124 100644 --- a/esp/services/ws_logaccess/WsLogAccessService.hpp +++ b/esp/services/ws_logaccess/WsLogAccessService.hpp @@ -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 diff --git a/system/jlib/jlog.hpp b/system/jlib/jlog.hpp index 92d05260394..425d5656b0f 100644 --- a/system/jlib/jlog.hpp +++ b/system/jlib/jlog.hpp @@ -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, @@ -1676,6 +1711,68 @@ struct LogQueryResultDetails unsigned int totalAvailable; }; + +typedef enum +{ + LOGACCESS_STATUS_unknown = 0, + LOGACCESS_STATUS_green = 1, + LOGACCESS_STATUS_yellow = 2, + LOGACCESS_STATUS_red = 3 +} LogAccessHealthStatusCode; + +struct LogAccessHealthStatus +{ + LogAccessHealthStatusCode code; + StringBuffer message; + + LogAccessHealthStatus(LogAccessHealthStatusCode code_) + { + code = code_; + } + + void appendMessage(const char * message_) + { + message.append(message_); + } +}; + + +inline const char * LogAccessHealthStatusToString(LogAccessHealthStatusCode statusCode) +{ + switch(statusCode) + { + case LOGACCESS_STATUS_green: + return "Green"; + case LOGACCESS_STATUS_yellow: + return "Yellow"; + case LOGACCESS_STATUS_red: + return "Red"; + default: + return "Unknown"; + } +}; + +struct LogAccessDebugReport +{ + StringBuffer SampleQueryReport; + StringBuffer PluginDebugReport; + StringBuffer ServerDebugReport; +}; + +struct LogAccessHealthReportDetails +{ + LogAccessHealthStatus status = LOGACCESS_STATUS_unknown; + LogAccessDebugReport DebugReport; + StringAttr Configuration; +}; + +struct LogAccessHealthReportOptions +{ + bool IncludeConfiguration = true; + bool IncludeDebugReport = 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 // -- Declares method to retrieve remote log access type (eg elasticstack, etc) @@ -1690,6 +1787,7 @@ interface IRemoteLogAccess : extends IInterface virtual IPropertyTree * queryLogMap() const = 0; virtual const char * fetchConnectionStr() const = 0; virtual bool supportsResultPaging() const = 0; + virtual void healthReport(LogAccessHealthReportOptions options, LogAccessHealthReportDetails & report) = 0; }; // Helper functions to construct log access filters @@ -1714,7 +1812,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 diff --git a/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.cpp b/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.cpp index 35b1decae34..6656186c5e3 100644 --- a/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.cpp +++ b/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.cpp @@ -1076,11 +1076,217 @@ bool AzureLogAnalyticsCurlClient::processSearchJsonResp(LogQueryResultDetails & if (!tree) throw makeStringExceptionV(-1, "%s: Could not parse query response", COMPONENT_NAME); + /*IPropertyTreeIterator * statsiter = tree->getElements("statistics"); + if (!statsiter) + DBGLOG("^^^^^^^^^^^^^^No stats found"); + + StringArray header; + ForEach(*statsiter) + { + Owned names = statsiter->query().getElements("."); + }*/ + resultDetails.totalReceived = processHitsJsonResp(tree->getElements("tables/rows"), tree->getElements("tables/columns"), returnbuf, format, true, reportHeader); resultDetails.totalAvailable = 0; return true; } +void AzureLogAnalyticsCurlClient::healthReport(LogAccessHealthReportOptions options, LogAccessHealthReportDetails & report) +{ + LogAccessHealthStatus status = LOGACCESS_STATUS_unknown; + try + { + if (options.IncludeConfiguration) + { + StringBuffer configuration; + configuration.set("{"); + + if (m_pluginCfg) + { + StringBuffer configJSON; + toJSON(m_pluginCfg, configJSON, 0, JSON_Format); + appendJSONStringValue(configuration, "ConfigurationTree", configJSON.str(), false); + } + else + { + status = LOGACCESS_STATUS_red; + appendJSONStringValue(status.message, "Message", "ALA Pluging Configuration tree is empty!!!", false); + } + + configuration.append(" }"); // close config info + report.Configuration.set(configuration.str()); + } + + if (options.IncludeDebugReport) + { + StringBuffer debugReport; + debugReport.set("{"); + debugReport.append("\"ConnectionInfo\": {"); + appendJSONStringValue(debugReport, "TargetALAWorkspaceID", m_logAnalyticsWorkspaceID.str(), true); + appendJSONStringValue(debugReport, "TargetALATenantID", m_aadTenantID.str(), true); + appendJSONStringValue(debugReport, "TargetALAClientID", m_aadClientID.str(), true); + debugReport.appendf(", \"TargetALASecret\": \"%sempty\"", m_aadClientSecret.length()==0 ? "" : "not "); + appendJSONValue(debugReport, "TargetsContainerLogV2", targetIsContainerLogV2 ? true : false); + appendJSONValue(debugReport, "ComponentsJoinedQueryEnabled", m_disableComponentNameJoins ? false : true); + appendJSONValue(debugReport, "BlobModeEnabled", m_blobMode ? true : false); + debugReport.append( "}"); //close conninfo + + debugReport.appendf(", { \"LogMaps\": {"); + debugReport.appendf("\"Global\": { "); + appendJSONStringValue(debugReport, "ColName", m_globalSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_globalIndexSearchPattern.str(), true); + appendJSONStringValue(debugReport, "TimeStampCol", m_globalIndexTimestampField.str(), true); + debugReport.append(" }"); // end Global + debugReport.appendf("\"Components\": { "); + appendJSONStringValue(debugReport, "ColName", m_componentsSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_componentsIndexSearchPattern.str(), true); + appendJSONStringValue(debugReport, "LookupKey", m_componentsLookupKeyColumn.str(), true); + appendJSONStringValue(debugReport, "TimeStampCol", m_globalIndexTimestampField.str(), true); + debugReport.appendf(" }"); // end Components + debugReport.appendf("\"Workunits\": { "); + appendJSONStringValue(debugReport, "ColName", m_workunitSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_workunitIndexSearchPattern.str(), true); + appendJSONStringValue(debugReport, "TimeStampCol", m_globalIndexTimestampField.str(), true); + debugReport.append(" }"); // end Workunits + debugReport.appendf("\"Audience\": { "); + appendJSONStringValue(debugReport, "ColName", m_audienceSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_audienceIndexSearchPattern.str(), true); + debugReport.appendf(" }"); // end Audience + debugReport.appendf("\"Class\": { "); + appendJSONStringValue(debugReport, "ColName", m_classSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_classIndexSearchPattern.str(), true); + debugReport.appendf(" }"); // end Class + debugReport.appendf("\"Instance\": { "); + appendJSONStringValue(debugReport, "ColName", m_instanceSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_instanceIndexSearchPattern.str(), true); + appendJSONStringValue(debugReport, "LookupKey", m_instanceLookupKeyColumn.str(), true); + debugReport.appendf(" }"); // end Instance + debugReport.appendf("\"Pod\": { "); + appendJSONStringValue(debugReport, "ColName", m_podSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_podIndexSearchPattern.str(), true); + debugReport.appendf(" }"); // end Pod + debugReport.appendf("\"TraceID\": { "); + appendJSONStringValue(debugReport, "ColName", m_traceSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_traceIndexSearchPattern.str(), true); + debugReport.appendf(" }"); // end TraceID + debugReport.appendf("\"SpanID\": { "); + appendJSONStringValue(debugReport, "ColName", m_spanSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_spanIndexSearchPattern.str(), true); + debugReport.appendf(" }"); // end SpanID + debugReport.appendf("\"Host\": { "); + appendJSONStringValue(debugReport, "ColName", m_hostSearchColName.str(), true); + appendJSONStringValue(debugReport, "Source", m_hostIndexSearchPattern.str(), true); + debugReport.appendf(" }"); // end Host + debugReport.append(" }"); //close logmaps + + debugReport.append(" }"); //close debugreport + report.DebugReport.PluginDebugReport.set(debugReport); + report.DebugReport.ServerDebugReport.set("{}"); + + appendJSONStringValue(status.message, "Message", "ALA Debug report succeeded", false); + } + + if (options.IncludeSampleQuery) + { + StringBuffer sampleQueryReport; + sampleQueryReport.append("{ \"SampleTokenRequest\": { "); + try + { + StringBuffer token; + requestLogAnalyticsAccessToken(token, m_aadClientID, m_aadClientSecret, m_aadTenantID); //throws if issues encountered + + appendJSONStringValue(sampleQueryReport, "Result", token.isEmpty() ? "Error - Empty token received" : "Success", true); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + status = LOGACCESS_STATUS_red; + appendJSONStringValue(status.message, "Result", "Exception while requesting sample token (%d) - %s", e->errorCode(), description.str()); + e->Release(); + } + catch(...) + { + appendJSONStringValue(status.message, "Message", "Unknown exception while requesting sample token", false); + status = LOGACCESS_STATUS_red; + } + sampleQueryReport.append(" }"); //close sample token request + + sampleQueryReport.append(", \"SampleQuery\": { "); + try + { + sampleQueryReport.appendf("\"Query\": { \"LogFormat\": \"JSON\","); + LogAccessLogFormat outputFormat = LOGACCESS_LOGFORMAT_json; + LogAccessConditions queryOptions; + + sampleQueryReport.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); + sampleQueryReport.appendf("\"TimeRange\": {\"Start\": \"%s\", \"End\": \"%s\" },", startstr.str(), endstr.str()); + + queryOptions.setTimeRange(range); + queryOptions.setLimit(5); + sampleQueryReport.appendf("\"Limit\": \"5\" }, "); + + StringBuffer queryString, queryIndex; + populateKQLQueryString(queryString, queryIndex, queryOptions); + + appendJSONStringValue(sampleQueryReport, "KQLQuery", queryString.str(), true); + appendJSONStringValue(sampleQueryReport, "QueryIndex", queryIndex.str(), true); + + StringBuffer logs; + LogQueryResultDetails resultDetails; + fetchLog(resultDetails, queryOptions, logs, outputFormat); + appendJSONValue(sampleQueryReport, "ResultCount", resultDetails.totalReceived); + if (resultDetails.totalReceived==0) + { + status = LOGACCESS_STATUS_yellow; + appendJSONStringValue(status.message, "Message", "Query succeeded but returned 0 log entries", false); + } + + appendJSONStringValue(sampleQueryReport, "Results", logs.str(), true); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + status = LOGACCESS_STATUS_red; + status.message.appendf("%s{\"Message\": \"Exception while executing sample ALA query (%d) - %s\"", status.message.length() == 0 ? "" : ", ", e->errorCode(), description.str()); + e->Release(); + } + catch(...) + { + appendJSONStringValue(status.message, "Message", "Unknown exception while executing sample ALA query", false); + status = LOGACCESS_STATUS_red; + } + sampleQueryReport.append(" }"); //close sample query + + report.DebugReport.SampleQueryReport.set(sampleQueryReport); + } + } + catch(...) + { + status = LOGACCESS_STATUS_red; + appendJSONStringValue(status.message, "Message", "Encountered unexpected exception during health report", false); + } + + report.status = status; +} + bool AzureLogAnalyticsCurlClient::fetchLog(LogQueryResultDetails & resultDetails, const LogAccessConditions & options, StringBuffer & returnbuf, LogAccessLogFormat format) { StringBuffer token; diff --git a/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.hpp b/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.hpp index 013cb0e5032..928ae2f0be7 100644 --- a/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.hpp +++ b/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.hpp @@ -101,4 +101,5 @@ class AzureLogAnalyticsCurlClient : public CInterfaceOf virtual IRemoteLogAccessStream * getLogReader(const LogAccessConditions & options, LogAccessLogFormat format) override; virtual IRemoteLogAccessStream * getLogReader(const LogAccessConditions & options, LogAccessLogFormat format, unsigned int pageSize) override; virtual bool supportsResultPaging() const override { return false;} + virtual void healthReport(LogAccessHealthReportOptions options, LogAccessHealthReportDetails & report) override; }; diff --git a/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp b/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp index 878946703a1..5775526e34f 100644 --- a/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp +++ b/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp @@ -252,7 +252,8 @@ const IPropertyTree * ElasticStackLogAccess::getIndexSearchStatus(const char * i if (!indexpattern || !*indexpattern) throw makeStringException(-1, "ElasticStackLogAccess::getIndexSearchStatus: indexpattern must be provided"); - VStringBuffer indexsearch("_cat/indices/%s?format=JSON", indexpattern); +// VStringBuffer indexsearch("_cat/indices/%s?format=JSON", indexpattern); + VStringBuffer indexsearch("_cat/indices?v"); return performAndLogESRequest(Client::HTTPMethod::GET, indexsearch.str(), "", "List of available indexes"); } @@ -262,6 +263,199 @@ const IPropertyTree * ElasticStackLogAccess::getESStatus() return performAndLogESRequest(Client::HTTPMethod::GET, "_cluster/health", "", "Target cluster health"); } + void ElasticStackLogAccess::healthReport(LogAccessHealthReportOptions options, LogAccessHealthReportDetails & report) + { + LogAccessHealthStatus status = LOGACCESS_STATUS_green; + /*try + { + report.appendf("\"ConnectionInfo\": { \"ConnectionString\": \"%s\" }", m_esConnectionStr.str()); + + report.append(", \"ConfigurationInfo\": { "); + if (m_pluginCfg) + { + StringBuffer configJSON; + toJSON(m_pluginCfg, configJSON, 0, JSON_Format|JSON_HideRootArrayObject); + report.appendf("\"ConfigurationTree\": %s", configJSON.str()); //json encode + } + else + { + report.append("\"Error\": \"Configuration tree is empty!!!\""); + status = LOGACCESS_STATUS_red; + } + 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\"}", m_componentsSearchColName.str(), m_componentsIndexSearchPattern.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\"}", m_instanceSearchColName.str(), m_instanceIndexSearchPattern.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("\"AvailableIndices\": "); + try + { + const IPropertyTree * is = getIndexSearchStatus(m_globalIndexSearchPattern); + if (is) + { + toJSON(is, report); + } + else + { + report.appendf("\"Could not populate Available Indices for Index %s\"", m_globalIndexSearchPattern.str()); + status = LOGACCESS_STATUS_yellow; + } + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + report.appendf("\"Exception fetching available ES indices (%d) - %s\"", e->errorCode(), description.str()); + e->Release(); + status = LOGACCESS_STATUS_yellow; + } + catch(...) + { + report.append("\"Unknown exception while fetching available ES indices\""); + status = LOGACCESS_STATUS_yellow; + } + + report.append(", \"TimestampField\": "); + try + { + const IPropertyTree * ts = getTimestampTypeFormat(m_globalIndexSearchPattern, m_globalIndexTimestampField); + if (ts) + { + toJSON(ts, report); + } + else + { + report.appendf("\"Could not populate AES timestamp format for IndexPattern %s\"", m_globalIndexSearchPattern.str()); + } + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + report.appendf("\"Exception fetching target ES timestamp format (%d) - %s\"", e->errorCode(), description.str()); + e->Release(); + status = LOGACCESS_STATUS_red; + } + catch(...) + { + report.append("\"Unknown exception while fetching target ES timestamp format\""); + status = LOGACCESS_STATUS_red; + } + + report.append(", \"ESStatus\": "); + try + { + StringBuffer out; + const IPropertyTree * esStatus = getESStatus(); + if (esStatus) + { + toJSON(esStatus, report); //extract esstatus info to set status green/yellow/red? + } + else + { + report.append("\"Could not populate ES Status\""); + status = LOGACCESS_STATUS_yellow; + } + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + report.appendf("\"Exception fetching ES Status (%d) - %s\"", e->errorCode(), description.str()); + e->Release(); + status = LOGACCESS_STATUS_red; + } + catch(...) + { + report.append("\"Unknown exception while fetching ES Status\""); + status = LOGACCESS_STATUS_red; + } + report.append(" } "); //close Server + } + report.append(" } "); //close Internals + + if (options.IncludeSampleQuery) + { + report.append(", \"SampleQuery\": { "); + try + { + report.appendf("\"Query\": { \"LogFormat\": \"JSON\","); + LogAccessLogFormat outputFormat = LOGACCESS_LOGFORMAT_json; + LogAccessConditions queryOptions; + report.appendf("\"Filter\": {\"type\": \"byComponent\", \"value\": \"eclwatch\" },"); + queryOptions.setFilter(getComponentLogAccessFilter("eclwatch")); + + 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 logs; + LogQueryResultDetails resultDetails; + fetchLog(resultDetails, queryOptions, logs, outputFormat); + report.appendf("\"ResultCount\": \"%d\", ", resultDetails.totalReceived); + if (resultDetails.totalReceived == 0) + status = LOGACCESS_STATUS_yellow; + + report.appendf("\"Results\": %s", logs.str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + report.appendf("\"Error\": \"Exception while executing sample query (%d) - %s\"", e->errorCode(), description.str()); + e->Release(); + status = LOGACCESS_STATUS_red; + } + catch(...) + { + report.append("\"Error\": \"Unknown exception while executing samplequery\""); + status = LOGACCESS_STATUS_red; + } + report.append(" }"); //close sample query + } + } + catch(...) + { + report.append("\"Error\": \"Encountered unexpected exception during health report\""); + status = LOGACCESS_STATUS_red; + } +*/ + //return status; + } + /* * Transform iterator of hits/fields to back-end agnostic response * diff --git a/system/logaccess/ElasticStack/ElasticStackLogAccess.hpp b/system/logaccess/ElasticStack/ElasticStackLogAccess.hpp index b0fd8c15383..c6a34e43116 100644 --- a/system/logaccess/ElasticStack/ElasticStackLogAccess.hpp +++ b/system/logaccess/ElasticStack/ElasticStackLogAccess.hpp @@ -103,4 +103,5 @@ class ElasticStackLogAccess : public CInterfaceOf virtual IRemoteLogAccessStream * getLogReader(const LogAccessConditions & options, LogAccessLogFormat format) override; virtual IRemoteLogAccessStream * getLogReader(const LogAccessConditions & options, LogAccessLogFormat format, unsigned int pageSize) override; virtual bool supportsResultPaging() const override { return true;} + virtual void healthReport(LogAccessHealthReportOptions options, LogAccessHealthReportDetails & report) override; }; diff --git a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp index 505e2a82778..181be8a616b 100644 --- a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp +++ b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp @@ -58,17 +58,9 @@ size_t stringCallback(char *contents, size_t size, size_t nmemb, void *userp) return size * nmemb; } -/* -* Constructs a curl based client request based on the provided connection string and targetURI -* The response is reported in the readBuffer -* Uses stringCallback to handle successfull curl requests -*/ -void GrafanaLogAccessCurlClient::submitQuery(std::string & readBuffer, const char * targetURI) +void submit(std::string & readBuffer, const char * url, const char * user, const char * pass) { - if (isEmptyString(m_grafanaConnectionStr.str())) - throw makeStringExceptionV(-1, "%s Cannot submit query, empty connection string detected!", COMPONENT_NAME); - - if (isEmptyString(targetURI)) + if (isEmptyString(url)) throw makeStringExceptionV(-1, "%s Cannot submit query, empty request URI detected!", COMPONENT_NAME); OwnedPtrCustomFree curlHandle = curl_easy_init(); @@ -79,10 +71,8 @@ void GrafanaLogAccessCurlClient::submitQuery(std::string & readBuffer, const cha char curlErrBuffer[CURL_ERROR_SIZE]; curlErrBuffer[0] = '\0'; - VStringBuffer requestURL("%s%s%s", m_grafanaConnectionStr.str(), m_dataSourcesAPIURI.str(), targetURI); - - if (curl_easy_setopt(curlHandle, CURLOPT_URL, requestURL.str()) != CURLE_OK) - throw makeStringExceptionV(-1, "%s: Log query request: Could not set 'CURLOPT_URL' (%s)!", COMPONENT_NAME, requestURL.str()); + if (curl_easy_setopt(curlHandle, CURLOPT_URL, url) != CURLE_OK) + throw makeStringExceptionV(-1, "%s: Log query request: Could not set 'CURLOPT_URL' (%s)!", COMPONENT_NAME, url); int curloptretcode = curl_easy_setopt(curlHandle, CURLOPT_HTTPAUTH, (long)CURLAUTH_BASIC); if (curloptretcode != CURLE_OK) @@ -95,18 +85,10 @@ void GrafanaLogAccessCurlClient::submitQuery(std::string & readBuffer, const cha throw makeStringExceptionV(-1, "%s: Log query request: Could not set 'CURLOPT_HTTPAUTH':'CURLAUTH_BASIC'!", COMPONENT_NAME); } - //allow annonymous connections?? - if (isEmptyString(m_grafanaUserName.str())) - throw makeStringExceptionV(-1, "%s: Log query request: Empty user name detected!", COMPONENT_NAME); - - //allow non-secure connections?? - if (isEmptyString(m_grafanaPassword.str())) - throw makeStringExceptionV(-1, "%s: Log query request: Empty password detected!", COMPONENT_NAME); - - if (curl_easy_setopt(curlHandle, CURLOPT_USERNAME, m_grafanaUserName.str())) + if (curl_easy_setopt(curlHandle, CURLOPT_USERNAME, user)) throw makeStringExceptionV(-1, "%s: Log query request: Could not set 'CURLOPT_USERNAME' option!", COMPONENT_NAME); - if (curl_easy_setopt(curlHandle, CURLOPT_PASSWORD, m_grafanaPassword.str())) + if (curl_easy_setopt(curlHandle, CURLOPT_PASSWORD, pass)) throw makeStringExceptionV(-1, "%s: Log query request: Could not set 'CURLOPT_PASSWORD' option!", COMPONENT_NAME); if (curl_easy_setopt(curlHandle, CURLOPT_POST, 0) != CURLE_OK) @@ -154,6 +136,32 @@ void GrafanaLogAccessCurlClient::submitQuery(std::string & readBuffer, const cha } } +/* +* Constructs a curl based client request based on the provided connection string and targetURI +* The response is reported in the readBuffer +* Uses stringCallback to handle successfull curl requests +*/ +void GrafanaLogAccessCurlClient::submitQuery(std::string & readBuffer, const char * targetURI, bool targetDataSource) +{ + if (isEmptyString(m_grafanaConnectionStr.str())) + throw makeStringExceptionV(-1, "%s Cannot submit query, empty connection string detected!", COMPONENT_NAME); + + if (isEmptyString(targetURI)) + throw makeStringExceptionV(-1, "%s Cannot submit query, empty request URI detected!", COMPONENT_NAME); + + VStringBuffer requestURL("%s%s%s", m_grafanaConnectionStr.str(), targetDataSource ? m_dataSourcesAPIURI.str() : "", targetURI); + + //allow annonymous connections?? + if (isEmptyString(m_grafanaUserName.str())) + throw makeStringExceptionV(-1, "%s: Log query request: Empty user name detected!", COMPONENT_NAME); + + //allow non-secure connections?? + if (isEmptyString(m_grafanaPassword.str())) + throw makeStringExceptionV(-1, "%s: Log query request: Empty password detected!", COMPONENT_NAME); + + submit(readBuffer, requestURL, m_grafanaUserName.str(), m_grafanaPassword.str()); +} + /* * This method consumes a JSON formatted data source response from a successful Grafana Loki query * It extracts the data source information and populates the m_targetDataSource structure and constructs @@ -442,7 +450,7 @@ void GrafanaLogAccessCurlClient::fetchDatasourceByName(const char * targetDataSo std::string readBuffer; VStringBuffer targetURI("/api/datasources/name/%s", targetDataSourceName); - submitQuery(readBuffer, targetURI.str()); + submitQuery(readBuffer, targetURI.str(), true); processDatasourceJsonResp(readBuffer); } @@ -452,16 +460,35 @@ void GrafanaLogAccessCurlClient::fetchDatasourceByName(const char * targetDataSo */ void GrafanaLogAccessCurlClient::fetchDatasources(std::string & readBuffer) { - submitQuery(readBuffer, "/"); + submitQuery(readBuffer, "/api/datasources/", false); } +void GrafanaLogAccessCurlClient::fetchDatasourceLabelValues(std::string & readBuffer, const char * label) +{ + if (isEmptyString(label)) + return; + + + StringBuffer labelValuesURI; + labelValuesURI.setf("/label/%s/values", label); + submitQuery(readBuffer, labelValuesURI.str(), true); +} /* * sumbits a Grafana Loki query to fetch all labels * The response is expected to be a JSON formatted list of labels */ void GrafanaLogAccessCurlClient::fetchLabels(std::string & readBuffer) { - submitQuery(readBuffer, "/label"); + submitQuery(readBuffer, "/label", true); +} + +/* +* sumbits a Grafana API query to fetch basic health info +* The response is expected to be a JSON formatted list of health entries +*/ +void GrafanaLogAccessCurlClient::fetchHealth(std::string & readBuffer) +{ + submitQuery(readBuffer, "/api/health", false); } /* @@ -707,7 +734,7 @@ bool GrafanaLogAccessCurlClient::fetchLog(LogQueryResultDetails & resultDetails, DBGLOG("FetchLog query: %s", fullQuery.str()); std::string readBuffer; - submitQuery(readBuffer, fullQuery.str()); + submitQuery(readBuffer, fullQuery.str(), true); processQueryJsonResp(resultDetails, readBuffer, returnbuf, format, options.getReturnColsMode(), true); } @@ -733,6 +760,208 @@ void processLogMapConfig(const IPropertyTree * logMapConfig, LogField * targetFi targetField->name = logMapConfig->queryProp(logMapSearchColAtt); } +void GrafanaLogAccessCurlClient::healthReport(LogAccessHealthReportOptions options, LogAccessHealthReportDetails & report) +{ + LogAccessHealthStatus status = LOGACCESS_STATUS_green; + /*try + { + report.append("\"ConnectionInfo\": { "); + report.appendf("\"ConnectionString\": \"%s\"", m_grafanaConnectionStr.str()); + report.appendf(", \"UserName\": \"%s\"", m_grafanaUserName.str()); + report.appendf(", \"PasswordProvided\": %s", !isEmptyString(m_grafanaPassword.str()) ? "true" : "false"); + report.appendf(", \"TargetDatasourceID\": \"%s\"", m_targetDataSource.id.str()); + report.appendf(", \"TargetDatasourceName\": \"%s\"", m_targetDataSource.name.str()); + report.appendf(", \"TargetLogsNamespace\": \"%s\"", m_targetNamespace.str()); + report.appendf(", \"ExpectedLogsFormat\": \"%s\"", m_expectedLogFormat.str()); + report.append(" }"); + + report.append(", \"ConfigurationInfo\": "); + toJSON(m_pluginCfg, report); + + report.append(", \"Internals\": { \"Plugin\": { \"LogMaps\": {"); + report.appendf("\"Global\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_globalSearchCol.name.str(), m_globalSearchCol.isStream ? "true" : "false"); + report.appendf(", \"Workunits\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_workunitsColumn.name.str(), m_workunitsColumn.isStream ? "true" : "false"); + report.appendf(", \"Components\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_componentsColumn.name.str(), m_componentsColumn.isStream ? "true" : "false"); + report.appendf(", \"Audience\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_audienceColumn.name.str(), m_audienceColumn.isStream ? "true" : "false"); + report.appendf(", \"LogClass\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_classColumn.name.str(), m_classColumn.isStream ? "true" : "false"); + report.appendf(", \"Instance\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_instanceColumn.name.str(), m_instanceColumn.isStream ? "true" : "false"); + report.appendf(", \"Pod\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_podColumn.name.str(), m_podColumn.isStream ? "true" : "false"); + report.appendf(", \"Container\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_containerColumn.name.str(), m_containerColumn.isStream ? "true" : "false"); + report.appendf(", \"Message\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_messageColumn.name.str(), m_messageColumn.isStream ? "true" : "false"); + report.appendf(", \"Node\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_nodeColumn.name.str(), m_nodeColumn.isStream ? "true" : "false"); + report.appendf(", \"DateTimstamp\": { \"ColName\": \"%s\", \"IsStream\": %s }", m_logDateTimstampColumn.name.str(), m_logDateTimstampColumn.isStream ? "true" : "false"); + report.append(" }"); //close LogMaps + report.append(" }"); //close Plugin + + report.append(", \"Server\": { \"Health\": { "); + try + { + std::string health; + fetchHealth(health); + report.appendf("\"GrafanaHealth\": %s", health.c_str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + report.appendf("\"Error\": \"Exception fetching Grafana health (%d) - %s\"", e->errorCode(), description.str()); + e->Release(); + } + catch(...) + { + report.append("\"Error\": \"Unknown exception while fetching target Grafana health\""); + } + report.append(" }");//close Health + + report.append(", \"AvailableDatasources\": "); + try + { + std::string availableDS; + fetchDatasources(availableDS); + report.append(availableDS.c_str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + report.appendf("\"Error\": \"Exception fetching available Datasources (%d) - %s\"", e->errorCode(), description.str()); + e->Release(); + } + catch(...) + { + report.append("\"Error\": \"Unknown exception while fetch Available DS on target Grafana\""); + } + + StringArray labels; + report.append(", \"AvailableLabels\":"); + try + { + std::string availableLabels; + fetchLabels(availableLabels); + + Owned labelsResp = createPTreeFromJSONString(availableLabels.c_str()); + if (!labelsResp) + { + report.append("\"Labels response not in expected JSON format!\""); + } + else + { + //const char * status = labelsResp->queryProp("status"); + //messages.append("Labels request status:"); + //messages.append(status); + report.append(" {"); + Owned labelsIter = labelsResp->getElements("data"); + bool firstLabel = true; + ForEach(*labelsIter) + { + std::string labelValues; + IPropertyTree & logMap = labelsIter->query(); + const char * label = logMap.queryProp("."); + if (firstLabel) + firstLabel = false; + else + report.append(", "); + + report.appendf("\"%s\": [ ", label); + fetchDatasourceLabelValues(labelValues, label); + Owned labelValResp = createPTreeFromJSONString(labelValues.c_str()); + Owned labelValsIter = labelValResp->getElements("data"); + + bool firstLabelValue = true; + ForEach(*labelValsIter) + { + if (firstLabelValue) + firstLabelValue = false; + else + report.append(", "); + + report.appendf("\"%s\"", labelValsIter->query().queryProp(".")); + } + report.append(" ]"); + } + report.append(" }"); + } + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + report.appendf("\"Exception fetching available labels (%d) - %s\"", e->errorCode(), description.str()); + e->Release(); + } + catch(...) + { + report.append("\"Unknown exception while fetching target Grafana/Loki labels\""); + } + + report.append(" }");//close Server + report.append(" }");//close Internals + + //messages.append(", \"Messages\": {"); + //messages.append(" }"); //close messages + + report.append(", \"SampleQuery\": { "); + try + { + report.appendf("\"Query\": { \"LogFormat\": \"JSON\","); + LogAccessLogFormat outputFormat = LOGACCESS_LOGFORMAT_json; + LogAccessConditions queryOptions; + + report.appendf("\"Filter\": {\"type\": \"byComponent\", \"value\": \"\" },"); + queryOptions.setFilter(getComponentLogAccessFilter("")); + + 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); + report.appendf("\"Limit\": \"5\" }, "); + queryOptions.setLimit(5); + + StringBuffer logs; + LogQueryResultDetails resultDetails; + fetchLog(resultDetails, queryOptions, logs, outputFormat); + report.appendf("\"ResultCount\": \"%d\", ", resultDetails.totalReceived); + if (resultDetails.totalReceived == 0) + status = LOGACCESS_STATUS_yellow; + + report.appendf("\"Results\": %s", logs.str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + report.appendf("\"Error\": \"Exception while executing sample Grafana/Loki query (%d) - %s\"", e->errorCode(), description.str()); + e->Release(); + status = LOGACCESS_STATUS_red; + } + catch(...) + { + report.append("\"Error\": \"Unknown exception while executing sample Grafana/Loki query\""); + } + report.append(" }"); //close sample query + } + catch(...) + { + report.append("\"Error\": \"Encountered unexpected exception during health report\""); + status = LOGACCESS_STATUS_red; + } +*/ + //return status; +} + GrafanaLogAccessCurlClient::GrafanaLogAccessCurlClient(IPropertyTree & logAccessPluginConfig) { m_pluginCfg.set(&logAccessPluginConfig); diff --git a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.hpp b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.hpp index 316dd097ad8..20a214e64d4 100644 --- a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.hpp +++ b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.hpp @@ -94,7 +94,9 @@ class GrafanaLogAccessCurlClient : public CInterfaceOf void fetchDatasourceByName(const char * targetDataSourceName); void fetchDatasources(std::string & readBuffer); void fetchLabels(std::string & readBuffer); - void submitQuery(std::string & readBuffer, const char * targetURI); + void fetchHealth(std::string & readBuffer); + void fetchDatasourceLabelValues(std::string & readBuffer, const char * label); + void submitQuery(std::string & readBuffer, const char * targetURI, bool targetDataSource); void populateQueryFilterAndStreamSelector(StringBuffer & queryString, StringBuffer & streamSelector, const ILogAccessFilter * filter); static void timestampQueryRangeString(StringBuffer & range, std::time_t from, std::time_t to); @@ -107,4 +109,5 @@ class GrafanaLogAccessCurlClient : public CInterfaceOf virtual IRemoteLogAccessStream * getLogReader(const LogAccessConditions & options, LogAccessLogFormat format) override; virtual IRemoteLogAccessStream * getLogReader(const LogAccessConditions & options, LogAccessLogFormat format, unsigned int pageSize) override; virtual bool supportsResultPaging() const override { return false;} -}; \ No newline at end of file + virtual void healthReport(LogAccessHealthReportOptions options, LogAccessHealthReportDetails & report) override; +};