ソースを参照

Merge pull request #14238 from asselitx/elk-log-hpcc-23426

HPCC-23426 Add support for JSON style TxSummary

Reviewed-By: Tim Klemm <tim.klemm@lexisnexisrisk.com>
Reviewed-By: Richard Chapman <rchapman@hpccsystems.com>
Richard Chapman 4 年 前
コミット
5dbfd4645c

+ 14 - 0
esp/bindings/SOAP/Platform/soapbind.cpp

@@ -123,6 +123,8 @@ int CHttpSoapBinding::onSoapRequest(CHttpRequest* request, CHttpResponse* respon
         {
             errcode = mex->errorCode();
             mex->serializeJSON(msgbuf, 0, true, true, true);
+            StringBuffer errMessage;
+            ctx->addTraceSummaryValue(LogMin, "msg", mex->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE);
             mex->Release();
         }
         catch (IException* e)
@@ -131,6 +133,8 @@ int CHttpSoapBinding::onSoapRequest(CHttpRequest* request, CHttpResponse* respon
             Owned<IMultiException> mex = MakeMultiException("Esp");
             mex->append(*e); // e is owned by mex
             mex->serializeJSON(msgbuf, 0, true, true, true);
+            StringBuffer errMessage;
+            ctx->addTraceSummaryValue(LogMin, "msg", e->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE);
         }
         catch (...)
         {
@@ -138,6 +142,7 @@ int CHttpSoapBinding::onSoapRequest(CHttpRequest* request, CHttpResponse* respon
             Owned<IMultiException> mex = MakeMultiException("Esp");
             mex->append(*MakeStringException(500, "Internal Server Error"));
             mex->serializeJSON(msgbuf, 0, true, true, true);
+            ctx->addTraceSummaryValue(LogMin, "msg", "Internal Server Error", TXSUMMARY_GRP_ENTERPRISE);
         }
         SetHTTPErrorStatus(errcode, response);
         response->setContentType(HTTP_TYPE_JSON);
@@ -156,6 +161,10 @@ int CHttpSoapBinding::onSoapRequest(CHttpRequest* request, CHttpResponse* respon
             soapFault.setown(makeSoapFault(request,mex, generateNamespace(*request->queryContext(), request, request->queryServiceName(), request->queryServiceMethod(), ns).str()));
             //SetHTTPErrorStatus(mex->errorCode(),response);
             SetHTTPErrorStatus(500,response);
+            StringBuffer errMessage;
+            ctx->addTraceSummaryValue(LogMin, "msg", mex->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE);
+            VStringBuffer fault("F%d", mex->errorCode());
+            ctx->addTraceSummaryValue(LogMin, "custom_fields.soapFaultCode", fault.str(), TXSUMMARY_GRP_ENTERPRISE);
             mex->Release();
         }
         catch (IException* e)
@@ -165,11 +174,16 @@ int CHttpSoapBinding::onSoapRequest(CHttpRequest* request, CHttpResponse* respon
             mex->append(*e); // e is owned by mex
             soapFault.setown(makeSoapFault(request,mex, generateNamespace(*request->queryContext(), request, request->queryServiceName(), request->queryServiceMethod(), ns).str()));
             SetHTTPErrorStatus(500,response);
+            StringBuffer errMessage;
+            ctx->addTraceSummaryValue(LogMin, "msg", e->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE);
+            VStringBuffer fault("F%d", mex->errorCode());
+            ctx->addTraceSummaryValue(LogMin, "custom_fields.soapFaultCode", fault.str(), TXSUMMARY_GRP_ENTERPRISE);
         }
         catch (...)
         {
             soapFault.setown(new CSoapFault(500,"Internal Server Error"));
             SetHTTPErrorStatus(500,response);
+            ctx->addTraceSummaryValue(LogMin, "msg", "Internal Server Error", TXSUMMARY_GRP_ENTERPRISE);
         }
         //response->setContentType(soapFault->get_content_type());
         response->setContentType(HTTP_TYPE_TEXT_XML_UTF8);

+ 9 - 2
esp/bindings/http/platform/httpbinding.cpp

@@ -45,6 +45,7 @@
 #include "dasds.hpp"
 #include "daclient.hpp"
 #include "workunit.hpp"
+#include "cumulativetimer.hpp"
 
 #define FILE_UPLOAD     "FileUploadAccess"
 #define DEFAULT_HTTP_PORT 80
@@ -685,7 +686,13 @@ bool EspHttpBinding::doAuth(IEspContext* ctx)
 {
     if(m_authtype.length() == 0 || stricmp(m_authtype.str(), "Basic") == 0)
     {
-        return basicAuth(ctx);
+        CumulativeTimer* timer = ctx->queryTraceSummaryCumulativeTimer(LogNormal, "custom_fields.basicAuthTime", TXSUMMARY_GRP_ENTERPRISE);
+        CumulativeTimer::Scope authScope(timer);
+
+        ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.authStart", TXSUMMARY_GRP_ENTERPRISE);
+        bool result = basicAuth(ctx);
+        ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.authEnd", TXSUMMARY_GRP_ENTERPRISE);
+        return result;
     }
 
     return false;
@@ -809,7 +816,7 @@ bool EspHttpBinding::basicAuth(IEspContext* ctx)
 
     m_secmgr->updateSettings(*user,securitySettings, ctx->querySecureContext());
 
-    ctx->addTraceSummaryTimeStamp(LogMin, "basicAuth");
+    ctx->addTraceSummaryTimeStamp(LogMin, "basicAuth", TXSUMMARY_GRP_CORE);
     return authorized;
 }
 

+ 14 - 0
esp/bindings/http/platform/httpprot.cpp

@@ -449,6 +449,13 @@ bool CHttpThread::onRequest()
         ESPLOG(LogMax, "Request from secure socket");
         m_socket.set(secure_sock);
         httpserver.setown(new CEspHttpServer(*secure_sock.get(), m_apport, m_viewConfig, getMaxRequestEntityLength()));
+        IEspContext* ctx = httpserver->queryContext();
+        if(ctx)
+        {
+            StringBuffer version;
+            secure_sock->get_ssl_version(version);
+            ctx->addTraceSummaryValue(LogMin, "custom_fields.sslProtocol", version.str(), TXSUMMARY_GRP_ENTERPRISE);
+        }
     }
     else
     {
@@ -549,6 +556,13 @@ void CPooledHttpThread::threadmain()
         }
         m_socket.set(secure_sock);
         httpserver.setown(new CEspHttpServer(*m_socket, m_apport, false, getMaxRequestEntityLength()));
+        IEspContext* ctx = httpserver->queryContext();
+        if(ctx)
+        {
+            StringBuffer version;
+            secure_sock->get_ssl_version(version);
+            ctx->addTraceSummaryValue(LogMin, "custom_fields.sslProtocol", version.str(), TXSUMMARY_GRP_ENTERPRISE);
+        }
     }
     else
     {

+ 30 - 1
esp/bindings/http/platform/httpservice.cpp

@@ -184,6 +184,8 @@ void checkSetCORSAllowOrigin(CHttpRequest *req, CHttpResponse *resp)
 
 int CEspHttpServer::processRequest()
 {
+    IEspContext* ctx = m_request->queryContext();
+    StringBuffer errMessage;
     m_request->setPersistentEnabled(m_apport->queryProtocol()->persistentEnabled() && !shouldClose);
     m_response->setPersistentEnabled(m_apport->queryProtocol()->persistentEnabled() && !shouldClose);
     m_response->enableCompression();
@@ -195,18 +197,21 @@ int CEspHttpServer::processRequest()
     catch(IEspHttpException* e)
     {
         m_response->sendException(e);
+        ctx->addTraceSummaryValue(LogMin, "msg", e->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE);
         e->Release();
         return 0;
     }
     catch (IException *e)
     {
         DBGLOG(e);
+        ctx->addTraceSummaryValue(LogMin, "msg", e->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE);
         e->Release();
         return 0;
     }
     catch (...)
     {
         IERRLOG("Unknown Exception - reading request [CEspHttpServer::processRequest()]");
+        ctx->addTraceSummaryValue(LogMin, "msg", "Unknown Exception - reading request [CEspHttpServer::processRequest()]", TXSUMMARY_GRP_ENTERPRISE);
         return 0;
     }
 
@@ -230,10 +235,27 @@ int CEspHttpServer::processRequest()
         ESPLOG(LogNormal,"sub service type: %s. parm: %s", getSubServiceDesc(stype), m_request->queryParamStr());
 
         m_request->updateContext();
-        IEspContext* ctx = m_request->queryContext();
         ctx->setServiceName(serviceName.str());
         ctx->setHTTPMethod(method.str());
         ctx->setServiceMethod(methodName.str());
+        ctx->addTraceSummaryValue(LogMin, "app.protocol", method.str(), TXSUMMARY_GRP_ENTERPRISE);
+        ctx->addTraceSummaryValue(LogMin, "app.service", serviceName.str(), TXSUMMARY_GRP_ENTERPRISE);
+        StringBuffer contentType;
+        m_request->getContentType(contentType);
+        ctx->addTraceSummaryValue(LogMin, "custom_fields.conttype", contentType.str(), TXSUMMARY_GRP_ENTERPRISE);
+        StringBuffer userAgent;
+        m_request->getHeader("User-Agent", userAgent);
+        ctx->addTraceSummaryValue(LogMin, "custom_fields.agent", userAgent.str(), TXSUMMARY_GRP_ENTERPRISE);
+        StringBuffer url;
+        if(m_request->queryPath())
+        {
+            url.append(m_request->queryPath());
+            if(m_request->queryParamStr())
+                url.appendf("?%s", m_request->queryParamStr());
+        }
+        ctx->addTraceSummaryValue(LogMin, "custom_fields.URL", url.str(), TXSUMMARY_GRP_ENTERPRISE);
+
+        m_response->setHeader(HTTP_HEADER_HPCC_GLOBAL_ID, ctx->getGlobalId());
 
         if(strieq(method.str(), OPTIONS_METHOD))
             return onOptions();
@@ -397,12 +419,18 @@ int CEspHttpServer::processRequest()
     catch(IEspHttpException* e)
     {
         m_response->sendException(e);
+        ctx->addTraceSummaryValue(LogMin, "msg", e->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE);
+        VStringBuffer fault("F%d", e->errorCode());
+        ctx->addTraceSummaryValue(LogMin, "custom_fields.soapFaultCode", fault.str(), TXSUMMARY_GRP_ENTERPRISE);
         e->Release();
         return 0;
     }
     catch (IException *e)
     {
         DBGLOG(e);
+        ctx->addTraceSummaryValue(LogMin, "msg", e->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE);
+        VStringBuffer fault("F%d", e->errorCode());
+        ctx->addTraceSummaryValue(LogMin, "custom_fields.soapFaultCode", fault.str(), TXSUMMARY_GRP_ENTERPRISE);
         e->Release();
         return 0;
     }
@@ -414,6 +442,7 @@ int CEspHttpServer::processRequest()
         UWARNLOG("METHOD: %s, PATH: %s, TYPE: %s, CONTENT-LENGTH: %" I64F "d", m_request->queryMethod(), m_request->queryPath(), m_request->getContentType(content_type).str(), len);
         if (len > 0)
             m_request->logMessage(LOGCONTENT, "HTTP request content received:\n");
+        ctx->addTraceSummaryValue(LogMin, "msg", "Unknown exception caught in CEspHttpServer::processRequest", TXSUMMARY_GRP_ENTERPRISE);
         return 0;
     }
 

+ 1 - 0
esp/bindings/http/platform/httpservice.hpp

@@ -167,6 +167,7 @@ public:
         m_request->setSocketReturner(returner);
         m_response->setSocketReturner(returner);
     }
+    IEspContext* queryContext() {return m_request->queryContext();}
 };
 
 

+ 12 - 3
esp/bindings/http/platform/httptransport.cpp

@@ -601,7 +601,7 @@ int CHttpMessage::receive(bool alwaysReadContent, IMultiException *me)
 {
     //Set the auth to AUTH_STATUS_NA as default. Later on, it may be changed to
     //other value (AUTH_STATUS_OK, AUTH_STATUS_FAIL, etc) when applicable.
-    m_context->addTraceSummaryValue(LogMin, "auth", AUTH_STATUS_NA);
+    m_context->addTraceSummaryValue(LogMin, "auth", AUTH_STATUS_NA, TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
     if (processHeaders(me)==-1)
         return -1;
 
@@ -618,7 +618,7 @@ int CHttpMessage::receive(bool alwaysReadContent, IMultiException *me)
     if (isUpload())
         return 0;
 
-    m_context->addTraceSummaryValue(LogMin, "contLen", m_content_length);
+    m_context->addTraceSummaryValue(LogMin, "contLen", m_content_length, TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
     if(m_content_length > 0)
     {
         readContent();
@@ -632,6 +632,8 @@ int CHttpMessage::receive(bool alwaysReadContent, IMultiException *me)
         if (getEspLogLevel()>LogNormal)
             DBGLOG("length of content read = %d", m_content.length());
     }
+    if(m_content_length != m_content.length())
+        m_context->addTraceSummaryValue(LogMin, "custom_fields.contRead", m_content.length(), TXSUMMARY_GRP_ENTERPRISE);
 
     bool decompressed = false;
     int compressType = 0;
@@ -1801,7 +1803,7 @@ int CHttpRequest::receive(IMultiException *me)
         }
     }
 
-    m_context->addTraceSummaryTimeStamp(LogMin, "rcv");
+    m_context->addTraceSummaryTimeStamp(LogMin, "rcv", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
     return 0;
 }
 
@@ -1872,6 +1874,13 @@ void CHttpRequest::updateContext()
         m_context->setUseragent(useragent.str());
         getHeader("Accept-Language", acceptLanguage);
         m_context->setAcceptLanguage(acceptLanguage.str());
+        StringBuffer callerId, globalId;
+        getHeader(HTTP_HEADER_HPCC_GLOBAL_ID, globalId);
+        if(globalId.length())
+            m_context->setGlobalId(globalId);
+        getHeader(HTTP_HEADER_HPCC_CALLER_ID, callerId);
+        if(callerId.length())
+            m_context->setCallerId(callerId);
     }
 }
 

+ 2 - 0
esp/bindings/http/platform/httptransport.ipp

@@ -53,6 +53,8 @@ enum MessageLogFlag
 #define HTTP_HEADER_CONTENT_ENCODING  "Content-Encoding"
 #define HTTP_HEADER_TRANSFER_ENCODING "Transfer-Encoding"
 #define HTTP_HEADER_ACCEPT_ENCODING   "Accept-Encoding"
