瀏覽代碼

HPCC-24577 Remote log fetch framework

- Defines remote log fetch interface
- Provides library loading logic
- Provide ES based remote log fetching plugin
- Provide log fetch ESP service
- Submodules ES client lib
- Adds Cmake switch for ELASTICSTACK_CLIENT)
- Adds description to logaccess plugin type field
- Improves existing logmap description, adds subfield descriptions

Signed-off-by: Rodrigo Pastrana <rodrigo.pastrana@lexisnexisrisk.com>
Rodrigo Pastrana 4 年之前
父節點
當前提交
e766e28425

+ 3 - 0
.gitmodules

@@ -64,3 +64,6 @@
 [submodule "nlp/nlp-engine"]
 	path = plugins/nlp/nlp-engine
 	url = https://github.com/hpcc-systems/nlp-engine.git
+[submodule "system/logaccess/ElasticStack/elasticlient"]
+	path = system/logaccess/ElasticStack/elasticlient
+	url = https://github.com/hpcc-systems/elasticlient.git

+ 1 - 0
cmake_modules/commonSetup.cmake

@@ -133,6 +133,7 @@ IF ("${COMMONSETUP_DONE}" STREQUAL "")
   option(INCLUDE_EE_PLUGINS "Install EE Plugins in Clienttool" OFF)
   option(INCLUDE_TREEVIEW "Build legacy treeview" OFF)
   option(INCLUDE_CONFIG_MANAGER "Build config manager" ON)
+  option(USE_ELASTICSTACK_CLIENT "Configure use of Elastic Stack client" ON)
   set(CUSTOM_PACKAGE_SUFFIX "" CACHE STRING "Custom package suffix to differentiate development builds")
 
      MACRO(SET_PLUGIN_PACKAGE plugin)

+ 1 - 0
esp/scm/espscm.cmake

@@ -49,6 +49,7 @@ set ( ESPSCM_SRCS
       ws_decoupledlogging.ecm
       ws_dali.ecm
       ws_resources.ecm
+      ws_logaccess.ecm
     )
 
 if (NOT CONTAINERIZED)

+ 109 - 0
esp/scm/ws_logaccess.ecm

@@ -0,0 +1,109 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+ESPenum LogAccessType : int
+{
+    All(0, "All"),
+    ByJobIdID(1, "ByJobIdID"),
+    ByComponent(2, "ByComponent"),
+    ByLogType(3, "ByLogType"),
+    ByTargetAudience(4, "ByTargetAudience")
+};
+
+ESPenum LogAccessLogFormat : int
+{
+    XML(0, "XML"),
+    JSON(1, "JSON"),
+    CSV(2, "CSV")
+} ;
+
+ESPStruct TimeRange
+{
+    xsdDateTime StartDate; //example 1980-03-23T10:20:15
+    xsdDateTime EndDate;   //example 1980-03-23T10:20:15
+};
+
+ESPRequest GetLogAccessInfoRequest
+{
+};
+
+ESPResponse GetLogAccessInfoResponse
+{
+    string RemoteLogManagerType;
+    string RemoteLogManagerConnectionString;
+};
+/*
+* Provides mechanism to query log entries
+*
+* Caller can query by JobId, component, log event type, or target audience by providing the appropriate
+* enumerated value in the LogCategory field, as well as the targeted value in the SearchByValue field.
+*
+* SearchbyValue is optional if LogCategory == ALL
+*
+* Caller should restrict the query to target a specific time range specified in the Range field.
+* By default, the first 100 log entries encountered are returned. Caller to pagenize using the
+* LogLineStartFrom field (specifies as zero-indexed start index) and the LogLineLimit (specifies the maximum
+* number of log entries to be returned)
+* Caller can specify which log columns should be reported via the Columns field, all available columns returned by default.
+*
+* The report format can be specified via the Format field: JSON|XML|CSV
+*/
+ESPRequest GetLogsRequest
+{
+    ESPenum LogAccessType LogCategory;
+    string SearchByValue; //Value used to identify target log entries.
+                          //Limited to values associated with the the LogCategory choice.
+                          //
+                          //If searching by "ByJobIdID", the SearchByValue should contain the jobid of interest
+                          //If searching by "ByComponent", the SearchByValue should contain the component of interest
+                          //If searching by "ByLogType", the SearchByValue should contain the 3 letter code associated with the log type of interest.
+                          // valid values at time of writing are:
+                          //    DIS - Disaster
+                          //    ERR - Error
+                          //    WRN - Warning
+                          //    INF - Information
+                          //    PRO - Progress
+                          //    MET - Metric
+                          //
+                          //If searching by "ByTargetAudience", the SearchByValue should contain the 3 letter code associated with the target audience of interest.
+                          // valid values at time of writing are:
+                          //    OPR - Operator
+                          //    USR - User
+                          //    PRO - Programmer
+                          //    ADT - Audit
+    ESPStruct TimeRange Range;
+    unsigned LogLineLimit(100);
+    int64 LogLineStartFrom(0);
+    ESParray<string> Columns;
+    LogAccessLogFormat Format("JSON");
+};
+
+ESPResponse GetLogsResponse
+{
+    string LogLines;
+};
+
+ESPservice [auth_feature("WsLogAccess:READ"), version("1.0"), default_client_version("1.0"), exceptions_inline("xslt/exceptions.xslt")] ws_logaccess
+{
+    ESPmethod GetLogAccessInfo(GetLogAccessInfoRequest, GetLogAccessInfoResponse);
+    ESPmethod GetLogs(GetLogsRequest, GetLogsResponse);
+};
+
+SCMexportdef(ws_logaccess);
+
+SCMapi(ws_logaccess) IClientws_logaccess *createws_logaccessClient();
+

+ 1 - 0
esp/services/CMakeLists.txt

@@ -44,3 +44,4 @@ if (NOT CONTAINERIZED)
   HPCC_ADD_SUBDIRECTORY (ws_config "PLATFORM")
   HPCC_ADD_SUBDIRECTORY (WsDeploy "PLATFORM")
 endif()
+HPCC_ADD_SUBDIRECTORY (ws_logaccess "PLATFORM")

+ 73 - 0
esp/services/ws_logaccess/CMakeLists.txt

@@ -0,0 +1,73 @@
+################################################################################
+#    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+
+
+# Component: ws_logaccess
+#####################################################
+# Description:
+# ------------
+#    Cmake Input File for ws_logaccess
+#####################################################
+
+project( ws_logaccess )
+
+
+include(${HPCC_SOURCE_DIR}/esp/scm/espscm.cmake)
+
+set (    SRCS
+         ${HPCC_SOURCE_DIR}/esp/scm/ws_logaccess.ecm
+         ${ESPSCM_GENERATED_DIR}/ws_logaccess_esp.cpp
+         ${CMAKE_CURRENT_SOURCE_DIR}/WsLogAccessService.cpp
+         ${CMAKE_CURRENT_SOURCE_DIR}/WsLogAccessPlugin.cpp
+    )
+
+include_directories (
+         ${HPCC_SOURCE_DIR}/esp/bindings/http/platform
+         ${HPCC_SOURCE_DIR}/esp/esplib
+         ${HPCC_SOURCE_DIR}/esp/platform
+         ${HPCC_SOURCE_DIR}/system/jlib
+         ${HPCC_SOURCE_DIR}/esp/services
+         ${HPCC_SOURCE_DIR}/common
+         ${HPCC_SOURCE_DIR}/system/security/securesocket
+         ${HPCC_SOURCE_DIR}/system/security/shared
+         ${HPCC_SOURCE_DIR}/system/include
+         ${HPCC_SOURCE_DIR}/common/remote
+         ${HPCC_SOURCE_DIR}/esp/clients
+         ${HPCC_SOURCE_DIR}/dali/base
+         ${HPCC_SOURCE_DIR}/common/dllserver
+         ${HPCC_SOURCE_DIR}/esp/bindings
+         ${HPCC_SOURCE_DIR}/esp/bindings/SOAP/xpp
+         ${HPCC_SOURCE_DIR}/esp/bindings/http/client
+         ${HPCC_SOURCE_DIR}/esp/http/platform
+         ${HPCC_SOURCE_DIR}/system/mp
+         ${HPCC_SOURCE_DIR}/system/xmllib
+         ${CMAKE_BINARY_DIR}
+         ${CMAKE_BINARY_DIR}/oss
+         ${CMAKE_BINARY_DIR}/esp/services/ws_logaccess
+         ${HPCC_SOURCE_DIR}/common/thorhelper
+    )
+
+ADD_DEFINITIONS( -D_USRDLL )
+
+HPCC_ADD_LIBRARY( ws_logaccess SHARED ${SRCS}  )
+add_dependencies (ws_logaccess espscm )
+install ( TARGETS ws_logaccess RUNTIME DESTINATION ${EXEC_DIR} LIBRARY DESTINATION ${LIB_DIR} )
+target_link_libraries ( ws_logaccess
+         jlib
+         xmllib
+         esphttp
+         SMCLib
+    )

+ 68 - 0
esp/services/ws_logaccess/WsLogAccessPlugin.cpp

@@ -0,0 +1,68 @@
+
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#pragma warning (disable : 4786)
+
+#include "ws_logaccess_esp.ipp"
+
+//ESP Bindings
+#include "httpprot.hpp"
+
+//ESP Service
+#include <ws_logaccess/WsLogAccessService.hpp>
+
+#include "espplugin.hpp"
+
+extern "C"
+{
+
+ESP_FACTORY IEspService * esp_service_factory(const char *name, const char* type, IPropertyTree *cfg, const char *process)
+{
+    if (strieq(type, "ws_logaccess"))
+    {
+        Cws_logaccessEx* service = new Cws_logaccessEx;
+        service->init(cfg, process, name);
+        return service;
+    }
+    return nullptr;
+}
+
+ESP_FACTORY IEspRpcBinding * esp_binding_factory(const char *name, const char* type, IPropertyTree *cfg, const char *process)
+{
+    //binding names ending in _http are being added so the names can be made more consistent and can therefore be automatically generated
+    //  the name also better reflects that these bindings are for all HTTP based protocols, not just SOAP
+    //  both "SoapBinding" and "_http" names instantiate the same objects.
+    if (strieq(type, "ws_logaccessSoapBinding")||strieq(type, "ws_logaccess_http"))
+    {
+#ifdef _DEBUG
+        http_soap_log_level log_level_ = hsl_all;
+#else
+        http_soap_log_level log_level_ = hsl_none;
+#endif
+        return new Cws_logaccessSoapBinding(cfg, name, process, log_level_);
+    }
+
+    return nullptr;
+}
+
+ESP_FACTORY IEspProtocol * esp_protocol_factory(const char *name, const char* type, IPropertyTree *cfg, const char *process)
+{
+    return http_protocol_factory(name, type, cfg, process);
+}
+
+} // extern "C"

