diff --git a/helm/managed/logging/loki-stack/README.md b/helm/managed/logging/loki-stack/README.md index 8942e7ec008..fb8931b93e0 100644 --- a/helm/managed/logging/loki-stack/README.md +++ b/helm/managed/logging/loki-stack/README.md @@ -89,8 +89,8 @@ HPCC logAccess requires access to the Grafana username/password. Those values mu The secret is expected to be in the 'esp' category, and be named 'grafana-logaccess'. The following key-value pairs are required (key names must be spelled exactly as shown here) - grafana-username - This should contain the Grafana username - grafana-password - This should contain the Grafana password + username - This should contain the Grafana username + password - This should contain the Grafana password The included 'create-grafana-logaccess-secret.sh' helper can be used to create the necessary secret. @@ -114,7 +114,7 @@ Example use: #### -The grafana hpcc logaccess values should provide Grafana connection information, such as the host, and port; the Loki datasource where the logs recide; the k8s namespace under which the logs were created; and the hpcc compnent log format (table|json|xml) +The grafana hpcc logaccess values should provide Grafana connection information, such as the host, and port; the Loki datasource where the logs reside; the k8s namespace under which the logs were created; and the hpcc compnent log format (table|json|xml) Example use: global: @@ -131,4 +131,5 @@ Example use: namespace: name: "hpcc" logFormat: - type: "json" \ No newline at end of file + type: "json" + diff --git a/helm/managed/logging/loki-stack/create-grafana-logaccess-secret.sh b/helm/managed/logging/loki-stack/create-grafana-logaccess-secret.sh index 3c86d5dc35d..f4c7efbed09 100755 --- a/helm/managed/logging/loki-stack/create-grafana-logaccess-secret.sh +++ b/helm/managed/logging/loki-stack/create-grafana-logaccess-secret.sh @@ -23,8 +23,8 @@ usage() echo "" echo "Expected directory structure:" echo "${secretsdir}/" - echo " grafana-password - Should contain Grafana user name" - echo " grafana-username - Should contain Grafana password" + echo " password - Should contain Grafana user name" + echo " username - Should contain Grafana password" } while [ "$#" -gt 0 ]; do diff --git a/helm/managed/logging/loki-stack/secrets-templates/grafana-password b/helm/managed/logging/loki-stack/secrets-templates/grafana-password deleted file mode 100644 index 8a4fe3bf61f..00000000000 --- a/helm/managed/logging/loki-stack/secrets-templates/grafana-password +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/helm/managed/logging/loki-stack/secrets-templates/password b/helm/managed/logging/loki-stack/secrets-templates/password new file mode 100644 index 00000000000..6b3a9a39380 --- /dev/null +++ b/helm/managed/logging/loki-stack/secrets-templates/password @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/helm/managed/logging/loki-stack/secrets-templates/grafana-username b/helm/managed/logging/loki-stack/secrets-templates/username similarity index 100% rename from helm/managed/logging/loki-stack/secrets-templates/grafana-username rename to helm/managed/logging/loki-stack/secrets-templates/username diff --git a/system/jlib/jstring.cpp b/system/jlib/jstring.cpp index b6898b7c4dd..b9d6c70c3c4 100644 --- a/system/jlib/jstring.cpp +++ b/system/jlib/jstring.cpp @@ -2367,14 +2367,14 @@ StringBuffer &encodeJSON(StringBuffer &s, const char *value) return encodeJSON(s, strlen(value), value); } -inline StringBuffer &encodeCSVChar(StringBuffer &s, const char *&ch, unsigned &remaining) +inline StringBuffer & encodeCSVChar(StringBuffer & encodedCSV, char ch) { - byte next = *ch; + byte next = ch; switch (next) { case '\"': - s.append("\""); - s.append(next); + encodedCSV.append("\""); + encodedCSV.append(next); break; //case '\n': // s.append("\\n"); @@ -2383,31 +2383,29 @@ inline StringBuffer &encodeCSVChar(StringBuffer &s, const char *&ch, unsigned &r // s.append("\\r"); // break; default: - s.append(next); + encodedCSV.append(next); break; } - ch++; - remaining--; - return s; + return encodedCSV; } -StringBuffer &encodeCSV(StringBuffer &s, unsigned size, const char *value) +StringBuffer & encodeCSVColumn(StringBuffer & encodedCSV, unsigned size, const char *rawCSVCol) { - if (!value) - return s; - s.ensureCapacity(size); // Minimum size that will be written - s.append("\""); - while (size) - encodeCSVChar(s, value, size); - s.append("\""); - return s; + if (!rawCSVCol) + return encodedCSV; + encodedCSV.ensureCapacity(size+2); // Minimum size that will be written + encodedCSV.append("\""); + for (size32_t i = 0; i < size; i++) + encodeCSVChar(encodedCSV, rawCSVCol[i]); + encodedCSV.append("\""); + return encodedCSV; } -StringBuffer &encodeCSV(StringBuffer &s, const char *value) +StringBuffer & encodeCSVColumn(StringBuffer & encodedCSV, const char *rawCSVCol) { - if (!value) - return s; - return encodeCSV(s, strlen(value), value); + if (!rawCSVCol) + return encodedCSV; + return encodeCSVColumn(encodedCSV, strlen(rawCSVCol), rawCSVCol); } bool checkUnicodeLiteral(char const * str, unsigned length, unsigned & ep, StringBuffer & msg) diff --git a/system/jlib/jstring.hpp b/system/jlib/jstring.hpp index 9bb5a0a9c21..b3fe7651daf 100644 --- a/system/jlib/jstring.hpp +++ b/system/jlib/jstring.hpp @@ -479,7 +479,10 @@ inline StringBuffer &delimitJSON(StringBuffer &s, bool addNewline=false, bool es return s; } -jlib_decl StringBuffer &encodeCSV(StringBuffer &s, const char *value); +/* +* Encodes a CSV column, not an entire CSV record +*/ +jlib_decl StringBuffer &encodeCSVColumn(StringBuffer &s, const char *value); jlib_decl StringBuffer &encodeJSON(StringBuffer &s, const char *value); jlib_decl StringBuffer &encodeJSON(StringBuffer &s, unsigned len, const char *value); diff --git a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp index 007f3b67a01..dcdfa0bf660 100644 --- a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp +++ b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.cpp @@ -38,8 +38,6 @@ static constexpr const char * DEFAULT_GRAFANA_PROTOCOL = "http"; static constexpr const char * DEFAULT_GRAFANA_PORT = "3000"; static constexpr const char * DEFAULT_DATASOURCE_ID = "1"; -static constexpr const char * GRAFANA_API_FETCH_DATASOURCES = "/api/datasources/"; -static constexpr const char * defaultStreamName = "namespace"; static constexpr const char * defaultNamespaceStream = "default"; static constexpr const char * defaultExpectedLogFormat = "table"; //"json"; @@ -49,6 +47,8 @@ static constexpr const char * logMapTimeStampColAtt = "@timeStampColumn"; static constexpr const char * logMapKeyColAtt = "@keyColumn"; static constexpr const char * logMapDisableJoinsAtt = "@disableJoins"; +static constexpr std::size_t defaultMaxRecordsPerFetch = 100; + /* * To be used as a callback for curl_easy_setopt to capture the response from a curl request */ @@ -79,14 +79,11 @@ void GrafanaLogAccessCurlClient::submitQuery(std::string & readBuffer, const cha char curlErrBuffer[CURL_ERROR_SIZE]; curlErrBuffer[0] = '\0'; - //char * encodedURI = curl_easy_escape(curlHandle, targetURI, strlen(targetURI)); VStringBuffer requestURL("%s%s%s", m_grafanaConnectionStr.str(), m_dataSourcesAPIURI.str(), targetURI); - //curl_free(encodedURI); 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()); - //CURLE_UNKNOWN_OPTION if not, or CURLE_NOT_BUILT_IN if the bitmask specified no supported authentication methods. int curloptretcode = curl_easy_setopt(curlHandle, CURLOPT_HTTPAUTH, (long)CURLAUTH_BASIC); if (curloptretcode != CURLE_OK) { @@ -106,17 +103,6 @@ void GrafanaLogAccessCurlClient::submitQuery(std::string & readBuffer, const cha if (isEmptyString(m_grafanaPassword.str())) throw makeStringExceptionV(-1, "%s: Log query request: Empty password detected!", COMPONENT_NAME); - //VStringBuffer basicAuth("%s:%s", m_grafanaUserName.get(), m_grafanaPassword.get()); - //curloptretcode = curl_easy_setopt(curlHandle, CURLOPT_USERPWD, basicAuth.str()); - //if (curloptretcode != CURLE_OK) - //{ - // if (curloptretcode == CURLE_UNKNOWN_OPTION) - // throw makeStringExceptionV(-1, "%s: Log query request: UNKNONW OPTION 'CURLOPT_HTTPAUTH'!", COMPONENT_NAME); - // if (curloptretcode == CURLE_NOT_BUILT_IN) - // throw makeStringExceptionV(-1, "%s: Log query request: bitmask specified no supported authentication methods 'CURLOPT_HTTPAUTH'!", COMPONENT_NAME); - // throw makeStringExceptionV(-1, "%s: Log query request: bitmask specified no supported authentication methods 'CURLOPT_HTTPAUTH'!", COMPONENT_NAME); - //} - if (curl_easy_setopt(curlHandle, CURLOPT_USERNAME, m_grafanaUserName.str())) throw makeStringExceptionV(-1, "%s: Log query request: Could not set 'CURLOPT_USERNAME' option!", COMPONENT_NAME); @@ -144,8 +130,8 @@ void GrafanaLogAccessCurlClient::submitQuery(std::string & readBuffer, const cha if (curl_easy_setopt(curlHandle, CURLOPT_ERRORBUFFER, curlErrBuffer) != CURLE_OK) throw makeStringExceptionV(-1, "%s: Log query request: Could not set 'CURLOPT_ERRORBUFFER' option!", COMPONENT_NAME); - //if (curl_easy_setopt(curlHandle, CURLOPT_FAILONERROR, 1L) != CURLE_OK) // non HTTP Success treated as error - // throw makeStringExceptionV(-1, "%s: Log query request: Could not set 'CURLOPT_FAILONERROR'option!", COMPONENT_NAME); + //If we set CURLOPT_FAILONERROR, we'll miss the actual error message returned in the response + //(curl_easy_setopt(curlHandle, CURLOPT_FAILONERROR, 1L) != CURLE_OK) // non HTTP Success treated as error try { @@ -213,9 +199,9 @@ void formatResultLine(StringBuffer & returnbuf, const char * resultLine, const c { case LOGACCESS_LOGFORMAT_xml: { - StringBuffer encodedXML; - encodeXML(resultLine, encodedXML); - returnbuf.appendf("<%s>%s", resultLineName, encodedXML.str(), resultLineName); + returnbuf.appendf("<%s>", resultLineName); + encodeXML(resultLine, returnbuf); + returnbuf.appendf("", resultLineName); isFirstLine = false; break; } @@ -224,18 +210,15 @@ void formatResultLine(StringBuffer & returnbuf, const char * resultLine, const c if (!isFirstLine) returnbuf.append(", "); + returnbuf.append("\""); + encodeJSON(returnbuf,resultLine); + returnbuf.append("\""); isFirstLine = false; - - StringBuffer encodedJSON; - encodeJSON(encodedJSON,resultLine); - returnbuf.appendf("\"%s\"", encodedJSON.str()); break; } case LOGACCESS_LOGFORMAT_csv: { - StringBuffer encodedCSV; - encodeCSV(encodedCSV, resultLine); //is it worth it to escape the entire line? - returnbuf.append(encodedCSV.str()); + encodeCSVColumn(returnbuf, resultLine); //Currently treating entire log line as a single CSV column returnbuf.newline(); isFirstLine = false; break; @@ -262,8 +245,7 @@ void processValues(StringBuffer & returnbuf, IPropertyTreeIterator * valuesIter, } else { - //exception? - DBGLOG("Detected unexpected response format!: %s", values.queryProp(".")); + throw makeStringExceptionV(-1, "%s: Detected unexpected Grafana/Loki values response format!: %s", COMPONENT_NAME, values.queryProp(".")); } } } @@ -336,7 +318,6 @@ void wrapResult(StringBuffer & returnbuf, IPropertyTree * result, LogAccessLogFo logLineIter.setown(result->getElements("values")); } - unsigned recsProcessed = 0; processValues(returnbuf, logLineIter, format, isFirstLine); } @@ -360,24 +341,7 @@ void GrafanaLogAccessCurlClient::processQueryJsonResp(LogQueryResultDetails & re if (!data) throw makeStringExceptionV(-1, "%s: Could no parse data element!", COMPONENT_NAME); - if (data->hasProp("result")) //if no data, empty query rep - { - //Adds the format prefix to the return buffer - resultsWrapStart(returnbuf, format, reportHeader); - - bool isFirstLine = true; - Owned resultIter = data->getElements("result"); - //many result elements can be returned, each with a unique set of labels - ForEach(*resultIter) - { - IPropertyTree & result = resultIter->query(); - wrapResult(returnbuf, &result, format, isFirstLine); - } - - //Adds the format postfix to the return buffer - resultsWrapEnd(returnbuf, format); - } - + //process stats first, in case reported entries returned can help preallocate return buffer? if (data->hasProp("stats")) { if (data->hasProp("stats/summary/totalEntriesReturned")) @@ -397,6 +361,25 @@ void GrafanaLogAccessCurlClient::processQueryJsonResp(LogQueryResultDetails & re "store": {"totalChunksRef": 0, "totalChunksDownloaded": 0, "chunksDownloadTime": 0, "chunk": {"headChunkBytes": 0,"headChunkLines": 0,"decompressedBytes": 0, "decompressedLines": 0,"compressedBytes": 0, "totalDuplicates": 0 }}}*/ + + if (data->hasProp("result")) //if no data, empty query rep + { + returnbuf.ensureCapacity(retrievedDocument.length());// this is difficult to predict, at least the size of the response? + //Adds the format prefix to the return buffer + resultsWrapStart(returnbuf, format, reportHeader); + + bool isFirstLine = true; + Owned resultIter = data->getElements("result"); + //many result elements can be returned, each with a unique set of labels + ForEach(*resultIter) + { + IPropertyTree & result = resultIter->query(); + wrapResult(returnbuf, &result, format, isFirstLine); + } + + //Adds the format postfix to the return buffer + resultsWrapEnd(returnbuf, format); + } } /* @@ -432,17 +415,9 @@ void GrafanaLogAccessCurlClient::fetchLabels(std::string & readBuffer) { submitQuery(readBuffer, "/label"); } -/* -void GrafanaLogAccessCurlClient::getMinReturnColumns(StringBuffer & columns) -{ - columns.appendf("{{.%s}}, {{.%s}}, {{.%s}}", - m_logDatestampColumn.name.get(), m_logTimestampColumn.name.get(), m_messageColumn.name.get()); -} -*/ /* * Creates query filter and stream selector strings for the LogQL query based on the filter options provided - */ void GrafanaLogAccessCurlClient::populateQueryFilterAndStreamSelector(StringBuffer & queryString, StringBuffer & streamSelector, const ILogAccessFilter * filter) { @@ -521,7 +496,6 @@ void GrafanaLogAccessCurlClient::populateQueryFilterAndStreamSelector(StringBuff { if (filter->getFieldName() == nullptr) throw makeStringExceptionV(-1, "%s: empty field name detected in filter by column!", COMPONENT_NAME); - //queryField = filter->getFieldName(); break; } //case LOGACCESS_FILTER_trace: @@ -550,6 +524,23 @@ void GrafanaLogAccessCurlClient::populateQueryFilterAndStreamSelector(StringBuff } } +/* +Translates LogAccess defined SortBy direction enum value to +the LogQL/Loki counterpart +*/ +const char * sortByDirection(SortByDirection direction) +{ + switch (direction) + { + case SORTBY_DIRECTION_ascending: + return "FORWARD"; + case SORTBY_DIRECTION_descending: + case SORTBY_DIRECTION_none: + default: + return "BACKWARD"; + } +} + /* * Constructs LogQL query based on filter options, and sets Loki specific query parameters, submits query, processes responce and returns the log entries in the desired format @@ -567,10 +558,35 @@ bool GrafanaLogAccessCurlClient::fetchLog(LogQueryResultDetails & resultDetails, StringBuffer fullQuery; fullQuery.set("/query_range?"); - fullQuery.append("direction=BACKWARD"); //other option seems to be "FORWARD" this should in future be handled by sort clause + + if (options.getSortByConditions().length() > 0) + { + if (options.getSortByConditions().length() > 1) + UWARNLOG("%s: LogQL sorting is only supported by one field!", COMPONENT_NAME); + + SortByCondition condition = options.getSortByConditions().item(0); + switch (condition.byKnownField) + { + case LOGACCESS_MAPPEDFIELD_timestamp: + break; + case LOGACCESS_MAPPEDFIELD_jobid: + case LOGACCESS_MAPPEDFIELD_component: + case LOGACCESS_MAPPEDFIELD_class: + case LOGACCESS_MAPPEDFIELD_audience: + case LOGACCESS_MAPPEDFIELD_instance: + case LOGACCESS_MAPPEDFIELD_host: + case LOGACCESS_MAPPEDFIELD_unmapped: + default: + throw makeStringExceptionV(-1, "%s: LogQL sorting is only supported by ingest timestamp!", COMPONENT_NAME); + } + + const char * direction = sortByDirection(condition.direction); + if (!isEmptyString(direction)) + fullQuery.appendf("direction=%s", direction); + } +DBGLOG("%s: getting query limit", COMPONENT_NAME); fullQuery.append("&limit=").append(std::to_string(options.getLimit()).c_str()); fullQuery.append("&query="); - //At this point the log field appears as a detected field and is not formated // Detected fields //if output is json: @@ -645,7 +661,7 @@ bool GrafanaLogAccessCurlClient::fetchLog(LogQueryResultDetails & resultDetails, std::string readBuffer; submitQuery(readBuffer, fullQuery.str()); - processQueryJsonResp(resultDetails, readBuffer, returnbuf, format, false); + processQueryJsonResp(resultDetails, readBuffer, returnbuf, format, true); //DBGLOG("Query fetchLog result: %s", readBuffer.c_str()); } catch(IException * e) @@ -689,11 +705,11 @@ GrafanaLogAccessCurlClient::GrafanaLogAccessCurlClient(IPropertyTree & logAccess { DBGLOG("Grafana LogAccess: loading esp/grafana-logaccess secret"); - getSecretKeyValue(m_grafanaUserName.clear(), secretTree, "grafana-username"); + getSecretKeyValue(m_grafanaUserName.clear(), secretTree, "username"); if (isEmptyString(m_grafanaUserName.str())) throw makeStringExceptionV(-1, "%s: Empty Grafana user name detected!", COMPONENT_NAME); - getSecretKeyValue(m_grafanaPassword.clear(), secretTree, "grafana-password"); + getSecretKeyValue(m_grafanaPassword.clear(), secretTree, "password"); if (isEmptyString(m_grafanaPassword.str())) throw makeStringExceptionV(-1, "%s: Empty Grafana password detected!", COMPONENT_NAME); } @@ -705,14 +721,12 @@ GrafanaLogAccessCurlClient::GrafanaLogAccessCurlClient(IPropertyTree & logAccess if (isEmptyString(m_grafanaUserName.str()) || isEmptyString(m_grafanaPassword.str())) { OWARNLOG("%s: Grafana credentials not found in secret, searching in grafana logaccess configuration", COMPONENT_NAME); - //this should come from a secret - if (logAccessPluginConfig.hasProp("connection/@user")) - m_grafanaUserName.set(logAccessPluginConfig.queryProp("connection/@user")); + + if (logAccessPluginConfig.hasProp("connection/@username")) + m_grafanaUserName.set(logAccessPluginConfig.queryProp("connection/@username")); if (logAccessPluginConfig.hasProp("connection/@password")) m_grafanaPassword.set(logAccessPluginConfig.queryProp("connection/@password")); - - //throw makeStringExceptionV(-1, "%s: Could not fetch %s information!", COMPONENT_NAME, "grafana-logaccess");} } //this is very important, without this, we can't target the correct datasource @@ -806,16 +820,45 @@ GrafanaLogAccessCurlClient::GrafanaLogAccessCurlClient(IPropertyTree & logAccess DBGLOG("%s: targeting: '%s' - datasource: '%s'", COMPONENT_NAME, m_grafanaConnectionStr.str(), m_dataSourcesAPIURI.str()); } +class GrafanaLogaccessStream : public CInterfaceOf +{ +public: + virtual bool readLogEntries(StringBuffer & record, unsigned & recsRead) override + { + DBGLOG("%s: GrafanaLogaccessStream readLogEntries called", COMPONENT_NAME); + LogQueryResultDetails resultDetails; + m_remoteLogAccessor->fetchLog(resultDetails, m_options, record, m_outputFormat); + recsRead = resultDetails.totalReceived; + DBGLOG("%s: GrafanaLogaccessStream readLogEntries returned %d records", COMPONENT_NAME, recsRead); + + return false; + } + + GrafanaLogaccessStream(IRemoteLogAccess * grafanaQueryClient, const LogAccessConditions & options, LogAccessLogFormat format, unsigned int pageSize) + { + DBGLOG("%s: GrafanaLogaccessStream created", COMPONENT_NAME); + m_remoteLogAccessor.set(grafanaQueryClient); + m_outputFormat = format; + m_pageSize = pageSize; + m_options = options; + } + +private: + unsigned int m_pageSize; + bool m_hasBeenScrolled = false; + LogAccessLogFormat m_outputFormat; + LogAccessConditions m_options; + Owned m_remoteLogAccessor; +}; + IRemoteLogAccessStream * GrafanaLogAccessCurlClient::getLogReader(const LogAccessConditions & options, LogAccessLogFormat format) { - return nullptr; -// return getLogReader(options, format, defaultMaxRecordsPerFetch); + return getLogReader(options, format, defaultMaxRecordsPerFetch); } IRemoteLogAccessStream * GrafanaLogAccessCurlClient::getLogReader(const LogAccessConditions & options, LogAccessLogFormat format, unsigned int pageSize) { - return nullptr; -// return new AzureLogAnalyticsStream(this, options, format, pageSize); + return new GrafanaLogaccessStream(this, options, format, pageSize); } extern "C" IRemoteLogAccess * createInstance(IPropertyTree & logAccessPluginConfig) diff --git a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.hpp b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.hpp index 3b9e286f784..fb6f71cff98 100644 --- a/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.hpp +++ b/system/logaccess/Grafana/CurlClient/GrafanaCurlClient.hpp @@ -36,21 +36,10 @@ struct GrafanaDataSource StringAttr name = DEFAULT_DATASOURCE_NAME; StringAttr id = DEFAULT_DATASOURCE_INDEX; StringAttr uid; - //basicAuthPassword - //version - //basicAuthUser - //access = proxy - //isDefault - //withCredentials - //url http://myloki4hpcclogs:3100 - //secureJsonFields - //user - //password - //basicAuth - //jsonData - //typeLogoUrl - //readOnly - //database + //Other Grafana datasource attributes: + //basicAuthPassword, version, basicAuthUser, access = proxy, isDefault + //withCredentials, url http://myloki4hpcclogs:3100, secureJsonFields + //user, password, basicAuth, jsonData, typeLogoUrl, readOnly, database }; struct LogField @@ -94,7 +83,7 @@ class GrafanaLogAccessCurlClient : public CInterfaceOf //LogField m_logTraceIDColumn = LogField("TRC"); //LogField m_logSpanIDColumn = LogField("SPN"); - StringAttr m_expectedLogFormat; //json|table/xml + StringAttr m_expectedLogFormat; //json|table|xml public: GrafanaLogAccessCurlClient(IPropertyTree & logAccessPluginConfig); @@ -105,9 +94,6 @@ class GrafanaLogAccessCurlClient : public CInterfaceOf void fetchLabels(std::string & readBuffer); void submitQuery(std::string & readBuffer, const char * targetURI); - //void getMinReturnColumns(StringBuffer & columns); - //void getDefaultReturnColumns(StringBuffer & columns); - // searchMetaData(StringBuffer & search, const LogAccessReturnColsMode retcolmode, const StringArray & selectcols, unsigned size = defaultEntryLimit, offset_t from = defaultEntryStart); void populateQueryFilterAndStreamSelector(StringBuffer & queryString, StringBuffer & streamSelector, const ILogAccessFilter * filter); static void timestampQueryRangeString(StringBuffer & range, std::time_t from, std::time_t to); diff --git a/testing/unittests/jlibtests.cpp b/testing/unittests/jlibtests.cpp index d5cca3b2864..893a894b814 100644 --- a/testing/unittests/jlibtests.cpp +++ b/testing/unittests/jlibtests.cpp @@ -66,9 +66,6 @@ class JlibTraceTest : public CppUnit::TestFixture CPPUNIT_TEST(manualTestsEventsOutput); CPPUNIT_TEST(manualTestsDeclaredFailures); CPPUNIT_TEST(manualTestScopeEnd); - CPPUNIT_TEST(testActiveSpans); - CPPUNIT_TEST(testSpanFetchMethods); - //CPPUNIT_TEST(testJTraceJLOGExporterprintResources); //CPPUNIT_TEST(testJTraceJLOGExporterprintAttributes); CPPUNIT_TEST(manualTestsDeclaredSpanStartTime); @@ -826,6 +823,30 @@ class JlibTraceTest : public CppUnit::TestFixture CPPUNIT_TEST_SUITE_REGISTRATION( JlibTraceTest ); CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( JlibTraceTest, "JlibTraceTest" ); +class JlibStringTest : public CppUnit::TestFixture +{ +public: + CPPUNIT_TEST_SUITE(JlibStringTest); + CPPUNIT_TEST(testEncodeCSVColumn); + CPPUNIT_TEST_SUITE_END(); + +protected: +void testEncodeCSVColumn() + { + const char * csvCol1 = "hello,world"; + StringBuffer encodedCSV; + encodeCSVColumn(encodedCSV, csvCol1); + CPPUNIT_ASSERT_EQUAL_STR(encodedCSV.str(), "\"hello,world\""); + + const char * csvCol2 = "hello world, \"how are you?\""; + encodedCSV.clear(); + encodeCSVColumn(encodedCSV, csvCol2); + CPPUNIT_ASSERT_EQUAL_STR(encodedCSV.str(), "\"hello world, \"\"how are you?\"\"\""); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION( JlibStringTest ); +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( JlibStringTest, "JlibStringTest" ); class JlibSemTest : public CppUnit::TestFixture {