From 9224ed6ab61d24254970fb193af783417e9de268 Mon Sep 17 00:00:00 2001 From: Rodrigo Pastrana Date: Wed, 20 Nov 2024 00:01:32 -0500 Subject: [PATCH] HPCC-32874 Add WsLogAcces Health Report Method - Adds healthreport method to logaccess interface - Implements healthreport method for all logaccess plugins Signed-off-by: Rodrigo Pastrana --- esp/scm/ws_logaccess.ecm | 12 +- .../ws_logaccess/WsLogAccessService.cpp | 26 ++ .../ws_logaccess/WsLogAccessService.hpp | 1 + system/jlib/jlog.hpp | 3 +- .../AzureLogAnalyticsCurlClient.cpp | 102 ++++++- .../AzureLogAnalyticsCurlClient.hpp | 1 + .../ElasticStack/ElasticStackLogAccess.cpp | 158 +++++++++++ .../ElasticStack/ElasticStackLogAccess.hpp | 1 + .../Grafana/CurlClient/GrafanaCurlClient.cpp | 257 ++++++++++++++++-- .../Grafana/CurlClient/GrafanaCurlClient.hpp | 5 +- 10 files changed, 534 insertions(+), 32 deletions(-) diff --git a/esp/scm/ws_logaccess.ecm b/esp/scm/ws_logaccess.ecm index 5232f983275..c7b59f384fe 100644 --- a/esp/scm/ws_logaccess.ecm +++ b/esp/scm/ws_logaccess.ecm @@ -229,10 +229,20 @@ 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 +{ +}; + +ESPResponse GetHealthReportResponse +{ + ESParray messages; +}; + +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..12fff4fe17f 100644 --- a/esp/services/ws_logaccess/WsLogAccessService.cpp +++ b/esp/services/ws_logaccess/WsLogAccessService.cpp @@ -385,3 +385,29 @@ bool Cws_logaccessEx::onGetLogs(IEspContext &context, IEspGetLogsRequest &req, I return true; } + +bool Cws_logaccessEx::onGetHealthReport(IEspContext &context, IEspGetHealthReportRequest &req, IEspGetHealthReportResponse &resp) +{ + StringArray testMessages; + bool success = true; + if (!queryRemoteLogAccessor()) + { + testMessages.append("LogAccess plugin not available, review logAccess configuration!"); + success = false; + } + else + { + try + { + queryRemoteLogAccessor()->healthReport(testMessages); + } + catch(...) + { + testMessages.append("Encountered unknown exception while performing connectivity test"); + } + } + + resp.setMessages(testMessages); + + return success; +} \ No newline at end of file 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..01e3a69c961 100644 --- a/system/jlib/jlog.hpp +++ b/system/jlib/jlog.hpp @@ -1690,6 +1690,7 @@ interface IRemoteLogAccess : extends IInterface virtual IPropertyTree * queryLogMap() const = 0; virtual const char * fetchConnectionStr() const = 0; virtual bool supportsResultPaging() const = 0; + virtual bool healthReport(StringArray & messages) = 0; }; // Helper functions to construct log access filters @@ -1714,7 +1715,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 9269737a836..be6fe3eb80a 100644 --- a/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.cpp +++ b/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.cpp @@ -556,7 +556,6 @@ bool generateHPCCLogColumnstAllColumns(StringBuffer & kql, const char * colName, else kql.append("LogEntrySource, TimeOfCommand, SourceSystem"); - return true; } @@ -1058,6 +1057,107 @@ bool AzureLogAnalyticsCurlClient::processSearchJsonResp(LogQueryResultDetails & return true; } +bool AzureLogAnalyticsCurlClient::healthReport(StringArray & messages) +{ + try + { + StringBuffer scratch; + scratch.setf("Target Azure Log Analytics workspace ID: '%s'", m_logAnalyticsWorkspaceID.str()); + messages.append(scratch.str()); + + scratch.setf("Target Azure Log Analytics tenant ID: '%s'", m_aadTenantID.str()); + messages.append(scratch.str()); + + scratch.setf("Target Azure Log Analytics client ID: '%s'", m_aadClientID.str()); + messages.append(scratch.str()); + + scratch.setf("Target Azure Log Analytics secret is%s empty", m_aadClientSecret.length()==0 ? "" : " not"); + messages.append(scratch.str()); + + scratch.setf("Targets ContainerLogV%c", targetIsContainerLogV2 ? '2' : '1'); + messages.append(scratch.str()); + + scratch.setf("Components query joins %senabled", m_disableComponentNameJoins ? "not " : ""); + messages.append(scratch.str()); + + if (m_pluginCfg) + { + StringBuffer configXML; + toXML(m_pluginCfg, configXML); + + scratch.setf("Configuration tree: '%s'", configXML.str()); + messages.append(scratch.str()); + } + else + { + messages.append("Configuration tree is empty!!!"); + } + + scratch.set("LogMaps:"); + scratch.appendf("\n\tGlobal column: '%s', index pattern: '%s', timestamp column: '%s'", m_globalSearchColName.str(), m_globalIndexSearchPattern.str(), m_globalIndexTimestampField.str()); + scratch.appendf("\n\tWorkunits column: '%s', index pattern: '%s'", m_workunitSearchColName.str(), m_workunitIndexSearchPattern.str()); + scratch.appendf("\n\tComponents column: '%s', index pattern: '%s'", m_componentsSearchColName.str(), m_componentsIndexSearchPattern.str()); + scratch.appendf("\n\tAudience column: '%s', index pattern: '%s'", m_audienceSearchColName.str(), m_audienceIndexSearchPattern.str()); + scratch.appendf("\n\tLog Class column: '%s', index pattern: '%s'", m_classSearchColName.str(), m_classIndexSearchPattern.str()); + scratch.appendf("\n\tInstance column: '%s', index pattern: '%s'", m_instanceSearchColName.str(), m_instanceIndexSearchPattern.str()); + scratch.appendf("\n\tPod column: '%s', index pattern: '%s'", m_podSearchColName.str(), m_podIndexSearchPattern.str()); + scratch.appendf("\n\tTraceID column: '%s', index pattern: '%s'", m_traceSearchColName.str(), m_traceIndexSearchPattern.str()); + scratch.appendf("\n\tSpanID column: '%s', index pattern: '%s'", m_spanSearchColName.str(), m_spanIndexSearchPattern.str()); + scratch.appendf("\n\tHost column: '%s', index pattern: '%s'", m_hostSearchColName.str(), m_hostIndexSearchPattern.str()); + messages.append(scratch.str()); + + try + { + LogAccessLogFormat outputFormat = LOGACCESS_LOGFORMAT_xml; + LogAccessConditions queryOptions; + + queryOptions.setFilter(getComponentLogAccessFilter("")); + + struct LogAccessTimeRange range; + CDateTime endtt; + endtt.setNow(); + range.setEnd(endtt); + + CDateTime startt; + startt.setNow(); + startt.adjustTimeSecs(-60); //an hour ago + range.setStart(startt); + + StringBuffer startstr; + startt.getString(startstr); + + queryOptions.setTimeRange(range); + queryOptions.setLimit(100); + + StringBuffer logs; + LogQueryResultDetails resultDetails; + fetchLog(resultDetails, queryOptions, logs, outputFormat); + scratch.setf("Sample ALA query resulted in %d log records.", resultDetails.totalReceived); + messages.append(scratch.str()); + messages.append(logs.str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("Exception while executing sample Grafana/Loki query (%d) - %s", e->errorCode(), description.str()); + messages.append(scratch.str()); + e->Release(); + } + catch(...) + { + messages.append("Unknown exception while executing sample Grafana/Loki query"); + } + } + catch(...) + { + messages.append("Encountered unexpected exception during health report"); + return false; + } + + return true; +} + 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 d2b92460f4e..b29639108db 100644 --- a/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.hpp +++ b/system/logaccess/Azure/LogAnalytics/CurlClient/AzureLogAnalyticsCurlClient.hpp @@ -100,4 +100,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 bool healthReport(StringArray & messages) override; }; diff --git a/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp b/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp index 878946703a1..f97c5dc5b10 100644 --- a/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp +++ b/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp @@ -262,6 +262,164 @@ const IPropertyTree * ElasticStackLogAccess::getESStatus() return performAndLogESRequest(Client::HTTPMethod::GET, "_cluster/health", "", "Target cluster health"); } + bool ElasticStackLogAccess::healthReport(StringArray & messages) + { + try + { + StringBuffer scratch; + scratch.setf("Connection string: '%s'", m_esConnectionStr.str()); + messages.append(scratch.str()); + + if (m_pluginCfg) + { + StringBuffer configXML; + toXML(m_pluginCfg, configXML); + + scratch.setf("Configuration tree: '%s'", configXML.str()); + messages.append(scratch.str()); + } + else + { + messages.append("Configuration tree is empty!!!"); + } + + scratch.set("LogMaps:"); + scratch.appendf("\n\tGlobal column: '%s', index pattern: '%s', timestamp column: '%s'", m_globalSearchColName.str(), m_globalIndexSearchPattern.str(), m_globalIndexTimestampField.str()); + scratch.appendf("\n\tWorkunits column: '%s', index pattern: '%s'", m_workunitSearchColName.str(), m_workunitIndexSearchPattern.str()); + scratch.appendf("\n\tComponents column: '%s', index pattern: '%s'", m_componentsSearchColName.str(), m_componentsIndexSearchPattern.str()); + scratch.appendf("\n\tAudience column: '%s', index pattern: '%s'", m_audienceSearchColName.str(), m_audienceIndexSearchPattern.str()); + scratch.appendf("\n\tLog Class column: '%s', index pattern: '%s'", m_classSearchColName.str(), m_classIndexSearchPattern.str()); + scratch.appendf("\n\tInstance column: '%s', index pattern: '%s'", m_instanceSearchColName.str(), m_instanceIndexSearchPattern.str()); + scratch.appendf("\n\tPod column: '%s', index pattern: '%s'", m_podSearchColName.str(), m_podIndexSearchPattern.str()); + scratch.appendf("\n\tTraceID column: '%s', index pattern: '%s'", m_traceSearchColName.str(), m_traceIndexSearchPattern.str()); + scratch.appendf("\n\tSpanID column: '%s', index pattern: '%s'", m_spanSearchColName.str(), m_spanIndexSearchPattern.str()); + scratch.appendf("\n\tHost column: '%s', index pattern: '%s'", m_hostSearchColName.str(), m_hostIndexSearchPattern.str()); + messages.append(scratch.str()); + + try + { + StringBuffer out; + const IPropertyTree * status = getESStatus(); + if (status) + { + toXML(status, out); + scratch.setf("\n\tES Status: '%s'", out.str()); + messages.append(scratch.str()); + } + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("\n\tException fetching ES Status (%d) - %s", e->errorCode(), description.str()); + messages.append(description.str()); + e->Release(); + } + catch(...) + { + messages.append("\n\tUnknown exception while fetching ES Status"); + } + + try + { + StringBuffer out; + const IPropertyTree * is = getIndexSearchStatus(m_globalIndexSearchPattern); + if (is) + { + toXML(is, out.clear()); + scratch.setf("ES available indices: '%s'", out.str()); + messages.append(scratch.str()); + } + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("\n\tException fetching available ES indices (%d) - %s", e->errorCode(), description.str()); + messages.append(description.str()); + e->Release(); + } + catch(...) + { + messages.append("\n\tUnknown exception while fetching available ES indices"); + } + + try + { + StringBuffer out; + const IPropertyTree * ts = getTimestampTypeFormat(m_globalIndexSearchPattern, m_globalIndexTimestampField); + if (ts) + { + toXML(ts, out.clear()); + scratch.setf("ES timestamp field '%s' format for index '%s': '%s'", m_globalIndexTimestampField.str(), m_globalIndexSearchPattern.str(), out.str()); + messages.append(scratch.str()); + } + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("\n\tException fetching target ES timestamp format (%d) - %s", e->errorCode(), description.str()); + messages.append(description.str()); + e->Release(); + } + catch(...) + { + messages.append("\n\tUnknown exception while fetching target ES timestamp format"); + } + + try + { + LogAccessLogFormat outputFormat = LOGACCESS_LOGFORMAT_xml; + LogAccessConditions queryOptions; + + queryOptions.setFilter(getComponentLogAccessFilter("eclwatch")); + + struct LogAccessTimeRange range; + CDateTime endtt; + endtt.setNow(); + range.setEnd(endtt); + + CDateTime startt; + startt.setNow(); + startt.adjustTimeSecs(-60); //an hour ago + range.setStart(startt); + + StringBuffer startstr; + startt.getString(startstr); + + queryOptions.setTimeRange(range); + queryOptions.setLimit(100); + + StringBuffer logs; + LogQueryResultDetails resultDetails; + fetchLog(resultDetails, queryOptions, logs, outputFormat); + scratch.setf("Sample ALA query resulted in %d log records.", resultDetails.totalReceived); + messages.append(scratch.str()); + messages.append(logs.str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("Exception while executing sample Grafana/Loki query (%d) - %s", e->errorCode(), description.str()); + messages.append(scratch.str()); + e->Release(); + } + catch(...) + { + messages.append("Unknown exception while executing sample Grafana/Loki query"); + } + } + catch(...) + { + messages.append("Encountered unexpected exception during health report"); + return false; + } + + return true; + } + /* * 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..7cfcfb7cb0d 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 bool healthReport(StringArray & messages) override; }; diff --git a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp index 505e2a82778..c89ea49311f 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,180 @@ void processLogMapConfig(const IPropertyTree * logMapConfig, LogField * targetFi targetField->name = logMapConfig->queryProp(logMapSearchColAtt); } +bool GrafanaLogAccessCurlClient::healthReport(StringArray & messages) +{ + try + { + StringBuffer scratch; + scratch.setf("\n\tConnection string: '%s'", m_grafanaConnectionStr.str()); + scratch.appendf("\n\tGrafana user name: '%s'", m_grafanaUserName.str()); + scratch.appendf("\n\tGrafana password is %s empty", isEmptyString(m_grafanaPassword.str()) ? "" : " not"); + scratch.appendf("\n\tConfigured target Grafana datasource id: '%s'", m_targetDataSource.id.str()); + scratch.appendf("\n\tConfigured target Grafana datasource name: '%s'", m_targetDataSource.name.str()); + scratch.appendf("\n\tConfigured target logs namespace: '%s'", m_targetNamespace.str()); + scratch.appendf("\n\tConfigured expected logs format: '%s'", m_expectedLogFormat.str()); + messages.append(scratch.str()); + + scratch.set("LogMaps:"); + scratch.appendf("\n\tGlobal column name: '%s', is %s stream.", m_globalSearchCol.name.str(), m_globalSearchCol.isStream ? "" : " not"); + scratch.appendf("\n\tWorkunits column name: '%s', is %s stream.", m_workunitsColumn.name.str(), m_workunitsColumn.isStream ? "" : " not"); + scratch.appendf("\n\tComponents column name: '%s', is %s stream.", m_componentsColumn.name.str(), m_componentsColumn.isStream ? "" : " not"); + scratch.appendf("\n\tAudience column name: '%s', is %s stream.", m_audienceColumn.name.str(), m_audienceColumn.isStream ? "" : " not"); + scratch.appendf("\n\tLog Class column name: '%s', is %s stream.", m_classColumn.name.str(), m_classColumn.isStream ? "" : " not"); + scratch.appendf("\n\tInstance column name: '%s', is %s stream.", m_instanceColumn.name.str(), m_instanceColumn.isStream ? "" : " not"); + scratch.appendf("\n\tPod column name: '%s', is %s stream.", m_podColumn.name.str(), m_podColumn.isStream ? "" : " not"); + scratch.appendf("\n\tContainer column name: '%s', is %s stream.", m_containerColumn.name.str(), m_containerColumn.isStream ? "" : " not"); + scratch.appendf("\n\tMessage column name: '%s', is %s stream.", m_messageColumn.name.str(), m_messageColumn.isStream ? "" : " not"); + scratch.appendf("\n\tNode column name: '%s', is %s stream.", m_nodeColumn.name.str(), m_nodeColumn.isStream ? "" : " not"); + scratch.appendf("\n\tDateTimstamp column name: '%s', is %s stream.", m_logDateTimstampColumn.name.str(), m_logDateTimstampColumn.isStream ? "" : " not"); + messages.append(scratch.str()); + + scratch.set("Reported Grafana health:"); + try + { + std::string health; + fetchHealth(health); + scratch.append("\n\t"); + scratch.append(health.c_str()); + messages.append(scratch.str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("\n\tException fetching Grafana health (%d) - %s", e->errorCode(), description.str()); + messages.append(description.str()); + e->Release(); + } + catch(...) + { + messages.append("\n\tUnknown exception while fetching target Grafana health"); + } + + scratch.set("\n\tAvailable Datasources on target Grafana: "); + try + { + std::string availableDS; + fetchDatasources(availableDS); + scratch.appendf("\n\t%s", availableDS.c_str()); + messages.append(scratch.str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("\n\tException fetching available Datasources (%d) - %s", e->errorCode(), description.str()); + messages.append(scratch.str()); + e->Release(); + } + catch(...) + { + messages.append("\n\tUnknown exxception while fetch Available DS on target Grafana"); + } + + StringArray labels; + + scratch.set("Available labels on target Grafana/Loki: "); + try + { + std::string availableLabels; + fetchLabels(availableLabels); + + Owned labelsResp = createPTreeFromJSONString(availableLabels.c_str()); + if (!labelsResp) + { + messages.append("Labels response not expected JSON format"); + } + else + { + const char * status = labelsResp->queryProp("status"); + messages.append("Labels request status:"); + messages.append(status); + + Owned labelsIter = labelsResp->getElements("data"); + ForEach(*labelsIter) + { + std::string labelValues; + IPropertyTree & logMap = labelsIter->query(); + const char * label = logMap.queryProp("."); + scratch.setf("\n%s:", label); + fetchDatasourceLabelValues(labelValues, label); + Owned labelValResp = createPTreeFromJSONString(labelValues.c_str()); + Owned labelValsIter = labelValResp->getElements("data"); + + ForEach(*labelValsIter) + { + scratch.appendf("\n\t%s", labelValsIter->query().queryProp(".")); + } + messages.append(scratch); + } + } + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("\n\tException fetching available labels (%d) - %s", e->errorCode(), description.str()); + messages.append(scratch.str()); + e->Release(); + } + catch(...) + { + messages.append("\n\tUnknown exxception while fetching target Grafana/Loki labels"); + } + + try + { + LogAccessLogFormat outputFormat = LOGACCESS_LOGFORMAT_xml; + LogAccessConditions queryOptions; + + queryOptions.setFilter(getComponentLogAccessFilter("")); + + struct LogAccessTimeRange range; + CDateTime endtt; + endtt.setNow(); + range.setEnd(endtt); + + CDateTime startt; + startt.setNow(); + startt.adjustTimeSecs(-60); //an hour ago + range.setStart(startt); + + StringBuffer startstr; + startt.getString(startstr); + + queryOptions.setTimeRange(range); + queryOptions.setLimit(100); + + StringBuffer logs; + LogQueryResultDetails resultDetails; + fetchLog(resultDetails, queryOptions, logs, outputFormat); + scratch.setf("Sample Grafana/Loki query resulted in %d log records.", resultDetails.totalReceived); + messages.append(scratch.str()); + messages.append(logs.str()); + } + catch(IException * e) + { + StringBuffer description; + e->errorMessage(description); + scratch.setf("Exception while executing sample Grafana/Loki query (%d) - %s", e->errorCode(), description.str()); + messages.append(scratch.str()); + e->Release(); + } + catch(...) + { + messages.append("Unknown exception while executing sample Grafana/Loki query"); + } + } + catch(...) + { + messages.append("Encountered unexpected exception during health report"); + return false; + } + + return true; +} + 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..1f0f30da718 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;} + virtual bool healthReport(StringArray & messages) override;// { return false;} }; \ No newline at end of file