+ 120 - 0
esp/services/ws_logaccess/WsLogAccessService.cpp

@@ -0,0 +1,120 @@
+#include <ws_logaccess/WsLogAccessService.hpp>
+
+Cws_logaccessEx::Cws_logaccessEx()
+{
+}
+
+Cws_logaccessEx::~Cws_logaccessEx()
+{
+}
+
+bool Cws_logaccessEx::onGetLogAccessInfo(IEspContext &context, IEspGetLogAccessInfoRequest &req, IEspGetLogAccessInfoResponse &resp)
+{
+    bool success = true;
+    if (m_remoteLogAccessor)
+    {
+        resp.setRemoteLogManagerType(m_remoteLogAccessor->getRemoteLogAccessType());
+        resp.setRemoteLogManagerConnectionString(m_remoteLogAccessor->fetchConnectionStr());
+    }
+    else
+        success = false;
+
+    return success;
+}
+
+void Cws_logaccessEx::init(const IPropertyTree *cfg, const char *process, const char *service)
+{
+    LOG(MCdebugProgress,"WsLogAccessService loading remote log access plug-in...");
+
+    try
+    {
+        m_remoteLogAccessor.set(queryRemoteLogAccessor());
+
+        if (m_remoteLogAccessor == nullptr)
+            LOG(MCerror,"WsLogAccessService could not load remote log access plugin!");
+    }
+    catch (IException * e)
+    {
+        StringBuffer msg;
+        e->errorMessage(msg);
+        LOG(MCoperatorWarning,"WsLogAccessService could not load remote log access plug-in: %s", msg.str());
+        e->Release();
+    }
+}
+
+LogAccessTimeRange requestedRangeToLARange(IConstTimeRange & reqrange)
+{
+    struct LogAccessTimeRange range;
+
+    range.setStart(reqrange.getStartDate());
+    range.setEnd(reqrange.getEndDate());
+
+    return range;
+}
+
+bool Cws_logaccessEx::onGetLogs(IEspContext &context, IEspGetLogsRequest &req, IEspGetLogsResponse & resp)
+{
+    if (!m_remoteLogAccessor)
+        throw makeStringException(-1, "WsLogAccess: Remote Log Access plug-in not available!");
+
+    CLogAccessType searchByCategory = req.getLogCategory();
+    const char * searchByValue = req.getSearchByValue();
+    if (searchByCategory != CLogAccessType_All && isEmptyString(searchByValue))
+        throw makeStringException(-1, "WsLogAccess::onGetLogs: Must provide log category");
+
+    LogAccessConditions logFetchOptions;
+    switch (searchByCategory)
+    {
+        case CLogAccessType_All:
+            logFetchOptions.setFilter(getWildCardLogAccessFilter());
+            break;
+        case CLogAccessType_ByJobIdID:
+            logFetchOptions.setFilter(getJobIDLogAccessFilter(searchByValue));
+            break;
+        case CLogAccessType_ByComponent:
+            logFetchOptions.setFilter(getComponentLogAccessFilter(searchByValue));
+            break;
+        case CLogAccessType_ByLogType:
+        {
+            LogMsgClass logType = LogMsgClassFromAbbrev(searchByValue);
+            if (logType == MSGCLS_unknown)
+                throw makeStringExceptionV(-1, "Invalid Log Type 3-letter code encountered: '%s' - Available values: 'DIS,ERR,WRN,INF,PRO,MET'", searchByValue);
+
+            logFetchOptions.setFilter(getClassLogAccessFilter(logType));
+            break;
+        }
+        case CLogAccessType_ByTargetAudience:
+        {
+            MessageAudience targetAud = LogMsgAudFromAbbrev(searchByValue);
+            if (targetAud == MSGAUD_unknown || targetAud == MSGAUD_all)
+                throw makeStringExceptionV(-1, "Invalid Target Audience 3-letter code encountered: '%s' - Available values: 'OPR,USR,PRO,ADT'", searchByValue);
+
+            logFetchOptions.setFilter(getAudienceLogAccessFilter(targetAud));
+            break;
+        }
+        case LogAccessType_Undefined:
+        default:
+            throw makeStringException(-1, "Invalid remote log access request type");
+    }
+
+    LogAccessTimeRange range = requestedRangeToLARange(req.getRange());
+    const StringArray & cols = req.getColumns();
+
+    unsigned limit = req.getLogLineLimit();
+
+    __int64 startFrom = req.getLogLineStartFrom();
+    if (startFrom < 0)
+        throw makeStringExceptionV(-1, "WsLogAccess: Encountered invalid LogLineStartFrom value: '%lld'", startFrom);
+
+    logFetchOptions.setTimeRange(range);
+    logFetchOptions.copyLogFieldNames(cols);
+    logFetchOptions.setLimit(limit);
+    logFetchOptions.setStartFrom((offset_t)startFrom);
+
+    StringBuffer logcontent;
+    m_remoteLogAccessor->fetchLog(logFetchOptions, logcontent, logAccessFormatFromName(req.getFormat()));
+
+    resp.setLogLines(logcontent.str());
+
+    return true;
+}

+ 36 - 0
esp/services/ws_logaccess/WsLogAccessService.hpp

@@ -0,0 +1,36 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef _WsLOGACCESS_HPP_
+#define _WsLOGACCESS_HPP_
+
+#include "ws_logaccess_esp.ipp"
+
+class Cws_logaccessEx : public Cws_logaccess
+{
+private:
+    Owned<IRemoteLogAccess> m_remoteLogAccessor;
+public:
+
+    Cws_logaccessEx();
+    virtual ~Cws_logaccessEx();
+    virtual void init(const IPropertyTree *cfg, const char *process, const char *service);
+    virtual bool onGetLogAccessInfo(IEspContext &context, IEspGetLogAccessInfoRequest &req, IEspGetLogAccessInfoResponse &resp);
+    virtual bool onGetLogs(IEspContext &context, IEspGetLogsRequest &req, IEspGetLogsResponse & resp);
+};
+
+#endif

+ 64 - 0
helm/hpcc/values.schema.json

@@ -249,6 +249,9 @@
         "logging": {
           "$ref": "#/definitions/logging"
         },
+        "logAccess": {
+          "$ref": "#/definitions/logAccess"
+        },
         "egress": {
           "$ref": "#/definitions/egress"
         },
@@ -1744,6 +1747,67 @@
       "description": "The time to wait before startup probing fails (in seconds). Default 300",
       "type": "integer"
     },