+#define HTTP_HEADER_HPCC_GLOBAL_ID    "Global-Id"
+#define HTTP_HEADER_HPCC_CALLER_ID    "Caller-Id"
 
 class esp_http_decl CHttpMessage : implements IHttpMessage, public CInterface
 {

+ 1 - 1
esp/esdllib/esdl_transformer2.cpp

@@ -1718,7 +1718,7 @@ int Esdl2Transformer::process(IEspContext &ctx, EsdlProcessMode mode, const char
         tctx.skip_root = false;
         tctx.root_type.set(root->queryName());
 
-        root->process(tctx, &in, root_type);
+        root->process(tctx, &in, root_type, nullptr, true);
         rc = tctx.counter;
     }
     catch (...)

+ 3 - 0
esp/platform/cumulativetimer.hpp

@@ -47,6 +47,8 @@ public:
 
     inline void setLogLevel(const LogLevel _logLevel) { logLevel = _logLevel; };
     inline LogLevel getLogLevel() const { return logLevel; }
+    inline void setGroup(const unsigned int _group) { group = _group; }
+    inline unsigned int getGroup() const { return group; }
     inline unsigned __int64 getTotalMillis() const { return mTotalTime; }
     inline void reset() { mTotalTime = 0; }
 
@@ -101,6 +103,7 @@ private:
     unsigned __int64 mNestingDepth;
     unsigned __int64 mTotalTime;
     LogLevel logLevel = 0;
+    unsigned int group = TXSUMMARY_GRP_CORE;
 
 private:
     CumulativeTimer& operator =(const CumulativeTimer&) = delete;

+ 2 - 0
esp/platform/espcfg.cpp

@@ -297,6 +297,8 @@ CEspConfig::CEspConfig(IProperties* inputs, IPropertyTree* envpt, IPropertyTree*
     m_options.logReq = readLogRequest(m_cfg->queryProp("@logRequests"));
     m_options.logResp = m_cfg->getPropBool("@logResponses", false);
     m_options.txSummaryLevel = m_cfg->getPropInt("@txSummaryLevel", LogMin);
+    m_options.txSummaryStyle = readTxSummaryStyle(m_cfg->queryProp("@txSummaryStyle"));
+    m_options.txSummaryGroup = readTxSummaryGroup(m_cfg->queryProp("@txSummaryGroup"));
     m_options.txSummaryResourceReq = m_cfg->getPropBool("@txSummaryResourceReq", false);
     m_options.frameTitle.set(m_cfg->queryProp("@name"));
     m_options.slowProcessingTime = m_cfg->getPropInt("@slowProcessingTime", 30) * 1000; //in msec

+ 3 - 1
esp/platform/espcfg.ipp

@@ -94,11 +94,13 @@ struct esp_option
     LogRequest logReq;
     bool logResp;
     LogLevel txSummaryLevel;
+    unsigned int txSummaryStyle;
+    unsigned int txSummaryGroup;
     bool txSummaryResourceReq;
     StringAttr frameTitle;
     unsigned slowProcessingTime; //default 30 seconds
 
-    esp_option() : logReq(LogRequestsNever), logResp(false), logLevel(LogMin), txSummaryLevel(LogMin), txSummaryResourceReq(false), slowProcessingTime(30000)
+    esp_option() : logReq(LogRequestsNever), logResp(false), logLevel(LogMin), txSummaryLevel(LogMin), txSummaryStyle(TXSUMMARY_OUT_TEXT), txSummaryGroup(TXSUMMARY_GRP_CORE), txSummaryResourceReq(false), slowProcessingTime(30000)
     { }
 };
 

+ 106 - 17
esp/platform/espcontext.cpp

@@ -71,6 +71,7 @@ private:
     BoolHash  m_optGroups;
 
     Owned<CTxSummary> m_txSummary;
+
     unsigned    m_active;
     unsigned    m_creationTime;
     unsigned    m_processingTime;
@@ -88,6 +89,9 @@ private:
     Owned<IEspSecureContext> m_secureContext;
 
     StringAttr   m_transactionID;
+    StringBuffer   m_globalId;
+    StringBuffer   m_localId;
+    StringBuffer   m_callerId;
     IHttpMessage* m_request;
 
 public:
@@ -112,13 +116,19 @@ public:
         updateTraceSummaryHeader();
         m_secureContext.setown(secureContext);
         m_SecurityHandler.setSecureContext(secureContext);
+        appendGloballyUniqueId(m_localId);
+        // use localId as globalId unless we receive another
+        m_globalId.set(m_localId);
     }
 
     ~CEspContext()
     {
         flushTraceSummary();
         if (m_txSummary)
-            m_txSummary->log(getTxSummaryLevel());
+        {
+            m_txSummary->tailor(this);
+            m_txSummary->log(getTxSummaryLevel(), getTxSummaryGroup(), getTxSummaryStyle());
+        }
     }
     virtual void addOptions(unsigned opts){options|=opts;}
     virtual void removeOptions(unsigned opts){opts&=~opts;}
@@ -503,40 +513,48 @@ public:
         return m_txSummary.get();
     }
 
-    virtual void addTraceSummaryValue(LogLevel logLevel, const char *name, const char *value)
+    virtual void addTraceSummaryValue(LogLevel logLevel, const char *name, const char *value, const unsigned int group = TXSUMMARY_GRP_CORE)
     {
         if (m_txSummary && !isEmptyString(name))
-            m_txSummary->append(name, value, logLevel);
+            m_txSummary->append(name, value, logLevel, group);
     }
 
-    virtual void addTraceSummaryValue(LogLevel logLevel, const char *name, __int64 value)
+    virtual void addTraceSummaryValue(LogLevel logLevel, const char *name, __int64 value, const unsigned int group = TXSUMMARY_GRP_CORE)
     {
         if (m_txSummary && !isEmptyString(name))
-            m_txSummary->append(name, value, logLevel);
+            m_txSummary->append(name, value, logLevel, group);
     }
 
-    virtual void addTraceSummaryTimeStamp(LogLevel logLevel, const char *name)
+    virtual void addTraceSummaryDoubleValue(LogLevel logLevel, const char *name, double value, const unsigned int group = TXSUMMARY_GRP_CORE)
     {
         if (m_txSummary && !isEmptyString(name))
-            m_txSummary->append(name, m_txSummary->getElapsedTime(), logLevel, "ms");
+            m_txSummary->append(name, value, logLevel, group);
+    }
+
+    virtual void addTraceSummaryTimeStamp(LogLevel logLevel, const char *name, const unsigned int group = TXSUMMARY_GRP_CORE)
+    {
+        if (m_txSummary && !isEmptyString(name))
+            m_txSummary->append(name, m_txSummary->getElapsedTime(), logLevel, group, "ms");
     }
     virtual void flushTraceSummary()
     {
         updateTraceSummaryHeader();
         if (m_txSummary)
         {
-            m_txSummary->set("auth", authStatus.get(), LogMin);
-            m_txSummary->append("total", m_processingTime, LogMin, "ms");
+            m_txSummary->set("auth", authStatus.get(), LogMin, TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
+            m_txSummary->append("total", m_processingTime, LogMin, TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE, "ms");
         }
     }
-    virtual void addTraceSummaryCumulativeTime(LogLevel logLevel, const char* name, unsigned __int64 time)
+
+    virtual void addTraceSummaryCumulativeTime(LogLevel logLevel, const char* name, unsigned __int64 time, const unsigned int group = TXSUMMARY_GRP_CORE)
     {
         if (m_txSummary && !isEmptyString(name))
-            m_txSummary->updateTimer(name, time, logLevel);
+            m_txSummary->updateTimer(name, time, logLevel, group);
     }
-    virtual CumulativeTimer* queryTraceSummaryCumulativeTimer(const char* name)
+
+    virtual CumulativeTimer* queryTraceSummaryCumulativeTimer(LogLevel logLevel, const char *name, const unsigned int group = TXSUMMARY_GRP_CORE)
     {
-        return (m_txSummary ? m_txSummary->queryTimer(name) : NULL);
+        return (m_txSummary ? m_txSummary->queryTimer(name, logLevel, group) : NULL);
     }
     virtual void cancelTxSummary()
     {
@@ -552,6 +570,11 @@ public:
         authStatus.set(status);
     }
 
+    virtual const char* queryAuthStatus()
+    {
+        return authStatus.str();
+    }
+
     virtual void setAuthenticationMethod(const char* method)
     {
         authenticationMethod.set(method);
@@ -601,6 +624,28 @@ public:
     {
         return m_request;
     }
+
+    virtual void setGlobalId(const char* id)
+    {
+        m_globalId.set(id);
+    }
+    virtual const char* getGlobalId()
+    {
+        return m_globalId.str();
+    }
+    virtual void setCallerId(const char* id)
+    {
+        m_callerId.set(id);
+    }
+    virtual const char* getCallerId()
+    {
+        return m_callerId.str();
+    }
+    // No setLocalId() - it should be set once only when constructed
+    virtual const char* getLocalId()
+    {
+        return m_localId.str();
+    }
 };
 
 //---------------------------------------------------------
@@ -686,7 +731,7 @@ void CEspContext::updateTraceSummaryHeader()
 {
     if (m_txSummary)
     {
-        m_txSummary->set("activeReqs", m_active, LogMin);
+        m_txSummary->set("activeReqs", m_active, LogMin, TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
         VStringBuffer user("%s%s%s", (queryUserId() ? queryUserId() : ""), (m_peer.length() ? "@" : ""), m_peer.str());
         if (!user.isEmpty())
             m_txSummary->set("user", user.str(), LogMin);
@@ -708,11 +753,11 @@ void CEspContext::updateTraceSummaryHeader()
             reqSummary.append("v").append(m_clientVer);
         }
         if (!reqSummary.isEmpty())
-            m_txSummary->set("req", reqSummary.str(), LogMin);
+            m_txSummary->set("req", reqSummary.str(), LogMin, TXSUMMARY_GRP_CORE);
         if (m_hasException)
         {
-            m_txSummary->set("excepttime", m_exceptionTime, LogMin);
-            m_txSummary->set("exceptcode", m_exceptionCode, LogMin);
+            m_txSummary->set("excepttime", m_exceptionTime, LogMin, TXSUMMARY_GRP_CORE);
+            m_txSummary->set("exceptcode", m_exceptionCode, LogMin, TXSUMMARY_GRP_CORE);
         }
     }
 }
@@ -901,6 +946,50 @@ LogLevel getTxSummaryLevel()
     return LogMin;
 }
 
