// ball_recordjsonformatter.h                                         -*-C++-*-

#ifndef INCLUDED_BALL_RECORDJSONFORMATTER
#define INCLUDED_BALL_RECORDJSONFORMATTER

#include <bsls_ident.h>
BSLS_IDENT("$Id: $")

//@PURPOSE: Provide a formatter for log records that renders output in JSON.
//
//@CLASSES:
//  ball::RecordJsonFormatter: formatter for rendering log records in JSON
//
//@SEE_ALSO: ball_record, ball_recordattributes
//
//@DESCRIPTION: This component provides a function object class,
// 'ball::RecorJsonFormatter', that formats a log record as JSON text elements
// according to a format specification (see {'Record Format Specification'}).
// 'ball::RecordJsonFormatter' is designed to match the function signature
// expected by many concrete 'ball::Observer' implementations that publish log
// records (for example, see 'ball::FileObserver2::setLogFileFunctor').
//
// NOTE: 'ball::RecorJsonFormatter' renders individual log records as JSON,
// but, for example, a resulting log file would contain a sequence of JSON
// strings, which is not itself valid JSON text.
//
///Record Format Specification
///---------------------------
// A format specification is, itself, a JSON array, supplied to a
// 'RecordJsonFormatter' object by the 'setFormat' function.  If no format is
// specified, the default format is used.  Each array element specifies the
// format of a log record field or a user-defined attribute.  Here is a simple
// example:
//..
//  [{"timestamp":{"format":"iso8601"}}, "pid", "tid", "severity", "message"]
//..
// would a result in a log record like:
//..
// { "timestamp": "2020-08-28T14:43:50.375Z",
//   "pid": 2313,
//   "tid": 12349388604,
//   "severity": "INFO",
//   "message": "Hello, world!"
//  }
//..
// Again, the format specification is a JSON array, each element of which can
// be one of the following:
//
//: o A string containing the name of the fixed record field or the name of the
//:   user-defined attribute, in which case the field or attribute will be
//:   published in the default format.  For example, '[timestamp]', would
//:   display the timestamp as: '{"timestamp": "2020-08-28T14:43:50.375Z"}'.
//:
//: o A JSON object having the name of the fixed field or user-defined
//:   attribute and a set of key-values pairs used to customize the output
//:   format.  For example,
//:   '[{"timestamp": {"name": "My Time", "format": "bdePrint"}}]', would
//:   display the timestamp as: '{"My Time": "28AUG2020_14:43:50.375"}'.
//
///Field Format Specification
/// - - - - - - - - - - - - -
// The following table lists the predefined string values for each fixed field
// and user-defined attributes in the log record:
//..
//     Tag                 Description                   Example
// --------------    -------------------------        -------------
// "timestamp"       creation date and time           ["timestamp"]
// "pid"             process id of creator            ["pid"]
// "tid"             thread id of creator             ["tid"]
// "file"            file where created  (__FILE__)   ["file"]
// "line"            line number in file (__LINE__)   ["line"]
// "category"        category of logged record        ["category"]
// "severity"        severity of logged record        ["severity"]
// "message"         log message text                 ["message"]
// "attributes"      all user-defiend attributes      ["attributes"]
// <attribute name>  specific user-defined attribute  ["bas.uuid"]
//..
// The output format of each field can be customized by replacing a string
// value in the JSON array with a JSON object having the same name and a set of
// key-value pairs (attributes).
//
///Verifying the Format Specification for 'setFormat'
///- - - - - - - - - - - - - - - - - - - - - -- - - -
// The sections that follow describe the set of fields that can be provided in
// the format specification supplied to 'setFormat'.
// 'RecordJsonFormatter::setFormat' will ignore fields in the provided format
// specification that are unknown, but will report an error if a known field
// contains a property that is not supported.  For example: a format
// specification '["pid", { "timestamp" : {"unknown field!": "value"} }] will
// be accepted, but '["pid", {"timestamp": {"format": "unknown format" }}]'
// will produce an error.
//
// Each key-value pair of a JSON object that specifies a format of an output of
// a fixed record field or a user-defined attribute has the following
// constrains:
//
//: o The key is a string of known value listed in the column "Key" in the
//:   tables below.  Any string that does not match the listed values is
//:   ignored.
//:
//: o The value is a string of known value (except for the "name" key) in the
//:   column "Value Constraint" in the tables below.  If the value does not
//:   match the string values specified in the tables, the format specification
//:   is considered to be inconsistent with the expected schema, and is
//:   rejected by the 'RecordJsonFormatter::setFormat' method.
//
///The "timestamp" field format
///-  -  -  -  -  -  -  -  -  -
// The format attributes of the "timestamp" object are given in the following
// table:
//..
//                                              Value             Default
//             Key              Description     Constraint        Value
//  ------------------------  ----------------  -----------     ------------
//  "name"                    name by which     JSON string     "timestamp"
//                            "timestamp" will
//                            be published
//
//  "format"                  datetime format   "iso8601",     "iso8601"
//                                              "bdePrint"
//                                              (*Note*)
//
//  "fractionalSecPrecision"  second precision  "none",         "microseconds"
//                                              "milliseconds",
//                                              "microseconds"
//
//  "timeZone"                time zone         "utc",          "utc"
//                                              "local"
//..
// *Note*: The default "bdePrint" format denotes the following datetime format:
//..
//   DDMonYYYY_HH:MM:SS.mmm
//..
// For example, the following record format specification:
//..
//   [ { "timestamp": { "name": "Time",
//                      "fractionalSecPrecision": "microseconds",
//                      "timeZone": "local" } }
//   ]
//..
// would a result in a log record like:
//..
//   { "Time": "28AUG2020_17:43:50.375345" }
//..
//
///The "pid" (process Id) field format
/// -  -  -  -  -  -  -  -  -  -  -  -
// The format attributes of the process Id field are given in the following
// table:
//..
//                                                   Value        Default
//    Key                 Description                Constraint   Value
//  ------    -------------------------------------  -----------  -------
//  "name"    name by which "pid" will be published  JSON string  "pid"
//..
// For example, the following record format specification:
//..
//   [ { "pid": { "name": "Process Id" } } ]
//..
// would a result in a log record like:
//..
//   { "Process Id": 2313 }
//..
//
///The "tid" (thread Id) field format
///-  -  -  -  -  -  -  -  -  -  -  -
// The format attributes of the thread Id field are given in the following
// table:
//..
//                                                     Value        Default
//    Key                   Description                Constraint   Value
//  --------    -------------------------------------  -----------  ---------
//  "name"      name by which "tid" will be published  JSON string  "tid"
//
//  "format"    output format                          "decimal",   "decimal"
//                                                     "hex"
//..
// For example, the following record format specification:
//..
//   [ { "tid": { "name": "Thread Id",
//                "format": "hex" } }
//   ]
//..
// would a result in a log record like:
//..
//   { "Thread Id": 0xA7654EFF3540 }
//..
//
///The "file" field format
/// -  -  -  -  -  -  -  -  -  -  -  -
// The format attributes of the "file" field are given in the following
// table:
//..
//                                                                Default
//    Key          Description             Value Constraint       Value
//  ------    --------------------  ----------------------------- -------
//  "name"    name by which "file"  JSON string                   "file"
//            will be published
//
//  "path"    file path             "full" (__FILE__),            "full"
//                                  "file" (basename of __FILE__)
//..
// For example, the following record format specification:
//..
//   [ { "file": { "name": "File",
//                 "path": "file" } }
//   ]
//..
// would a result in a log record like:
//..
//   { "File": "test.cpp" }
//..
//
///The "line" field format
/// -  -  -  -  -  -  -  -
// The format attributes of the "line" field are given in the following
// table:
//..
//                                                    Value        Default
//    Key                Description                  Constraint   Value
//  ------   ---------------------------------------  -----------  -------
//  "name"   name by which "line" will be published   JSON string  "line"
//..
// For example, the following record format specification:
//..
//   [ { "line": { "name": "Line" } } ]
//..
// would a result in a log record like:
//..
//   { "Line": 512 }
//..
//
///The "category" field format
///  -  -  -  -  -  -  -  -  -
// The format attributes of the "category" field are given in the following
// table:
//..
//                                                      Value         Default
//    Key                 Description                   Constraint    Value
//  ------  ------------------------------------------  -----------  ----------
//  "name"  name by which "category" will be published  JSON string  "category"
//..
// For example, the following record format specification:
//..
//   [ { "category": { "name": "Category" } } ]
//..
// would a result in a log record like:
//..
//   { "category": "Server" }
//..
//
///The "severity" field format
///  -  -  -  -  -  -  -  -  -
// The format attributes of the "severity" field are given in the following
// table:
//..
//                                                      Value         Default
//    Key                  Description                  Constraint    Value
//  ------  ------------------------------------------  -----------  ----------
//  "name"  name by which "severity" will be published  JSON string  "severity"
//..
// For example, the following record format specification:
//..
//   [ { "severity": { "name": "severity" } } ]
//..
// would a result in a log record like:
//..
//   { "Severity": "ERROR" }
//..
//
///The "message" field format
///  -  -  -  -  -  -  -  -  -
// A message is a JSON string which is a sequence of zero or more Unicode
// characters, wrapped in double quotes, using backslash escapes: (\", \\, \/,
// \b, \f, \n, \r, \t, \u{4 hex digits}).
//
// The format attributes of the "message" field are given in the following
// table:
//..
//                                                     Value         Default
//    Key                 Description                  Constraint    Value
//  ------  -----------------------------------------  -----------  ---------
//  "name"  name by which "message" will be published  JSON string  "message"
//..
// For example, the following record format specification:
//..
//   [ { "message": { "name": "msg" } } ]
//..
// would a result in a log record like:
//..
//   { "msg": "Log message" }
//..
//
///The "attributes" format
///-  -  -  -  -  -  -  -  -  -  -
// The "attributes" JSON object has no attributes. For example, the following
// record format specification:
//..
//   [ "attributes" ]
//..
// would (assuming their are two attributes "bas.requestid" and
// "mylib.security") result in a log record like:
//..
//   { "bas.requestid": 12345, "mylib.security": "My Security" }
//..
//
///A user-defined attribute format
///-  -  -  -  -  -  -  -  -  -  -
// Each user-defined attribute has a single "name" attribute that can be used
// to rename the user-defined attribute:
//..
//                                         Value        Default
//    Key         Description              Constraint   Value
//  ------   ----------------------------  -----------  -------
//  "name"   name by which a user-defined  JSON string   none
//           attribute will be published
//..
// For example, the following record format specification:
//..
//   [ { "bas.uuid": { "name": "BAS.UUID" } } ]
//..
// would a result in a log record like:
//..
//   { "BAS.UUID": 3593 }
//..
//
///The Record Separator
///--------------------
// The record separator is a string that is printed after each formatted
// record.  The default value of the record separator is a single newline, but
// it can be set to any string of the user's choice using the
// 'RecordJsonFormatter::setRecordSeparator' function.
//
///Usage
///-----
// This section illustrates intended use of this component.
//
///Example: Format log records as JSON and render them to 'stdout'
///- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Suppose an application needs to format log records as JSON and output them
// to 'stdout'.
//
// First we instantiate a JSON record formatter:
//..
//   ball::RecordJsonFormatter formatter;
//..
// Next we set a format specification to the newly created 'formatter':
//..
//  int rc = formatter.setFormat("[\"tid\",\"message\"]");
//  assert(0 == rc);
//..
// The chosen format specification indicates that, when a record is formatted
// using 'formatter', the thread Id attribute of the record will be output
// followed by the message attribute of the record.
//
// Then we create a default 'ball::Record' and set the thread Id and message
// attributes of the record to dummy values:
//..
//  ball::Record record;
//
//  record.fixedFields().setThreadID(6);
//  record.fixedFields().setMessage("Hello, World!");
//..
// Next, invocation of the 'formatter' function object to format 'record' to
// 'bsl::cout':
//..
//  formatter(bsl::cout, record);
//..
// yields this output, which is terminated by a single newline:
//..
//  {"tid":6,"message":"Hello, World!"}
//..
// Finally, we change the record separator and format the same record again:
//..
//  formatter.setFormat("\n\n");
//  formatter(bsl::cout, record);
//..
// The record is printed in the same format, but now terminated by two
// newlines:
//..
//  {"tid":6,"message":"Hello, World!"}
//
//..

