瀏覽代碼

HPCC-23426 Add support for JSON style TxSummary

Add basic support for printing TxSummary as JSON, text or both. The output
style is configurable in the ESPProcess. Each element added to the summary
is associated with a group and log level. An element is printed only when
it matches the configured group and log level.

An element name can include JSON style dot path notation to indicate its
place in the output hierarchy for JSON style output. Element names with
dots output in the text style are printed as-is, they are not structured
in a hierarchy.

Standardize the timer interface to always require group and level which are
used to find matching timers. Timers with the same name only are not
considered a match and an exception is thrown.

The summary takes a profile to tailor its output just prior to serialization
in several ways:

 - It can rename entries for different output styles and groups.
 - A callback function can add entries or perform other steps as required.

Add additional entries in a separate 'enterprise' group for tracking
timings and values likely not of interest to existing ESP services.

Unittests are updated to exercise the new features.

Signed-off-by: Terrence Asselin <terrence.asselin@lexisnexisrisk.com>
Terrence Asselin 4 年之前
父節點
當前提交
d8d47d1e48

+ 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

@@ -159,6 +159,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