+const unsigned int readTxSummaryStyle(char const* style)
+{
+    if (isEmptyString(style))
+        return TXSUMMARY_OUT_TEXT;
+
+    if (strieq(style, "text"))
+        return TXSUMMARY_OUT_TEXT;
+    if (strieq(style, "json"))
+        return TXSUMMARY_OUT_JSON;
+    if (strieq(style, "all"))
+        return TXSUMMARY_OUT_TEXT | TXSUMMARY_OUT_JSON;
+
+    return TXSUMMARY_OUT_TEXT;
+}
+
+const unsigned int readTxSummaryGroup(char const* group)
+{
+    if (isEmptyString(group))
+        return TXSUMMARY_GRP_CORE;
+
+    if (strieq(group, "core"))
+        return TXSUMMARY_GRP_CORE;
+    if (strieq(group, "enterprise"))
+        return TXSUMMARY_GRP_ENTERPRISE;
+    if (strieq(group, "all"))
+        return TXSUMMARY_GRP_CORE | TXSUMMARY_GRP_ENTERPRISE;
+
+    return TXSUMMARY_GRP_CORE;
+}
+
+const unsigned int getTxSummaryStyle()
+{
+    if (getContainer())
+        return getContainer()->getTxSummaryStyle();
+    return TXSUMMARY_OUT_TEXT;
+}
+
+const unsigned int getTxSummaryGroup()
+{
+    if (getContainer())
+        return getContainer()->getTxSummaryGroup();
+    return TXSUMMARY_GRP_CORE;
+}
+
 bool getTxSummaryResourceReq()
 {
     if (getContainer())

+ 4 - 0
esp/platform/espcontext.hpp

@@ -130,6 +130,10 @@ esp_http_decl LogLevel getEspLogLevel();
 esp_http_decl LogRequest getEspLogRequests();
 esp_http_decl bool getEspLogResponses();
 esp_http_decl LogLevel getTxSummaryLevel();
+esp_http_decl const unsigned int getTxSummaryStyle();
+esp_http_decl const unsigned int readTxSummaryStyle(const char* style);
+esp_http_decl const unsigned int getTxSummaryGroup();
+esp_http_decl const unsigned int readTxSummaryGroup(const char* group);
 esp_http_decl bool getTxSummaryResourceReq();
 esp_http_decl unsigned getSlowProcessingTime();
 

+ 8 - 0
esp/platform/espp.hpp

@@ -57,6 +57,8 @@ private:
     LogRequest m_logReq;
     bool m_logResp;
     LogLevel txSummaryLevel;
+    unsigned int txSummaryStyle;
+    unsigned int txSummaryGroup;
     bool txSummaryResourceReq;
     unsigned m_slowProcessingTime;
     StringAttr m_frameTitle;
@@ -83,6 +85,8 @@ public:
         m_logReq = config->m_options.logReq;
         m_logResp = config->m_options.logResp;
         txSummaryLevel = config->m_options.txSummaryLevel;
+        txSummaryStyle = config->m_options.txSummaryStyle;
+        txSummaryGroup = config->m_options.txSummaryGroup;
         txSummaryResourceReq = config->m_options.txSummaryResourceReq;
         m_slowProcessingTime = config->m_options.slowProcessingTime;
         m_frameTitle.set(config->m_options.frameTitle);
@@ -155,12 +159,16 @@ public:
     void setLogRequests(LogRequest logReq) { m_logReq = logReq; }
     void setLogResponses(bool logResp) { m_logResp = logResp; }
     void setTxSummaryLevel(LogLevel level) { txSummaryLevel = level; }
+    void setTxSummaryStyle(unsigned int style) { txSummaryStyle = style; }
+    void setTxSummaryGroup(unsigned int group) { txSummaryGroup = group; }
     void setTxSummaryResourceReq(bool logReq) { txSummaryResourceReq = logReq; }
 
     LogLevel getLogLevel() { return m_logLevel; }
     LogRequest getLogRequests() { return m_logReq; }
     bool getLogResponses() { return m_logResp; }
     LogLevel getTxSummaryLevel() { return txSummaryLevel; }
+    unsigned int getTxSummaryStyle() { return txSummaryStyle; }
+    unsigned int getTxSummaryGroup() { return txSummaryGroup; }
     bool getTxSummaryResourceReq() { return txSummaryResourceReq; }
     void setFrameTitle(const char* title)  { m_frameTitle.set(title); }
     const char* getFrameTitle()  { return m_frameTitle.get(); }

+ 536 - 51
esp/platform/txsummary.cpp

@@ -20,22 +20,316 @@
 #include "jutil.hpp"
 #include <algorithm>
 
-using std::find_if;
-using std::for_each;
+#define MATCH_ENTRY [&](const EntryValue& e) {return strieq(e.get()->name, pathPart);}
 
-#define VALIDATE_KEY(k) if (!(k) || !(*k)) return false
-#define MATCH_KEY       [&](const Entry& entry) { return stricmp(entry.key.str(), key) == 0; }
 
-bool operator < (const StringAttr& a, const StringAttr& b)
+inline bool validate(const char* k)
 {
-    return (stricmp(a.str(), b.str()) < 0);
+    // Empty or null keys are invalid
+    if(isEmptyString(k))
+        return false;
+    // Keys containing an empty path portion
+    // are invalid
+    if(strstr(k, ".."))
+        return false;
+    // Keys beginning or ending in the path
+    // delimiter are considered equivalent to
+    // containing an empty path portion and
+    // therefore are invalid
+    if( ('.' == k[0]) || ('.' == k[strlen(k)-1]))
+        return false;
+    return true;
 }
 
-CTxSummary::CTxSummary(unsigned creationTime)
-: m_creationTime(creationTime ? creationTime : msTick())
+CTxSummary::TxEntryBase::TxEntryBase(const char* _key, const LogLevel _logLevel, const unsigned int _group, const char* _suffix, bool _jsonQuoted)
+    : logLevel(_logLevel), group(_group), suffix(_suffix), shouldJsonQuote(_jsonQuoted)
+{
+    // parse dot-delimited key
+    // 'name' is set to the rightmost element of _key
+    // 'fullname' is the entire _key
+
+    if(isEmptyString(_key))
+    {
+        fullname.set(_key);
+        name = nullptr;
+    }
+    else
+    {
+        fullname.set(_key);
+        const char* finger = strrchr(fullname.str(), '.');
+        if(finger)
+            name = ++finger;
+        else
+            name = fullname.str();
+    }
+}
+
+bool CTxSummary::TxEntryBase::shouldSerialize(const LogLevel requestedLevel, const unsigned int requestedGroup)
+{
+    if(logLevel > requestedLevel)
+        return false;
+
+    if(!(group & requestedGroup))
+        return false;
+
+    return true;
+}
+
+StringBuffer& CTxSummary::TxEntryStr::serialize(StringBuffer& buf, const LogLevel requestedLevel, const unsigned int requestedGroup, const unsigned int requestedStyle)
+{
+    if(!shouldSerialize(requestedLevel, requestedGroup) || isEmptyString(name))
+        return buf;
+
+    if(requestedStyle & TXSUMMARY_OUT_JSON)
+    {
+        appendJSONStringValue(buf, name, value.get(), false, shouldJsonQuote);
+    }
+    else if (requestedStyle & TXSUMMARY_OUT_TEXT)
+    {
+        buf.append(fullname);
+
+        if (value.length())
+            buf.appendf("=%s;", value.str());
+        else
+            buf.append(';');
+    }
+
+    return buf;
+}
+
+bool CTxSummary::TxEntryTimer::shouldSerialize(const LogLevel requestedLevel, const unsigned int requestedGroup)
+{
+    if(value->getLogLevel() > requestedLevel)
+        return false;
+
+    if(!(value->getGroup() & requestedGroup))
+        return false;
+
+    return true;
+}
+
+StringBuffer& CTxSummary::TxEntryTimer::serialize(StringBuffer& buf, const LogLevel requestedLevel, const unsigned int requestedGroup, const unsigned int requestedStyle)
 {
+    if(!shouldSerialize(requestedLevel, requestedGroup))
+        return buf;
+
+    if(requestedStyle & TXSUMMARY_OUT_JSON)
+    {
+        appendJSONValue(buf, name, value->getTotalMillis());
+    }
+    else if (requestedStyle & TXSUMMARY_OUT_TEXT)
+    {
+        buf.append(fullname);
+        buf.appendf("=%" I64F "ums;", value->getTotalMillis());
+    }
+
+    return buf;
 }
 
+bool CTxSummary::TxEntryObject::append(CTxSummary::TxEntryBase* entry)
+{
+    // Do we already have a child entry with the name of 'entry'?
+    auto it = std::find_if(m_children.begin(), m_children.end(), [&](const EntryValue& e){return strieq(e->name, entry->name);});
+    if(it == m_children.end())
+    {
+        // Create a new entry
+        EntryValue ev;
+        ev.setown(entry);
+        m_children.push_back(ev);
+        return true;
+    }
+    else
+    {
+        // 'entry' has a duplicate name, so can't be inserted
+        return false;
+    }
+}
+
+bool CTxSummary::TxEntryObject::set(CTxSummary::TxEntryBase* entry)
+{
+    // Do we already have a child entry with the name of 'entry'?
+    auto it = std::find_if(m_children.begin(), m_children.end(), [&](const EntryValue& e){return strieq(e->name, entry->name);});
+    if(it == m_children.end())
+    {
+        // Create a new entry
+        EntryValue ev;
+        ev.setown(entry);
+        m_children.push_back(ev);
+    }
+    else
+    {
+        // Replace an existing entry
+        it->setown(entry);
+    }
+
+    return true;
+}
+
+// Return the leftmost part of path, as delimited by '.'
+// If no delimiters, then return entire path
+size_t getFirstPathPart(StringAttr& key, const char* path)
+{
+    const char* finger = path;
+    size_t partLen = 0;
+
+    if(isEmptyString(path))
+        return 0;
+
+    // ignore an empty path part
+    if('.' == finger[0])
+        finger++;
+    const char* dot = strchr(finger, '.');
+    if(dot)
+    {
+        partLen = dot - finger;
+        key.set(finger, partLen);
+    } else {
+        key.set(finger);
+        partLen = key.length();
+    }
+
+    return partLen;
+}
+
+CTxSummary::TxEntryBase* CTxSummary::TxEntryObject::queryEntry(const char* path)
+{
+    StringAttr pathPart;
+    const char* pathRemainder = path;
+    size_t pathPartLen = 0;
+
+    pathPartLen = getFirstPathPart(pathPart, path);
+    pathRemainder += pathPartLen;
+
+    if(pathPartLen > 0)
+    {
+        // Does this object contain a child with the name
+        // of the first part of the path?
+        auto it = std::find_if(m_children.begin(), m_children.end(), MATCH_ENTRY);
+
+        if(it != m_children.end())
+        {
+            // skip over the dot delimiter
+            if('.' == *pathRemainder)
+                pathRemainder++;
+
+            // If there is more to the path then traverse down.
+            // Otherwise the child we found is the non-object
+            // leaf node fully identified by the path.
+            if(pathRemainder && *pathRemainder)
+                return it->get()->queryEntry(pathRemainder);
+            else
+                return it->get();
+        } else {
+            return nullptr;
+        }
+    } else {
+        return nullptr;
+    }
+}
+
+bool CTxSummary::TxEntryObject::contains(const char* path)
+{
+    if(queryEntry(path))
+        return true;
+    else
+        return false;
+}
+
+CTxSummary::TxEntryBase* CTxSummary::TxEntryObject::ensurePath(const char* path)
+{
+    StringAttr pathPart;
+    const char* pathRemainder = path;
+    size_t pathPartLen = 0;
+
+    if(!validate(path))
+    {
+        DBGLOG("CTxSummary malformed entry name: (%s)", path);
+    }
+
+    pathPartLen = getFirstPathPart(pathPart, path);
+    pathRemainder += pathPartLen;
+
+    if(pathPartLen > 0 && pathRemainder && *pathRemainder)
+    {
+        // skip over the dot delimiter
+        if('.' == *pathRemainder)
+            pathRemainder++;
+
+        auto it = std::find_if(m_children.begin(), m_children.end(), MATCH_ENTRY);
+        if(it != m_children.end())
+        {
+            // This child found has name 'pathPart' so ensure it
+            // has the remainder of the path created
+            return it->get()->ensurePath(pathRemainder);
+        }
+        else
+        {
+            // Create a new entry, add it to the list of children
+            // and ensure it has the remainder of the path created
+            EntryValue ev;
+            ev.setown(new TxEntryObject(pathPart, this->queryLogLevel(), this->queryGroup()));
+            m_children.push_back(ev);
+            return ev->ensurePath(pathRemainder);
+        }
+    } else {
+
+        // When pathRemainder is nullptr or empty, then we've
+        // ensured the path exists up to the rightmost (leaf) part.
+        return this;
+    }
+}
+
+unsigned __int64 CTxSummary::TxEntryObject::size()
+{
+    unsigned __int64 size = 0;
+
+    for(const auto& entry : m_children)
+        size += entry->size();
+
+    return size;
+}
+
+void CTxSummary::TxEntryObject::clear()
+{
+    m_children.clear();
+}
+
+StringBuffer& CTxSummary::TxEntryObject::serialize(StringBuffer& buf, const LogLevel requestedLevel, const unsigned int requestedGroup, const unsigned int requestedStyle)
+{
+    if(!shouldSerialize(requestedLevel, requestedGroup))
+        return buf;
+
+    if(requestedStyle & TXSUMMARY_OUT_JSON)
+    {
+        appendJSONNameOrDelimit(buf, name);
+        buf.append('{');
+        for( auto it=m_children.begin(); it != m_children.end(); it++)
+        {
+            // after the first entry is serialised, delimit for the next
+            if(it!=m_children.begin())
+                appendJSONNameOrDelimit(buf, nullptr);
+            it->get()->serialize(buf, requestedLevel, requestedGroup, requestedStyle);
+        }
+        // in the event that the last child entry isn't serialized due to
+        // filtering LogLevel, group or style, remove trailing delimiter: ', '
+        const char* trailing = strrchr(buf.str(), ',');
+        if(trailing && streq(trailing, ", "))
+        {
+            buf.setCharAt(buf.length()-2, '}');
+            buf.trimRight();
+        }
+        else
+            buf.append('}');
+
+    }
+
+    return buf;
+}
+
+CTxSummary::CTxSummary(unsigned creationTime)
+: m_creationTime(creationTime ? creationTime : msTick())
+{}
+
 CTxSummary::~CTxSummary()
 {
     clear();
@@ -44,14 +338,13 @@ CTxSummary::~CTxSummary()
 unsigned __int64 CTxSummary::size() const
 {
     CriticalBlock block(m_sync);
-    return m_entries.size() + m_timers.size();
+    return m_entries.size();
 }
 
 void CTxSummary::clear()
 {
     CriticalBlock block(m_sync);
     m_entries.clear();
-    m_timers.clear();
 }
 
 unsigned CTxSummary::getElapsedTime() const
@@ -65,100 +358,292 @@ bool CTxSummary::contains(const char* key) const
     return __contains(key);
 }
 
-bool CTxSummary::append(const char* key, const char* value, const LogLevel logLevel)
+bool CTxSummary::appendSerialized(const char* key, const char* value, const LogLevel logLevel, const unsigned int group, bool jsonQuoted, const char* suffix)
 {
-    VALIDATE_KEY(key);
+    if(!validate(key))
+        return false;
 
     CriticalBlock block(m_sync);
 
     if (__contains(key))
         return false;
 
-    m_entries.push_back({key, value, logLevel});
+    EntryValue entry;
+    entry.setown(new TxEntryStr(key, value, logLevel, group, jsonQuoted, suffix));
+    m_entries.push_back(entry);
+
     return true;
 }
 
-bool CTxSummary::set(const char* key, const char* value, const LogLevel logLevel)
+bool CTxSummary::setSerialized(const char* key, const char* value, const LogLevel logLevel, const unsigned int group, bool jsonQuoted, const char* suffix)
 {
-    VALIDATE_KEY(key);
+    return setEntry(key, new TxEntryStr(key, value, logLevel, group, jsonQuoted, suffix));
+}
 
-    CriticalBlock block(m_sync);
-    Entries::iterator it = find_if(m_entries.begin(), m_entries.end(), MATCH_KEY);
+// Searches for the entry named 'key' and replaces it with the new entry 'value'
+// If not found, then it adds the new entry 'value' to the end of the list
+bool CTxSummary::setEntry(const char* key, TxEntryBase* value)
+{
+    if(!validate(key))
+         return false;
 
-    if (it != m_entries.end())
+    bool found = false;
+    EntryValue entry;
+    entry.setown(value);
+    for(auto entryItr = m_entries.begin(); entryItr != m_entries.end(); entryItr++)
     {
-        it->value.set(value);
-        it->logLevel = logLevel;
+        if(strieq(key, entryItr->get()->fullname.str()))
+        {
+            found = true;
+            auto finger = m_entries.erase(entryItr);
+            m_entries.insert(finger, entry);
+            break;
+        }
+    }
+    if(!found)
+    {
+        // Although the new key is not an exact match for any
+        // existing entry, ensure that it is not a path prefix for
+        // an existing entry. See __contains for discussion.
+        if(__contains(key))
+            return false;
+        m_entries.push_back(entry);
     }
-    else
-        m_entries.push_back({key, value, logLevel});
 
     return true;
 }
 
-CumulativeTimer* CTxSummary::queryTimer(const char* name)
+// Set a new entry named 'destKey' with it's value copied from the entry named 'sourceKey'
+// Group and LogLevel of the new entry are set as passed in.
+bool CTxSummary::setCopyValueOf(const char* destKey, const char* sourceKey, const LogLevel logLevel, const unsigned int group)
 {
-    if (!name || !*name)
-        return NULL;
+    if(!validate(destKey))
+        return false;
 
     CriticalBlock block(m_sync);
-    TimerValue& timer = m_timers[name];
 
-    if (!timer)
+    if(!__contains(sourceKey))
+        return false;
+
+    TxEntryBase* sourceEntry = queryEntry(sourceKey);
+    Owned<TxEntryBase> newEntry = sourceEntry->cloneWithNewName(destKey);
+    newEntry->setLogLevel(logLevel);
+    newEntry->setGroup(group);
+    return setEntry(destKey, newEntry.getLink());
+}
+
+CumulativeTimer* CTxSummary::queryTimer(const char* key, LogLevel level, const unsigned int group)
+{
+    if(isEmptyString(key))
+        return nullptr;
+
+    if(!validate(key))
+        throw makeStringExceptionV(-1, "CTxSummary::queryTimer Key (%s) is malformed", key);
+
+    CriticalBlock block(m_sync);
+
+    Linked<TxEntryBase> entry = queryEntry(key);
+
+    if(entry)
     {
-        timer.setown(new CumulativeTimer());
+        // Is the entry a Cumulative Timer?
+        TxEntryTimer* timer = dynamic_cast<TxEntryTimer*>(entry.get());
+        if(!timer)
+        {
+            throw makeStringExceptionV(-1, "CTxSummary::queryTimer Key (%s) already exists for a non-timer entry", key);
+        }
+        else
+        {
+            if(timer->queryLogLevel() == level && timer->queryGroup() == group)
+                return timer->value.get();
+            else
+                throw makeStringExceptionV(-1, "CTxSummary::queryTimer Search for key (%s) logLevel (%d) group (%d) found mismatched timer with logLevel (%d) group (%d)", key, level, group, timer->queryLogLevel(), timer->queryGroup());
+        }
+    } else {
+        // The CumulativeTimer does not yet exist
+        // create it and add to the list of entries
+        // For new timer only, initialize with level and group values
+        Owned<CumulativeTimer> newTimer = new CumulativeTimer();
+        newTimer->setGroup(group);
+        newTimer->setLogLevel(level);
+        EntryValue newEntry;
+        newEntry.setown(new TxEntryTimer(key, newTimer.get()));
+        m_entries.push_back(newEntry);
+        return newTimer.get();
     }
-
-    return timer;
 }
 
-bool CTxSummary::updateTimer(const char* name, unsigned long long delta, const LogLevel logLevel)
+bool CTxSummary::updateTimer(const char* name, unsigned long long delta, const LogLevel logLevel, const unsigned int group)
 {
     Owned<CumulativeTimer> timer;
 
-    timer.set(queryTimer(name));
+    CriticalBlock block(m_sync);
+
+    timer.set(queryTimer(name, logLevel, group));
 
     if (!timer)
         return false;
 
     timer->add(delta);
-    timer->setLogLevel(logLevel);
     return true;
 }
 
-void CTxSummary::serialize(StringBuffer& buffer, const LogLevel logLevel) const
+bool CTxSummary::tailor(IEspContext* ctx)
+{
+    if(m_profile)
+        return m_profile->tailorSummary(ctx);
+
+    return false;
+}
+
+void CTxSummary::setProfile(ITxSummaryProfile* profile)
+{
+    m_profile.set(profile);
+}
+
+void CTxSummary::serialize(StringBuffer& buffer, const LogLevel logLevel, const unsigned int group, const unsigned int requestedStyle) const
 {
     CriticalBlock block(m_sync);
 
-    for (const Entry& entry : m_entries)
+    if(requestedStyle & TXSUMMARY_OUT_TEXT)
     {
-        if (entry.logLevel > logLevel)
-            continue;
-
-        if (entry.value.length())
-            buffer.appendf("%s=%s;", entry.key.str(), entry.value.str());
-        else
-            buffer.appendf("%s;", entry.key.str());
+        // For text serialization, we create a new entry if it has
+        // a profile mapping it to a new name, then serialize it.
+        // Otherwise, we just serialize the existing entry.
+        for(auto entry : m_entries)
+        {
+            StringBuffer effectiveName;
+            bool mapped = false;
+            if(m_profile)
+            {
+                mapped = m_profile->getEffectiveName(effectiveName, entry->fullname.str(), group, requestedStyle);
+                if(mapped)
+                {
+                    if(__contains(effectiveName.str()))
+                    {
+                        WARNLOG("Prevented output of duplicate TxSummary entry '%s', renamed from '%s'", effectiveName.str(), entry->fullname.get());
+                        continue;
+                    }
+                    Owned<TxEntryBase> renamed = entry->cloneWithNewName(effectiveName);
+                    renamed->serialize(buffer, logLevel, group, TXSUMMARY_OUT_TEXT);
+                }
+            }
+            if(!mapped)
+                entry->serialize(buffer, logLevel, group, TXSUMMARY_OUT_TEXT);
+        }
     }
-
-    for (const std::pair<TimerKey, TimerValue>& entry : m_timers)
+    else if(requestedStyle & TXSUMMARY_OUT_JSON)
     {
-        if (entry.second->getLogLevel() <= logLevel)
-            buffer.appendf("%s=%" I64F "ums;", entry.first.str(), entry.second->getTotalMillis());
+        // Create a TxEntryObject as a kind of JSON 'DOM' with copies of the entries.
+        // The root should be output for all Log Levels and all output styles.
+        TxEntryObject root(nullptr, LogMin, TXSUMMARY_OUT_TEXT | TXSUMMARY_OUT_JSON);
+        for(auto entry : m_entries)
+        {
+            StringBuffer effectiveName(entry->fullname.str());
+            if(m_profile)
+            {
+                bool mapped = m_profile->getEffectiveName(effectiveName, entry->fullname.str(), group, requestedStyle);
+                if(mapped && root.contains(effectiveName.str()))
+                {
+                    WARNLOG("Prevented output of duplicate TxSummary entry '%s', renamed from '%s'", effectiveName.str(), entry->fullname.str());
+                    continue;
+                }
+            }
+            TxEntryBase* parent = root.ensurePath(effectiveName.str());
+            parent->append(entry->cloneWithNewName(effectiveName.str()));
+        }
+        // Serialize our purpose-built TxEntryObject
+        root.serialize(buffer, logLevel, group, TXSUMMARY_OUT_JSON);
     }
 }
 
-void CTxSummary::log(const LogLevel logLevel)
+void CTxSummary::log(const LogLevel logLevel, const unsigned int requestedGroup, const unsigned int requestedStyle)
 {
-    if (__contains("user") || __contains("req"))
+    if(__contains("user") || __contains("req"))
     {
         StringBuffer summary;
-        serialize(summary, logLevel);
-        DBGLOG("TxSummary[%s]", summary.str());
+
+        // Support simultaneous output of all styles of TxSummary
+        if(requestedStyle & TXSUMMARY_OUT_TEXT)
+        {
+            serialize(summary, logLevel, requestedGroup, TXSUMMARY_OUT_TEXT);
+            DBGLOG("TxSummary[%s]", summary.str());
+        }
+
+        if(requestedStyle & TXSUMMARY_OUT_JSON)
+        {
+            serialize(summary.clear(), logLevel, requestedGroup, TXSUMMARY_OUT_JSON);
+            DBGLOG("%s", summary.str());
+        }
     }
 }
 
 bool CTxSummary::__contains(const char* key) const
 {
-    return find_if(m_entries.begin(), m_entries.end(), MATCH_KEY) != m_entries.end();
+    // Because now a key can be a path, we can't just
+    // compare the key to the fullname of all entries.
+    // We need to ensure that adding the new key won't
+    // duplicate or disrupt any part of an already-added
+    // path.
+    //
+    // If either the complete key or the entry fullname
+    // is a prefix of the other on the boundary of a dot
+    // delimiter, then we already have an entry named 'key'
+    // For example:
+    //
+    // entries: foo.bar = 5, foo.baz = 6
+    // key  : foo = 7
+    //
+    // We say it contains foo because foo is the parent entry
+    // holding the bar and baz values. We can't append a
+    // duplicate entry foo with value 7.
+
+    for(const auto& pair : m_entries)
+    {
+        const char* k = key;
+        const char* e = pair->fullname.str();
+        if( nullptr == k || nullptr == e)
+            return false;
+
+        bool done = false;
+        while(!done)
+        {
+            if('\0' == *k)
+            {
+                done = true;
+                if('\0' == *e || '.' == *e)
+                    return true;
+            }
+            else if('\0' == *e)
+            {
+                done = true;
+                if('\0' == *k || '.' == *k)
+                    return true;
+            }
+            else if(toupper(*k) != toupper(*e))
+            {
+                done = true;
+            }
+            else
+            {
+                k++;
+                e++;
+            }
+        }
+    }
+    return false;
+}
+
+CTxSummary::TxEntryBase* CTxSummary::queryEntry(const char* key)
+{
+    if(!validate(key))
+        return nullptr;
+
+    for(auto entry : m_entries)
+    {
+        if(strieq(key, entry->fullname.str()))
+            return entry.get();
+    }
+
+    return nullptr;
 }

+ 286 - 37
esp/platform/txsummary.hpp

@@ -19,6 +19,7 @@
 #define TXSUMMARY_HPP
 
 #include "jiface.hpp"
+#include "jstring.hpp"
 #include "jmutex.hpp"
 #include "cumulativetimer.hpp"
 #include "tokenserialization.hpp"
@@ -27,7 +28,19 @@
 #include <list>
 #include <map>
 
-class CTxSummary : extends CInterface
+// Using the existing esp_cfg_decl in this package required including
+// espcfg.ipp which has additional includes that weren't found by all
+// other packagates including txsummary.hpp, triggering changes to
+// several other CMakeLists.txt files. This seemed cleaner.
+#ifdef TXSUMMARY_EXPORTS
+    #define txsummary_decl DECL_EXPORT
+#else
+    #define txsummary_decl DECL_IMPORT
+#endif
+
+interface ITxSummaryProfile;
+
+class txsummary_decl CTxSummary : extends CInterface
 {
 public:
     // Construct an instance with the given creation time. A non-zero value
@@ -50,80 +63,316 @@ public:
     virtual unsigned getElapsedTime() const;
 
     // Appends all summary entries to the given buffer.
-    virtual void serialize(StringBuffer& buffer, const LogLevel logLevel = LogMin) const;
+    //
+    // JSON serialization supports string, integer numeric, true, false,
+    // null, and object values. Arrays are not supported.
+    virtual void serialize(StringBuffer& buffer, const LogLevel logLevel = LogMin, const unsigned int group = TXSUMMARY_GRP_CORE, const unsigned int requestedStyle = TXSUMMARY_OUT_TEXT) const;
 
     // Adds the unique key and value to the end of the summary.
     // Returns true if the key value pair are added to the summary. Returns
     // false if the key is NULL, empty, or not unique within the summary.
-    virtual bool append(const char* key, const char* value, const LogLevel logLevel = LogMin);
+    //
+    // The key can be a dot-delimited JSON style path for declaring the
+    // entry's position in a hierarcy used when the summary is serialized in a
+    // style such as JSON. When output as text, the key is used verbatim as the
+    // entry name.
     template <typename TValue, typename TSuffix = const char*, class TSerializer = TokenSerializer>
-    bool append(const char* key, const TValue& value, const LogLevel logLevel = LogMin, const TSuffix& suffix = "", const TSerializer& serializer = TSerializer());
+        bool append(const char* key, const TValue& value, const LogLevel logLevel = LogMin, const unsigned int group = TXSUMMARY_GRP_CORE, const TSuffix& suffix = "", const TSerializer& serializer = TSerializer());
 
-    // Updates the value associated with an existing key, or appends the key
-    // and value to the summary if it is not already found. Returns false if
-    // the key is NULL or empty. Returns true otherwise.
-    virtual bool set(const char* key, const char* value, const LogLevel logLevel = LogMin);
+    // Updates the value, logLevel, group and suffix associated with an existing
+    // key, or appends the keyand value to the summary if it is not already
+    // found. Returns false if the key is NULL or empty. Returns true otherwise.
     template <typename TValue, typename TSuffix = const char*, class TSerializer = TokenSerializer>
-    bool set(const char* key, const TValue& value, const LogLevel logLevel = LogMin, const TSuffix& suffix = "", const TSerializer& serializer = TSerializer());
+        bool set(const char* key, const TValue& value, const LogLevel logLevel = LogMin, const unsigned int group = TXSUMMARY_GRP_CORE, const TSuffix& suffix = "", const TSerializer& serializer = TSerializer());
+
+    // Similar to the above set functions, but pulls the value from an existing entry
+    // named 'sourceKey'
+    virtual bool setCopyValueOf(const char* destKey, const char* sourceKey, const LogLevel logLevel = LogMin, const unsigned int group = TXSUMMARY_GRP_CORE);
 
-    void log(const LogLevel logLevel);
+    void log(const LogLevel logLevel, const unsigned int group = TXSUMMARY_GRP_CORE, const unsigned int requestedStyle = TXSUMMARY_OUT_TEXT);
 
     // Fetches an existing or new instance of a named CumulativeTime. The name
-    // must not be NULL or empty, but it may be a duplicate of an existing
-    // key-value pair. Duplication, while permitted, should be avoided due to
-    // the potential for confusion.
-    virtual CumulativeTimer* queryTimer(const char* name);
+    // must not be NULL or empty, duplication is not permitted.
+    // The supplied level and group must match those set on the found entry or
+    // it is not considered a match, and an exception is thrown.
+    virtual CumulativeTimer* queryTimer(const char* key, LogLevel level, const unsigned int group);
 
     // Adds the given milliseconds to an existing or new named CumulativeTimer.
     // The same conditions as for getTimer apply.
-    virtual bool updateTimer(const char* name, unsigned long long delta, const LogLevel logLevel = LogMin);
+    // The supplied level and group must match those set on the found entry or
+    // it is not considered a match, and an exception is thrown.
+    virtual bool updateTimer(const char* name, unsigned long long delta, const LogLevel logLevel, const unsigned int group);
+
+    // Call back into the ITxSummaryProfile, if set, to customize the contents
+    // of the summary prior to serialization.
+    virtual bool tailor(IEspContext* ctx);
+
+    // Take an ITxSummaryProfile instance that can rename entries and customize
+    // the contents of the summary prior to serialization.
+    virtual void setProfile(ITxSummaryProfile* profile);
 
 protected:
     // Log the summary contents on destruction.
     ~CTxSummary();
 
 private:
+    class TxEntryBase;
+    struct TxEntryStr;
+
+    virtual bool appendSerialized(const char* key, const char* value, const LogLevel logLevel, const unsigned int group, bool jsonQuoted, const char* suffix);
+    virtual bool setSerialized(const char* key, const char* value, const LogLevel logLevel, const unsigned int group, bool jsonQuoted, const char* suffix);
+    virtual bool setEntry(const char* key, TxEntryBase* value);
+
+    // Returns true if the summary contains an entry matching key
     bool __contains(const char* key) const;
 
-    struct Entry
+    // Returns the entry matching key if found, or nullptr otherwise
+    TxEntryBase* queryEntry(const char* key);
+
+    // Each entry is a subclass of TxEntryBase that allows us
+    // to keep type information along with the value to use for
+    // serialization. Each Entry has these primary attributes:
+    //
+    //  name
+    //      The name when serialized. It may be in the form
+    //      of a simplified dot-delimited JSON-style path.
+    //      That form is used to give structure when
+    //      serializing to JSON. The implementation tracks
+    //      the name in two ways- as 'name' and 'fullname'
+    //      to aid with serialization.
+    //
+    //  logLevel
+    //      The minimum logLevel that must be requested during
+    //      serialization in order for this element to appear
+    //      in the output.
+    //
+    //  group
+    //      A bit flag indicating which groups this element
+    //      belongs to. Serialization can specify which groups
+    //      are included in output
+    //
+    //  value
+    //      Subclasses of TxEntryBase include a value member of
+    //      a type appropriate for the entry.
+    //
+    class TxEntryBase : public CInterface
+    {
+        public:
+            // The complete name of the entry, may be a dot-delimited
+            // path.
+            StringAttr fullname;
+
+            // The rightmost path part of fullname. During serialization
+            // to JSON an entry's fullname is tokenized and TxEntryObjects
+            // are created- one from each token of the path, with its
+            // name equal to the path token.
+            const char* name;
+
+            // Some Entry subclasses can assign a suffix to serialize
+            // after its value
+            StringAttr suffix = nullptr;
+
+            bool shouldJsonQuote = false;
+
+            TxEntryBase(const char* _key, const LogLevel _logLevel, const unsigned int _group, const char* _suffix, bool jsonQuoted);
+            virtual bool contains(const char* path) {return false;}
+            virtual TxEntryBase* queryEntry(const char* path) {return nullptr;}
+            virtual TxEntryBase* ensurePath(const char* path) {return nullptr;}
+            virtual bool append(TxEntryBase* entry) {return false;}
+            virtual bool set(TxEntryBase* entry) {return false;}
+            virtual unsigned __int64 size() {return 1;}
+            virtual void clear() {};
+            virtual bool shouldSerialize(const LogLevel requestedLevel, const unsigned int requestedGroup);
+            virtual StringBuffer& serialize(StringBuffer& buf, const LogLevel requestedLevel, const unsigned int requestedGroup, const unsigned int requestedStyle) = 0;
+            virtual TxEntryBase* cloneWithNewName(const char* newName = nullptr) = 0;
+            virtual void setLogLevel(const LogLevel level) {logLevel = level;}
+            virtual LogLevel queryLogLevel() {return logLevel;}
+            virtual void setGroup(const unsigned int grp) {group = grp;}
+            virtual unsigned int queryGroup() {return group;}
+
+        private:
+            // The minimum logLevel at which this entry is serialized
+            LogLevel logLevel = LogMax;
+
+            // The bit field indicating with groups this entry belongs to
+            unsigned int group = TXSUMMARY_GRP_CORE;
+    };
+
+    struct TxEntryStr : public TxEntryBase
     {
-        StringAttr key;
         StringAttr value;
-        LogLevel logLevel = LogMax;
 
-        Entry(const char* _key, const char* _value, const LogLevel _logLevel) : key(_key), value(_value), logLevel(_logLevel) {}
+        TxEntryStr(const char* _key, const char* _value, const LogLevel _logLevel, const unsigned int _group, bool _jsonQuoted, const char* _suffix)
+            : TxEntryBase(_key, _logLevel, _group, _suffix, _jsonQuoted), value(_value) {}
+
+        virtual TxEntryBase* cloneWithNewName(const char* newName = nullptr) override
+        {
+            return new TxEntryStr(newName ? newName : fullname.get(), value.get(), queryLogLevel(), queryGroup(), shouldJsonQuote, suffix);
+        }
+
+        virtual StringBuffer& serialize(StringBuffer& buf, const LogLevel requestedLevel, const unsigned int requestedGroup, const unsigned int requestedStyle) override;
+    };
+
+    struct TxEntryTimer : public TxEntryBase
+    {
+        // The CumulativeTimer object owned by this Entry needs to manage
+        // the unsigned int and LogLevel values. This is because external
+        // users of the TxSummary class expect to be able to set those values
+        // on the CumulativeTimer object. So the logLevel and group values
+        // in the TxEntryBase are not used for this subclass.
+
+        Owned<CumulativeTimer> value;
+
+        TxEntryTimer(const char* _key, CumulativeTimer* _value, const LogLevel _logLevel = LogMin, const unsigned int _group = TXSUMMARY_GRP_CORE)
+            : TxEntryBase(_key, _logLevel, _group, nullptr, false)
+        {
+            value.set(_value);
+        }
+
+        virtual TxEntryBase* cloneWithNewName(const char* newName = nullptr) override
+        {
+            return new TxEntryTimer(newName ? newName : fullname.str(), value.get(), value->getLogLevel(), value->getGroup());
+        }
+
+        // Uses logLevel and group settings stored in the contained CumulativeTimer object
+        virtual bool shouldSerialize(const LogLevel requestedLevel, const unsigned int requestedGroup) override;
+        virtual StringBuffer& serialize(StringBuffer& buf, const LogLevel requestedLevel, const unsigned int requestedGroup, const unsigned int requestedStyle) override;
+        virtual void setLogLevel(const LogLevel level) override {value->setLogLevel(level);}
+        virtual LogLevel queryLogLevel() override {return value->getLogLevel();}
+        virtual void setGroup(const unsigned int grp) override {value->setGroup(grp);}
+        virtual unsigned int queryGroup() override {return value->getGroup();}
     };
 
-    using Entries = std::list<Entry>;
-    using TimerKey = StringAttr;
-    using TimerValue = Linked<CumulativeTimer>;
-    using Timers = std::map<TimerKey, TimerValue>;
+    using EntryValue = Linked<TxEntryBase>;
+    using EntriesInOrder = std::list<EntryValue>;
+
+    struct TxEntryObject : public TxEntryBase
+    {
+        // Container for holding child entries
+        // Keeps an ordered list to serialize entries in order and
+
+        TxEntryObject(const char* _key, const LogLevel _logLevel = LogMin, const unsigned int _group = TXSUMMARY_GRP_CORE)
+            : TxEntryBase(_key, _logLevel, _group, nullptr, false) {}
+        // Our use does not include cloning TxEntryObjects
+        virtual TxEntryBase* cloneWithNewName(const char* newName = nullptr) override {return nullptr;}
+        virtual bool contains(const char* path) override;
+        virtual TxEntryBase* queryEntry(const char* path) override;
+        virtual TxEntryBase* ensurePath(const char* path) override;
+        virtual bool append(TxEntryBase* entry) override;
+        virtual bool set(TxEntryBase* entry) override;
+        virtual unsigned __int64 size() override;
+        virtual void clear() override;
+        virtual StringBuffer& serialize(StringBuffer& buf, const LogLevel requestedLevel, const unsigned int requestedGroup, const unsigned int requestedStyle) override;
+
+        EntriesInOrder m_children;
+    };
 
     mutable CriticalSection m_sync;
-    Entries         m_entries;
-    Timers          m_timers;
-    unsigned        m_creationTime;
+    unsigned m_creationTime;
+    EntriesInOrder m_entries;
+    Linked<ITxSummaryProfile> m_profile;
 };
 
-
-// Convenience wrapper of the default append method.
 template <typename TValue, typename TSuffix, class TSerializer>
-inline bool CTxSummary::append(const char* key, const TValue& value, const LogLevel logLevel, const TSuffix& suffix, const TSerializer& serializer)
+inline bool CTxSummary::append(const char* key, const TValue& value, const LogLevel logLevel, const unsigned int group, const TSuffix& suffix,  const TSerializer& serializer)
 {
-    StringBuffer buffer;
+    StringBuffer buffer, suffixBuf;
     serializer.serialize(value, buffer);
-    serializer.serialize(suffix, buffer);
-    return append(key, serializer.str(buffer), logLevel);
+    serializer.serialize(suffix, suffixBuf);
+    bool shouldQuote = true;
+    if(std::is_arithmetic<TValue>())
+        shouldQuote = false;
+    return appendSerialized(key, serializer.str(buffer), logLevel, group, shouldQuote, serializer.str(suffixBuf));
 }
 
-// Convenience wrapper of the default set method.
 template <typename TValue, typename TSuffix, class TSerializer>
-inline bool CTxSummary::set(const char* key, const TValue& value, const LogLevel logLevel, const TSuffix& suffix, const TSerializer& serializer)
+inline bool CTxSummary::set(const char* key, const TValue& value, const LogLevel logLevel, const unsigned int group, const TSuffix& suffix, const TSerializer& serializer)
 {
-    StringBuffer buffer;
+    StringBuffer buffer, suffixBuf;
     serializer.serialize(value, buffer);
-    serializer.serialize(suffix, buffer);
-    return set(key, serializer.str(buffer), logLevel);
+    serializer.serialize(suffix, suffixBuf);
+    bool shouldQuote = true;
+    if(std::is_arithmetic<TValue>())
+        shouldQuote = false;
+    return setSerialized(key, serializer.str(buffer), logLevel, group, shouldQuote, serializer.str(suffixBuf));
 }
 
+class txsummary_decl TxSummaryMapVal
+{
+    public:
+        TxSummaryMapVal(unsigned int _group, unsigned int _style, const char* _newName, bool _replaceOriginal)
+            : group(_group), style(_style), newName(_newName), replaceOriginal(_replaceOriginal) {}
+
+        unsigned int group;
+        unsigned int style;
+        StringAttr newName;
+        bool replaceOriginal;
+};
+
+interface ITxSummaryProfile : extends IInterface
+{
+    virtual bool tailorSummary(IEspContext* ctx) = 0;
+    virtual void addMap(const char* name, TxSummaryMapVal mapval) = 0;
+    virtual bool getEffectiveName(StringBuffer& effectiveName, const char* name, const unsigned int outputGroup, const unsigned int outputStyle) = 0;
+    virtual const char* queryEffectiveName(const char* name, const unsigned int outputGroup, const unsigned int outputStyle) = 0;
+};
+
+class txsummary_decl CTxSummaryProfileBase : public CInterface, implements ITxSummaryProfile
+{
+    public:
+
+        IMPLEMENT_IINTERFACE;
+
+        virtual void addMap(const char* name, TxSummaryMapVal mapval) override
+        {
+            mapping.insert({name, mapval});
+        }
+
+        // Return true if a mapping was found. Set effectiveName parameter to
+        // the effective name- the name to use in serialzed output.
+        // effectiveName is set to the new name in the first matching mapping
+        // found, otherwise the passed in
+        virtual bool getEffectiveName(StringBuffer& effectiveName, const char* name, const unsigned int outputGroup, const unsigned int outputStyle) override
+        {
+            auto nameMatches = mapping.equal_range(name);
+            for(auto it = nameMatches.first; it != nameMatches.second; it++)
+            {
+                unsigned int mappingGroup = it->second.group;
+                unsigned int mappingStyle = it->second.style;
+                if((mappingGroup & outputGroup) && (mappingStyle & outputStyle))
+                {
+                    effectiveName.set(it->second.newName);
+                    return true;
+                }
+            }
+            // we didn't find a match above
+            effectiveName.set(name);
+            return false;
+        }
+
+        // Return the the effective name- the name to use in serialzed output.
+        // returns the new name in the first matching mapping found, otherwise
+        // returns the passed in 'name'.
+        virtual const char* queryEffectiveName(const char* name, const unsigned int outputGroup, const unsigned int outputStyle) override
+        {
+            auto nameMatches = mapping.equal_range(name);
+            for(auto it = nameMatches.first; it != nameMatches.second; it++)
+            {
+                unsigned int mappingGroup = it->second.group;
+                unsigned int mappingStyle = it->second.style;
+                if((mappingGroup & outputGroup) && (mappingStyle & outputStyle))
+                {
+                    return it->second.newName.get();
+                }
+            }
+            // we didn't find a match above
+            return name;
+        }
+
+    private:
+        using NameToMapVal = std::multimap<std::string, TxSummaryMapVal>;
+        NameToMapVal mapping;
+};
+
+
 #endif // TXSUMMARY_HPP

+ 1 - 1
esp/protocols/http/CMakeLists.txt

@@ -70,7 +70,7 @@ include_directories(
     ${HPCC_SOURCE_DIR}/common/thorhelper
     )
 
-add_definitions(-DESPHTTP_EXPORTS -DESP_TIMING -D_USRDLL -DESP_PLUGIN)
+add_definitions(-DESPHTTP_EXPORTS -DESP_TIMING -D_USRDLL -DESP_PLUGIN -DTXSUMMARY_EXPORTS)
 
 HPCC_ADD_LIBRARY(esphttp SHARED ${SRCS})
 add_dependencies(esphttp  espscm)

+ 25 - 5
esp/scm/esp.ecm

@@ -85,6 +85,13 @@ typedef enum LogRequest_
     LogRequestsAlways
 } LogRequest;
 