#include <balscm_version.h>

#include <bslma_allocator.h>
#include <bslma_stdallocator.h>
#include <bslma_usesbslmaallocator.h>

#include <bslmf_isbitwisemoveable.h>
#include <bslmf_movableref.h>
#include <bslmf_nestedtraitdeclaration.h>

#include <bsls_keyword.h>

#include <bsl_functional.h>
#include <bsl_iosfwd.h>
#include <bsl_string.h>
#include <bsl_vector.h>

#include <bsl_ostream.h>

namespace BloombergLP {
namespace baljsn { class SimpleFormatter; }
namespace ball {

class Record;
class RecordAttributes;
class RecordJsonFormatter_FieldFormatter;

                        // =========================
                        // class RecordJsonFormatter
                        // =========================

class RecordJsonFormatter {
    // This class provides a function object that formats a log record as JSON
    // text elements and renders them to an output stream.  The overloaded
    // 'operator()' provided by the class formats log record according to JSON
    // message format specification supplied at construction (either by the
    // 'setFormat' manipulator or by default) and outputs the result to the
    // stream.  This functor type is designed to match the function signature
    // expected by many concrete 'ball::Observer' implementations that publish
    // log records (for example, see 'ball::FileObserver2::setLogFileFunctor').

    // PRIVATE TYPES
    typedef bslmf::MovableRefUtil  MoveUtil;
        // 'MoveUtil' is an alias for 'bslmf::MovableRefUtil'.