+    "logAccess": {
+      "type" : "object",
+      "description" : "Remote log access information",
+      "properties" : {
+        "name": {
+          "type": "string"
+        },
+        "type": {
+          "type": "string",
+          "description": "Name of HPCC LogAccess plugin type such as 'elasticstack'"
+        },
+        "connection" : {
+          "$ref" : "#/definitions/logAccessConnection"
+        },
+        "logMaps": {
+          "description": "A list of log maps",
+          "type": "array",
+          "items": {
+            "$ref" : "#/definitions/logMap"
+           }
+        }
+      }
+    },
+    "logAccessConnection": {
+      "type": "object",
+      "description" : "Connection information for target remote log access",
+      "properties": {
+        "protocol": {
+          "type": "string"
+        },
+        "host": {
+          "type": "string"
+        },
+        "port": {
+          "type": "integer"
+        }
+      }
+    },
+    "logMap": {
+      "type": "object",
+      "description" : "Provides log-store mapping to searchable HPCC log columns",
+      "properties": {
+        "type": {
+          "type": "string",
+          "description" : "The searchable HPCC log column to be mapped - 'global' applies to all known fields",
+          "enum": [ "global", "workunits", "components", "audience", "class" ]
+        },
+        "timeStampColumn": {
+          "description" : "Name of timestamp column related to mapped field (only requried for 'global' mapping)",
+          "type": "string"
+        },
+        "searchColumn": {
+          "description" : "Name of column mapped to HPCC log entry column",
+          "type": "string"
+        },
+        "storeName": {
+          "description" : "Name of container housing mapped HPCC log column",
+          "type": "string"
+        }
+      }
+    },
     "sinks": {
       "type": "array",
       "items" : {

+ 1 - 0
initfiles/componentfiles/configschema/xsd/buildset.xml

@@ -74,6 +74,7 @@
           <AuthenticateFeature description="Access to WS STORE service" path="WsStoreAccess" resource="WsStoreAccess" service="ws_store"/>
           <AuthenticateFeature description="Access to WS Decoupled Log service" path="WsDecoupledLogAccess" resource="WsDecoupledLogAccess" service="ws_decoupledlogging"/>
           <AuthenticateFeature description="Access to sign ECL code" path="CodeSignAccess" resource="CodeSignAccess" service="ws_codesign"/>
+          <AuthenticateFeature description="Access to LogAccess service" path="WsLogAccess" resource="WsLogAccess" service="ws_logaccess"/>
           <ProcessFilters>
             <Platform name="Windows">
               <ProcessFilter name="any">

+ 2 - 0
initfiles/componentfiles/configschema/xsd/esp_service_smc.xsd

@@ -135,6 +135,8 @@
                                                          service="ws_esdlconfig"/>
                                     <AuthenticateFeature description="Access to ELK integration service"
                                                          path="WsELKAccess" resource="WsELKAccess" service="ws_elk"/>
+                                    <AuthenticateFeature description="Access to LogAccess service"
+                                                         path="WsLogAccess" resource="WsLogAccess" service="ws_logaccess"/>
                                     <ProcessFilters>
                                         <Platform name="Windows">
                                             <ProcessFilter name="any">

+ 36 - 1
initfiles/componentfiles/configxml/@temp/esp_service_WsSMC.xsl

@@ -137,6 +137,10 @@ This is required by its binding with ESP service '<xsl:value-of select="$espServ
             <xsl:with-param name="bindingNode" select="$bindingNode"/>
             <xsl:with-param name="authNode" select="$authNode"/>
         </xsl:apply-templates>
+        <xsl:apply-templates select="." mode="ws_logaccess">
+            <xsl:with-param name="bindingNode" select="$bindingNode"/>
+            <xsl:with-param name="authNode" select="$authNode"/>
+        </xsl:apply-templates>
     </xsl:template>
 
     <!-- WS-SMC -->
@@ -733,7 +737,7 @@ This is required by its binding with ESP service '<xsl:value-of select="$espServ
       </xsl:copy>
    </xsl:template>
 
-    <!-- ws_dali -->
+<!-- ws_dali -->
     <xsl:template match="EspService" mode="ws_dali">
         <xsl:param name="bindingNode"/>
         <xsl:param name="authNode"/>
@@ -763,6 +767,37 @@ This is required by its binding with ESP service '<xsl:value-of select="$espServ
          <xsl:apply-templates select="@*[string(.) != '']|node()" mode="copy"/>
       </xsl:copy>
    </xsl:template>
+
+    <!-- ws_logAccess -->
+    <xsl:template match="EspService" mode="ws_logaccess">
+        <xsl:param name="bindingNode"/>
+        <xsl:param name="authNode"/>
+
+        <xsl:variable name="serviceType" select="'ws_logaccess'"/>
+        <xsl:variable name="serviceName" select="concat($serviceType, '_', @name, '_', $process)"/>
+        <xsl:variable name="bindName" select="concat($serviceType, '_', $bindingNode/@name, '_', $process)"/>
+        <xsl:variable name="bindType" select="'ws_logaccessSoapBinding'"/>
+        <xsl:variable name="servicePlugin">
+            <xsl:call-template name="defineServicePlugin">
+                <xsl:with-param name="plugin" select="'ws_logaccess'"/>
+            </xsl:call-template>
+        </xsl:variable>
+        <EspService name="{$serviceName}" type="{$serviceType}" plugin="{$servicePlugin}">
+        </EspService>
+        <EspBinding name="{$bindName}" service="{$serviceName}" protocol="{$bindingNode/@protocol}" type="{$bindType}"
+            plugin="{$servicePlugin}" netAddress="0.0.0.0" port="{$bindingNode/@port}">
+            <xsl:call-template name="bindAuthentication">
+                <xsl:with-param name="bindingNode" select="$bindingNode"/>
+                <xsl:with-param name="authMethod" select="$authNode/@method"/>
+                <xsl:with-param name="service" select="'ws_logaccess'"/>
+            </xsl:call-template>
+        </EspBinding>
+    </xsl:template>
+   <xsl:template match="*" mode="copy">
+      <xsl:copy>
+         <xsl:apply-templates select="@*[string(.) != '']|node()" mode="copy"/>
+      </xsl:copy>
+   </xsl:template>
    
    <xsl:template match="@*" mode="copy">
       <xsl:if test="string(.) != ''">

+ 4 - 0
initfiles/componentfiles/configxml/buildsetCC.xml.in

@@ -225,6 +225,10 @@
                         path="CodeSignAccess"
                         resource="CodeSignAccess"
                         service="ws_codesign"/>
+                    <AuthenticateFeature description="Access to LogAccess service"
+                        path="WsLogAccess"
+                        resource="WsLogAccess"
+                        service="ws_logaccess"/>
                     <ProcessFilters>
                         <Platform name="Windows">
                             <ProcessFilter name="any">

+ 15 - 0
initfiles/etc/DIR_NAME/environment.xml.in

@@ -428,6 +428,11 @@
                          path="WsDecoupledLogAccess"
                          resource="WsDecoupledLogAccess"
                          service="ws_decoupledlogging"/>
+    <AuthenticateFeature authenticate="Yes"
+                         description="Access to LogAccess service"
+                         path="WsLogAccess"
+                         resource="WsLogAccess"
+                         service="ws_logaccess"/>
     <Authenticate access="Read"
                   description="Root access to SMC service"
                   path="/"
@@ -768,6 +773,11 @@
                          path="WsDecoupledLogAccess"
                          resource="WsDecoupledLogAccess"
                          service="ws_decoupledlogging"/>
+    <AuthenticateFeature authenticate="Yes"
+                         description="Access to LogAccess service"
+                         path="WsLogAccess"
+                         resource="WsLogAccess"
+                         service="ws_logaccess"/>
     <ProcessFilters>
      <Platform name="Windows">
       <ProcessFilter name="any">
@@ -1164,6 +1174,11 @@
                           path="WsDecoupledLogAccess"
                           resource="WsDecoupledLogAccess"
                           service="ws_decoupledlogging"/>
+     <AuthenticateFeature authenticate="Yes"
+                         description="Access to Log Access service"
+                         path="WsLogAccess"
+                         resource="WsLogAccess"
+                         service="ws_logaccess"/>
      <ProcessFilters>
       <Platform name="Windows">
        <ProcessFilter name="any">

+ 1 - 0
system/CMakeLists.txt

@@ -22,6 +22,7 @@ HPCC_ADD_SUBDIRECTORY (aws)
 HPCC_ADD_SUBDIRECTORY (azure)
 
 if (NOT JLIB_ONLY)
+   HPCC_ADD_SUBDIRECTORY (logaccess)
    HPCC_ADD_SUBDIRECTORY (hrpc)
    HPCC_ADD_SUBDIRECTORY (tbb_sm)
    HPCC_ADD_SUBDIRECTORY (mp)

+ 170 - 0
system/jlib/jlog.cpp

@@ -917,6 +917,7 @@ void ComponentLogMsgFilter::addToPTree(IPropertyTree * tree) const
     tree->addPropTree("filter", filterTree);
 }
 
+
 bool RegexLogMsgFilter::includeMessage(const LogMsg & msg) const 
 { 
     if(localFlag && msg.queryRemoteFlag()) return false; 
@@ -3329,3 +3330,172 @@ IComponentLogFileCreator * createComponentLogFileCreator(const char *_component)
 {
     return new CComponentLogFileCreator(_component);
 }
+
+ILogAccessFilter * getLogAccessFilterFromPTree(IPropertyTree * xml)
+{
+    if (xml == nullptr)
+        throw makeStringException(-2,"getLogAccessFilterFromPTree: input tree cannot be null");
+
+    StringBuffer type;
+    xml->getProp("@type", type);
+    if (streq(type.str(), "jobid"))
+        return new FieldLogAccessFilter(xml, LOGACCESS_FILTER_jobid);
+    else if (streq(type.str(), "audience"))
+        return new FieldLogAccessFilter(xml, LOGACCESS_FILTER_audience);
+    else if (streq(type.str(), "class"))
+        return new FieldLogAccessFilter(xml, LOGACCESS_FILTER_class);
+    else if (streq(type.str(), "component"))
+        return new FieldLogAccessFilter(xml, LOGACCESS_FILTER_component);
+    else if (streq(type.str(), "and"))
+        return new BinaryLogAccessFilter(xml, LOGACCESS_FILTER_and);
+    else if (streq(type.str(), "or"))
+        return new BinaryLogAccessFilter(xml, LOGACCESS_FILTER_or);
+    else
+        throwUnexpectedX("getLogAccessFilterFromPTree : unrecognized LogAccessFilter type");
+}
+
+ILogAccessFilter * getWildCardLogAccessFilter()
+{
+    return new FieldLogAccessFilter("", LOGACCESS_FILTER_wildcard);
+}
+
+ILogAccessFilter * getJobIDLogAccessFilter(const char * jobId)
+{
+    return new FieldLogAccessFilter(jobId, LOGACCESS_FILTER_jobid);
+}
+
+ILogAccessFilter * getComponentLogAccessFilter(const char * component)
+{
+    return new FieldLogAccessFilter(component, LOGACCESS_FILTER_component);
+}
+
+ILogAccessFilter * getAudienceLogAccessFilter(MessageAudience audience)
+{
+    return new FieldLogAccessFilter(LogMsgAudienceToFixString(audience), LOGACCESS_FILTER_audience);
+}
+
+ILogAccessFilter * getClassLogAccessFilter(LogMsgClass logclass)
+{
+    return new FieldLogAccessFilter(LogMsgClassToFixString(logclass), LOGACCESS_FILTER_class);
+}
+
+ILogAccessFilter * getBinaryLogAccessFilter(ILogAccessFilter * arg1, ILogAccessFilter * arg2, LogAccessFilterType type)
+{
+    return new BinaryLogAccessFilter(arg1, arg2, type);
+}
+
+ILogAccessFilter * getBinaryLogAccessFilterOwn(ILogAccessFilter * arg1, ILogAccessFilter * arg2, LogAccessFilterType type)
+{
+    ILogAccessFilter * ret = new BinaryLogAccessFilter(arg1, arg2, type);
+    arg1->Release();
+    arg2->Release();
+    return ret;
+}
+
+
+// LOG ACCESS HELPER METHODS
+
+// Fetches log entries - based on provided filter, via provided IRemoteLogAccess instance
+bool fetchLog(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, ILogAccessFilter * filter, LogAccessTimeRange timeRange, const StringArray & cols, LogAccessLogFormat format)
+{
+    LogAccessConditions logFetchOptions;
+    logFetchOptions.setTimeRange(timeRange);
+    logFetchOptions.setFilter(filter);
+    logFetchOptions.copyLogFieldNames(cols); //ensure these fields are declared in m_logMapping->queryProp("WorkUnits/@contentcolumn")? or in LogMap/Fields?"
+
+    return logAccess.fetchLog(logFetchOptions, returnbuf, format);
+}
+
+// Fetches log entries based on provided JobID, via provided IRemoteLogAccess instance
+bool fetchJobIDLog(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, const char *jobid, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format = LOGACCESS_LOGFORMAT_json)
+{
+    return fetchLog(returnbuf, logAccess, getJobIDLogAccessFilter(jobid), timeRange, cols, format);
+}
+
+// Fetches log entries based on provided component name, via provided IRemoteLogAccess instance
+bool fetchComponentLog(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, const char * component, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format = LOGACCESS_LOGFORMAT_json)
+{
+    return fetchLog(returnbuf, logAccess, getComponentLogAccessFilter(component), timeRange, cols, format);
+}
+
+// Fetches log entries based on provided audience, via provided IRemoteLogAccess instance
+bool fetchLogByAudience(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, MessageAudience audience, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format = LOGACCESS_LOGFORMAT_json)
+{
+    return fetchLog(returnbuf, logAccess, getAudienceLogAccessFilter(audience), timeRange, cols, format);
+}
+
+// Fetches log entries based on provided log message class, via provided IRemoteLogAccess instance
+bool fetchLogByClass(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, LogMsgClass logclass, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format = LOGACCESS_LOGFORMAT_json)
+{
+    return fetchLog(returnbuf, logAccess, getClassLogAccessFilter(logclass), timeRange, cols, format);
+}
+
+//logAccessPluginConfig expected to contain connectivity and log mapping information
+typedef IRemoteLogAccess * (*newLogAccessPluginMethod_t_)(IPropertyTree & logAccessPluginConfig);
+
+IRemoteLogAccess * queryRemoteLogAccessor()
+{
+    Owned<IPropertyTree> logAccessPluginConfig = getGlobalConfigSP()->getPropTree("logAccess");
+#ifdef LOGACCESSDEBUG
+    if (!logAccessPluginConfig)
+    {
+        const char *simulatedGlobalYaml = R"!!(global:
+          logAccess:
+            name: "localES"
+            type: "elasticstack"
+            connection:
+              protocol: "http"
+              host: "localhost"
+              port: 9200
+            logMaps:
+            - type: "global"
+              storeName: "filebeat-7.9.3-*"
+              searchColumn: "message"
+              timeStampColumn: "created_ts"
+            - type: "workunits"
+              storeName: "filebeat-7.9.3-*"
+              searchColumn: "hpcc.log.jobid"
+            - type: "components"
+              searchColumn: "kubernetes.container.name"
+            - type: "audience"
+              searchColumn: "hpcc.log.audience"
+            - type: "class"
+              searchColumn: "hpcc.log.class"
+        )!!";
+        Owned<IPropertyTree> testTree = createPTreeFromYAMLString(simulatedGlobalYaml, ipt_none, ptr_ignoreWhiteSpace, nullptr);
+        logAccessPluginConfig.setown(testTree->getPropTree("global/logAccess"));
+    }
+#endif
+
+    if (!logAccessPluginConfig)
+        throw makeStringException(-1, "RemoteLogAccessLoader: logaccess configuration not available!");
+
+    constexpr const char * methodName = "queryRemoteLogAccessor";
+    constexpr const char * instFactoryName = "createInstance";
+
+    StringBuffer libName; //lib<type>logaccess.so
+    StringBuffer type;
+    logAccessPluginConfig->getProp("@type", type);
+    if (type.isEmpty())
+        throw makeStringExceptionV(-1, "%s RemoteLogAccess plugin kind not specified.", methodName);
+    libName.append("lib").append(type.str()).append("logaccess");
+
+    //Load the DLL/SO
+    HINSTANCE logAccessPluginLib = LoadSharedObject(libName.str(), true, false);
+    if(logAccessPluginLib == nullptr)
+        throw makeStringExceptionV(-1, "%s cannot load library '%s'", methodName, libName.str());
+
+    newLogAccessPluginMethod_t_ xproc = nullptr;
+    xproc = (newLogAccessPluginMethod_t_)GetSharedProcedure(logAccessPluginLib, instFactoryName);
+    if (xproc == nullptr)
+        throw makeStringExceptionV(-1, "%s cannot locate procedure %s in library '%s'", methodName, instFactoryName, libName.str());
+
+    //Call logaccessplugin instance factory and return the new instance
+    DBGLOG("Calling '%s' in log access plugin '%s'", instFactoryName, libName.str());
+    IRemoteLogAccess * pLogAccessPlugin = dynamic_cast<IRemoteLogAccess*>(xproc(*logAccessPluginConfig));
+
+    if (pLogAccessPlugin == nullptr)
+        throw makeStringExceptionV(-1, "%s Log Access Plugin '%s' failed to instantiate in call to %s", methodName, libName.str(), instFactoryName);
+
+    return pLogAccessPlugin;
+}

+ 255 - 4
system/jlib/jlog.hpp

@@ -34,6 +34,7 @@
 #include "jdebug.hpp"
 #include "jptree.hpp"
 #include "jsocket.hpp"
+#include "jtime.hpp"
 
 typedef enum
 {
@@ -58,6 +59,7 @@ typedef enum
  * LOG MESSAGE CLASS:                                                                   */
 typedef enum
 {
+    MSGCLS_unknown     = 0x00, // Invalid/unknown log message class
     MSGCLS_disaster    = 0x01, // Any unrecoverable or critical system errors
     MSGCLS_error       = 0x02, // Recoverable/not critical Errors
     MSGCLS_warning     = 0x04, // Warnings
@@ -162,7 +164,7 @@ inline const char * LogMsgAudienceToFixString(LogMsgAudience audience)
         return("UNK ");
     }
 }
-inline unsigned LogMsgAudFromAbbrev(char const * abbrev)
+inline MessageAudience LogMsgAudFromAbbrev(char const * abbrev)
 {
     if(strnicmp(abbrev, "OPR", 3)==0)
         return MSGAUD_operator;
@@ -174,7 +176,7 @@ inline unsigned LogMsgAudFromAbbrev(char const * abbrev)
         return MSGAUD_audit;
     if(strnicmp(abbrev, "ALL", 3)==0)
         return MSGAUD_all;
-    return 0;
+    return MSGAUD_unknown;
 }
 
 inline const char * LogMsgClassToVarString(LogMsgClass msgClass)
@@ -219,7 +221,7 @@ inline const char * LogMsgClassToFixString(LogMsgClass msgClass)
     }
 }
 