+
+#define TXSUMMARY_GRP_CORE        0x00000001
+#define TXSUMMARY_GRP_ENTERPRISE  0x00000002
+
+#define TXSUMMARY_OUT_TEXT      0x00000001
+#define TXSUMMARY_OUT_JSON      0x00000002
+
 #define ESPCTX_NO_NAMESPACES    0x00000001
 #define ESPCTX_WSDL             0x00000010
 #define ESPCTX_WSDL_EXT         0x00000100
@@ -186,13 +193,15 @@ interface IEspContext : extends IInterface
     virtual void addCustomerHeader(const char* name, const char* val) = 0;
 
     virtual CTxSummary* queryTxSummary()=0;
-    virtual void addTraceSummaryValue(unsigned logLevel, const char *name, const char *value)=0;
-    virtual void addTraceSummaryValue(unsigned logLevel, const char *name, __int64 value)=0;
-    virtual void addTraceSummaryTimeStamp(unsigned logLevel, const char *name)=0;
-    virtual void addTraceSummaryCumulativeTime(unsigned logLevel, const char* name, unsigned __int64 time)=0;
-    virtual CumulativeTimer* queryTraceSummaryCumulativeTimer(const char* name)=0;
+    virtual void addTraceSummaryValue(unsigned logLevel, const char *name, const char *value, const unsigned int group = TXSUMMARY_GRP_CORE)=0;
+    virtual void addTraceSummaryValue(unsigned logLevel, const char *name, __int64 value, const unsigned int group = TXSUMMARY_GRP_CORE)=0;
+    virtual void addTraceSummaryDoubleValue(unsigned logLevel, const char *name, double value, const unsigned int group = TXSUMMARY_GRP_CORE)=0;
+    virtual void addTraceSummaryTimeStamp(unsigned logLevel, const char *name, const unsigned int group = TXSUMMARY_GRP_CORE)=0;
+    virtual void addTraceSummaryCumulativeTime(unsigned logLevel, const char* name, unsigned __int64 time, const unsigned int group = TXSUMMARY_GRP_CORE)=0;
+    virtual CumulativeTimer* queryTraceSummaryCumulativeTimer(unsigned logLevel, const char *name, const unsigned int group = TXSUMMARY_GRP_CORE)=0;
     virtual void cancelTxSummary()=0;
 