  public:
    // TYPES
    typedef bsl::vector<RecordJsonFormatter_FieldFormatter*> FieldFormatters;
        // 'FieldFormatters' is an alias for a vector of the
        // 'RecordJsonFormatter_FieldFormatter' objects, each of which is
        // initialized from the format specification and responsible for
        // rendering one field of a 'ball::Record' to the output JSON stream.

    typedef bsl::allocator<char> allocator_type;

  private:
    // DATA
    bsl::string     d_formatSpec;       // format specification (in JSON)

    bsl::string     d_recordSeparator;  // string to print after each record

    FieldFormatters d_fieldFormatters;  // field formatters configured
                                        // according to the format
                                        // specification

    // CLASS METHODS
    static
    void releaseFieldFormatters(FieldFormatters *formatters);
        // Destroy the field formatters contained in the specified
        // 'formatters'.

  public:
    // TRAITS
    BSLMF_NESTED_TRAIT_DECLARATION(RecordJsonFormatter,
                                   bslma::UsesBslmaAllocator);

    // CREATORS
    explicit RecordJsonFormatter(
                           const allocator_type& allocator = allocator_type());
        // Create a record JSON formatter having a default format specification
        // and record separator.  Optionally specify an 'allocator' (e.g., the
        // address of a 'bslma::Allocator' object) to supply memory; otherwise,
        // the allocator is used.  The default format specification is:
        //..
        //  ["timestamp", "processId", "threadId", "severity", "file", "line",
        //   "category",  "message", "attributes"]
        //..
        // The default record separator is "\n".