-inline unsigned LogMsgClassFromAbbrev(char const * abbrev)
+inline LogMsgClass LogMsgClassFromAbbrev(char const * abbrev)
 {
     if(strnicmp(abbrev, "DIS", 3)==0)
         return MSGCLS_disaster;
@@ -235,7 +237,7 @@ inline unsigned LogMsgClassFromAbbrev(char const * abbrev)
         return MSGCLS_metric;
     if(strnicmp(abbrev, "ALL", 3)==0)
         return MSGCLS_all;
-    return 0;
+    return MSGCLS_unknown;
 }
 
 typedef unsigned LogMsgDetail;
@@ -1343,4 +1345,253 @@ interface IComponentLogFileCreator : extends IInterface
 extern jlib_decl IComponentLogFileCreator * createComponentLogFileCreator(IPropertyTree * _properties, const char *_component);
 extern jlib_decl IComponentLogFileCreator * createComponentLogFileCreator(const char *_logDir, const char *_component);
 extern jlib_decl IComponentLogFileCreator * createComponentLogFileCreator(const char *_component);
+
+struct LogAccessTimeRange
+{
+private:
+    CDateTime startt;
+    CDateTime endt;
+
+public:
+    void setStart(const CDateTime start)
+    {
+        startt = start;
+    }
+
+    void setEnd(const CDateTime  end)
+    {
+        endt = end;
+    }
+
+    void setStart(const char * start, bool local = true)
+    {
+        startt.setString(start,nullptr,local);
+    }
+
+    void setEnd(const char * end, bool local = true)
+    {
+        endt.setString(end,nullptr,local);
+    }
+
+    const CDateTime & getEndt() const
+    {
+        return endt;
+    }
+
+    const CDateTime & getStartt() const
+    {
+        return startt;
+    }
+};
+
+//---------------------------------------------------------------------------
+// Log retrieval interfaces and definitions
+
+typedef enum
+{
+    LOGACCESS_FILTER_jobid,
+    LOGACCESS_FILTER_class,
+    LOGACCESS_FILTER_audience,
+    LOGACCESS_FILTER_component,
+    LOGACCESS_FILTER_or,
+    LOGACCESS_FILTER_and,
+    LOGACCESS_FILTER_wildcard,
+    LOGACCESS_FILTER_unknown
+} LogAccessFilterType;
+
+inline const char * logAccessFilterTypeToString(LogAccessFilterType field)
+{
+    switch(field)
+    {
+    case LOGACCESS_FILTER_jobid:
+        return "jobid";
+    case LOGACCESS_FILTER_class:
+        return "class";
+    case LOGACCESS_FILTER_audience:
+        return "audience";
+    case LOGACCESS_FILTER_component:
+        return "component";
+    case LOGACCESS_FILTER_or:
+        return "or";
+    case LOGACCESS_FILTER_and:
+        return "and";
+    case LOGACCESS_FILTER_wildcard:
+         return "*" ;
+    default:
+        return "UNKNOWN";
+    }
+}
+
+inline unsigned logAccessFilterTypeFromName(char const * name)
+{
+    if (isEmptyString(name))
+        return LOGACCESS_FILTER_unknown;
+
+    if(strieq(name, "jobid"))
+        return LOGACCESS_FILTER_jobid;
+    if(strieq(name, "class"))
+        return LOGACCESS_FILTER_class;
+    if(strieq(name, "audience"))
+        return LOGACCESS_FILTER_audience;
+    if(strieq(name, "component"))
+        return LOGACCESS_FILTER_component;
+    if(strieq(name, "or"))
+        return LOGACCESS_FILTER_or;
+    if(strieq(name, "and"))
+        return LOGACCESS_FILTER_and;
+    return LOGACCESS_FILTER_unknown;
+}
+
+interface jlib_decl ILogAccessFilter : public IInterface
+{
+ public:
+    virtual void addToPTree(IPropertyTree * tree) const = 0;
+    virtual void toString(StringBuffer & out) const = 0;
+    virtual LogAccessFilterType filterType() const = 0;
+};
+
+struct LogAccessConditions
+{
+private:
+    Owned<ILogAccessFilter> filter;
+    StringArray logFieldNames;
+    LogAccessTimeRange timeRange;
+    unsigned limit = 100;
+    offset_t startFrom = 0;
+
+public:
+    LogAccessConditions & operator = (const LogAccessConditions & l)
+    {
+        copyLogFieldNames(l.logFieldNames);
+        limit = l.limit;
+        timeRange = l.timeRange;
+        setFilter(LINK(l.filter));
+        startFrom = l.startFrom;
+
+        return *this;
+    }
+
+    ILogAccessFilter * queryFilter() const
+    {
+        return filter.get();
+    }
+    void setFilter(ILogAccessFilter * _filter)
+    {
+        filter.setown(_filter);
+    }
+
+    void appendLogFieldName(const char * fieldname)
+    {
+        if (!logFieldNames.contains(fieldname))
+            logFieldNames.append(fieldname);
+    }
+
+    void copyLogFieldNames(const StringArray & fields)
+    {
+        ForEachItemIn(fieldsindex,fields)
+        {
+            appendLogFieldName(fields.item(fieldsindex));
+        }
+    }
+
+    inline const StringArray & queryLogFieldNames() const
+    {
+        return logFieldNames;
+    }
+
+    unsigned getLimit() const
+    {
+        return limit;
+    }
+
+    void setLimit(unsigned limit = 100)
+    {
+        this->limit = limit;
+    }
+
+    const StringArray& getLogFieldNames() const
+    {
+        return logFieldNames;
+    }
+
+    void setLogFieldNames(const StringArray &logFieldNames)
+    {
+        this->logFieldNames = logFieldNames;
+    }
+
+    offset_t getStartFrom() const
+    {
+        return startFrom;
+    }
+
+    void setStartFrom(offset_t startFrom)
+    {
+        this->startFrom = startFrom;
+    }
+
+    const LogAccessTimeRange & getTimeRange() const
+    {
+        return timeRange;
+    }
+
+    void setTimeRange(const LogAccessTimeRange &timeRange)
+    {
+        this->timeRange = timeRange;
+    }
+};
+
+typedef enum
+{
+    LOGACCESS_LOGFORMAT_xml,
+    LOGACCESS_LOGFORMAT_json,
+    LOGACCESS_LOGFORMAT_csv
+} LogAccessLogFormat;
+
+inline LogAccessLogFormat logAccessFormatFromName(const char * name)
+{
+    if (isEmptyString(name))
+        throw makeStringException(-1, "Encountered empty Log Access Format name");
+
+    if(strieq(name, "xml"))
+        return LOGACCESS_LOGFORMAT_xml;
+    else if(strieq(name, "json"))
+        return LOGACCESS_LOGFORMAT_json;
+    else if(strieq(name, "csv"))
+        return LOGACCESS_LOGFORMAT_csv;
+    else
+        throw makeStringExceptionV(-1, "Encountered unknown Log Access Format name: '%s'", name);
+}
+
+// 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)
+//                      -- Declares method to retrieve active logmap (mapping between target log store and known log columns)
+//                      -- Declares method to retrieve target log store connectivity information
+interface IRemoteLogAccess : extends IInterface
+{
+    virtual bool fetchLog(const LogAccessConditions & options, StringBuffer & returnbuf, LogAccessLogFormat format) = 0;
+
+    virtual const char * getRemoteLogAccessType() const = 0;
+    virtual IPropertyTree * queryLogMap() const = 0;
+    virtual const char * fetchConnectionStr() const = 0;
+};
+
+// Helper functions to construct log access filters
+extern jlib_decl ILogAccessFilter * getLogAccessFilterFromPTree(IPropertyTree * tree);
+extern jlib_decl ILogAccessFilter * getJobIDLogAccessFilter(const char * jobId);
+extern jlib_decl ILogAccessFilter * getComponentLogAccessFilter(const char * component);
+extern jlib_decl ILogAccessFilter * getAudienceLogAccessFilter(MessageAudience audience);
+extern jlib_decl ILogAccessFilter * getClassLogAccessFilter(LogMsgClass logclass);
+extern jlib_decl ILogAccessFilter * getBinaryLogAccessFilter(ILogAccessFilter * arg1, ILogAccessFilter * arg2, LogAccessFilterType type);
+extern jlib_decl ILogAccessFilter * getBinaryLogAccessFilterOwn(ILogAccessFilter * arg1, ILogAccessFilter * arg2, LogAccessFilterType type);
+extern jlib_decl ILogAccessFilter * getWildCardLogAccessFilter();
+
+// Helper functions to actuate log access query
+extern jlib_decl bool fetchLog(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, ILogAccessFilter * filter, LogAccessTimeRange timeRange, const StringArray & cols, LogAccessLogFormat format);
+extern jlib_decl bool fetchJobIDLog(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, const char *jobid, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
+extern jlib_decl bool fetchComponentLog(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, const char * component, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
+extern jlib_decl bool fetchLogByAudience(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, MessageAudience audience, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
+extern jlib_decl bool fetchLogByClass(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, LogMsgClass logclass, LogAccessTimeRange timeRange, StringArray & cols, LogAccessLogFormat format);
+extern jlib_decl IRemoteLogAccess * queryRemoteLogAccessor();
+
 #endif

+ 103 - 3
system/jlib/jlog.ipp

@@ -60,8 +60,6 @@ enum
     MSGFILTER_regex
     };
 
-
-
 // Implementations of filter which pass all or no messages
 
 //MORE: This would benefit from more code moved into this base class
@@ -253,7 +251,6 @@ private:
     bool                      localFlag;
 };
 
-
 // Implementation of filter using component
 
 class ComponentLogMsgFilter : public CLogMsgFilter
@@ -837,6 +834,109 @@ private:
 #endif
 };
 
