xref: /trafficserver/proxy/logging/LogUtils.cc (revision 745d38f6)
1 /** @file
2 
3  This file contains a set of utility routines that are used throughout the
4  logging implementation.
5 
6   @section license License
7 
8   Licensed to the Apache Software Foundation (ASF) under one
9   or more contributor license agreements.  See the NOTICE file
10   distributed with this work for additional information
11   regarding copyright ownership.  The ASF licenses this file
12   to you under the Apache License, Version 2.0 (the
13   "License"); you may not use this file except in compliance
14   with the License.  You may obtain a copy of the License at
15 
16       http://www.apache.org/licenses/LICENSE-2.0
17 
18   Unless required by applicable law or agreed to in writing, software
19   distributed under the License is distributed on an "AS IS" BASIS,
20   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21   See the License for the specific language governing permissions and
22   limitations under the License.
23  */
24 
25 #include <tscore/ink_align.h>
26 #include "tscore/ink_config.h"
27 #include "tscore/ink_string.h"
28 #include <tscore/ink_assert.h>
29 #include <tscore/BufferWriter.h>
30 
31 #ifdef TEST_LOG_UTILS
32 
33 #include "unit-tests/test_LogUtils.h"
34 
35 #else
36 
37 #include <MIME.h>
38 
39 #endif
40 
41 #include <cassert>
42 #include <cstdio>
43 #include <cstdlib>
44 #include <cstdarg>
45 #include <cstring>
46 #include <ctime>
47 #include <string_view>
48 #include <cstdint>
49 
50 #include <sys/time.h>
51 #include <sys/types.h>
52 #include <sys/stat.h>
53 #include <unistd.h>
54 #include <sys/socket.h>
55 #include <netinet/in.h>
56 #include <arpa/inet.h>
57 #include <netdb.h>
58 
59 #include "records/P_RecProcess.h"
60 // REC_SIGNAL_LOGGING_ERROR    is defined in I_RecSignals.h
61 // REC_SIGNAL_LOGGING_WARNING  is defined in I_RecSignals.h
62 
63 #include "LogUtils.h"
64 #include "LogLimits.h"
65 
66 /*-------------------------------------------------------------------------
67   LogUtils::timestamp_to_str
68 
69   This routine will convert a timestamp (seconds) into a short string,
70   of the format "%Y%m%d.%Hh%Mm%Ss".
71 
72   Since the resulting buffer is passed in, this routine is thread-safe.
73   Return value is the number of characters placed into the array, not
74   including the NULL.
75   -------------------------------------------------------------------------*/
76 
77 int
timestamp_to_str(long timestamp,char * buf,int size)78 LogUtils::timestamp_to_str(long timestamp, char *buf, int size)
79 {
80   static const char *format_str = "%Y%m%d.%Hh%Mm%Ss";
81   struct tm res;
82   struct tm *tms;
83   tms = ink_localtime_r((const time_t *)&timestamp, &res);
84   return strftime(buf, size, format_str, tms);
85 }
86 
87 /*-------------------------------------------------------------------------
88   LogUtils::timestamp_to_netscape_str
89 
90   This routine will convert a timestamp (seconds) into a string compatible
91   with the Netscape logging formats.
92 
93   This routine is intended to be called from the (single) logging thread,
94   and is therefore NOT MULTITHREADED SAFE.  There is a single, static,
95   string buffer that the time string is constructed into and returned.
96   -------------------------------------------------------------------------*/
97 
98 char *
timestamp_to_netscape_str(long timestamp)99 LogUtils::timestamp_to_netscape_str(long timestamp)
100 {
101   static char timebuf[64]; // NOTE: not MT safe
102   static long last_timestamp = 0;
103 
104   // safety check
105   if (timestamp < 0) {
106     static char bad_time[] = "Bad timestamp";
107     return bad_time;
108   }
109   //
110   // since we may have many entries per second, lets only do the
111   // formatting if we actually have a new timestamp.
112   //
113 
114   if (timestamp != last_timestamp) {
115     //
116     // most of this garbage is simply to find out the offset from GMT,
117     // taking daylight savings into account.
118     //
119     struct tm res;
120     struct tm *tms = ink_localtime_r((const time_t *)&timestamp, &res);
121 #if defined(solaris)
122     long zone = (tms->tm_isdst > 0) ? altzone : timezone;
123 #else
124     long zone = -tms->tm_gmtoff; // double negative!
125 #endif
126     int offset;
127     char sign;
128 
129     if (zone >= 0) {
130       offset = zone / 60;
131       sign   = '-';
132     } else {
133       offset = zone / -60;
134       sign   = '+';
135     }
136 
137     static char gmtstr[16];
138     int glen = snprintf(gmtstr, 16, "%c%.2d%.2d", sign, offset / 60, offset % 60);
139 
140     strftime(timebuf, 64 - glen, "%d/%b/%Y:%H:%M:%S ", tms);
141     ink_strlcat(timebuf, gmtstr, sizeof(timebuf));
142     last_timestamp = timestamp;
143   }
144   return timebuf;
145 }
146 
147 /*-------------------------------------------------------------------------
148   LogUtils::timestamp_to_date_str
149 
150   This routine will convert a timestamp (seconds) into a W3C compatible
151   date string.
152   -------------------------------------------------------------------------*/
153 
154 char *
timestamp_to_date_str(long timestamp)155 LogUtils::timestamp_to_date_str(long timestamp)
156 {
157   static char timebuf[64]; // NOTE: not MT safe
158   static long last_timestamp = 0;
159 
160   // safety check
161   if (timestamp < 0) {
162     static char bad_time[] = "Bad timestamp";
163     return bad_time;
164   }
165   //
166   // since we may have many entries per second, lets only do the
167   // formatting if we actually have a new timestamp.
168   //
169 
170   if (timestamp != last_timestamp) {
171     struct tm res;
172     struct tm *tms = ink_localtime_r((const time_t *)&timestamp, &res);
173     strftime(timebuf, 64, "%Y-%m-%d", tms);
174     last_timestamp = timestamp;
175   }
176   return timebuf;
177 }
178 
179 /*-------------------------------------------------------------------------
180   LogUtils::timestamp_to_time_str
181 
182   This routine will convert a timestamp (seconds) into a W3C compatible
183   time string.
184   -------------------------------------------------------------------------*/
185 
186 char *
timestamp_to_time_str(long timestamp)187 LogUtils::timestamp_to_time_str(long timestamp)
188 {
189   static char timebuf[64]; // NOTE: not MT safe
190   static long last_timestamp = 0;
191 
192   // safety check
193   if (timestamp < 0) {
194     static char bad_time[] = "Bad timestamp";
195     return bad_time;
196   }
197   //
198   // since we may have many entries per second, lets only do the
199   // formatting if we actually have a new timestamp.
200   //
201 
202   if (timestamp != last_timestamp) {
203     struct tm res;
204     struct tm *tms = ink_localtime_r((const time_t *)&timestamp, &res);
205     strftime(timebuf, 64, "%H:%M:%S", tms);
206     last_timestamp = timestamp;
207   }
208   return timebuf;
209 }
210 
211 /*-------------------------------------------------------------------------
212   LogUtils::manager_alarm
213 
214   This routine provides a convenient abstraction for sending the traffic
215   server manager process an alarm.  The logging system can send
216   LOG_ALARM_N_TYPES different types of alarms, as defined in LogUtils.h.
217   Subsequent alarms of the same type will override the previous alarm
218   entry.
219   -------------------------------------------------------------------------*/
220 
221 void
manager_alarm(LogUtils::AlarmType alarm_type,const char * msg,...)222 LogUtils::manager_alarm(LogUtils::AlarmType alarm_type, const char *msg, ...)
223 {
224   char msg_buf[LOG_MAX_FORMATTED_LINE];
225 
226   ink_assert(alarm_type >= 0 && alarm_type < LogUtils::LOG_ALARM_N_TYPES);
227 
228   if (msg == nullptr) {
229     snprintf(msg_buf, sizeof(msg_buf), "No Message");
230   } else {
231     va_list ap;
232     va_start(ap, msg);
233     vsnprintf(msg_buf, LOG_MAX_FORMATTED_LINE, msg, ap);
234     va_end(ap);
235   }
236 
237   switch (alarm_type) {
238   case LogUtils::LOG_ALARM_ERROR:
239     RecSignalManager(REC_SIGNAL_LOGGING_ERROR, msg_buf);
240     break;
241 
242   case LogUtils::LOG_ALARM_WARNING:
243     RecSignalManager(REC_SIGNAL_LOGGING_WARNING, msg_buf);
244     break;
245 
246   default:
247     ink_assert(false);
248   }
249 }
250 
251 /*-------------------------------------------------------------------------
252   LogUtils::strip_trailing_newline
253 
254   This routine examines the given string buffer to see if the last
255   character before the trailing NULL is a newline ('\n').  If so, it will
256   be replaced with a NULL, thus stripping it and reducing the length of
257   the string by one.
258   -------------------------------------------------------------------------*/
259 
260 void
strip_trailing_newline(char * buf)261 LogUtils::strip_trailing_newline(char *buf)
262 {
263   if (buf != nullptr) {
264     int len = ::strlen(buf);
265     if (len > 0) {
266       if (buf[len - 1] == '\n') {
267         buf[len - 1] = '\0';
268       }
269     }
270   }
271 }
272 
273 /*-------------------------------------------------------------------------
274   LogUtils::escapify_url_common
275 
276   This routine will escapify a URL to remove spaces (and perhaps other ugly
277   characters) from a URL and replace them with a hex escape sequence.
278   Since the escapes are larger (multi-byte) than the characters being
279   replaced, the string returned will be longer than the string passed.
280 
281   This is a worker function called by escapify_url and pure_escapify_url.  These
282   functions differ on whether the function tries to detect and avoid
283   double URL encoding (escapify_url) or not (pure_escapify_url)
284   -------------------------------------------------------------------------*/
285 
286 namespace
287 {
288 char *
escapify_url_common(Arena * arena,char * url,size_t len_in,int * len_out,char * dst,size_t dst_size,const unsigned char * map,bool pure_escape)289 escapify_url_common(Arena *arena, char *url, size_t len_in, int *len_out, char *dst, size_t dst_size, const unsigned char *map,
290                     bool pure_escape)
291 {
292   // codes_to_escape is a bitmap encoding the codes that should be escaped.
293   // These are all the codes defined in section 2.4.3 of RFC 2396
294   // (control, space, delims, and unwise) plus the tilde. In RFC 2396
295   // the tilde is an "unreserved" character, but we escape it because
296   // historically this is what the traffic_server has done.
297   // Note that we leave codes beyond 127 unmodified.
298   //
299   static const unsigned char codes_to_escape[32] = {
300     0xFF, 0xFF, 0xFF,
301     0xFF,             // control
302     0xB4,             // space " # %
303     0x00, 0x00,       //
304     0x0A,             // < >
305     0x00, 0x00, 0x00, //
306     0x1E, 0x80,       // [ \ ] ^ `
307     0x00, 0x00,       //
308     0x1F,             // { | } ~ DEL
309     0x00, 0x00, 0x00,
310     0x00, // all non-ascii characters unmodified
311     0x00, 0x00, 0x00,
312     0x00, //               .
313     0x00, 0x00, 0x00,
314     0x00, //               .
315     0x00, 0x00, 0x00,
316     0x00 //               .
317   };
318 
319   static char hex_digit[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
320 
321   if (!url || (dst && dst_size < len_in)) {
322     *len_out = 0;
323     return nullptr;
324   }
325 
326   if (!map) {
327     map = codes_to_escape;
328   }
329 
330   // Count specials in the url, assuming that there won't be any.
331   //
332   int count        = 0;
333   char *p          = url;
334   char *in_url_end = url + len_in;
335 
336   while (p < in_url_end) {
337     unsigned char c = *p;
338     if (map[c / 8] & (1 << (7 - c % 8))) {
339       ++count;
340     }
341     ++p;
342   }
343 
344   if (!count) {
345     // The common case, no escapes, so just return the source string.
346     //
347     *len_out = len_in;
348     if (dst) {
349       ink_strlcpy(dst, url, dst_size);
350     }
351     return url;
352   }
353 
354   // For each special char found, we'll need an escape string, which is
355   // three characters long.  Count this and allocate the string required.
356   //
357   // make sure we take into account the characters we are substituting
358   // for when we calculate out_len !!! in other words,
359   // out_len = len_in + 3*count - count
360   //
361   size_t out_len = len_in + 2 * count;
362 
363   if (dst && out_len > dst_size) {
364     *len_out = 0;
365     return nullptr;
366   }
367 
368   // To play it safe, we null terminate the string we return in case
369   // a module that expects null-terminated strings calls escapify_url,
370   // so we allocate an extra byte for the EOS
371   //
372   char *new_url;
373 
374   if (dst) {
375     new_url = dst;
376   } else {
377     new_url = arena->str_alloc(out_len + 1);
378   }
379 
380   char *from = url;
381   char *to   = new_url;
382 
383   while (from < in_url_end) {
384     unsigned char c = *from;
385     if (map[c / 8] & (1 << (7 - c % 8))) {
386       /*
387        * If two characters following a '%' don't need to be encoded, then it must
388        * mean that the three character sequence is already encoded.  Just copy it over.
389        */
390       if (!pure_escape && (*from == '%') && ((from + 2) < in_url_end)) {
391         unsigned char c1   = *(from + 1);
392         unsigned char c2   = *(from + 2);
393         bool needsEncoding = ((map[c1 / 8] & (1 << (7 - c1 % 8))) || (map[c2 / 8] & (1 << (7 - c2 % 8))));
394         if (!needsEncoding) {
395           out_len -= 2;
396           Debug("log-utils", "character already encoded..skipping %c, %c, %c", *from, *(from + 1), *(from + 2));
397           *to++ = *from++;
398           continue;
399         }
400       }
401 
402       *to++ = '%';
403       *to++ = hex_digit[c / 16];
404       *to++ = hex_digit[c % 16];
405     } else {
406       *to++ = *from;
407     }
408     from++;
409   }
410   *to = '\0'; // null terminate string
411 
412   *len_out = out_len;
413   return new_url;
414 }
415 } // namespace
416 
417 char *
escapify_url(Arena * arena,char * url,size_t len_in,int * len_out,char * dst,size_t dst_size,const unsigned char * map)418 LogUtils::escapify_url(Arena *arena, char *url, size_t len_in, int *len_out, char *dst, size_t dst_size, const unsigned char *map)
419 {
420   return escapify_url_common(arena, url, len_in, len_out, dst, dst_size, map, false);
421 }
422 
423 char *
pure_escapify_url(Arena * arena,char * url,size_t len_in,int * len_out,char * dst,size_t dst_size,const unsigned char * map)424 LogUtils::pure_escapify_url(Arena *arena, char *url, size_t len_in, int *len_out, char *dst, size_t dst_size,
425                             const unsigned char *map)
426 {
427   return escapify_url_common(arena, url, len_in, len_out, dst, dst_size, map, true);
428 }
429 
430 /*-------------------------------------------------------------------------
431   LogUtils::remove_content_type_attributes
432 
433   HTTP allows content types to have attributes following the main type and
434   subtype.  For example, attributes of text/html might be charset=iso-8859.
435   The content type attributes are not logged, so this function strips them
436   from the given buffer, if present.
437   -------------------------------------------------------------------------*/
438 
439 void
remove_content_type_attributes(char * type_str,int * type_len)440 LogUtils::remove_content_type_attributes(char *type_str, int *type_len)
441 {
442   if (!type_str) {
443     *type_len = 0;
444     return;
445   }
446   // Look for a semicolon and cut out everything after that
447   //
448   char *p = static_cast<char *>(memchr(type_str, ';', *type_len));
449   if (p) {
450     *type_len = p - type_str;
451   }
452 }
453 
454 /*
455 int
456 LogUtils::ip_to_str (unsigned ip, char *str, unsigned len)
457 {
458     int ret = snprintf (str, len, "%u.%u.%u.%u",
459                             (ip >> 24) & 0xff,
460                             (ip >> 16) & 0xff,
461                             (ip >> 8)  & 0xff,
462                             ip         & 0xff);
463 
464     return ((ret <= (int)len)? ret : (int)len);
465 }
466 */
467 
468 // return the seconds remaining until the time of the next roll given
469 // the current time, the rolling offset, and the rolling interval
470 //
471 int
seconds_to_next_roll(time_t time_now,int rolling_offset,int rolling_interval)472 LogUtils::seconds_to_next_roll(time_t time_now, int rolling_offset, int rolling_interval)
473 {
474   struct tm lt;
475   ink_localtime_r((const time_t *)&time_now, &lt);
476   int sidl = lt.tm_sec + lt.tm_min * 60 + lt.tm_hour * 3600;
477   int tr   = rolling_offset * 3600;
478   return ((tr >= sidl ? (tr - sidl) % rolling_interval : (86400 - (sidl - tr)) % rolling_interval));
479 }
480 
481 ts::TextView
get_unrolled_filename(ts::TextView rolled_filename)482 LogUtils::get_unrolled_filename(ts::TextView rolled_filename)
483 {
484   auto unrolled_name = rolled_filename;
485 
486   // A rolled log will look something like:
487   //   squid.log_some.hostname.com.20191029.18h15m02s-20191029.18h30m02s.old
488   auto suffix = rolled_filename;
489 
490   suffix.remove_prefix_at('.');
491   // Using the above squid.log example, suffix now looks like:
492   //   log_some.hostname.com.20191029.18h15m02s-20191029.18h30m02s.old
493 
494   // Some suffixes do not have the hostname.  Rolled diags.log files will look
495   // something like this, for example:
496   //   diags.log.20191114.21h43m16s-20191114.21h43m17s.old
497   //
498   // For these, the second delimeter will be a period. For this reason, we also
499   // split_prefix_at with a period as well.
500   if (suffix.split_prefix_at('_') || suffix.split_prefix_at('.')) {
501     // ' + 1' to remove the '_' or second '.':
502     return unrolled_name.remove_suffix(suffix.size() + 1);
503   }
504   // If there isn't a '.' or an '_' after the first '.', then this
505   // doesn't look like a rolled file.
506   return rolled_filename;
507 }
508 
509 // Checks if the file pointed to by full_filename either is a regular
510 // file or a pipe and has write permission, or, if the file does not
511 // exist, if the path prefix of full_filename names a directory that
512 // has both execute and write permissions, so there will be no problem
513 // creating the file. If the size_bytes pointer is not NULL, it returns
514 // the size of the file through it.
515 // Also checks the current size limit for the file. If there is a
516 // limit and has_size_limit is not null, *has_size_limit is set to
517 // true. If there is no limit and has_size_limit is not null,
518 // *has_size_limit is set to false.  If there is a limit and if the
519 // current_size_limit_bytes pointer is not null, it returns the limit
520 // through it.
521 //
522 // returns:
523 //  0 on success
524 // -1 on system error (no permission, etc.)
525 //  1 if the file full_filename points to is neither a regular file
526 //    nor a pipe
527 //
528 int
file_is_writeable(const char * full_filename,off_t * size_bytes,bool * has_size_limit,uint64_t * current_size_limit_bytes)529 LogUtils::file_is_writeable(const char *full_filename, off_t *size_bytes, bool *has_size_limit, uint64_t *current_size_limit_bytes)
530 {
531   int ret_val = 0;
532   int e;
533   struct stat stat_data;
534 
535   e = stat(full_filename, &stat_data);
536   if (e == 0) {
537     // stat succeeded, check if full_filename points to a regular
538     // file/fifo and if so, check if file has write permission
539     //
540     if (!(S_ISREG(stat_data.st_mode) || S_ISFIFO(stat_data.st_mode))) {
541       ret_val = 1;
542     } else if (!(stat_data.st_mode & S_IWUSR)) {
543       errno   = EACCES;
544       ret_val = -1;
545     }
546     if (size_bytes) {
547       *size_bytes = stat_data.st_size;
548     }
549   } else {
550     // stat failed
551     //
552     if (errno != ENOENT) {
553       // can't stat file
554       //
555       ret_val = -1;
556     } else {
557       // file does not exist, check that the prefix is a directory with
558       // write and execute permissions
559 
560       char *dir;
561       char *prefix = nullptr;
562 
563       // search for forward or reverse slash in full_filename
564       // starting from the end
565       //
566       const char *slash = strrchr(full_filename, '/');
567       if (slash) {
568         size_t prefix_len = slash - full_filename + 1;
569         prefix            = new char[prefix_len + 1];
570         memcpy(prefix, full_filename, prefix_len);
571         prefix[prefix_len] = 0;
572         dir                = prefix;
573       } else {
574         dir = (char *)"."; // full_filename has no prefix, use .
575       }
576 
577       // check if directory is executable and writeable
578       //
579       e = access(dir, X_OK | W_OK);
580       if (e < 0) {
581         ret_val = -1;
582       } else {
583         if (size_bytes) {
584           *size_bytes = 0;
585         }
586       }
587 
588       if (prefix) {
589         delete[] prefix;
590       }
591     }
592   }
593 
594   // check for the current filesize limit
595   //
596   if (ret_val == 0) {
597     struct rlimit limit_data;
598     e = getrlimit(RLIMIT_FSIZE, &limit_data);
599     if (e < 0) {
600       ret_val = -1;
601     } else {
602       if (limit_data.rlim_cur != static_cast<rlim_t> RLIM_INFINITY) {
603         if (has_size_limit) {
604           *has_size_limit = true;
605         }
606         if (current_size_limit_bytes) {
607           *current_size_limit_bytes = limit_data.rlim_cur;
608         }
609       } else {
610         if (has_size_limit) {
611           *has_size_limit = false;
612         }
613       }
614     }
615   }
616 
617   return ret_val;
618 }
619 
620 namespace
621 {
622 // Get a string out of a MIMEField using one of its member functions, and put it into a buffer writer, terminated with a nul.
623 //
624 void
marshalStr(ts::FixedBufferWriter & bw,const MIMEField & mf,const char * (MIMEField::* get_func)(int * length)const)625 marshalStr(ts::FixedBufferWriter &bw, const MIMEField &mf, const char *(MIMEField::*get_func)(int *length) const)
626 {
627   int length;
628   const char *data = (mf.*get_func)(&length);
629 
630   if (!data or (*data == '\0')) {
631     // Empty string.  This is a problem, since it would result in two successive nul characters, which indicates the end of the
632     // marshaled hearer.  Change the string to a single blank character.
633 
634     static const char Blank[] = " ";
635     data                      = Blank;
636     length                    = 1;
637   }
638 
639   bw << std::string_view(data, length) << '\0';
640 }
641 
642 void
unmarshalStr(ts::FixedBufferWriter & bw,const char * & data)643 unmarshalStr(ts::FixedBufferWriter &bw, const char *&data)
644 {
645   bw << '{';
646 
647   while (*data) {
648     bw << *(data++);
649   }
650 
651   // Skip over terminal nul.
652   ++data;
653 
654   bw << '}';
655 }
656 
657 } // end anonymous namespace
658 
659 namespace LogUtils
660 {
661 // Marshals header tags and values together, with a single terminating nul character.  Returns buffer space required.  'buf' points
662 // to where to put the marshaled data.  If 'buf' is null, no data is marshaled, but the function returns the amount of space that
663 // would have been used.
664 //
665 int
marshalMimeHdr(MIMEHdr * hdr,char * buf)666 marshalMimeHdr(MIMEHdr *hdr, char *buf)
667 {
668   std::size_t bwSize = buf ? SIZE_MAX : 0;
669 
670   ts::FixedBufferWriter bw(buf, bwSize);
671 
672   if (hdr) {
673     MIMEFieldIter mfIter;
674     const MIMEField *mfp = hdr->iter_get_first(&mfIter);
675 
676     while (mfp) {
677       marshalStr(bw, *mfp, &MIMEField::name_get);
678       marshalStr(bw, *mfp, &MIMEField::value_get);
679 
680       mfp = hdr->iter_get_next(&mfIter);
681     }
682   }
683 
684   bw << '\0';
685 
686   return int(INK_ALIGN_DEFAULT(bw.extent()));
687 }
688 
689 // Unmarshaled/printable format is {{{tag1}:{value1}}{{tag2}:{value2}} ... }
690 //
691 int
unmarshalMimeHdr(char ** buf,char * dest,int destLength)692 unmarshalMimeHdr(char **buf, char *dest, int destLength)
693 {
694   ink_assert(*buf != nullptr);
695 
696   const char *data = *buf;
697 
698   ink_assert(data != nullptr);
699 
700   ts::FixedBufferWriter bw(dest, destLength);
701 
702   bw << '{';
703 
704   int pairEndFallback{0}, pairEndFallback2{0}, pairSeparatorFallback{0};
705 
706   while (*data) {
707     if (!bw.error()) {
708       pairEndFallback2 = pairEndFallback;
709       pairEndFallback  = bw.size();
710     }
711 
712     // Add open bracket of pair.
713     //
714     bw << '{';
715 
716     // Unmarshal field name.
717     unmarshalStr(bw, data);
718 
719     bw << ':';
720 
721     if (!bw.error()) {
722       pairSeparatorFallback = bw.size();
723     }
724 
725     // Unmarshal field value.
726     unmarshalStr(bw, data);
727 
728     // Add close bracket of pair.
729     bw << '}';
730 
731   } // end for loop
732 
733   bw << '}';
734 
735   if (bw.error()) {
736     // The output buffer wasn't big enough.
737 
738     static std::string_view FULL_ELLIPSES("...}}}");
739 
740     if ((pairSeparatorFallback > pairEndFallback) and ((pairSeparatorFallback + 7) <= destLength)) {
741       // In the report, we can show the existence of the last partial tag/value pair, and maybe part of the value.  If we only
742       // show part of the value, we want to end it with an elipsis, to make it clear it's not complete.
743 
744       bw.reduce(destLength - FULL_ELLIPSES.size());
745       bw << FULL_ELLIPSES;
746 
747     } else if (pairEndFallback and (pairEndFallback < destLength)) {
748       bw.reduce(pairEndFallback);
749       bw << '}';
750 
751     } else if ((pairSeparatorFallback > pairEndFallback2) and ((pairSeparatorFallback + 7) <= destLength)) {
752       bw.reduce(destLength - FULL_ELLIPSES.size());
753       bw << FULL_ELLIPSES;
754 
755     } else if (pairEndFallback2 and (pairEndFallback2 < destLength)) {
756       bw.reduce(pairEndFallback2);
757       bw << '}';
758 
759     } else if (destLength > 1) {
760       bw.reduce(1);
761       bw << '}';
762 
763     } else {
764       bw.reduce(0);
765     }
766   }
767 
768   *buf += INK_ALIGN_DEFAULT(data - *buf + 1);
769 
770   return bw.size();
771 }
772 
773 } // end namespace LogUtils
774