    RecordJsonFormatter(
                      const RecordJsonFormatter& original,
                      const allocator_type&      allocator = allocator_type());
        // Create a record JSON formatter initialized to the value of the
        // specified 'original' record formatter.  Optionally specify an
        // 'allocator' (e.g., the address of a 'bslma::Allocator' object) to
        // supply memory; otherwise, the default allocator is used.

    RecordJsonFormatter(bslmf::MovableRef<RecordJsonFormatter> original)
                                                         BSLS_KEYWORD_NOEXCEPT;
        // Create a record JSON formatter having the same format specification
        // and record separator as in the specified 'original' formatter, and
        // adopting all outstanding memory allocations and the allocator
        // associated with the 'original' formatter.  'original' is left in a
        // valid but unspecified state.

    RecordJsonFormatter(bslmf::MovableRef<RecordJsonFormatter> original,
                        const allocator_type&                  allocator);
        // Create a record JSON formatter, having the same format specification
        // and record separator as in the specified 'original' formatter.  The
        // format specification of 'original' is moved to the new object, and
        // all outstanding memory allocations and the specified 'allocator' are
        // adopted if 'allocator == original.allocator()'.  'original' is left
        // in a valid but unspecified state.

    ~RecordJsonFormatter();
        // Destroy this object.

    // MANIPULATORS
    RecordJsonFormatter& operator=(const RecordJsonFormatter& rhs);
        // Assign to this object the value of the specified 'rhs' object, and
        // return a reference providing modifiable access to this object.

