This is an automated email from the ASF dual-hosted git repository.
zwoop pushed a commit to branch 9.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/9.2.x by this push:
new 0affc17d0 Add escape json for logging (#8886)
0affc17d0 is described below
commit 0affc17d08700c176206ef3c39c60e0cf1f73db8
Author: Hiroaki Nakamura <[email protected]>
AuthorDate: Fri Jun 10 01:22:56 2022 +0900
Add escape json for logging (#8886)
* Add escape json for logging
* Modify escape_json to escape DEL (0x7f) correctly
* Modify escape_json for logging
(Copied from
https://github.com/apache/trafficserver/pull/8886#discussion_r892896951)
Co-authored-by: Walt Karas <[email protected]>
* Fix escape of doublequote in escape_json
* Escape forward slash in escape_json
* Removed a comment-outed line in LogAccess::unmarshal_http_text_json
* Fix slicing for escape_json in LogAccess.cc
* Cast char index to int to avoid char-subscripts warnings
* Add AuTest for json log escape
Co-authored-by: Walt Karas <[email protected]>
Co-authored-by: Walt Karas <[email protected]>
(cherry picked from commit d18721884615958f151eee084244c506552c6512)
---
doc/admin-guide/logging/formatting.en.rst | 8 +-
proxy/logging/LogAccess.cc | 163 +++++++++++++++++++++
proxy/logging/LogAccess.h | 2 +
proxy/logging/LogBuffer.cc | 8 +-
proxy/logging/LogBuffer.h | 4 +-
proxy/logging/LogField.cc | 9 +-
proxy/logging/LogField.h | 4 +-
proxy/logging/LogFile.cc | 7 +-
proxy/logging/LogFile.h | 9 +-
proxy/logging/LogFormat.cc | 8 +-
proxy/logging/LogFormat.h | 8 +-
proxy/logging/LogObject.cc | 4 +-
proxy/logging/YamlLogConfigDecoders.cc | 18 ++-
tests/gold_tests/logging/gold/field-json-test.gold | 3 +
tests/gold_tests/logging/log-field-json.test.py | 109 ++++++++++++++
15 files changed, 343 insertions(+), 21 deletions(-)
diff --git a/doc/admin-guide/logging/formatting.en.rst
b/doc/admin-guide/logging/formatting.en.rst
index 328dfb69f..4b982af3d 100644
--- a/doc/admin-guide/logging/formatting.en.rst
+++ b/doc/admin-guide/logging/formatting.en.rst
@@ -39,7 +39,8 @@ fairly simple: every format must contain a ``Format``
attribute, which is the
string defining the contents of each line in the log, and may also contain an
optional ``Interval`` attribute defining the log aggregation interval for
any logs which use the format (see :ref:`admin-logging-type-summary` for more
-information).
+information). It may also contain an optional ``escape`` attribute defining the
+escape of log fields. Possible values for ``escape`` are ``json`` or ``none``.
The return value from the ``format`` function is the log format object which
may then be supplied to the appropriate ``log.*`` functions that define your
@@ -877,3 +878,8 @@ Some examples below ::
'%<cqup[0:30]>' // the first 30 characters of <cqup>.
'%<cqup[-10:]>' // the last 10 characters of <cqup>.
'%<cqup[:-5]>' // everything except the last 5 characters of <cqup>.
+
+Note when ``escape`` in ``format`` is set to ``json``, the start is the
+position before escaping JSON strings, and escaped values are sliced at
+the length (= end - start). If slicing cuts in the middle of escaped
+characters, the whole character is removed.
diff --git a/proxy/logging/LogAccess.cc b/proxy/logging/LogAccess.cc
index b776986ba..f14e03c04 100644
--- a/proxy/logging/LogAccess.cc
+++ b/proxy/logging/LogAccess.cc
@@ -643,6 +643,141 @@ LogAccess::unmarshal_str(char **buf, char *dest, int len,
LogSlice *slice)
return -1;
}
+namespace
+{
+class EscLookup
+{
+public:
+ static const char NO_ESCAPE{'\0'};
+ static const char LONG_ESCAPE{'\x01'};
+
+ static char
+ result(char c)
+ {
+ return _lu.table[static_cast<unsigned char>(c)];
+ }
+
+private:
+ struct _LUT {
+ _LUT();
+
+ char table[1 << 8];
+ };
+
+ inline static _LUT const _lu;
+};
+
+EscLookup::_LUT::_LUT()
+{
+ for (unsigned i = 0; i < ' '; ++i) {
+ table[i] = LONG_ESCAPE;
+ }
+ for (unsigned i = '\x7f'; i < sizeof(table); ++i) {
+ table[i] = LONG_ESCAPE;
+ }
+
+ // Short escapes.
+ //
+ table[static_cast<int>('\b')] = 'b';
+ table[static_cast<int>('\t')] = 't';
+ table[static_cast<int>('\n')] = 'n';
+ table[static_cast<int>('\f')] = 'f';
+ table[static_cast<int>('\r')] = 'r';
+ table[static_cast<int>('\\')] = '\\';
+ table[static_cast<int>('\"')] = '"';
+ table[static_cast<int>('/')] = '/';
+}
+
+char
+nibble(int nib)
+{
+ return nib >= 0xa ? 'a' + (nib - 0xa) : '0' + nib;
+}
+
+} // end anonymous namespace
+
+static int
+escape_json(char *dest, const char *buf, int len)
+{
+ int escaped_len = 0;
+
+ for (int i = 0; i < len; i++) {
+ char c = buf[i];
+ char ec = EscLookup::result(c);
+ if (__builtin_expect(EscLookup::NO_ESCAPE == ec, 1)) {
+ if (dest) {
+ if (escaped_len + 1 > len) {
+ break;
+ }
+ *dest++ = c;
+ }
+ escaped_len++;
+
+ } else if (EscLookup::LONG_ESCAPE == ec) {
+ if (dest) {
+ if (escaped_len + 6 > len) {
+ break;
+ }
+ *dest++ = '\\';
+ *dest++ = 'u';
+ *dest++ = '0';
+ *dest++ = '0';
+ *dest++ = nibble(static_cast<unsigned char>(c) >> 4);
+ *dest++ = nibble(c & 0x0f);
+ }
+ escaped_len += 6;
+
+ } else { // Short escape.
+ if (dest) {
+ if (escaped_len + 2 > len) {
+ break;
+ }
+ *dest++ = '\\';
+ *dest++ = ec;
+ }
+ escaped_len += 2;
+ }
+ } // end for
+ return escaped_len;
+}
+
+int
+LogAccess::unmarshal_str_json(char **buf, char *dest, int len, LogSlice *slice)
+{
+ Debug("log-escape", "unmarshal_str_json start, len=%d, slice=%p", len,
slice);
+ ink_assert(buf != nullptr);
+ ink_assert(*buf != nullptr);
+ ink_assert(dest != nullptr);
+
+ char *val_buf = *buf;
+ int val_len = static_cast<int>(::strlen(val_buf));
+ int escaped_len = escape_json(nullptr, val_buf, val_len);
+
+ *buf += LogAccess::strlen(val_buf); // this is how it was stored
+
+ if (slice && slice->m_enable) {
+ int offset, n;
+
+ n = slice->toStrOffset(escaped_len, &offset);
+ Debug("log-escape", "unmarshal_str_json start, n=%d, offset=%d", n,
offset);
+ if (n <= 0) {
+ return 0;
+ }
+
+ if (n >= len) {
+ return -1;
+ }
+
+ return escape_json(dest, (val_buf + offset), n);
+ }
+
+ if (escaped_len < len) {
+ escape_json(dest, val_buf, escaped_len);
+ return escaped_len;
+ }
+ return -1;
+}
+
int
LogAccess::unmarshal_ttmsf(char **buf, char *dest, int len)
{
@@ -813,6 +948,34 @@ LogAccess::unmarshal_http_text(char **buf, char *dest, int
len, LogSlice *slice)
return res1 + res2 + res3 + 2;
}
+int
+LogAccess::unmarshal_http_text_json(char **buf, char *dest, int len, LogSlice
*slice)
+{
+ ink_assert(buf != nullptr);
+ ink_assert(*buf != nullptr);
+ ink_assert(dest != nullptr);
+
+ char *p = dest;
+
+ int res1 = unmarshal_str_json(buf, p, len);
+ if (res1 < 0) {
+ return -1;
+ }
+ p += res1;
+ *p++ = ' ';
+ int res2 = unmarshal_str_json(buf, p, len - res1 - 1, slice);
+ if (res2 < 0) {
+ return -1;
+ }
+ p += res2;
+ *p++ = ' ';
+ int res3 = unmarshal_http_version(buf, p, len - res1 - res2 - 2);
+ if (res3 < 0) {
+ return -1;
+ }
+ return res1 + res2 + res3 + 2;
+}
+
/*-------------------------------------------------------------------------
LogAccess::unmarshal_http_status
diff --git a/proxy/logging/LogAccess.h b/proxy/logging/LogAccess.h
index 5dba8d808..2f2956d6d 100644
--- a/proxy/logging/LogAccess.h
+++ b/proxy/logging/LogAccess.h
@@ -304,12 +304,14 @@ public:
static int unmarshal_int_to_str(char **buf, char *dest, int len);
static int unmarshal_int_to_str_hex(char **buf, char *dest, int len);
static int unmarshal_str(char **buf, char *dest, int len, LogSlice *slice =
nullptr);
+ static int unmarshal_str_json(char **buf, char *dest, int len, LogSlice
*slice = nullptr);
static int unmarshal_ttmsf(char **buf, char *dest, int len);
static int unmarshal_int_to_date_str(char **buf, char *dest, int len);
static int unmarshal_int_to_time_str(char **buf, char *dest, int len);
static int unmarshal_int_to_netscape_str(char **buf, char *dest, int len);
static int unmarshal_http_version(char **buf, char *dest, int len);
static int unmarshal_http_text(char **buf, char *dest, int len, LogSlice
*slice = nullptr);
+ static int unmarshal_http_text_json(char **buf, char *dest, int len,
LogSlice *slice = nullptr);
static int unmarshal_http_status(char **buf, char *dest, int len);
static int unmarshal_ip(char **buf, IpEndpoint *dest);
static int unmarshal_ip_to_str(char **buf, char *dest, int len);
diff --git a/proxy/logging/LogBuffer.cc b/proxy/logging/LogBuffer.cc
index 8fdfb8c09..0b091e3d5 100644
--- a/proxy/logging/LogBuffer.cc
+++ b/proxy/logging/LogBuffer.cc
@@ -445,7 +445,7 @@ LogBuffer::max_entry_bytes()
int
LogBuffer::resolve_custom_entry(LogFieldList *fieldlist, char *printf_str,
char *read_from, char *write_to, int write_to_len,
long timestamp, long timestamp_usec, unsigned
buffer_version, LogFieldList *alt_fieldlist,
- char *alt_printf_str)
+ char *alt_printf_str, LogEscapeType
escape_type)
{
if (fieldlist == nullptr || printf_str == nullptr) {
return 0;
@@ -501,7 +501,7 @@ LogBuffer::resolve_custom_entry(LogFieldList *fieldlist,
char *printf_str, char
++markCount;
if (field != nullptr) {
char *to = &write_to[bytes_written];
- res = field->unmarshal(&read_from, to, write_to_len -
bytes_written);
+ res = field->unmarshal(&read_from, to, write_to_len -
bytes_written, escape_type);
if (res < 0) {
SiteThrottledNote("%s", buffer_size_exceeded_msg);
@@ -549,7 +549,7 @@ LogBuffer::resolve_custom_entry(LogFieldList *fieldlist,
char *printf_str, char
-------------------------------------------------------------------------*/
int
LogBuffer::to_ascii(LogEntryHeader *entry, LogFormatType type, char *buf, int
buf_len, const char *symbol_str, char *printf_str,
- unsigned buffer_version, const char *alt_format)
+ unsigned buffer_version, const char *alt_format,
LogEscapeType escape_type)
{
ink_assert(entry != nullptr);
ink_assert(type == LOG_FORMAT_CUSTOM || type == LOG_FORMAT_TEXT);
@@ -642,7 +642,7 @@ LogBuffer::to_ascii(LogEntryHeader *entry, LogFormatType
type, char *buf, int bu
}
int ret = resolve_custom_entry(fieldlist, printf_str, read_from, write_to,
buf_len, entry->timestamp, entry->timestamp_usec,
- buffer_version, alt_fieldlist,
alt_printf_str);
+ buffer_version, alt_fieldlist,
alt_printf_str, escape_type);
delete alt_fieldlist;
ats_free(alt_printf_str);
diff --git a/proxy/logging/LogBuffer.h b/proxy/logging/LogBuffer.h
index a6e168b2f..aa911cd76 100644
--- a/proxy/logging/LogBuffer.h
+++ b/proxy/logging/LogBuffer.h
@@ -189,10 +189,10 @@ public:
// static functions
static size_t max_entry_bytes();
static int to_ascii(LogEntryHeader *entry, LogFormatType type, char *buf,
int max_len, const char *symbol_str, char *printf_str,
- unsigned buffer_version, const char *alt_format =
nullptr);
+ unsigned buffer_version, const char *alt_format =
nullptr, LogEscapeType escape_type = LOG_ESCAPE_NONE);
static int resolve_custom_entry(LogFieldList *fieldlist, char *printf_str,
char *read_from, char *write_to, int write_to_len,
long timestamp, long timestamp_us, unsigned
buffer_version, LogFieldList *alt_fieldlist = nullptr,
- char *alt_printf_str = nullptr);
+ char *alt_printf_str = nullptr,
LogEscapeType escape_type = LOG_ESCAPE_NONE);
static void
destroy(LogBuffer *&lb)
diff --git a/proxy/logging/LogField.cc b/proxy/logging/LogField.cc
index 34d23dd18..0a396ed29 100644
--- a/proxy/logging/LogField.cc
+++ b/proxy/logging/LogField.cc
@@ -592,12 +592,19 @@ LogField::marshal_agg(char *buf)
string that represents the ASCII value of the field.
-------------------------------------------------------------------------*/
unsigned
-LogField::unmarshal(char **buf, char *dest, int len)
+LogField::unmarshal(char **buf, char *dest, int len, LogEscapeType escape_type)
{
if (!m_alias_map) {
if (m_unmarshal_func ==
reinterpret_cast<UnmarshalFunc>(LogAccess::unmarshal_str) ||
m_unmarshal_func ==
reinterpret_cast<UnmarshalFunc>(LogAccess::unmarshal_http_text)) {
UnmarshalFuncWithSlice func =
reinterpret_cast<UnmarshalFuncWithSlice>(m_unmarshal_func);
+ if (escape_type == LOG_ESCAPE_JSON) {
+ if (m_unmarshal_func ==
reinterpret_cast<UnmarshalFunc>(LogAccess::unmarshal_str)) {
+ func =
reinterpret_cast<UnmarshalFuncWithSlice>(LogAccess::unmarshal_str_json);
+ } else if (m_unmarshal_func ==
reinterpret_cast<UnmarshalFunc>(LogAccess::unmarshal_http_text)) {
+ func =
reinterpret_cast<UnmarshalFuncWithSlice>(LogAccess::unmarshal_http_text_json);
+ }
+ }
return (*func)(buf, dest, len, &m_slice);
}
return (*m_unmarshal_func)(buf, dest, len);
diff --git a/proxy/logging/LogField.h b/proxy/logging/LogField.h
index cc4361453..3d6b1348a 100644
--- a/proxy/logging/LogField.h
+++ b/proxy/logging/LogField.h
@@ -32,6 +32,8 @@
#include "LogFieldAliasMap.h"
#include "Milestones.h"
+enum LogEscapeType { LOG_ESCAPE_NONE, LOG_ESCAPE_JSON };
+
class LogAccess;
struct LogSlice {
@@ -133,7 +135,7 @@ public:
unsigned marshal_len(LogAccess *lad);
unsigned marshal(LogAccess *lad, char *buf);
unsigned marshal_agg(char *buf);
- unsigned unmarshal(char **buf, char *dest, int len);
+ unsigned unmarshal(char **buf, char *dest, int len, LogEscapeType
escape_type = LOG_ESCAPE_NONE);
void display(FILE *fd = stdout);
bool operator==(LogField &rhs);
void updateField(LogAccess *lad, char *val, int len);
diff --git a/proxy/logging/LogFile.cc b/proxy/logging/LogFile.cc
index acf956c81..c575a08d2 100644
--- a/proxy/logging/LogFile.cc
+++ b/proxy/logging/LogFile.cc
@@ -63,9 +63,10 @@
-------------------------------------------------------------------------*/
LogFile::LogFile(const char *name, const char *header, LogFileFormat format,
uint64_t signature, size_t ascii_buffer_size,
- size_t max_line_size, int pipe_buffer_size)
+ size_t max_line_size, int pipe_buffer_size, LogEscapeType
escape_type)
: m_file_format(format),
m_name(ats_strdup(name)),
+ m_escape_type(escape_type),
m_header(ats_strdup(header)),
m_signature(signature),
m_max_line_size(max_line_size),
@@ -83,7 +84,7 @@ LogFile::LogFile(const char *name, const char *header,
LogFileFormat format, uin
m_fd = -1;
m_ascii_buffer_size = (ascii_buffer_size < max_line_size ? max_line_size :
ascii_buffer_size);
- Debug("log-file", "exiting LogFile constructor, m_name=%s, this=%p", m_name,
this);
+ Debug("log-file", "exiting LogFile constructor, m_name=%s, this=%p,
escape_type=%d", m_name, this, escape_type);
}
/*-------------------------------------------------------------------------
@@ -627,7 +628,7 @@ LogFile::write_ascii_logbuffer3(LogBufferHeader
*buffer_header, const char *alt_
}
int bytes = LogBuffer::to_ascii(entry_header, format_type,
&ascii_buffer[fmt_buf_bytes], m_max_line_size - 1, fieldlist_str,
- printf_str, buffer_header->version,
alt_format);
+ printf_str, buffer_header->version,
alt_format, get_escape_type());
if (bytes > 0) {
fmt_buf_bytes += bytes;
diff --git a/proxy/logging/LogFile.h b/proxy/logging/LogFile.h
index 7a7618265..2d603d5e1 100644
--- a/proxy/logging/LogFile.h
+++ b/proxy/logging/LogFile.h
@@ -43,7 +43,7 @@ class LogFile : public LogBufferSink, public RefCountObj
{
public:
LogFile(const char *name, const char *header, LogFileFormat format, uint64_t
signature, size_t ascii_buffer_size = 4 * 9216,
- size_t max_line_size = 9216, int pipe_buffer_size = 0);
+ size_t max_line_size = 9216, int pipe_buffer_size = 0, LogEscapeType
escape_type = LOG_ESCAPE_NONE);
LogFile(const LogFile &);
~LogFile() override;
@@ -83,6 +83,12 @@ public:
return m_file_format;
}
+ LogEscapeType
+ get_escape_type() const
+ {
+ return m_escape_type;
+ }
+
const char *
get_format_name() const
{
@@ -120,6 +126,7 @@ public:
private:
char *m_name;
+ LogEscapeType m_escape_type;
public:
BaseLogFile *m_log; // BaseLogFile backs the actual file on disk
diff --git a/proxy/logging/LogFormat.cc b/proxy/logging/LogFormat.cc
index c1f190189..c3e8f8142 100644
--- a/proxy/logging/LogFormat.cc
+++ b/proxy/logging/LogFormat.cc
@@ -177,7 +177,7 @@ LogFormat::init_variables(const char *name, const char
*fieldlist_str, const cha
form %<symbol>.
-------------------------------------------------------------------------*/
-LogFormat::LogFormat(const char *name, const char *format_str, unsigned
interval_sec)
+LogFormat::LogFormat(const char *name, const char *format_str, unsigned
interval_sec, LogEscapeType escape_type)
: m_interval_sec(0),
m_interval_next(0),
m_agg_marshal_space(nullptr),
@@ -189,7 +189,8 @@ LogFormat::LogFormat(const char *name, const char
*format_str, unsigned interval
m_field_count(0),
m_printf_str(nullptr),
m_aggregate(false),
- m_format_str(nullptr)
+ m_format_str(nullptr),
+ m_escape_type(escape_type)
{
setup(name, format_str, interval_sec);
@@ -218,7 +219,8 @@ LogFormat::LogFormat(const LogFormat &rhs)
m_printf_str(nullptr),
m_aggregate(false),
m_format_str(nullptr),
- m_format_type(rhs.m_format_type)
+ m_format_type(rhs.m_format_type),
+ m_escape_type(rhs.m_escape_type)
{
if (m_valid) {
if (m_format_type == LOG_FORMAT_TEXT) {
diff --git a/proxy/logging/LogFormat.h b/proxy/logging/LogFormat.h
index 60c3a4235..8d6cfca1b 100644
--- a/proxy/logging/LogFormat.h
+++ b/proxy/logging/LogFormat.h
@@ -52,7 +52,7 @@ enum LogFileFormat {
class LogFormat : public RefCountObj
{
public:
- LogFormat(const char *name, const char *format_str, unsigned interval_sec =
0);
+ LogFormat(const char *name, const char *format_str, unsigned interval_sec =
0, LogEscapeType escape_type = LOG_ESCAPE_NONE);
LogFormat(const char *name, const char *fieldlist_str, const char
*printf_str, unsigned interval_sec = 0);
LogFormat(const LogFormat &rhs);
@@ -95,6 +95,11 @@ public:
{
return m_format_type;
}
+ LogEscapeType
+ escape_type() const
+ {
+ return m_escape_type;
+ }
char *
printf_str() const
{
@@ -158,6 +163,7 @@ private:
bool m_aggregate;
char *m_format_str;
LogFormatType m_format_type;
+ LogEscapeType m_escape_type;
public:
LINK(LogFormat, link);
diff --git a/proxy/logging/LogObject.cc b/proxy/logging/LogObject.cc
index e8d45c62c..f9a4e2ce0 100644
--- a/proxy/logging/LogObject.cc
+++ b/proxy/logging/LogObject.cc
@@ -122,8 +122,8 @@ LogObject::LogObject(LogConfig *cfg, const LogFormat
*format, const char *log_di
// compute_signature is a static function
m_signature = compute_signature(m_format, m_basename, m_flags);
- m_logFile =
- new LogFile(m_filename, header, file_format, m_signature,
cfg->ascii_buffer_size, cfg->max_line_size, m_pipe_buffer_size);
+ m_logFile = new LogFile(m_filename, header, file_format, m_signature,
cfg->ascii_buffer_size, cfg->max_line_size,
+ m_pipe_buffer_size, format->escape_type());
if (m_reopen_after_rolling) {
m_logFile->open_file();
diff --git a/proxy/logging/YamlLogConfigDecoders.cc
b/proxy/logging/YamlLogConfigDecoders.cc
index fca2817f5..328f2752a 100644
--- a/proxy/logging/YamlLogConfigDecoders.cc
+++ b/proxy/logging/YamlLogConfigDecoders.cc
@@ -28,7 +28,7 @@
#include <algorithm>
#include <memory>
-std::set<std::string> valid_log_format_keys = {"name", "format", "interval"};
+std::set<std::string> valid_log_format_keys = {"name", "format", "interval",
"escape"};
std::set<std::string> valid_log_filter_keys = {"name", "action", "condition"};
namespace YAML
@@ -69,7 +69,21 @@ convert<std::unique_ptr<LogFormat>>::decode(const Node
&node, std::unique_ptr<Lo
interval = node["interval"].as<unsigned>();
}
- logFormat.reset(new LogFormat(name.c_str(), format.c_str(), interval));
+ // escape type
+ LogEscapeType escape_type = LOG_ESCAPE_NONE; // default value
+ if (node["escape"]) {
+ std::string escape = node["escape"].as<std::string>();
+ if (!strncasecmp(escape.c_str(), "json", 4)) {
+ escape_type = LOG_ESCAPE_JSON;
+ } else if (!strncasecmp(escape.c_str(), "none", 4)) {
+ escape_type = LOG_ESCAPE_NONE;
+ } else {
+ throw YAML::ParserException(node.Mark(), "invalid 'escape' argument '" +
escape + "' for format name '" + name + "'");
+ }
+ Note("'escape' attribute for LogFormat object is; %s", escape.c_str());
+ }
+
+ logFormat.reset(new LogFormat(name.c_str(), format.c_str(), interval,
escape_type));
return true;
}
diff --git a/tests/gold_tests/logging/gold/field-json-test.gold
b/tests/gold_tests/logging/gold/field-json-test.gold
new file mode 100644
index 000000000..8bdc8c20e
--- /dev/null
+++ b/tests/gold_tests/logging/gold/field-json-test.gold
@@ -0,0 +1,3 @@
+{"foo":"ab\td\/ef","foo-slice":"\td"}
+{"foo":"ab\u001fd\/ef","foo-slice":"\u001fd"}
+{"foo":"abc\u007fde","foo-slice":"c"}
diff --git a/tests/gold_tests/logging/log-field-json.test.py
b/tests/gold_tests/logging/log-field-json.test.py
new file mode 100644
index 000000000..cd2110bb7
--- /dev/null
+++ b/tests/gold_tests/logging/log-field-json.test.py
@@ -0,0 +1,109 @@
+'''
+'''
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+import os
+
+Test.Summary = '''
+Test log fields.
+'''
+
+ts = Test.MakeATSProcess("ts", enable_cache=False)
+server = Test.MakeOriginServer("server")
+
+request_header = {'timestamp': 100, "headers": "GET /test-1 HTTP/1.1\r\nHost:
test-1\r\n\r\n", "body": ""}
+response_header = {
+ 'timestamp': 100,
+ "headers": "HTTP/1.1 200 OK\r\nTest: 1\r\nContent-Type:
application/json\r\nConnection: close\r\nContent-Type:
application/json\r\n\r\n",
+ "body": "Test 1"}
+server.addResponse("sessionlog.json", request_header, response_header)
+server.addResponse("sessionlog.json",
+ {'timestamp': 101,
+ "headers": "GET /test-2 HTTP/1.1\r\nHost: test-2\r\n\r\n",
+ "body": ""},
+ {'timestamp': 101,
+ "headers": "HTTP/1.1 200 OK\r\nTest: 2\r\nContent-Type:
application/jason\r\nConnection: close\r\nContent-Type:
application/json\r\n\r\n",
+ "body": "Test 2"})
+server.addResponse("sessionlog.json",
+ {'timestamp': 102,
+ "headers": "GET /test-3 HTTP/1.1\r\nHost: test-3\r\n\r\n",
+ "body": ""},
+ {'timestamp': 102,
+ "headers": "HTTP/1.1 200 OK\r\nTest: 3\r\nConnection:
close\r\nContent-Type: application/json\r\n\r\n",
+ "body": "Test 3"})
+
+nameserver = Test.MakeDNServer("dns", default='127.0.0.1')
+
+ts.Disk.records_config.update({
+ 'proxy.config.net.connections_throttle': 100,
+ 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}",
+ 'proxy.config.dns.resolv_conf': 'NULL'
+})
+# setup some config file for this server
+ts.Disk.remap_config.AddLine(
+ 'map / http://localhost:{}/'.format(server.Variables.Port)
+)
+
+ts.Disk.logging_yaml.AddLines(
+ '''
+logging:
+ formats:
+ - name: custom
+ escape: json
+ format: '{"foo":"%<{Foo}cqh>","foo-slice":"%<{Foo}cqh[2:-3]>"}'
+ logs:
+ - filename: field-json-test
+ format: custom
+'''.split("\n")
+)
+
+# #########################################################################
+# at the end of the different test run a custom log file should exist
+# Because of this we expect the testruns to pass the real test is if the
+# customlog file exists and passes the format check
+Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'field-json-test.log'),
+ exists=True, content='gold/field-json-test.gold')
+
+# first test is a miss for default
+tr = Test.AddTestRun()
+# Wait for the micro server
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(nameserver)
+# Delay on readiness of our ssl ports
+tr.Processes.Default.StartBefore(Test.Processes.ts)
+
+tr.Processes.Default.Command = 'curl --verbose --header "Host: test-1"
--header "Foo: ab\td/ef" http://localhost:{0}/test-1' .format(
+ ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = 'curl --verbose --header "Host: test-2"
--header "Foo: ab\x1fd/ef" http://localhost:{0}/test-2' .format(
+ ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = 'curl --verbose --header "Host: test-3"
--header "Foo: abc\x7fde" http://localhost:{0}/test-3' .format(
+ ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+
+# Wait for log file to appear, then wait one extra second to make sure TS is
done writing it.
+test_run = Test.AddTestRun()
+test_run.Processes.Default.Command = (
+ os.path.join(Test.Variables.AtsTestToolsDir, 'condwait') + ' 60 1 -f ' +
+ os.path.join(ts.Variables.LOGDIR, 'field-json-test.log')
+)
+test_run.Processes.Default.ReturnCode = 0