+
     virtual ESPSerializationFormat getResponseFormat()=0;
     virtual void setResponseFormat(ESPSerializationFormat fmt)=0;
 
@@ -216,10 +225,17 @@ interface IEspContext : extends IInterface
     virtual void setAuthError(AuthError error)=0;
     virtual AuthError getAuthError()=0;
     virtual void setAuthStatus(const char * status)=0;
+    virtual const char* queryAuthStatus()=0;
     virtual const char * getRespMsg()=0;
     virtual void setRespMsg(const char * msg)=0;
     virtual void setRequest(IHttpMessage* req) = 0;
     virtual IHttpMessage* queryRequest() = 0;
+
+    virtual void setGlobalId(const char* id)=0;
+    virtual const char* getGlobalId()=0;
+    virtual void setCallerId(const char* id)=0;
+    virtual const char* getCallerId()=0;
+    virtual const char* getLocalId()=0;
 };
 
 
@@ -240,6 +256,10 @@ interface IEspContainer : extends IInterface
     virtual void setLogResponses(bool logResp) = 0;
     virtual void setTxSummaryLevel(LogLevel level) = 0;
     virtual LogLevel getTxSummaryLevel() = 0;
+    virtual unsigned int getTxSummaryStyle() = 0;
+    virtual void setTxSummaryStyle(unsigned int style) = 0;
+    virtual unsigned int getTxSummaryGroup() = 0;
+    virtual void setTxSummaryGroup(unsigned int group) = 0;
     virtual bool getTxSummaryResourceReq() = 0;
     virtual void setTxSummaryResourceReq(bool req) = 0;
     virtual void log(LogLevel level, const char*,...) __attribute__((format(printf, 3, 4))) = 0;

+ 1 - 0
esp/services/esdl_svc_engine/CMakeLists.txt