    RecordJsonFormatter& operator=(bslmf::MovableRef<RecordJsonFormatter> rhs);
        // Assign to this object the format specification and record separator
        // of the specified 'rhs' object, and return a reference providing
        // modifiable access to this object.  The format specification and
        // record separator of 'rhs' are moved to this object, and all
        // outstanding memory allocations and the allocator associated with
        // 'rhs' are adopted if 'allocator() == rhs.allocator()'.  'rhs' is
        // left in a valid but unspecified state.

    int setFormat(const bsl::string_view& format);
        // Set the message format specification (see
        // {'Record Format Specification'}) of this record JSON formatter to
        // the specified 'format'.  Return 0 on success, and a non-zero value
        // otherwise (if 'format' is not valid JSON *or* not a JSON conforming
        // to the expected schema).

    void setRecordSeparator(const bsl::string_view& recordSeparator);
        // Set the record separator for this record JSON formatter to the
        // specified 'recordSeparator'.  The 'recordSeparator' will be printed
        // by each invocation of 'operator()' after the formatted record.  The
        // default is a single newline character, "\n".

    // ACCESSORS
    void operator()(bsl::ostream& stream, const Record& record) const;
        // Format the specified 'record' according to the current 'format' and
        // 'recordSeparator' to the specified 'stream'.

    const bsl::string& format() const;
        // Return the message format specification of this record JSON
        // formatter.  See {'Record Format Specification'}.

    const bsl::string& recordSeparator() const;
        // Return the record separator of this record JSON formatter.

                                  // Aspects

    const allocator_type& allocator() const;
        // Return the allocator used by this object to supply memory.

};

// FREE OPERATORS
bool operator==(const RecordJsonFormatter& lhs,
                const RecordJsonFormatter& rhs);
    // Return 'true' if the specified 'lhs' and 'rhs' record formatters have
    // the same value, and 'false' otherwise.  Two record formatters have the
    // same value if they have the same format specification and record
    // separator.

bool operator!=(const RecordJsonFormatter& lhs,
                const RecordJsonFormatter& rhs);
    // Return 'true' if the specified 'lhs' and 'rhs' record formatters do not
    // have the same value, and 'false' otherwise.  Two record formatters
    // differ in value if their format specifications or record separators
    // differ.

// ============================================================================
//                              INLINE DEFINITIONS
// ============================================================================