+class CLogAccessFilter : public CInterfaceOf<ILogAccessFilter> {};
+
+class FieldLogAccessFilter : public CLogAccessFilter
+{
+public:
+    FieldLogAccessFilter(const char * _value, LogAccessFilterType _filterType) : value(_value), type(_filterType) {}
+    FieldLogAccessFilter(IPropertyTree * tree, LogAccessFilterType _filterType)
+    {
+        type = _filterType;
+        VStringBuffer xpath("@%s", logAccessFilterTypeToString(type));
+        value.set(tree->queryProp(xpath.str()));
+    }
+
+    void addToPTree(IPropertyTree * tree) const
+    {
+        IPropertyTree * filterTree = createPTree(ipt_caseInsensitive);
+        filterTree->setProp("@type", logAccessFilterTypeToString(type));
+        filterTree->setProp("@value", value);
+        tree->addPropTree("filter", filterTree);
+    }
+
+    void toString(StringBuffer & out) const
+    {
+        out.set(value);
+    }
+
+    LogAccessFilterType filterType() const
+    {
+        return type;
+    }
+
+protected:
+    StringAttr value;
+    LogAccessFilterType type;
+};
+
+class BinaryLogAccessFilter : public CLogAccessFilter
+{
+public:
+    BinaryLogAccessFilter(ILogAccessFilter * _arg1, ILogAccessFilter * _arg2, LogAccessFilterType _type) : arg1(_arg1), arg2(_arg2)
+    {
+        setType(_type);
+    }
+
+    BinaryLogAccessFilter(IPropertyTree * tree, LogAccessFilterType _type)
+    {
+        setType(_type);
+        Owned<IPropertyTreeIterator> iter = tree->getElements("filter");
+        ForEach(*iter)
+        {
+            ILogAccessFilter *filter = getLogAccessFilterFromPTree(&(iter->query()));
+            if (!arg1.get())
+                arg1.setown(filter);
+            else if (!arg2.get())
+                arg2.setown(filter);
+            else
+                arg2.setown(getBinaryLogAccessFilterOwn(arg2.getClear(), filter, type));
+        }
+    }
+
+    void addToPTree(IPropertyTree * tree) const
+    {
+        IPropertyTree * filterTree = createPTree(ipt_caseInsensitive);
+        filterTree->setProp("@type", logAccessFilterTypeToString(type));
+        arg1->addToPTree(filterTree);
+        arg2->addToPTree(filterTree);
+        tree->addPropTree("filter", filterTree);
+    }
+
+    void toString(StringBuffer & out) const
+    {
+        StringBuffer tmp;
+        out.set("( ");
+        arg1->toString(tmp);
+        out.appendf(" %s %s ", tmp.str(), logAccessFilterTypeToString(type));
+        arg2->toString(tmp.clear());
+        out.appendf(" %s )", tmp.str());
+    }
+
+    LogAccessFilterType filterType() const
+    {
+        return type;
+    }
+
+private:
+    Linked<ILogAccessFilter> arg1;
+    Linked<ILogAccessFilter> arg2;
+    LogAccessFilterType type;
+
+    void setType(LogAccessFilterType _type)
+    {
+        switch(_type)
+        {
+        case LOGACCESS_FILTER_or:
+        case LOGACCESS_FILTER_and:
+            type = _type;
+            break;
+        default:
+            throwUnexpectedX("BinaryLogAccessFilter : detected invalid BinaryLogAccessFilter type");
+        }
+    }
+};
+
 // Reset logging-related thread-local variables, when a threadpool starts
 extern void resetThreadLogging();
 

+ 2 - 0
system/jlib/jscm.hpp