@@ -30,6 +30,7 @@ set (   SRCS
         esdl_svc_engine.cpp
         esdl_store.cpp
         esdl_monitor.cpp
+        esdl_summary_profile.cpp
     )
 
 include_directories (

+ 62 - 23
esp/services/esdl_svc_engine/esdl_binding.cpp

@@ -42,6 +42,7 @@
 
 #include "loggingagentbase.hpp"
 #include "httpclient.ipp"
+#include "esdl_summary_profile.hpp"
 
 class EsdlSvcReporter : public EsdlDefReporter
 {
@@ -251,6 +252,12 @@ void EsdlServiceImpl::init(const IPropertyTree *cfg,
 
         xpath.setf("EspBinding[@service=\"%s\"]", service); //get this service's binding cfg
         m_oEspBindingCfg.set(espcfg->queryPropTree(xpath.str()));
+
+        xpath.setf("CustomBindingParameters/CustomBindingParameter[@key=\"UseDefaultEnterpriseTxSummaryProfile\"]/@value");
+        bool useDefaultSummaryProfile = m_oEspBindingCfg->getPropBool(xpath.str());
+        // Possible future update is to read profile settings from configuration
+        if(useDefaultSummaryProfile)
+            m_txSummaryProfile.setown(new CTxSummaryProfileEsdl);
     }
     else
         throw MakeStringException(-1, "Could not access ESDL service configuration: esp process '%s' service name '%s'", process, service);
@@ -820,7 +827,7 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
                                            unsigned int flags)
 {
     const char *mthName = mthdef.queryName();
-    context.addTraceSummaryValue(LogMin, "method", mthName);
+    context.addTraceSummaryValue(LogMin, "method", mthName, TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
     const char* srvName = srvdef.queryName();
 
     if (m_serviceScriptError.length()) //checked further along in shared code, but might as well avoid extra overhead
@@ -886,6 +893,12 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
     else
         ESPLOG(LogMin,"DESDL: Transaction ID could not be generated!");
 
+    // In the future we could instantiate a profile from the service configuration.
+    // For now use a profile created on service initialization.
+    CTxSummary* txSummary = context.queryTxSummary();
+    if(txSummary)
+        txSummary->setProfile(m_txSummaryProfile);
+
     EsdlMethodImplType implType = EsdlMethodImplUnknown;
 
     if(stricmp(mthName, "echotest")==0 || mthdef.hasProp("EchoTest"))
@@ -966,9 +979,9 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
              }
 
              writer.setown(dynamic_cast<IXmlWriterExt *>(javactx->bindParamWriter(m_esdl, javaPackage, mthdef.queryRequestType(), "request")));
-             context.addTraceSummaryTimeStamp(LogNormal, "srt-reqproc");
+             context.addTraceSummaryTimeStamp(LogNormal, "srt-reqproc", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
              m_pEsdlTransformer->process(context, EsdlRequestMode, srvdef.queryName(), mthdef.queryName(), *req, writer, 0, NULL);
-             context.addTraceSummaryTimeStamp(LogNormal, "end-reqproc");
+             context.addTraceSummaryTimeStamp(LogNormal, "end-reqproc", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
 
              javactx->paramWriterCommit(writer);
              javactx->callFunction();
@@ -979,7 +992,7 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
 
              Owned<IXmlWriterExt> finalRespWriter = createIXmlWriterExt(0, 0, NULL, (flags & ESDL_BINDING_RESPONSE_JSON) ? WTJSONRootless : WTStandard);
              m_pEsdlTransformer->processHPCCResult(context, mthdef, origResp.str(), finalRespWriter, logdata, txResultFlags, ns, schema_location);
-
+             // TODO: Modify processHPCCResult to return record count
              out.append(finalRespWriter->str());
         }
         else if (implType==EsdlMethodImplCpp)
@@ -989,9 +1002,9 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
             //Preprocess Request
             StringBuffer reqcontent;
             unsigned xflags = (isPublishedQuery(implType)) ? ROXIEREQ_FLAGS : ESDLREQ_FLAGS;
-            context.addTraceSummaryTimeStamp(LogNormal, "srt-reqproc");
+            context.addTraceSummaryTimeStamp(LogNormal, "srt-reqproc", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
             m_pEsdlTransformer->process(context, EsdlRequestMode, srvdef.queryName(), mthdef.queryName(), *req, reqWriter.get(), xflags, NULL);
-            context.addTraceSummaryTimeStamp(LogNormal, "end-reqproc");
+            context.addTraceSummaryTimeStamp(LogNormal, "end-reqproc", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
 
             reqcontent.set(reqWriter->str());
             context.addTraceSummaryTimeStamp(LogNormal, "serialized-xmlreq");
@@ -1013,10 +1026,11 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
             xproc(ctxbuf.str(), reqcontent.str(), origResp);
             context.addTraceSummaryTimeStamp(LogNormal, "end-cppcall");
 
-            context.addTraceSummaryTimeStamp(LogNormal, "srt-procres");
+            context.addTraceSummaryTimeStamp(LogNormal, "srt-procres", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
             Owned<IXmlWriterExt> finalRespWriter = createIXmlWriterExt(0, 0, NULL, (flags & ESDL_BINDING_RESPONSE_JSON) ? WTJSONRootless : WTStandard);
             m_pEsdlTransformer->processHPCCResult(context, mthdef, origResp.str(), finalRespWriter, logdata, txResultFlags, ns, schema_location);
-            context.addTraceSummaryTimeStamp(LogNormal, "end-procres");
+            // TODO: Modify processHPCCResult to return record count
+            context.addTraceSummaryTimeStamp(LogNormal, "end-procres", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
 
             out.append(finalRespWriter->str());
         }
@@ -1030,9 +1044,10 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
             //Preprocess Request
             StringBuffer reqcontent;
             unsigned xflags = (isPublishedQuery(implType)) ? ROXIEREQ_FLAGS : ESDLREQ_FLAGS;
-            context.addTraceSummaryTimeStamp(LogNormal, "srt-reqproc");
-            m_pEsdlTransformer->process(context, EsdlRequestMode, srvdef.queryName(), mthdef.queryName(), *req, reqWriter.get(), xflags, NULL);
-            context.addTraceSummaryTimeStamp(LogNormal, "end-reqproc");
+            context.addTraceSummaryTimeStamp(LogNormal, "srt-reqproc", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
+            int recordCount = m_pEsdlTransformer->process(context, EsdlRequestMode, srvdef.queryName(), mthdef.queryName(), *req, reqWriter.get(), xflags, NULL);
+            context.addTraceSummaryTimeStamp(LogNormal, "end-reqproc", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
+            context.addTraceSummaryValue(LogNormal, "custom_fields.recCount", recordCount, TXSUMMARY_GRP_ENTERPRISE);
 
             if(isPublishedQuery(implType))
                 tgtctx.setown(createTargetContext(context, tgtcfg.get(), srvdef, mthdef, req));
@@ -1044,14 +1059,14 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
                 runServiceScript(context, scriptContext, srvdef, mthdef, reqcontent, origResp, txResultFlags, ns, schema_location);
             else
                 handleFinalRequest(context, scriptContext, tgtcfg, tgtctx, srvdef, mthdef, ns, reqcontent, origResp, isPublishedQuery(implType), implType==EsdlMethodImplProxy, soapmsg);
-            context.addTraceSummaryTimeStamp(LogNormal, "end-HFReq");
+            context.addTraceSummaryTimeStamp(LogNormal, "end-HFReq", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
 
             if (isPublishedQuery(implType))
             {
-                context.addTraceSummaryTimeStamp(LogNormal, "srt-procres");
+                context.addTraceSummaryTimeStamp(LogNormal, "srt-procres", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
                 Owned<IXmlWriterExt> respWriter = createIXmlWriterExt(0, 0, NULL, (flags & ESDL_BINDING_RESPONSE_JSON) ? WTJSONRootless : WTStandard);
                 m_pEsdlTransformer->processHPCCResult(context, mthdef, origResp.str(), respWriter.get(), logdata, txResultFlags, ns, schema_location);
-                context.addTraceSummaryTimeStamp(LogNormal, "end-procres");
+                context.addTraceSummaryTimeStamp(LogNormal, "end-procres", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
                 out.append(respWriter->str());
                 runPostEsdlScript(context, scriptContext, srvdef, mthdef, out, txResultFlags, ns, schema_location);
             }
@@ -1073,6 +1088,7 @@ bool EsdlServiceImpl::handleResultLogging(IEspContext &espcontext, IEsdlScriptCo
     StringBuffer temp;
     if (scriptContext)
     {
+        espcontext.addTraceSummaryTimeStamp(LogNormal, "custom_fields.srt-preLogScripts", TXSUMMARY_GRP_ENTERPRISE);
         IEsdlTransformSet *servicePLTs = m_transforms->queryMethodEntryPoint("", ESDLScriptEntryPoint_PreLogging);
         IEsdlTransformSet *methodPLTs = m_transforms->queryMethodEntryPoint(mthdef.queryName(), ESDLScriptEntryPoint_PreLogging);
 
@@ -1084,10 +1100,11 @@ bool EsdlServiceImpl::handleResultLogging(IEspContext &espcontext, IEsdlScriptCo
             scriptContext->toXML(temp, ESDLScriptCtxSection_LogData);
             logdata = temp.str();
         }
+        espcontext.addTraceSummaryTimeStamp(LogNormal, "custom_fields.end-preLogScripts", TXSUMMARY_GRP_ENTERPRISE);
     }
 
     bool success = true;
-    espcontext.addTraceSummaryTimeStamp(LogNormal, "srt-resLogging");
+    espcontext.addTraceSummaryTimeStamp(LogNormal, "srt-resLogging", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
     if (loggingManager())
     {
         Owned<IEspLogEntry> entry = loggingManager()->createLogEntry();
@@ -1105,7 +1122,7 @@ bool EsdlServiceImpl::handleResultLogging(IEspContext &espcontext, IEsdlScriptCo
         success = loggingManager()->updateLog(entry, logresp);
         ESPLOG(LogMin,"ESDLService: Attempted to log ESP transaction: %s", logresp.str());
     }
-    espcontext.addTraceSummaryTimeStamp(LogNormal, "end-resLogging");
+    espcontext.addTraceSummaryTimeStamp(LogNormal, "end-resLogging", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
     return success;
 }
 
@@ -1476,6 +1493,9 @@ void EsdlServiceImpl::sendTargetSOAP(IEspContext & context,
         httpclient->setPassword(password.str());
     }
 
+    Owned<IProperties> headers = createProperties();
+    headers->setProp(HTTP_HEADER_HPCC_GLOBAL_ID, context.getGlobalId());
+    headers->setProp(HTTP_HEADER_HPCC_CALLER_ID, context.getLocalId());
     StringBuffer status;
     StringBuffer clreq(req);
 
@@ -1483,11 +1503,13 @@ void EsdlServiceImpl::sendTargetSOAP(IEspContext & context,
     ESPLOG(LogMax,"OUTGOING Request: %s", clreq.str());
     {
         EspTimeSection timing("Calling out to query");
-        context.addTraceSummaryTimeStamp(LogMin, "startcall");
-        httpclient->sendRequest("POST", "text/xml", clreq, resp, status,true);
-        context.addTraceSummaryTimeStamp(LogMin, "endcall");
+        context.addTraceSummaryTimeStamp(LogMin, "startcall", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
+        httpclient->sendRequest(headers, "POST", "text/xml", clreq, resp, status, true);
+        context.addTraceSummaryTimeStamp(LogMin, "endcall", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
     }
 
+    context.addTraceSummaryValue(LogMin, "custom_fields.ext_resp_len", resp.length(), TXSUMMARY_GRP_ENTERPRISE);
+
     if (status.length()==0)
     {
         UERRLOG("EsdlBindingImpl::sendTargetSOAP sendRequest() status not reported, response content: %s", resp.str());
@@ -2205,7 +2227,7 @@ int EsdlBindingImpl::onGetInstantQuery(IEspContext &context,
     StringBuffer source;
     StringBuffer orderstatus;
 
-    context.addTraceSummaryTimeStamp(LogMin, "reqRecvd");
+    context.addTraceSummaryTimeStamp(LogMin, "reqRecvd", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
     Owned<IMultiException> me = MakeMultiException(source.appendf("EsdlBindingImpl::%s()", methodName).str());
 
     IEsdlDefMethod *mthdef = NULL;
@@ -2256,11 +2278,12 @@ int EsdlBindingImpl::onGetInstantQuery(IEspContext &context,
                     else
                       response->setContentType(HTTP_TYPE_TEXT_XML_UTF8);
                     response->setStatus(HTTP_STATUS_OK);
+                    context.addTraceSummaryTimeStamp(LogMin, "custom_fields.RspSndSt", TXSUMMARY_GRP_ENTERPRISE);
                     response->send();
                     returnSocket(response);
 
                     unsigned timetaken = msTick() - context.queryCreationTime();
-                    context.addTraceSummaryTimeStamp(LogMin, "respSent");
+                    context.addTraceSummaryTimeStamp(LogMin, "respSent", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
 
                     ESPLOG(LogMax,"EsdlBindingImpl:onGetInstantQuery response: %s", out.str());
 
@@ -2455,6 +2478,10 @@ int EsdlBindingImpl::HandleSoapRequest(CHttpRequest* request,
 
     try
     {
+        ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.soapRequestStart", TXSUMMARY_GRP_ENTERPRISE);
+        ctx->addTraceSummaryValue(LogMin, "app.name", m_processName.get(), TXSUMMARY_GRP_ENTERPRISE);
+        ctx->addTraceSummaryValue(LogMin, "custom_fields.esp_service_type", "desdl", TXSUMMARY_GRP_ENTERPRISE);
+
         in = request->queryContent();
         if (!in || !*in)
             throw makeWsException( ERR_ESDL_BINDING_BADREQUEST, WSERR_CLIENT,  "ESP", "SOAP content not found" );
@@ -2466,6 +2493,7 @@ int EsdlBindingImpl::HandleSoapRequest(CHttpRequest* request,
         IEsdlDefMethod *mthdef=NULL;
         if (getSoapMethodInfo(in, reqname, ns))
         {
+            ctx->addTraceSummaryValue(LogMin, "custom_fields.soap", reqname.str(), TXSUMMARY_GRP_ENTERPRISE);
             if (m_proxyInfo)
             {
                 StringBuffer proxyMethod(reqname);
@@ -2475,7 +2503,10 @@ int EsdlBindingImpl::HandleSoapRequest(CHttpRequest* request,
                 StringBuffer proxyAddress;
                 bool resetForwardedFor = false;
                 if (checkForMethodProxy(request->queryServiceName(), proxyMethod, proxyAddress, resetForwardedFor))
+                {
+                    ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.soapRequestEnd", TXSUMMARY_GRP_ENTERPRISE);
                     return forwardProxyMessage(proxyAddress, request, response, resetForwardedFor);
+                }
             }
 
             StringBuffer nssrv;
@@ -2575,11 +2606,12 @@ int EsdlBindingImpl::HandleSoapRequest(CHttpRequest* request,
             response->setContent(out.str());
             response->setContentType(HTTP_TYPE_TEXT_XML_UTF8);
             response->setStatus(HTTP_STATUS_OK);
+            ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.RspSndSt", TXSUMMARY_GRP_ENTERPRISE);
             response->send();
             returnSocket(response);
 
             unsigned timetaken = msTick() - ctx->queryCreationTime();
-            ctx->addTraceSummaryTimeStamp(LogMin, "respSent");
+            ctx->addTraceSummaryTimeStamp(LogMin, "respSent", TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE);
 
             m_pESDLService->handleResultLogging(*ctx, scriptContext, *srvdef, *mthdef, tgtctx.get(), pt, soapmsg.str(), origResp.str(), out.str(), logdata.str());
 
@@ -2593,12 +2625,15 @@ int EsdlBindingImpl::HandleSoapRequest(CHttpRequest* request,
         StringBuffer text;
         text.appendf("Parsing xml error: %s.", exml.getMessage().c_str());
         ESPLOG(LogMax, "%s\n", text.str());
-
+        ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.soapRequestEnd", TXSUMMARY_GRP_ENTERPRISE);
         throw makeWsException(  ERR_ESDL_BINDING_BADREQUEST, WSERR_SERVER, "Esp", "%s", text.str() );
     }
     catch (IException *e)
     {
         const char * source = request->queryServiceName();
+        StringBuffer msg;
+        ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.soapRequestEnd", TXSUMMARY_GRP_ENTERPRISE);
+        ctx->addTraceSummaryValue(LogMin, "custom_fields._soap_call_error_msg", e->errorMessage(msg).str(), TXSUMMARY_GRP_ENTERPRISE);
         handleSoapRequestException(e, source);
     }
     catch(...)
@@ -2606,9 +2641,11 @@ int EsdlBindingImpl::HandleSoapRequest(CHttpRequest* request,
         StringBuffer text;
         text.append( "EsdlBindingImpl could not process SOAP request" );
         ESPLOG(LogMax,"%s", text.str());
+        ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.soapRequestEnd", TXSUMMARY_GRP_ENTERPRISE);
         throw makeWsException( ERR_ESDL_BINDING_INTERNERR, WSERR_SERVER, "ESP", "%s", text.str() );
     }
 
+    ctx->addTraceSummaryTimeStamp(LogMin, "custom_fields.soapRequestEnd", TXSUMMARY_GRP_ENTERPRISE);
     return 0;
 }
 
@@ -3449,6 +3486,7 @@ void EsdlBindingImpl::handleHttpPost(CHttpRequest *request, CHttpResponse *respo
         }
     }
 
+    request->queryContext()->addTraceSummaryTimeStamp(LogMin, "custom_fields.HttpPostStart", TXSUMMARY_GRP_ENTERPRISE);
     switch (sstype)
     {
         case sub_serv_roxie_builder:
@@ -3478,6 +3516,7 @@ void EsdlBindingImpl::handleHttpPost(CHttpRequest *request, CHttpResponse *respo
             break;
         }
     }
+    request->queryContext()->addTraceSummaryTimeStamp(LogMin, "custom_fields.HttpPostEnd", TXSUMMARY_GRP_ENTERPRISE);
 }
 
 int EsdlBindingImpl::onRoxieRequest(CHttpRequest* request, CHttpResponse* response, const char * method)

+ 2 - 0
esp/services/esdl_svc_engine/esdl_binding.hpp

@@ -32,6 +32,7 @@
 #include "esdl_store.hpp"
 #include "esdl_monitor.hpp"
 #include "espplugin.ipp"
+#include "txsummary.hpp"
 
 static const char* ESDL_METHOD_DESCRIPTION="description";
 static const char* ESDL_METHOD_HELP="help";
@@ -90,6 +91,7 @@ private:
     MethodAccessMaps            m_methodAccessMaps;
     StringBuffer                m_defaultFeatureAuth;
     MapStringTo<Owned<String> > m_explicitNamespaces;
+    Owned<ITxSummaryProfile>    m_txSummaryProfile;
 
 #ifndef LINK_STATICALLY
     Owned<ILoadedDllEntry> javaPluginDll;

+ 216 - 0
esp/services/esdl_svc_engine/esdl_summary_profile.cpp

@@ -0,0 +1,216 @@
+/*##############################################################################
+
+    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 "esdl_summary_profile.hpp"
+#include "espcontext.hpp"
+
+
+
+bool CTxSummaryProfileEsdl::tailorSummary(IEspContext* ctx)
+{
+    if(!ctx)
+        return false;
+
+    CTxSummary* txsummary = ctx->queryTxSummary();
+
+    if(!txsummary)
+        return false;
+
+    // setup name mappings
+    configure();
+
+    // root
+    txsummary->set("sys", "esp", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    SCMStringBuffer utcTime;
+    Owned<IJlibDateTime> now = createDateTimeNow();
+    now->getGmtString(utcTime);
+    txsummary->set("creation_timestamp", utcTime.str(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+    txsummary->set("log_type", "INFO", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("event_code", "SUMMARY", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("format_ver", "1.2", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+    // id
+    txsummary->set("id.global", ctx->getGlobalId(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("id.caller", ctx->getCallerId(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("id.local", ctx->getLocalId(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("id.ref", ctx->queryTransactionID(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+    // app
+    txsummary->set("app.resp_time_ms", ctx->queryProcessingTime(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+    // caller
+    // entry formerly named 'user' now broken into separate authid and ip fields
+    if (ctx->queryUserId())
+    {
+        txsummary->set("caller.authid", ctx->queryUserId(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+    StringBuffer peer;
+    ctx->getPeer(peer);
+    if (peer.length())
+    {
+        txsummary->set("caller.ip", peer.str(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+
+    // local
+    StringBuffer srvAddress;
+    short port;
+    ctx->getServAddress(srvAddress, port);
+    if (srvAddress.length())
+    {
+        txsummary->set("local.ip", srvAddress.str(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+
+    // custom_fields
+    txsummary->set("custom_fields.port", port, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+    StringBuffer sysinfo;
+    getSystemTraceInfo(sysinfo);
+    unsigned int cpu = parseSystemInfo("PU=", sysinfo);
+    unsigned int mal = parseSystemInfo("MAL=", sysinfo);
+    unsigned int mu = parseSystemInfo("MU=", sysinfo);
+
+    txsummary->set("custom_fields.cpusage_pct", cpu, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("custom_fields.musage_pct", mu, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("custom_fields.mal", mal, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+    // Update once retry is implemented
+    txsummary->set("custom_fields.qRetryStatus", 0, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    short isInternal = 0;
+    ISecUser* secUser = ctx->queryUser();
+    if (secUser)
+    {
+        if (secUser->getStatus()==SecUserStatus_Inhouse)
+            isInternal = 1;
+        txsummary->set("custom_fields.companyid", secUser->getRealm(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+    txsummary->set("custom_fields.internal", isInternal, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("custom_fields.wsdlver", ctx->getClientVersion(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->set("custom_fields.esp_build", getBuildVersion(), LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+
+    // Review the different possible entries recording failure
+    // and set the final status_code and status entries accordingly
+    if (streq(ctx->queryAuthStatus(), AUTH_STATUS_FAIL))
+    {
+        txsummary->set("app.status_code", "Failed to authenticate user", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        txsummary->set("msg", "Failed to authenticate user", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+    else if (txsummary->contains("soapFaultCode") || txsummary->contains("custom_fields.msg"))
+    {
+        txsummary->setCopyValueOf("app.status_code", "msg", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        txsummary->set("app.status", "FAILED", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+    else if (txsummary->contains("custom_fields._soap_call_error_msg"))
+    {
+        txsummary->setCopyValueOf("app.status_code", "custom_fields._soap_call_error_msg", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        txsummary->set("app.status", "FAILED", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+    else
+    {
+        txsummary->set("app.status_code", "S", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        txsummary->set("app.status", "SUCCESS", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+
+    // Use the _soap_call_error_msg as our final message if it exists
+    // Otherwise, if no other message has been added, then add one for success
+    if (txsummary->contains("custom_fields._soap_call_error_msg"))
+    {
+        txsummary->setCopyValueOf("msg", "custom_fields._soap_call_error_msg", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+    if (!txsummary->contains("msg"))
+    {
+        txsummary->set("msg", "Success", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    }
+    // msg is set appropriately, copy it's value to custom_fields.msg
+    txsummary->setCopyValueOf("custom_fields.msg", "msg", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->setCopyValueOf("custom_fields.httpmethod", "app.protocol", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->setCopyValueOf("custom_fields.method", "method", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+    txsummary->setCopyValueOf("custom_fields.txid", "id.ref", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+    return true;
+}
+
+unsigned int CTxSummaryProfileEsdl::parseSystemInfo(const char* name, StringBuffer& sysinfo)
+{
+    const char* finger = sysinfo.str();
+
+    while(finger && *finger)
+    {
+        if(hasPrefix(finger, name, true))
+        {
+            // found our info field, skip over the name
+            finger+=strlen(name);
+
+            // skip any whitespace
+            while(finger && ' ' == *finger)
+                finger++;
+
+            return strtoul(finger, NULL, 10);
+        }
+        finger++;
+    }
+    return 0;
+}
+
+void CTxSummaryProfileEsdl::configure()
+{
+    unsigned int ALL_GROUPS = TXSUMMARY_GRP_CORE|TXSUMMARY_GRP_ENTERPRISE;
+
+    addMap("activeReqs", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.activerecs", true});
+    addMap("auth", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.auth_status", true});
+    addMap("contLen", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.contLen", true});
+    addMap("endcall", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.endCallOut", true});
+    addMap("end-HFReq", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.endHandleFinalReq", true});
+    addMap("end-procres", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.endEsdlRespBld", true});
+    addMap("end-reqproc", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.endEsdlReqBld", true});
+    addMap("end-resLogging", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.endSendLogInfo", true});
+
+    addMap("method", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "app.method", true});
+    addMap("rcv", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.rcvdReq", true});
+    addMap("reqRecvd", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.rcvdReq", true});
+    addMap("respSent", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.rspSndEnd", true});
+    addMap("srt-procres", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.startEsdlRespBld", true});
+    addMap("srt-reqproc", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.startEsdlReqBld", true});
+    addMap("srt-resLogging", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.startSendLogInfo", true});
+
+    addMap("startcall", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.startCallOut", true});
+    addMap("total", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.comp", true});
+
+    // For plugins
+
+    addMap("dbAuthenticate", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.dbAuthenticate", true});
+    addMap("dbGetEffectiveAccess", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.dbGetEffectiveAccess", true});
+    addMap("dbGetSettingAccess", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.dbGetSettingAccess", true});
+    addMap("dbValidateSettings", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.dbValidateSettings", true});
+
+    addMap("endLnaaAuthSendRequest", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.endLnaaAuthSendRequest", true});
+    addMap("endLnaaGetSessionIdSendRequest", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.endLnaaGetSessionIdSendRequest", true});
+    addMap("endLnaaGetUerDataSendRequest", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.endLnaaGetUerDataSendRequest", true});
+
+    addMap("lnaaWsCallTime", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.lnaaWsCallTime", true});
+    addMap("MysqlTime", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.MySQLTime", true});
+    addMap("ResourcePool_Mysql", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.ResourcePool_MySql", true});
+    addMap("ResourcePool_Sybase", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.ResourcePool_Sybase", true});
+
+    addMap("startLnaaAuthSendRequest", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.startLnaaAuthSendRequest", true});
+    addMap("startLnaaGetSessionIdSendRequest", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.startLnaaGetSessionIdSendRequest", true});
+    addMap("startLnaaGetUerDataSendRequest", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.startLnaaGetUerDataSendRequest", true});
+
+    addMap("SybaseTime", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.SybaseTime", true});
+    addMap("ValidateSourceIP", {ALL_GROUPS, TXSUMMARY_OUT_JSON, "custom_fields.ValidateSourceIP", true});
+}

+ 46 - 0
esp/services/esdl_svc_engine/esdl_summary_profile.hpp

@@ -0,0 +1,46 @@
+/*##############################################################################
+
+    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 ESDL_SUMMARY_PROFILE_HPP
+#define ESDL_SUMMARY_PROFILE_HPP
+
+#include "jiface.hpp"
+#include "jstring.hpp"
+#include "esp.hpp"
+#include "txsummary.hpp"
+
+// Contains a list of mappings used to rename a TxEntry when being output
+// under conditions that match the group and style in the mapping.
+//
+// Each mapping has: original_name, group_flags, style_flags, new_name
+//
+// Serialization always specifies outputGroup flags and outputStyle flags.
+// Use the mapped new name to output when:
+//      group_flags & outputGroup == true and style_flags & outputStyle == true
+//
+
+class CTxSummaryProfileEsdl : public CTxSummaryProfileBase
+{
+    public:
+        virtual bool tailorSummary(IEspContext* ctx) override;
+
+    private:
+        unsigned int parseSystemInfo(const char* name, StringBuffer& sysinfo);
+        void configure();
+};
+
+#endif //ESDL_SUMMARY_PROFILE_HPP

+ 6 - 0
initfiles/componentfiles/configschema/xsd/esp.xsd

@@ -351,6 +351,12 @@
                 <xs:attribute name="txSummaryLevel" type="xs:nonNegativeInteger" hpcc:presetValue="1"
                               hpcc:displayName="Tx Summary Level"
                               hpcc:tooltip="Sets the TxSummary level [0: none, 1: min, 5: noraml, 10: max]"/>
+                <xs:attribute name="txSummaryStyle" type="xs:string" hpcc:presetValue="text"
+                              hpcc:displayName="Tx Summary Output Style"
+                              hpcc:tooltip="Sets the style of TxSummary: [text|json|all]"/>
+                <xs:attribute name="txSummaryGroup" type="xs:string" hpcc:presetValue="text"
+                              hpcc:displayName="Tx Summary Output Groups"
+                              hpcc:tooltip="Selects which groups of entries are output in the TxSummary: [core|enterprise|all]"/>
                 <xs:attribute name="txSummaryResourceReq" type="xs:boolean" hpcc:presetValue="false"
                               hpcc:displayName="Tx Summary Resource Req"
                               hpcc:tooltip="Log TxSummary for Resource Requests"/>

+ 15 - 1
initfiles/componentfiles/configxml/esp.xsd.in

@@ -995,7 +995,21 @@
             <xs:attribute name="txSummaryLevel" type="xs:nonNegativeInteger" use="optional" default="1">
                 <xs:annotation>
                     <xs:appinfo>
-                        <tooltip>Sets the TxSummary level [0: none, 1: min, 5: noraml, 10: max]</tooltip>
+                        <tooltip>Sets the TxSummary level [0: none, 1: min, 5: normal, 10: max]</tooltip>
+                    </xs:appinfo>
+                </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="txSummaryStyle" type="xs:text" use="optional" default="text">
+                <xs:annotation>
+                    <xs:appinfo>
+                        <tooltip>Sets the style of TxSummary: [text|json|all]</tooltip>
+                    </xs:appinfo>
+                </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="txSummaryGroup" type="xs:text" use="optional" default="core">
+                <xs:annotation>
+                    <xs:appinfo>
+                        <tooltip>Selects which groups of entries are output in the TxSummary: [core|enterprise|all]</tooltip>
                     </xs:appinfo>
                 </xs:annotation>
             </xs:attribute>

+ 6 - 0
system/security/securesocket/securesocket.cpp

@@ -160,6 +160,12 @@ public:
 
     void readTimeout(void* buf, size32_t min_size, size32_t max_size, size32_t &size_read, unsigned timeout, bool useSeconds);
 
+    virtual StringBuffer& get_ssl_version(StringBuffer& ver)
+    {
+        ver.set(SSL_get_version(m_ssl));
+        return ver;
+    }
+
     //The following are the functions from ISocket that haven't been implemented.
 
 

+ 1 - 0
system/security/securesocket/securesocket.hpp

@@ -50,6 +50,7 @@ interface ISecureSocket : implements ISocket
 {
     virtual int secure_accept(int logLevel=1) = 0;
     virtual int secure_connect(int logLevel=1) = 0;
+    virtual StringBuffer& get_ssl_version(StringBuffer& ver) = 0;
 };
 
 // One instance per program running

+ 1 - 0
testing/unittests/CMakeLists.txt

@@ -38,6 +38,7 @@ set (    SRCS
          cryptotests.cpp
          hqltests.cpp
          esdltests.cpp
+         txSummarytests.cpp
     )
 
 if (NOT CONTAINERIZED)

+ 395 - 0
testing/unittests/txSummarytests.cpp

@@ -0,0 +1,395 @@
+/*##############################################################################
+
+    Copyright (C) 2020 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.
+############################################################################## */
+
+//#ifdef _USE_CPPUNIT
+#include "unittests.hpp"
+#include "txsummary.hpp"
+#include "espcontext.hpp"
+
+
+
+
+class TxSummaryTests : public CppUnit::TestFixture
+{
+    CPPUNIT_TEST_SUITE(TxSummaryTests);
+        CPPUNIT_TEST(testPath);
+        CPPUNIT_TEST(testShortPath);
+        CPPUNIT_TEST(testIllegalKeys);
+        CPPUNIT_TEST(testTypes);
+        CPPUNIT_TEST(testSet);
+        CPPUNIT_TEST(testSizeClear);
+        CPPUNIT_TEST(testProfile);
+        CPPUNIT_TEST(testFilter);
+
+    CPPUNIT_TEST_SUITE_END();
+
+public:
+    TxSummaryTests(){}
+
+    void validateResults(StringBuffer& testResult, const char* expectedResult, const char* testName, const char* step=nullptr, bool dbgout=false)
+    {
+        // remove newline formatting if any
+        testResult.stripChar('\n');
+        testResult.stripChar(' ');
+        if( !strieq(testResult.str(), expectedResult))
+        {
+            printf("Mismatch (%s %s): Test Result vs Expected Result\n", testName, step ? step : "");
+            printf("test:\n%s\nexpected:\n%s\n", testResult.str(), expectedResult);
+            fflush(stdout);
+            throw MakeStringException(100, "Failed Test (%s %s)", testName, step ? step : "");
+        } else if( dbgout ){
+            printf("test:\n%s\nexpected:\n%s\n", testResult.str(), expectedResult);
+        }
+    }
+
+    void testPath()
+    {
+        constexpr const char* resultJSON = R"!!({"app":{"global_id":"global-val","local_id":"local-val"},"root1":"root1-val","one":{"two":{"three":"123","four":"124"},"dos":{"three":"123"}}})!!";
+        constexpr const char* resultText = R"!!(app.global_id=global-val;app.local_id=local-val;root1=root1-val;one.two.three=123;one.two.four=124;one.dos.three=123;)!!";
+        const char* testName="testJsonPath";
+
+        Owned<CTxSummary> tx = new CTxSummary();
+        tx->append("app.global_id", "global-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("app.local_id", "local-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("root1", "root1-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("one.two.three", "123", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("one.two.four", "124", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("one.dos.three", "123", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        StringBuffer output;
+        tx->serialize(output, LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON);
+        validateResults(output, resultJSON, testName, "json");
+
+        tx->serialize(output.clear(), LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_TEXT);
+        validateResults(output, resultText, testName, "text");
+    }
+
+    void testShortPath()
+    {
+        constexpr const char* resultJSON = R"!!({"a":"a-val","b":{"c":"bc-val","d":"bd-val"},"e":"e-val"})!!";
+        constexpr const char* resultText = R"!!(a=a-val;b.c=bc-val;b.d=bd-val;e=e-val;)!!";
+        const char* testName="testJsonShortPath";
+
+        Owned<CTxSummary> tx = new CTxSummary();
+        tx->append("a", "a-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("b.c", "bc-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("b.d", "bd-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("e", "e-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        StringBuffer output;
+        tx->serialize(output, LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON);
+        validateResults(output, resultJSON, testName, "json");
+
+        tx->serialize(output.clear(), LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_TEXT);
+        validateResults(output, resultText, testName, "text");
+    }
+
+    void testTypes()
+    {
+        VStringBuffer resultJSON("{\"emptystr\":\"\",\"nullstr\":\"\",\"int\":%i,\"uint\":%u,\"uint64\":%" I64F "u,\"querytimer\":42,\"updatetimernew\":23,\"bool\":1}", INT_MAX, UINT_MAX, ULONG_LONG_MAX );
+        VStringBuffer resultText("emptystr;nullstr;int=%i;uint=%u;uint64=%" I64F "u;querytimer=42ms;updatetimernew=23ms;bool=1;", INT_MAX, UINT_MAX, ULONG_LONG_MAX );
+        const char* testName="testTypes";
+        Owned<CTxSummary> tx = new CTxSummary();
+
+        // String
+        tx->append("emptystr", "", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        // we can't add nullptr when using the template append function
+        tx->append("nullstr", "", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Int
+        int val1 = INT_MAX;
+        tx->append("int", val1, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Unsigned Int
+        unsigned int val2 = UINT_MAX;
+        tx->append("uint", val2, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Unsigned Int64
+        unsigned __int64 val3 = ULONG_LONG_MAX;
+        tx->append("uint64", val3, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Cumulative Timer
+        CumulativeTimer* t = tx->queryTimer("querytimer", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        t->add(23);
+        tx->updateTimer("querytimer", 19, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->updateTimer("updatetimernew", 23, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->updateTimer("filteredoutbygroup", 5, LogMin, TXSUMMARY_GRP_CORE);
+
+        // Boolean
+        tx->append("bool", true, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        StringBuffer output;
+        tx->serialize(output, LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON);
+        validateResults(output, resultJSON.str(), testName, "json");
+
+        tx->serialize(output.clear(), LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_TEXT);
+        validateResults(output, resultText.str(), testName, "text");
+    }
+
+    void testIllegalKeys()
+    {
+        const char* testName="testIllegalKeys";
+        bool result = true;
+        Owned<CTxSummary> tx = new CTxSummary();
+
+        result = tx->append(nullptr, "foobar", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed null key", testName);
+
+        result = tx->append("", "foobar", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed empty key", testName);
+
+        result = tx->append("app", "foobar", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        result = tx->append("app", "secondval", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed duplicate key: (app)", testName);
+
+        // Fail due to 'app' present as a value
+        // Can't append a duplicate key whose first path part 'app' that would resolve
+        // to an object named 'app' holding a value named 'local'
+        result = tx->append("app.local", "foobar", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed duplicate key: (app.local)", testName);
+
+        result = tx->append("apple", "pie", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        result = tx->append("apple", "crisp", LogMin, TXSUMMARY_GRP_ENTERPRISE); // partial match on 'app', fail due to 'apple'
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed duplicate key: (apple)", testName);
+
+        result = tx->append("pineapple.cake", "upside", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        result = tx->append("pineapple.cake", "down", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed duplicate hierarchical key: (pineapple.cake)", testName);
+
+        result = tx->append(".rap", "foobar", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed malformed key with empty leading path part: (.rap)", testName);
+
+        result = tx->append("cap.", "foobar", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed malformed key with empty trailing path part: (cap.)", testName);
+
+        result = tx->append("bread..baker", "foobar", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        if(result)
+            throw MakeStringException(100, "Failed Test (%s) - allowed malformed key with empty path part: (bread..baker)", testName);
+
+        CumulativeTimer* timer = nullptr;
+        try
+        {
+            // expected exception thrown because "app" is not a timer
+            timer = tx->queryTimer("app", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        }
+        catch(IException* e)
+        {
+            e->Release();
+        }
+
+        if(timer)
+            throw MakeStringException(100, "Failed Test (%s) - string entry mistaken for CumulativeTimer", testName);
+    }
+
+    void testSet()
+    {
+        constexpr const char* result1 = R"!!({"one":99,"two":2,"three":3})!!";
+        constexpr const char* result2 = R"!!({"one":1,"two":"2","three":"3"})!!";
+        const char* testName="testSet";
+
+        Owned<CTxSummary> tx = new CTxSummary();
+        tx->append("one", 99u, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("two", 2u, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->updateTimer("three", 3, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        StringBuffer output;
+        tx->serialize(output, LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON);
+        validateResults(output, result1, testName, "before");
+
+        tx->set("one", 1U, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->set("two", "2", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        // Likely future update will disallow a set to change an entry
+        // between timer and scalar types
+        tx->set("three", "3", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        tx->serialize(output.clear(), LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON);
+        validateResults(output, result2, testName, "after");
+    }
+
+    void testSizeClear()
+    {
+        constexpr const char* result1 = R"!!(size=6)!!";
+        constexpr const char* result2 = R"!!(size=0)!!";
+        const char* testName="testSizeClear";
+
+        Owned<CTxSummary> tx = new CTxSummary();
+        tx->append("a", "a-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("b.c", "bc-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("b.d", "bd-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("e", "e-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("x.y.z", 77, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("x.y.a", 88U, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        VStringBuffer output("size=%" I64F "u", tx->size());
+        validateResults(output, result1, testName, "first sizing");
+
+        tx->clear();
+        output.clear().appendf("size=%" I64F "u", tx->size());
+        validateResults(output, result2, testName, "after clear");
+
+        tx->append("a", "a-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("b.c", "bc-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("b.d", "bd-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("e", "e-val", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("x.y.z", 77, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+        tx->append("x.y.a", 88U, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        output.clear().appendf("size=%" I64F "u", tx->size());
+        validateResults(output, result1, testName, "second sizing");
+    }
+
+    void testFilter()
+    {
+        constexpr const char* resultOps1JsonMax = R"!!({"dev-str":"q","dev-int":1,"dev-uint":2,"dev-uint64":3,"dev-timer":19,"dev-bool":0,"max-only":1})!!";
+        constexpr const char* resultOps1JsonMin = R"!!({"dev-str":"q","dev-int":1,"dev-uint":2,"dev-uint64":3,"dev-timer":19,"dev-bool":0})!!";
+        constexpr const char* resultOps2JsonMax = R"!!({"ops-str":"q","ops-int":1,"ops-uint":2,"ops-uint64":3,"ops-timer":23,"ops-bool":0,"max-only":1})!!";
+        constexpr const char* resultOps2JsonMin = R"!!({"ops-str":"q","ops-int":1,"ops-uint":2,"ops-uint64":3,"ops-timer":23,"ops-bool":0})!!";
+        constexpr const char* resultOps1TextMax = R"!!(dev-str=q;dev-int=1;dev-uint=2;dev-uint64=3;dev-timer=19ms;dev-bool=0;max-only=1;)!!";
+        constexpr const char* resultOps1TextMin = R"!!(dev-str=q;dev-int=1;dev-uint=2;dev-uint64=3;dev-timer=19ms;dev-bool=0;)!!";
+        constexpr const char* resultOps2TextMax = R"!!(ops-str=q;ops-int=1;ops-uint=2;ops-uint64=3;ops-timer=23ms;ops-bool=0;max-only=1;)!!";
+        constexpr const char* resultOps2TextMin = R"!!(ops-str=q;ops-int=1;ops-uint=2;ops-uint64=3;ops-timer=23ms;ops-bool=0;)!!";
+        const char* testName="testFilter";
+
+        Owned<CTxSummary> tx = new CTxSummary();
+        // String
+        tx->append("dev-str", "q", LogMin, TXSUMMARY_GRP_CORE);
+        tx->append("ops-str", "q", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Int
+        int val1 = 1;
+        tx->append("dev-int", val1, LogMin, TXSUMMARY_GRP_CORE);
+        tx->append("ops-int", val1, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Unsigned Int
+        unsigned int val2 = 2;
+        tx->append("dev-uint", val2, LogMin, TXSUMMARY_GRP_CORE);
+        tx->append("ops-uint", val2, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Unsigned Int64
+        unsigned __int64 val3 = 3;
+        tx->append("dev-uint64", val3, LogMin, TXSUMMARY_GRP_CORE);
+        tx->append("ops-uint64", val3, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Cumulative Timer
+        // create with a call to query, then update
+        CumulativeTimer* t = tx->queryTimer("dev-timer", LogMin, TXSUMMARY_GRP_CORE);
+        tx->updateTimer("dev-timer", 19, LogMin, TXSUMMARY_GRP_CORE);
+        // create with a call to update
+        tx->updateTimer("ops-timer", 23, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // Boolean
+        tx->append("dev-bool", false, LogMin, TXSUMMARY_GRP_CORE);
+        tx->append("ops-bool", false, LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // LogMax, all serialization styles
+        tx->append("max-only", true, LogMax, TXSUMMARY_OUT_TEXT | TXSUMMARY_OUT_JSON);
+
+        StringBuffer output;
+        tx->serialize(output, LogMax, TXSUMMARY_GRP_CORE, TXSUMMARY_OUT_JSON);
+        validateResults(output, resultOps1JsonMax, testName, "ops1 + json + max");
+
+        output.clear();
+        tx->serialize(output, LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON);
+        validateResults(output, resultOps2JsonMax, testName, "ops2 + json + max");
+
+        output.clear();
+        tx->serialize(output, LogMin, TXSUMMARY_GRP_CORE, TXSUMMARY_OUT_JSON);
+        validateResults(output, resultOps1JsonMin, testName, "ops1 + json + min");
+
+        output.clear();
+        tx->serialize(output, LogMin, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON);
+        validateResults(output, resultOps2JsonMin, testName, "ops2 + json + min");
+
+        output.clear();
+        tx->serialize(output, LogMax, TXSUMMARY_GRP_CORE, TXSUMMARY_OUT_TEXT);
+        validateResults(output, resultOps1TextMax, testName, "ops1 + text + max");
+
+        output.clear();
+        tx->serialize(output, LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_TEXT);
+        validateResults(output, resultOps2TextMax, testName, "ops2 + text + max");
+
+        output.clear();
+        tx->serialize(output, LogMin, TXSUMMARY_GRP_CORE, TXSUMMARY_OUT_TEXT);
+        validateResults(output, resultOps1TextMin, testName, "ops1 + text + min");
+
+        output.clear();
+        tx->serialize(output, LogMin, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_TEXT);
+        validateResults(output, resultOps2TextMin, testName, "ops2 + text + min");
+
+    }
+
+    class CTxSummaryProfileTest : public CTxSummaryProfileBase
+    {
+        public:
+            virtual bool tailorSummary(IEspContext* ctx) override {return true;}
+    };
+
+    void testProfile()
+    {
+        constexpr const char* resultJson = R"!!({"user":"testuser","name4json":"tval","three":{"four":"three-four","name4all":"v4a"}})!!";
+        constexpr const char* resultText = R"!!(user=testuser;name4text=tval;three.four=three-four;three.name4all=v4a;)!!";
+        const char* testName="testProfile";
+
+        Owned<CTxSummary> tx = new CTxSummary();
+        Linked<ITxSummaryProfile> profile = new CTxSummaryProfileTest();
+        tx->setProfile(profile);
+
+        profile->addMap("ops2-json", {TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_TEXT, "name4text", true});
+        profile->addMap("ops2-json", {TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON, "name4json", true});
+        profile->addMap("three.ops2-all", {TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_TEXT | TXSUMMARY_OUT_JSON, "three.name4all", true});
+        profile->addMap("ops1", {TXSUMMARY_GRP_CORE, TXSUMMARY_OUT_TEXT | TXSUMMARY_OUT_JSON, "ops1-noshow", true});
+
+        tx->append("user", "testuser", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // test matching the second profile entry with the same name, but different output style
+        tx->append("ops2-json", "tval", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // add hierarchy
+        tx->append("three.four", "three-four", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // test matching entry for all output styles
+        // test matching a hierarchical entry
+        tx->append("three.ops2-all", "v4a", LogMin, TXSUMMARY_GRP_ENTERPRISE);
+
+        // test matching entry for a group that isn't serialized
+        // also tests correct JSON delimiting when last child of
+        // object is filtered out from serialization
+        tx->append("ops1", "ops1val", LogMin, TXSUMMARY_GRP_CORE);
+
+        StringBuffer output;
+
+        tx->serialize(output.clear(), LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_JSON);
+        validateResults(output, resultJson, testName, "map flat name JSON out");
+
+        tx->serialize(output.clear(), LogMax, TXSUMMARY_GRP_ENTERPRISE, TXSUMMARY_OUT_TEXT);
+        validateResults(output, resultText, testName, "map flat name TEXT out");
+    }
+
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION( TxSummaryTests );
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( TxSummaryTests, "txsummary" );
+
+//#endif // _USE_CPPUNIT