                        // -------------------------
                        // class RecordJsonFormatter
                        // -------------------------

// CREATORS
inline
RecordJsonFormatter::RecordJsonFormatter(const RecordJsonFormatter& original,
                                         const allocator_type&      allocator)
: d_formatSpec(original.d_formatSpec, allocator)
, d_recordSeparator(original.d_recordSeparator, allocator)
, d_fieldFormatters(allocator)
{
    int rc = setFormat(d_formatSpec);
    (void) rc;
    BSLS_ASSERT(0 == rc);
}

inline
RecordJsonFormatter::RecordJsonFormatter(
    bslmf::MovableRef<RecordJsonFormatter> original) BSLS_KEYWORD_NOEXCEPT
: d_formatSpec(MoveUtil::move(MoveUtil::access(original).d_formatSpec)),
  d_recordSeparator(
      MoveUtil::move(MoveUtil::access(original).d_recordSeparator)),
  d_fieldFormatters(
      MoveUtil::move(MoveUtil::access(original).d_fieldFormatters))
{
}

inline
RecordJsonFormatter::RecordJsonFormatter(
    bslmf::MovableRef<RecordJsonFormatter> original,
    const allocator_type&                  allocator)
: d_formatSpec(MoveUtil::move(MoveUtil::access(original).d_formatSpec),
               allocator)
, d_recordSeparator(
      MoveUtil::move(MoveUtil::access(original).d_recordSeparator),
      allocator)
, d_fieldFormatters(allocator)
{
    if (MoveUtil::access(original).allocator() == allocator) {
        d_fieldFormatters = MoveUtil::move(
                                 MoveUtil::access(original).d_fieldFormatters);
    }
    else {
        int rc = setFormat(d_formatSpec);
        (void) rc;
        BSLS_ASSERT(0 == rc);
    }
}

// MANIPULATORS
inline
RecordJsonFormatter& RecordJsonFormatter::operator=(
                                    bslmf::MovableRef<RecordJsonFormatter> rhs)
{
    if (this != &MoveUtil::access(rhs)) {
        d_recordSeparator = MoveUtil::move(
                                  MoveUtil::access(rhs).d_recordSeparator);
        if (MoveUtil::access(rhs).allocator() == allocator()) {
            releaseFieldFormatters(&d_fieldFormatters);
            d_formatSpec      = MoveUtil::move(
                                      MoveUtil::access(rhs).d_formatSpec);
            d_fieldFormatters = MoveUtil::move(
                                      MoveUtil::access(rhs).d_fieldFormatters);
        }
        else {
            int rc = setFormat(MoveUtil::access(rhs).d_formatSpec);
            (void) rc;
            BSLS_ASSERT(0 == rc);
        }
    }

    return *this;
}

inline
void RecordJsonFormatter::setRecordSeparator(
                                       const bsl::string_view& recordSeparator)
{
    d_recordSeparator = recordSeparator;
}

// ACCESSORS
inline
const bsl::string& RecordJsonFormatter::format() const
{
    return d_formatSpec;
}

inline
const bsl::string& RecordJsonFormatter::recordSeparator() const
{
    return d_recordSeparator;
}

                                  // Aspects

inline
const RecordJsonFormatter::allocator_type&
RecordJsonFormatter::allocator() const
{
    return d_formatSpec.allocator();
}


}  // close package namespace

// FREE OPERATORS
inline
bool ball::operator==(const RecordJsonFormatter& lhs,
                      const RecordJsonFormatter& rhs)
{
    return lhs.format() == rhs.format() &&
           lhs.recordSeparator() == rhs.recordSeparator();
}

inline
bool ball::operator!=(const RecordJsonFormatter& lhs,
                      const RecordJsonFormatter& rhs)
{
    return !(lhs == rhs);
}

}  // close enterprise namespace

#endif

// ----------------------------------------------------------------------------
// Copyright 2020 Bloomberg Finance L.P.
//
// 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.
// ----------------------------- END-OF-FILE ----------------------------------