@@ -279,6 +279,8 @@ class StringBuffer;
 
 typedef enum
 {
+    // Unknown target audience
+    MSGAUD_unknown    = 0x00,
     // Target audience: system admins
     // Purpose: Information useful for administering the platform, diagnosing errors and
     //          resolving system issues

+ 19 - 0
system/logaccess/CMakeLists.txt

@@ -0,0 +1,19 @@
+###############################################################################
+#    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+
+IF((NOT WIN32) AND USE_ELASTICSTACK_CLIENT)
+  HPCC_ADD_SUBDIRECTORY (ElasticStack)
+ENDIF()

+ 108 - 0
system/logaccess/ElasticStack/CMakeLists.txt

@@ -0,0 +1,108 @@
+################################################################################
+#    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+
+project(ElasticStackLogAccess)
+
+
+# elasticlient dependency submodule
+
+include(ExternalProject)
+ExternalProject_Add(
+  elasticlient-build
+  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/elasticlient
+  BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/elasticlient
+  CMAKE_ARGS -DBUILD_ELASTICLIENT_TESTS=OFF -DBUILD_ELASTICLIENT_EXAMPLE=OFF -DUSE_ALL_SYSTEM_LIBS=OFF -DBUILD_CPR_TESTS=OFF -DUSE_SYSTEM_CURL=ON
+  BUILD_COMMAND ${CMAKE_MAKE_PROGRAM} cpr jsoncpp_lib elasticlient
+  BUILD_BYPRODUCTS
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libjsoncpp${CMAKE_SHARED_LIBRARY_SUFFIX}
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libelasticlient${CMAKE_SHARED_LIBRARY_SUFFIX}
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libcpr${CMAKE_SHARED_LIBRARY_SUFFIX}
+  INSTALL_COMMAND "")
+
+add_library(elasticlient SHARED IMPORTED GLOBAL)
+set_property(TARGET elasticlient
+  PROPERTY IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libelasticlient${CMAKE_SHARED_LIBRARY_SUFFIX})
+add_dependencies(elasticlient elasticlient-build)
+
+add_library(jsoncpp SHARED IMPORTED GLOBAL)
+set_property(TARGET jsoncpp
+  PROPERTY IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libjsoncpp${CMAKE_SHARED_LIBRARY_SUFFIX})
+add_dependencies(jsoncpp elasticlient-build)
+
+add_library(cpr SHARED IMPORTED GLOBAL)
+set_property(TARGET cpr
+  PROPERTY IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libcpr${CMAKE_SHARED_LIBRARY_SUFFIX})
+add_dependencies(cpr elasticlient-build)
+
+
+# elastistacklogaccess library
+# update cxx_flags to be less restrictive when building against elasticlient header files
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpermissive")
+
+set(srcs
+  ${CMAKE_CURRENT_SOURCE_DIR}/ElasticStackLogAccess.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/elasticlient/include/elasticlient/client.h
+)
+
+include_directories(
+  ${CMAKE_CURRENT_SOURCE_DIR}/elasticlient/include
+  ${CMAKE_CURRENT_SOURCE_DIR}/elasticlient/external/jsoncpp/include
+  ${CMAKE_CURRENT_SOURCE_DIR}/elasticlient/external/cpr/include
+  ${HPCC_SOURCE_DIR}/system/include
+  ${HPCC_SOURCE_DIR}/system/jlib
+)
+
+add_definitions(-DELASTICSTACKLOGACCESS_EXPORTS)
+HPCC_ADD_LIBRARY(elasticstacklogaccess SHARED ${srcs})
+target_link_libraries(elasticstacklogaccess
+  jlib
+  elasticlient
+  jsoncpp
+  cpr
+)
+
+install(TARGETS elasticstacklogaccess
+  RUNTIME DESTINATION ${EXEC_DIR}
+  LIBRARY DESTINATION ${LIB_DIR}
+)
+
+
+if(NOT PLUGIN)
+  install(CODE "set(ENV{LD_LIBRARY_PATH} \"\$ENV{LD_LIBRARY_PATH}:${PROJECT_BINARY_DIR}:${PROJECT_BINARY_DIR}/elasticlient/lib\")")
+  install(FILES
+    ${CMAKE_CURRENT_SOURCE_DIR}/elasticlient/LICENSE.txt
+    DESTINATION ${LIB_DIR}
+    RENAME elasticlient-LICENSE.txt)
+  install(CODE "file(RPATH_CHANGE
+    FILE \"${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libelasticlient${CMAKE_SHARED_LIBRARY_SUFFIX}\"
+    OLD_RPATH \"${PROJECT_BINARY_DIR}/elasticlient/lib\"
+    NEW_RPATH \"${LIB_PATH}\")")
+  install(CODE "file(RPATH_CHANGE
+    FILE \"${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libcpr${CMAKE_SHARED_LIBRARY_SUFFIX}\"
+    OLD_RPATH \"${PROJECT_BINARY_DIR}/elasticlient/lib\"
+    NEW_RPATH \"${LIB_PATH}\")")
+  install(FILES
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libelasticlient${CMAKE_SHARED_LIBRARY_SUFFIX}.2.1.0
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libelasticlient${CMAKE_SHARED_LIBRARY_SUFFIX}.2
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libelasticlient${CMAKE_SHARED_LIBRARY_SUFFIX}
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libjsoncpp${CMAKE_SHARED_LIBRARY_SUFFIX}.1.8.4
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libjsoncpp${CMAKE_SHARED_LIBRARY_SUFFIX}.19
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libjsoncpp${CMAKE_SHARED_LIBRARY_SUFFIX}
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libcpr${CMAKE_SHARED_LIBRARY_SUFFIX}.1.5.1
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libcpr${CMAKE_SHARED_LIBRARY_SUFFIX}.1
+    ${CMAKE_CURRENT_BINARY_DIR}/elasticlient/lib/libcpr${CMAKE_SHARED_LIBRARY_SUFFIX}
+    DESTINATION ${LIB_DIR})
+endif(NOT PLUGIN)

+ 543 - 0
system/logaccess/ElasticStack/ElasticStackLogAccess.cpp

@@ -0,0 +1,543 @@
+/*##############################################################################
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#include "ElasticStackLogAccess.hpp"
+
+#include "platform.h"
+
+#include <string>
+#include <vector>
+#include <iostream>
+#include <json/json.h>
+
+
+#ifdef _CONTAINERIZED
+//In containerized world, most likely Elastic Search host is their default k8s hostname
+static constexpr const char * DEFAULT_ES_HOST = "elasticsearch-master";
+#else
+//In baremetal, localhost is good guess as any
+static constexpr const char * DEFAULT_ES_HOST = "localhost";
+#endif
+
+static constexpr const char * DEFAULT_ES_PROTOCOL = "http";
+static constexpr const char * DEFAULT_ES_DOC_TYPE = "_doc";
+static constexpr const char * DEFAULT_ES_PORT = "9200";
+
+static constexpr int DEFAULT_ES_DOC_LIMIT = 100;
+static constexpr int DEFAULT_ES_DOC_START = 0;
+
+static constexpr const char * DEFAULT_TS_NAME = "@timestamp";
+static constexpr const char * DEFAULT_INDEX_PATTERN = "filebeat*";
+
+static constexpr const char * DEFAULT_HPCC_LOG_SEQ_COL         = "hpcc.log.sequence";
+static constexpr const char * DEFAULT_HPCC_LOG_TIMESTAMP_COL   = "hpcc.log.timestamp";
+static constexpr const char * DEFAULT_HPCC_LOG_PROCID_COL      = "hpcc.log.procid";
+static constexpr const char * DEFAULT_HPCC_LOG_THREADID_COL    = "hpcc.log.threadid";
+static constexpr const char * DEFAULT_HPCC_LOG_MESSAGE_COL     = "hpcc.log.message";
+static constexpr const char * DEFAULT_HPCC_LOG_JOBID_COL       = "hpcc.log.jobid";
+static constexpr const char * DEFAULT_HPCC_LOG_COMPONENT_COL   = "kubernetes.container.name";
+static constexpr const char * DEFAULT_HPCC_LOG_TYPE_COL        = "hpcc.log.class";
+static constexpr const char * DEFAULT_HPCC_LOG_AUD_COL         = "hpcc.log.audience";
+
+static constexpr const char * LOGMAP_INDEXPATTERN_ATT = "@storename";
+static constexpr const char * LOGMAP_SEARCHCOL_ATT = "@searchcolumn";
+static constexpr const char * LOGMAP_TIMESTAMPCOL_ATT = "@timestampcolumn";
+
+ElasticStackLogAccess::ElasticStackLogAccess(const std::vector<std::string> &hostUrlList, IPropertyTree & logAccessPluginConfig) : m_esClient(hostUrlList)
+{
+    if (!hostUrlList.at(0).empty())
+        m_esConnectionStr.set(hostUrlList.at(0).c_str());
+
+    m_pluginCfg.set(&logAccessPluginConfig);
+
+    m_globalIndexTimestampField.set(DEFAULT_TS_NAME);
+    m_globalIndexSearchPattern.set(DEFAULT_INDEX_PATTERN);
+
+    m_classIndexSearchPattern.set(DEFAULT_HPCC_LOG_TYPE_COL);
+    m_workunitSearchColName.set(DEFAULT_HPCC_LOG_JOBID_COL);
+    m_componentsSearchColName.set(DEFAULT_HPCC_LOG_COMPONENT_COL);
+    m_audienceSearchColName.set(DEFAULT_HPCC_LOG_AUD_COL);
+
+    Owned<IPropertyTreeIterator> logMapIter = m_pluginCfg->getElements("logMaps");
+    ForEach(*logMapIter)
+    {
+        IPropertyTree & logMap = logMapIter->query();
+        const char * logMapType = logMap.queryProp("@type");
+        if (streq(logMapType, "global"))
+        {
+            if (logMap.hasProp(LOGMAP_INDEXPATTERN_ATT))
+                m_globalIndexSearchPattern = logMap.queryProp(LOGMAP_INDEXPATTERN_ATT);
+            if (logMap.hasProp(LOGMAP_SEARCHCOL_ATT))
+                m_globalSearchColName = logMap.queryProp(LOGMAP_SEARCHCOL_ATT);
+            if (logMap.hasProp(LOGMAP_TIMESTAMPCOL_ATT))
+                m_globalIndexTimestampField = logMap.queryProp(LOGMAP_TIMESTAMPCOL_ATT);
+        }
+        else if (streq(logMapType, "workunits"))
+        {
+            if (logMap.hasProp(LOGMAP_INDEXPATTERN_ATT))
+                m_workunitIndexSearchPattern = logMap.queryProp(LOGMAP_INDEXPATTERN_ATT);
+            if (logMap.hasProp(LOGMAP_SEARCHCOL_ATT))
+                m_workunitSearchColName = logMap.queryProp(LOGMAP_SEARCHCOL_ATT);
+        }
+        else if (streq(logMapType, "components"))
+        {
+            if (logMap.hasProp(LOGMAP_INDEXPATTERN_ATT))
+                m_componentsIndexSearchPattern = logMap.queryProp(LOGMAP_INDEXPATTERN_ATT);
+            if (logMap.hasProp(LOGMAP_SEARCHCOL_ATT))
+                m_componentsSearchColName = logMap.queryProp(LOGMAP_SEARCHCOL_ATT);
+        }
+        else if (streq(logMapType, "class"))
+        {
+            if (logMap.hasProp(LOGMAP_INDEXPATTERN_ATT))
+                m_classIndexSearchPattern = logMap.queryProp(LOGMAP_INDEXPATTERN_ATT);
+            if (logMap.hasProp(LOGMAP_SEARCHCOL_ATT))
+                m_classSearchColName = logMap.queryProp(LOGMAP_SEARCHCOL_ATT);
+        }
+        else if (streq(logMapType, "audience"))
+        {
+            if (logMap.hasProp(LOGMAP_INDEXPATTERN_ATT))
+                m_audienceIndexSearchPattern = logMap.queryProp(LOGMAP_INDEXPATTERN_ATT);
+            if (logMap.hasProp(LOGMAP_SEARCHCOL_ATT))
+                m_audienceSearchColName = logMap.queryProp(LOGMAP_SEARCHCOL_ATT);
+        }
+    }
+
+#ifdef LOGACCESSDEBUG
+    StringBuffer out;
+    const IPropertyTree * status = getESStatus();
+    toXML(status, out);
+    fprintf(stdout, "ES Status: %s", out.str());
+
+    const IPropertyTree * is =  getIndexSearchStatus(m_globalIndexSearchPattern);
+    toXML(is, out);
+    fprintf(stdout, "ES available indexes: %s", out.str());
+
+    const IPropertyTree * ts = getTimestampTypeFormat(m_globalIndexSearchPattern, m_globalIndexTimestampField);
+    toXML(ts, out);
+    fprintf(stdout, "ES %s timestamp info: '%s'", m_globalIndexSearchPattern.str(), out.str());
+#endif
+
+}
+
+const IPropertyTree * ElasticStackLogAccess::performAndLogESRequest(Client::HTTPMethod httpmethod, const char * url, const char * reqbody, const char * logmessageprefix, LogMsgCategory reqloglevel = MCdebugProgress, LogMsgCategory resploglevel = MCdebugProgress)
+{
+    try
+    {
+        LOG(reqloglevel,"ESLogAccess: Requesting '%s'... ", logmessageprefix );
+        cpr::Response esREsponse = m_esClient.performRequest(httpmethod,url,reqbody);
+        Owned<IPropertyTree> response = createPTreeFromJSONString(esREsponse.text.c_str());
+        LOG(resploglevel,"ESLogAccess: '%s' response: '%s'", logmessageprefix, esREsponse.text.c_str());
+
+        return response.getClear();
+    }
+    catch (ConnectionException & ce)//std::runtime_error
+    {
+        LOG(MCuserError, "ESLogAccess: Encountered error requesting '%s': '%s'", logmessageprefix, ce.what());
+    }
+    catch (...)
+    {
+        LOG(MCuserError, "ESLogAccess: Encountered error requesting '%s'", logmessageprefix);
+    }
+    return nullptr;
+
+}
+
+const IPropertyTree * ElasticStackLogAccess::getTimestampTypeFormat(const char * indexpattern, const char * fieldname)
+{
+    if (isEmptyString(indexpattern))
+        throw makeStringException(-1, "ElasticStackLogAccess::getTimestampTypeFormat: indexpattern must be provided");
+
+    if (isEmptyString(fieldname))
+        throw makeStringException(-1, "ElasticStackLogAccess::getTimestampTypeFormat: fieldname must be provided");
+
+    VStringBuffer timestampformatreq("%s/_mapping/field/created_ts?include_type_name=true&format=JSON", indexpattern);
+    return performAndLogESRequest(Client::HTTPMethod::GET, timestampformatreq.str(), "", "getTimestampTypeFormat");
+}
+
+const IPropertyTree * ElasticStackLogAccess::getIndexSearchStatus(const char * indexpattern)
+{
+    if (!indexpattern || !*indexpattern)
+        throw makeStringException(-1, "ElasticStackLogAccess::getIndexSearchStatus: indexpattern must be provided");
+
+    VStringBuffer indexsearch("_cat/indices/%s?format=JSON", indexpattern);
+    return performAndLogESRequest(Client::HTTPMethod::GET, indexsearch.str(), "", "List of available indexes");
+
+}
+
+const IPropertyTree * ElasticStackLogAccess::getESStatus()
+{
+    return performAndLogESRequest(Client::HTTPMethod::GET, "_cluster/health", "", "Target cluster health");
+}
+
+/*
+ * Transform ES query response to back-end agnostic response
+ *
+ */
+void processESJsonResp(const cpr::Response & retrievedDocument, StringBuffer & returnbuf, LogAccessLogFormat format)
+{
+    if (retrievedDocument.status_code != 200)
+        throw makeStringExceptionV(-1, "ElasticSearch request failed: %s", retrievedDocument.status_line.c_str());
+
+    DBGLOG("Retrieved ES JSON DOC: %s", retrievedDocument.text.c_str());
+
+    Owned<IPropertyTree> tree = createPTreeFromJSONString(retrievedDocument.text.c_str());
+    if (!tree)
+        throw makeStringExceptionV(-1, "%s: Could not parse ElasticSearch query response", COMPONENT_NAME);
+
+    if (tree->getPropBool("timed_out", 0))
+        LOG(MCuserProgress,"ES Log Access: timeout reported");
+    if (tree->getPropInt("_shards/failed",0) > 0)
+        LOG(MCuserProgress,"ES Log Access: failed _shards reported");
+
+    DBGLOG("ES Log Access: hit count: '%d'", tree->getPropInt("hits/total/value"));
+
+    Owned<IPropertyTreeIterator> iter = tree->getElements("hits/hits/fields");
+    switch (format)
+    {
+        case LOGACCESS_LOGFORMAT_xml:
+        {
+            returnbuf.append("<lines>");
+
+            ForEach(*iter)
+            {
+                IPropertyTree & cur = iter->query();
+                returnbuf.append("<line>");
+                toXML(&cur,returnbuf);
+                returnbuf.append("</line>");
+            }
+            returnbuf.append("</lines>");
+            break;
+        }
+        case LOGACCESS_LOGFORMAT_json:
+        {
+            returnbuf.append("{\"lines\": [");
+            StringBuffer hitchildjson;
+            bool first = true;
+            ForEach(*iter)
+            {
+                IPropertyTree & cur = iter->query();
+                toJSON(&cur,hitchildjson.clear());
+                if (!first)
+                    returnbuf.append(", ");
+
+                first = false;
+                returnbuf.appendf("{\"fields\": [ %s ]}", hitchildjson.str());
+            }
+            returnbuf.append("]}");
+            break;
+        }
+        case LOGACCESS_LOGFORMAT_csv:
+        {
+            ForEach(*iter)
+            {
+                IPropertyTree & cur = iter->query();
+                bool first = true;
+                Owned<IPropertyTreeIterator> fieldelementsitr = cur.getElements("*");
+                ForEach(*fieldelementsitr)
+                {
+                    if (!first)
+                        returnbuf.append(", ");
+                    else
+                        first = false;
+
+                    returnbuf.append(fieldelementsitr->query().queryProp(".")); // commas in data should be escaped
+                }
+                returnbuf.append("\n");
+            }
+            break;
+        }
+        default:
+            break;
+    }
+}
+
+void esTimestampQueryRangeString(std::string & range, const char * timestampfield, std::time_t from, std::time_t to)
+{
+    if (isEmptyString(timestampfield))
+        throw makeStringException(-1, "ES Log Access: TimeStamp Field must be provided");
+
+    //Elastic Search Date formats can be customized, but if no format is specified then it uses the default:
+    //"strict_date_optional_time||epoch_millis"
+    // "%Y-%m-%d"'T'"%H:%M:%S"
+
+    //We'll report the timestamps as epoch_millis
+    range = "\"range\": { \"";
+    range += timestampfield;
+    range += "\": {";
+    range += "\"gte\": \"";
+    range += std::to_string(from*1000);
+    range += "\"";
+
+    if (to != -1) //aka 'to' has been initialized
+    {
+        range += ",\"lte\": \"";
+        range += std::to_string(to*1000);
+        range += "\"";
+    }
+    range += "} }";
+}
+
+/*
+ * Constructs ElasticSearch match clause
+ * Use for exact term matches such as a price, a product ID, or a username.
+ */
+void esTermQueryString(std::string & search, const char *searchval, const char *searchfield)
+{
+    //https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html
+    //You can use the term query to find documents based on a precise value such as a price, a product ID, or a username.
+
+    //Avoid using the term query for text fields.
+    //By default, Elasticsearch changes the values of text fields as part of analysis. This can make finding exact matches for text field values difficult.
+    if (isEmptyString(searchval) || isEmptyString(searchfield))
+        throw makeStringException(-1, "Could not create ES term query string: Either search value or search field is empty");
+
+    search += "\"term\": { \"";
+    search += searchfield;
+    search += "\" : { \"value\": \"";
+    search += searchval;
+    search += "\" } }";
+}
+
+/*
+ * Constructs ElasticSearch match clause
+ * Use for full-text search
+ */
+void esMatchQueryString(std::string & search, const char *searchval, const char *searchfield)
+{
+    //https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html
+    //Returns documents that match a provided text, number, date or boolean value. The provided text is analyzed before matching.
+    //The match query is the standard query for performing a full-text search, including options for fuzzy matching.
+    if (isEmptyString(searchval) || isEmptyString(searchfield))
+        throw makeStringException(-1, "Could not create ES match query string: Either search value or search field is empty");
+
+    search += "\"match\": { \"";
+    search += searchfield;
+    search += "\" : \"";
+    search += searchval;
+    search += "\" }";
+}
+
+/*
+ * Construct Elasticsearch query directives string
+ */
+void esSearchMetaData(std::string & search, const  StringArray & selectcols, unsigned size = DEFAULT_ES_DOC_LIMIT, offset_t from = DEFAULT_ES_DOC_START)
+{
+    //Query parameters:
+    //https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-body.html
+
+    //_source: https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-source-filtering.html
+    search += "\"_source\": false, \"fields\": [" ;
+
+    if (selectcols.length() > 0)
+    {
+        StringBuffer sourcecols;
+        ForEachItemIn(idx, selectcols)
+        {
+            sourcecols.appendf("\"%s\"", selectcols.item(idx));
+            if (idx < selectcols.length() -1)
+                sourcecols.append(",");
+        }
+
+        if (!sourcecols.isEmpty())
+            search += sourcecols.str() ;
+        else
+            search += "\"*\"";
+            //search += "!*.keyword"; //all fields ignoring the .keyword postfixed fields
+    }
+    else
+        //search += "!*.keyword"; //all fields ignoring the .keyword postfixed fields
+        search += "\"*\"";
+
+    search += "],";
+    search += "\"from\": ";
+    search += std::to_string(from);
+    search += ", \"size\": ";
+    search += std::to_string(size);
+    search += ", ";
+}
+
+/*
+ * Construct ES query string, execute query
+ */
+cpr::Response ElasticStackLogAccess::performESQuery(const LogAccessConditions & options)
+{
+    try
+    {
+        StringBuffer queryValue;
+        std::string queryField = m_globalSearchColName.str();
+        std::string queryIndex = m_globalIndexSearchPattern.str();
+
+        bool fullTextSearch = true;
+        bool wildCardSearch = false;
+
+        options.queryFilter()->toString(queryValue);
+        switch (options.queryFilter()->filterType())
+        {
+        case LOGACCESS_FILTER_jobid:
+        {
+            if (!m_workunitSearchColName.isEmpty())
+            {
+                queryField = m_workunitSearchColName.str();
+                fullTextSearch = false; //found dedicated components column
+            }
+
+            if (!m_workunitIndexSearchPattern.isEmpty())
+            {
+                queryIndex = m_workunitIndexSearchPattern.str();
+            }
+
+            DBGLOG("%s: Searching log entries by jobid: '%s'...", COMPONENT_NAME, queryValue.str() );
+            break;
+        }
+        case LOGACCESS_FILTER_class:
+        {
+            if (!m_classSearchColName.isEmpty())
+            {
+                queryField = m_classSearchColName.str();
+                fullTextSearch = false; //found dedicated components column
+            }
+
+            if (!m_classIndexSearchPattern.isEmpty())
+            {
+                queryIndex = m_classIndexSearchPattern.str();
+            }
+
+            DBGLOG("%s: Searching log entries by class: '%s'...", COMPONENT_NAME, queryValue.str() );
+            break;
+        }
+        case LOGACCESS_FILTER_audience:
+        {
+            if (!m_audienceSearchColName.isEmpty())
+            {
+                queryField = m_audienceSearchColName.str();
+                fullTextSearch = false; //found dedicated components column
+            }
+
+            if (!m_audienceIndexSearchPattern.isEmpty())
+            {
+                queryIndex = m_audienceIndexSearchPattern.str();
+            }
+
+            DBGLOG("%s: Searching log entries by target audience: '%s'...", COMPONENT_NAME, queryValue.str() );
+            break;
+        }
+        case LOGACCESS_FILTER_component:
+        {
+            if (!m_componentsSearchColName.isEmpty())
+            {
+                queryField = m_componentsSearchColName.str();
+                fullTextSearch = false; //found dedicated components column
+            }
+
+            if (!m_componentsIndexSearchPattern.isEmpty())
+            {
+                queryIndex = m_componentsIndexSearchPattern.str();
+            }
+
+            DBGLOG("%s: Searching '%s' component log entries...", COMPONENT_NAME, queryValue.str() );
+            break;
+        }
+        case LOGACCESS_FILTER_wildcard:
+        {
+            wildCardSearch = true;
+            DBGLOG("%s: Performing wildcard log entry search...", COMPONENT_NAME);
+            break;
+        }
+        case LOGACCESS_FILTER_or:
+            throw makeStringExceptionV(-1, "%s: Compound query criteria not currently supported: '%s'", COMPONENT_NAME, queryValue.str());
+            //"query":{"bool":{"must":[{"match":{"kubernetes.container.name.keyword":{"query":"eclwatch","operator":"or"}}},{"match":{"container.image.name.keyword":"hpccsystems\\core"}}]} }
+        case LOGACCESS_FILTER_and:
+            throw makeStringExceptionV(-1, "%s: Compound query criteria not currently supported: '%s'", COMPONENT_NAME, queryValue.str());
+            //"query":{"bool":{"must":[{"match":{"kubernetes.container.name.keyword":{"query":"eclwatch","operator":"and"}}},{"match":{"created_ts":"2021-08-25T20:23:04.923Z"}}]} }
+        default:
+            throw makeStringExceptionV(-1, "%s: Unknown query criteria type encountered: '%s'", COMPONENT_NAME, queryValue.str());
+        }
+
+        std::string fullRequest = "{";
+        esSearchMetaData(fullRequest, options.getLogFieldNames(), options.getLimit(), options.getStartFrom());
+
+        fullRequest += "\"query\": { \"bool\": {";
+
+        if(!wildCardSearch)
+        {
+            fullRequest += " \"must\": { ";
+
+            std::string criteria;
+            if (fullTextSearch) //are we performing a query on a blob, or exact term match?
+                esMatchQueryString(criteria, queryValue.str(), queryField.c_str());
+            else
+                esTermQueryString(criteria, queryValue.str(), queryField.c_str());
+
+            fullRequest += criteria;
+            fullRequest += "}, "; //end must, expect filter to follow
+        }
+
+        std::string filter = "\"filter\": {";
+        std::string range;
+
+        const LogAccessTimeRange & trange = options.getTimeRange();
+        //Bail out earlier?
+        if (trange.getStartt().isNull())
+            throw makeStringExceptionV(-1, "%s: start time must be provided!", COMPONENT_NAME);
+
+        esTimestampQueryRangeString(range, m_globalIndexTimestampField.str(), trange.getStartt().getSimple(),trange.getEndt().isNull() ? -1 : trange.getEndt().getSimple());
+
+        filter += range;
+        filter += "}"; //end filter
+
+        fullRequest += filter;
+        fullRequest += "}}}"; //end bool and query
+
+        DBGLOG("%s: Search string '%s'", COMPONENT_NAME, fullRequest.c_str());
+
+        return m_esClient.search(queryIndex.c_str(), DEFAULT_ES_DOC_TYPE, fullRequest);
+    }
+    catch (std::runtime_error &e)
+    {
+        const char * wha = e.what();
+        throw makeStringExceptionV(-1, "%s: fetchLog: Error searching doc: %s", COMPONENT_NAME, wha);
+    }
+    catch (IException * e)
+    {
+        StringBuffer mess;
+        e->errorMessage(mess);
+        e->Release();
+        throw makeStringExceptionV(-1, "%s: fetchLog: Error searching doc: %s", COMPONENT_NAME, mess.str());
+    }
+}
+
+bool ElasticStackLogAccess::fetchLog(const LogAccessConditions & options, StringBuffer & returnbuf, LogAccessLogFormat format)
+{
+    cpr::Response esresp = performESQuery(options);
+    processESJsonResp(esresp, returnbuf, format);
+
+    return true;
+}
+
+extern "C" IRemoteLogAccess * createInstance(IPropertyTree & logAccessPluginConfig)
+{
+    //constructing ES Connection string(s) here b/c ES Client explicit ctr requires conn string array
+
+    const char * protocol = logAccessPluginConfig.queryProp("connection/@protocol");
+    const char * host = logAccessPluginConfig.queryProp("connection/@host");
+    const char * port = logAccessPluginConfig.queryProp("connection/@port");
+
+    std::string elasticSearchConnString;
+    elasticSearchConnString = isEmptyString(protocol) ? DEFAULT_ES_PROTOCOL : protocol;
+    elasticSearchConnString.append("://");
+    elasticSearchConnString.append(isEmptyString(host) ? DEFAULT_ES_HOST : host);
+    elasticSearchConnString.append(":").append((!port || !*port) ? DEFAULT_ES_PORT : port);
+    elasticSearchConnString.append("/"); // required!
+
+    return new ElasticStackLogAccess({elasticSearchConnString}, logAccessPluginConfig);
+}

+ 85 - 0
system/logaccess/ElasticStack/ElasticStackLogAccess.hpp

@@ -0,0 +1,85 @@
+/*##############################################################################
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#pragma once
+
+#include "jlog.hpp"
+#include "jptree.hpp"
+#include "jstring.hpp"
+#include "jfile.ipp"
+
+
+#ifndef ELASTICSTACKLOGACCESS_EXPORTS
+#define ELASTICSTACKLOGACCESS_API DECL_IMPORT
+#else
+#define ELASTICSTACKLOGACCESS_API DECL_EXPORT
+#endif
+
+#define COMPONENT_NAME "ES Log Access"
+
+/* undef verify definitions to avoid collision in cpr submodule */
+#ifdef verify
+    //#pragma message("UNDEFINING 'verify' - Will be redefined by cpr" )
+    #undef verify
+#endif
+
+#include <cpr/response.h>
+#include <elasticlient/client.h>
+
+using namespace elasticlient;
+
+class ELASTICSTACKLOGACCESS_API ElasticStackLogAccess : public CInterfaceOf<IRemoteLogAccess>
+{
+private:
+    static constexpr const char * type = "elasticstack";
+
+    Owned<IPropertyTree> m_pluginCfg;
+
+    StringBuffer m_globalIndexSearchPattern;
+    StringBuffer m_globalSearchColName;
+    StringBuffer m_globalIndexTimestampField;
+
+    StringBuffer m_workunitSearchColName;
+    StringBuffer m_workunitIndexSearchPattern;
+
+    StringBuffer m_componentsSearchColName;
+    StringBuffer m_componentsIndexSearchPattern;
+
+    StringBuffer m_audienceSearchColName;
+    StringBuffer m_audienceIndexSearchPattern;
+
+    StringBuffer m_classSearchColName;
+    StringBuffer m_classIndexSearchPattern;
+
+    StringBuffer m_defaultDocType; //default doc type to query
+
+    elasticlient::Client m_esClient;
+    StringBuffer m_esConnectionStr;
+
+    // Elastic Stack specific
+    cpr::Response performESQuery(const LogAccessConditions & options);
+    const IPropertyTree * getESStatus();
+    const IPropertyTree * getIndexSearchStatus(const char * indexpattern);
+    const IPropertyTree * getTimestampTypeFormat(const char * indexpattern, const char * fieldname);
+    const IPropertyTree * performAndLogESRequest(Client::HTTPMethod httpmethod, const char * url, const char * reqbody, const char * logmessageprefix, LogMsgCategory reqloglevel, LogMsgCategory resploglevel);
+
+public:
+    ElasticStackLogAccess(const std::vector<std::string> &hostUrlList, IPropertyTree & logAccessPluginConfig);
+    virtual ~ElasticStackLogAccess() override = default;
+
+    // IRemoteLogAccess methods
+    virtual bool fetchLog(const LogAccessConditions & options, StringBuffer & returnbuf, LogAccessLogFormat format) override;
+    virtual const char * getRemoteLogAccessType() const override { return type; }
+    virtual IPropertyTree * queryLogMap() const override { return m_pluginCfg->queryPropTree("logmap");}
+    virtual const char * fetchConnectionStr() const override { return m_esConnectionStr.str();}
+};

+ 1 - 0
system/logaccess/ElasticStack/elasticlient

@@ -0,0 +1 @@
+Subproject commit 1eadb9294166f13fb23b1f1840fbf8102b5fb5cf