Attn: Add support for raw PEL creation

Attention handler needs to pass raw PEL's to phosphor logging in order
to submit PEL's on behalf of other components (e.g. hypervisor)

Signed-off-by: Ben Tyner <ben.tyner@ibm.com>
Change-Id: Id9a30728e7b463ac876b5dca023ca2627a25bb16
diff --git a/attn/attn_logging.cpp b/attn/attn_logging.cpp
index 014fc96..a55ac55 100644
--- a/attn/attn_logging.cpp
+++ b/attn/attn_logging.cpp
@@ -1,21 +1,421 @@
 #include <unistd.h>
 
 #include <attn/attn_logging.hpp>
+#include <attn/pel/pel_minimal.hpp>
 #include <phosphor-logging/log.hpp>
+
 namespace attn
 {
 
-/** @brief journal entry of type INFO using phosphor logging */
+/** @brief Journal entry of type INFO using phosphor logging */
 template <>
 void trace<INFO>(const char* i_message)
 {
     phosphor::logging::log<phosphor::logging::level::INFO>(i_message);
 }
 
-/** @brief add an event to the log for PEL generation */
-void event(EventType i_event, std::map<std::string, std::string>& i_additional)
+/** @brief Tuple containing information about ffdc files */
+using FFDCTuple =
+    std::tuple<util::FFDCFormat, uint8_t, uint8_t, sdbusplus::message::unix_fd>;
+
+/** @brief Gather messages from the journal */
+std::vector<std::string> sdjGetMessages(const std::string& field,
+                                        const std::string& fieldValue,
+                                        unsigned int max);
+
+/**
+ * Create FFDCTuple objects corresponding to the specified FFDC files.
+ *
+ * The D-Bus method to create an error log requires a vector of tuples to
+ * pass in the FFDC file information.
+ *
+ * @param   files - FFDC files
+ * @return  vector of FFDCTuple objects
+ */
+std::vector<FFDCTuple>
+    createFFDCTuples(const std::vector<util::FFDCFile>& files)
+{
+    std::vector<FFDCTuple> ffdcTuples{};
+    for (const util::FFDCFile& file : files)
+    {
+        ffdcTuples.emplace_back(
+            file.getFormat(), file.getSubType(), file.getVersion(),
+            sdbusplus::message::unix_fd(file.getFileDescriptor()));
+    }
+
+    return ffdcTuples;
+}
+
+/**
+ * @brief Create an FFDCFile object containing raw data
+ *
+ * Throws an exception if an error occurs.
+ *
+ * @param   i_buffer - raw data to add to ffdc faw data file
+ * @param   i_size - size of the raw data
+ * @return  FFDCFile object
+ */
+util::FFDCFile createFFDCRawFile(void* i_buffer, size_t i_size)
+{
+    util::FFDCFile file{util::FFDCFormat::Custom};
+
+    // Write buffer to file and then reset file description file offset
+    int fd = file.getFileDescriptor();
+    write(fd, static_cast<char*>(i_buffer), i_size);
+    lseek(fd, 0, SEEK_SET);
+
+    return file;
+}
+
+/**
+ * @brief Create an FFDCFile object containing the specified lines of text data
+ *
+ * Throws an exception if an error occurs.
+ *
+ * @param   lines - lines of text data to write to file
+ * @return  FFDCFile object
+ */
+util::FFDCFile createFFDCTraceFile(const std::vector<std::string>& lines)
+{
+    // Create FFDC file of type Text
+    util::FFDCFile file{util::FFDCFormat::Text};
+    int fd = file.getFileDescriptor();
+
+    // Write FFDC lines to file
+    std::string buffer;
+    for (const std::string& line : lines)
+    {
+        // Copy line to buffer.  Add newline if necessary.
+        buffer = line;
+        if (line.empty() || (line.back() != '\n'))
+        {
+            buffer += '\n';
+        }
+
+        // write buffer to file
+        write(fd, buffer.c_str(), buffer.size());
+    }
+
+    // Seek to beginning of file so error logging system can read data
+    lseek(fd, 0, SEEK_SET);
+
+    return file;
+}
+
+/**
+ * Create FDDC files from journal messages of relevant executables
+ *
+ * Parse the system journal looking for log entries created by the executables
+ * of interest for logging. For each of these entries create a ffdc trace file
+ * that will be used to create ffdc log entries. These files will be pushed
+ * onto the stack of ffdc files.
+ *
+ * @param   i_files - vector of ffdc files that will become log entries
+ */
+void createFFDCTraceFiles(std::vector<util::FFDCFile>& i_files)
+{
+    // Executables of interest
+    std::vector<std::string> executables{"openpower-hw-diags"};
+
+    for (const std::string& executable : executables)
+    {
+        try
+        {
+            // get journal messages
+            std::vector<std::string> messages =
+                sdjGetMessages("SYSLOG_IDENTIFIER", executable, 30);
+
+            // Create FFDC file containing the journal messages
+            if (!messages.empty())
+            {
+                i_files.emplace_back(createFFDCTraceFile(messages));
+            }
+        }
+        catch (const std::exception& e)
+        {
+            std::stringstream ss;
+            ss << "createFFDCFiles: " << e.what();
+            trace<level::INFO>(ss.str().c_str());
+        }
+    }
+}
+
+/**
+ * Create FFDCFile objects containing debug data to store in the error log.
+ *
+ * If an error occurs, the error is written to the journal but an exception
+ * is not thrown.
+ *
+ * @param   i_buffer - raw data (if creating raw dump ffdc entry in log)
+ * @return  vector of FFDCFile objects
+ */
+std::vector<util::FFDCFile> createFFDCFiles(char* i_buffer = nullptr,
+                                            size_t i_size  = 0)
+{
+    std::vector<util::FFDCFile> files{};
+
+    // Create raw dump file
+    if ((nullptr != i_buffer) && (0 != i_size))
+    {
+        files.emplace_back(createFFDCRawFile(i_buffer, i_size));
+    }
+
+    // Create trace dump file
+    createFFDCTraceFiles(files);
+
+    return files;
+}
+
+/**
+ * Get file descriptor of exisitng PEL
+ *
+ * The backend logging code will search for a PEL having the provided PEL ID
+ * and return a file descriptor to a file containing this PEL's  raw PEL data.
+ *
+ * @param  i_pelid - the PEL ID
+ * @return file descriptor of file containing the raw PEL data
+ */
+int getPelFd(uint32_t i_pelId)
+{
+    // GetPEL returns file descriptor (int)
+    int fd = -1;
+
+    // Sdbus call specifics
+    constexpr auto service   = "xyz.openbmc_project.Logging";
+    constexpr auto path      = "/xyz/openbmc_project/logging";
+    constexpr auto interface = "org.open_power.Logging.PEL";
+    constexpr auto function  = "GetPEL";
+
+    // Get the PEL file descriptor
+    try
+    {
+        auto bus    = sdbusplus::bus::new_default_system();
+        auto method = bus.new_method_call(service, path, interface, function);
+        method.append(i_pelId);
+
+        auto resp = bus.call(method);
+
+        sdbusplus::message::unix_fd msgFd;
+        resp.read(msgFd);
+        fd = dup(msgFd); // -1 if not found
+    }
+    catch (const sdbusplus::exception::SdBusError& e)
+    {
+        std::stringstream ss;
+        ss << "getPelFd: " << e.what();
+        trace<level::INFO>(ss.str().c_str());
+    }
+
+    // File descriptor or -1 if not found or call failed
+    return fd;
+}
+
+/**
+ * Create a PEL for the specified event type
+ *
+ * The additional data provided in the map will be placed in a user data
+ * section of the PEL and may additionally contain key words to trigger
+ * certain behaviors by the backend logging code. Each set of data described
+ * in the vector of ffdc data will be placed in additional user data sections.
+ *
+ * @param  i_event - the event type
+ * @param  i_additional - map of additional data
+ * @param  9_ffdc - vector of ffdc data
+ * @return The created PEL's platform log-id
+ */
+uint32_t createPel(std::string i_event,
+                   std::map<std::string, std::string> i_additional,
+                   std::vector<util::FFDCTuple> i_ffdc)
+{
+    // CreatePELWithFFDCFiles returns log-id and platform log-id
+    std::tuple<uint32_t, uint32_t> pelResp = {0, 0};
+
+    // Sdbus call specifics
+    constexpr auto level     = "xyz.openbmc_project.Logging.Entry.Level.Error";
+    constexpr auto service   = "xyz.openbmc_project.Logging";
+    constexpr auto path      = "/xyz/openbmc_project/logging";
+    constexpr auto interface = "org.open_power.Logging.PEL";
+    constexpr auto function  = "CreatePELWithFFDCFiles";
+
+    // Need to provide pid when using create or create-with-ffdc methods
+    i_additional.emplace("_PID", std::to_string(getpid()));
+
+    // Create the PEL
+    try
+    {
+        auto bus    = sdbusplus::bus::new_default_system();
+        auto method = bus.new_method_call(service, path, interface, function);
+        method.append(i_event, level, i_additional, i_ffdc);
+
+        auto resp = bus.call(method);
+
+        resp.read(pelResp);
+    }
+    catch (const sdbusplus::exception::SdBusError& e)
+    {
+        std::stringstream ss;
+        ss << "createPel: " << e.what();
+        trace<level::INFO>(ss.str().c_str());
+    }
+
+    // pelResp<0> == log-id, pelResp<1> = platform log-id
+    return std::get<1>(pelResp);
+}
+
+/*
+ * Create a PEL from raw PEL data
+ *
+ * The backend logging code will create a PEL based on the specified PEL data.
+ *
+ * @param   i_buffer - buffer containing a raw PEL
+ */
+void createPelRaw(std::vector<uint8_t>& i_buffer)
+{
+    // Sdbus call specifics
+    constexpr auto event     = "xyz.open_power.Attn.Error.Terminate";
+    constexpr auto level     = "xyz.openbmc_project.Logging.Entry.Level.Error";
+    constexpr auto service   = "xyz.openbmc_project.Logging";
+    constexpr auto path      = "/xyz/openbmc_project/logging";
+    constexpr auto interface = "xyz.openbmc_project.Logging.Create";
+    constexpr auto function  = "Create";
+
+    // Create FFDC file from buffer data
+    util::FFDCFile pelFile{util::FFDCFormat::Text};
+    auto fd = pelFile.getFileDescriptor();
+
+    write(fd, i_buffer.data(), i_buffer.size());
+    lseek(fd, 0, SEEK_SET);
+
+    auto filePath = pelFile.getPath();
+
+    // Additional data for log
+    std::map<std::string, std::string> additional;
+    additional.emplace("RAWPEL", filePath.string());
+    additional.emplace("_PID", std::to_string(getpid()));
+
+    // Create the PEL
+    try
+    {
+        auto bus    = sdbusplus::bus::new_default_system();
+        auto method = bus.new_method_call(service, path, interface, function);
+        method.append(event, level, additional);
+        bus.call_noreply(method);
+    }
+    catch (const sdbusplus::exception::SdBusError& e)
+    {
+        std::stringstream ss;
+        ss << "createPelRaw: " << e.what();
+        trace<level::INFO>(ss.str().c_str());
+    }
+}
+
+/**
+ * Create a PEL from an existing PEL
+ *
+ * Create a new PEL based on the specified raw PEL and submit the new PEL
+ * to the backend logging code as a raw PEL. Note that  additional data map
+ * here contains data to be committed to the PEL and it can also be used to
+ * create the PEL as it contains needed information.
+ *
+ * @param   i_buffer - buffer containing a raw PEL
+ * @param   i_additional - additional data to be added to the new PEL
+ */
+void createPelCustom(std::vector<uint8_t>& i_rawPel,
+                     std::map<std::string, std::string> i_additional)
+{
+    // create PEL object from buffer
+    auto tiPel = std::make_unique<pel::PelMinimal>(i_rawPel);
+
+    // The additional data contains the TI info as well as the value for the
+    // subystem that provided the TI info. Get the subystem from additional
+    // data and then populate the prmary SRC and SRC words for the custom PEL
+    // based on the sybsystem's TI info.
+    uint8_t subsystem = std::stoi(i_additional["Subsystem"]);
+    tiPel->setSubsystem(subsystem);
+
+    if (static_cast<uint8_t>(pel::SubsystemID::hypervisor) == subsystem)
+    {
+        // populate hypervisor SRC words
+        tiPel->setSrcWords(std::array<uint32_t, pel::numSrcWords>{
+            (uint32_t)std::stoul(i_additional["0x10 SRC Word 12"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x14 SRC Word 13"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x18 SRC Word 14"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x1c SRC Word 15"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x20 SRC Word 16"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x24 SRC Word 17"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x28 SRC Word 18"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x2c SRC Word 19"], 0, 16)});
+
+        // populate hypervisor primary SRC
+        std::array<char, pel::asciiStringSize> srcChars{'0'};
+        std::string srcString = i_additional["SrcAscii"];
+        srcString.copy(srcChars.data(),
+                       std::min(srcString.size(), pel::asciiStringSize), 0);
+        tiPel->setAsciiString(srcChars);
+    }
+    else
+    {
+        // Populate hostboot SRC words - note HB word 0 from the shared info
+        // data (additional data "0x10 HB Word") is reflected in the PEL as
+        // "reason code" so we zero it here. Also note that the first word
+        // in this group of words starts at word 0 and word 1 does not exits.
+        tiPel->setSrcWords(std::array<uint32_t, pel::numSrcWords>{
+            (uint32_t)0x00000000,
+            (uint32_t)std::stoul(i_additional["0x14 HB Word 2"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x18 HB Word 3"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x1c HB Word 4"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x20 HB Word 5"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x24 HB Word 6"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x28 HB Word 7"], 0, 16),
+            (uint32_t)std::stoul(i_additional["0x2c HB Word 8"], 0, 16)});
+
+        // populate hostboot primary SRC
+        std::array<char, pel::asciiStringSize> srcChars{'0'};
+        std::string srcString = i_additional["0x30 error_data"];
+        srcString.copy(srcChars.data(),
+                       std::min(srcString.size(), pel::asciiStringSize), 0);
+        tiPel->setAsciiString(srcChars);
+    }
+
+    // set severity, event type and action flags
+    tiPel->setSeverity(static_cast<uint8_t>(pel::Severity::termination));
+    tiPel->setType(static_cast<uint8_t>(pel::EventType::na));
+    tiPel->setAction(static_cast<uint16_t>(pel::ActionFlags::service |
+                                           pel::ActionFlags::report |
+                                           pel::ActionFlags::call));
+
+    // The raw PEL that we used as the basis for this custom PEL contains the
+    // attention handler trace data and does not needed to be in this PEL so
+    // we remove it here.
+    tiPel->setSectionCount(tiPel->getSectionCount() - 1);
+
+    // Update the raw PEL with the new custom PEL data
+    tiPel->raw(i_rawPel);
+
+    // create PEL from raw data
+    createPelRaw(i_rawPel);
+}
+
+/**
+ * Log an event handled by the attention handler
+ *
+ * Basic (non TI) events will generate a standard message-registry based PEL
+ *
+ * TI events will create two PEL's. One PEL will be informational and will
+ * contain trace information relevent to attention handler. The second PEL
+ * will be specific to the TI type (including the primary SRC) and will be
+ * based off of the TI information provided to the attention handler through
+ * shared TI info data area.
+ *
+ * @param  i_event - The event type
+ * @param  i_additional - Additional PEL data
+ * @param  i_ffdc - FFDC PEL data
+ */
+void event(EventType i_event, std::map<std::string, std::string>& i_additional,
+           const std::vector<util::FFDCFile>& i_ffdc)
 {
     bool eventValid = false; // assume no event created
+    bool tiEvent    = false; // assume not a terminate event
 
     std::string eventName;
 
@@ -28,15 +428,13 @@
         case EventType::Terminate:
             eventName  = "org.open_power.Attn.Error.Terminate";
             eventValid = true;
+            tiEvent    = true;
             break;
         case EventType::Vital:
             eventName  = "org.open_power.Attn.Error.Vital";
             eventValid = true;
             break;
         case EventType::HwDiagsFail:
-            eventName  = "org.open_power.HwDiags.Error.Fail";
-            eventValid = true;
-            break;
         case EventType::AttentionFail:
             eventName  = "org.open_power.Attn.Error.Fail";
             eventValid = true;
@@ -48,93 +446,118 @@
 
     if (true == eventValid)
     {
-        // Get access to logging interface and method for creating log
-        auto bus = sdbusplus::bus::new_default_system();
+        // Create PEL with additional data and FFDC data. The newly created
+        // PEL's platform log-id will be returned.
+        auto pelId =
+            createPel(eventName, i_additional, createFFDCTuples(i_ffdc));
 
-        // using direct create method (for additional data)
-        auto method = bus.new_method_call(
-            "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging",
-            "xyz.openbmc_project.Logging.Create", "CreateWithFFDCFiles");
+        // If this is a TI event we will create an additional PEL that is
+        // specific to the subsystem that generated the TI.
+        if (true == tiEvent)
+        {
+            // get file descriptor and size of information PEL
+            int pelFd = getPelFd(pelId);
 
-        // Create FFDC files containing debug data to store in error log
-        std::vector<util::FFDCFile> files{createFFDCFiles()};
+            // if PEL found, read into buffer
+            if (-1 != pelFd)
+            {
+                auto pelSize = lseek(pelFd, 0, SEEK_END);
+                lseek(pelFd, 0, SEEK_SET);
 
-        // Create FFDC tuples used to pass FFDC files to D-Bus method
-        std::vector<FFDCTuple> ffdcTuples{createFFDCTuples(files)};
+                // read information PEL into buffer
+                std::vector<uint8_t> buffer(pelSize);
+                read(pelFd, buffer.data(), buffer.size());
+                close(pelFd);
 
-        // attach additional data
-        method.append(eventName,
-                      "xyz.openbmc_project.Logging.Entry.Level.Error",
-                      i_additional, ffdcTuples);
-
-        // log the event
-        bus.call_noreply(method);
+                // create PEL from buffer
+                createPelCustom(buffer, i_additional);
+            }
+        }
     }
 }
 
-/** @brief commit checkstop event to log */
+/** @brief Commit checkstop event to log */
 void eventCheckstop(std::map<std::string, std::string>& i_errors)
 {
+    // Additional data for log
     std::map<std::string, std::string> additionalData;
 
-    // TODO need multi-error/multi-callout stuff here
-
-    // if analyzer isolated errors
-    if (!(i_errors.empty()))
-    {
-        // FIXME TEMP CODE - begin
-
-        std::string signature = i_errors.begin()->first;
-        std::string chip      = i_errors.begin()->second;
-
-        additionalData["_PID"]      = std::to_string(getpid());
-        additionalData["SIGNATURE"] = signature;
-        additionalData["CHIP_ID"]   = chip;
-
-        // FIXME TEMP CODE -end
-
-        event(EventType::Checkstop, additionalData);
-    }
+    // Create log event with additional data and FFDC data
+    event(EventType::Checkstop, additionalData, createFFDCFiles(nullptr, 0));
 }
 
-/** @brief commit special attention TI event to log */
-void eventTerminate(std::map<std::string, std::string> i_additionalData)
+/**
+ * Commit special attention TI event to log
+ *
+ * Create a event log with provided additional information and standard
+ * FFDC data plus TI FFDC data
+ *
+ * @param i_additional - Additional log data
+ * @param i_ti_InfoData - TI FFDC data
+ */
+void eventTerminate(std::map<std::string, std::string> i_additionalData,
+                    char* i_tiInfoData)
 {
-    event(EventType::Terminate, i_additionalData);
+    // Create log event with aodditional data and FFDC data
+    event(EventType::Terminate, i_additionalData,
+          createFFDCFiles(i_tiInfoData, 0x53));
 }
 
-/** @brief commit SBE vital event to log */
+/** @brief Commit SBE vital event to log */
 void eventVital()
 {
+    // Additional data for log
     std::map<std::string, std::string> additionalData;
 
-    additionalData["_PID"] = std::to_string(getpid());
-
-    event(EventType::Vital, additionalData);
+    // Create log event with additional data and FFDC data
+    event(EventType::Vital, additionalData, createFFDCFiles(nullptr, 0));
 }
 
-/** @brief commit analyzer failure event to log */
+/**
+ * Commit analyzer failure event to log
+ *
+ * Create an event log containing the specified error code.
+ *
+ * @param i_error - Error code
+ */
 void eventHwDiagsFail(int i_error)
 {
+    // Additiona data for log
     std::map<std::string, std::string> additionalData;
-
-    additionalData["_PID"] = std::to_string(getpid());
-
-    event(EventType::HwDiagsFail, additionalData);
-}
-
-/** @brief commit attention handler failure event to log */
-void eventAttentionFail(int i_error)
-{
-    std::map<std::string, std::string> additionalData;
-
-    additionalData["_PID"]       = std::to_string(getpid());
     additionalData["ERROR_CODE"] = std::to_string(i_error);
 
-    event(EventType::AttentionFail, additionalData);
+    // Create log event with additional data and FFDC data
+    event(EventType::HwDiagsFail, additionalData, createFFDCFiles(nullptr, 0));
 }
 
-/** @brief parse systemd journal message field */
+/**
+ * Commit attention handler failure event to log
+ *
+ * Create an event log containing the specified error code.
+ *
+ * @param i_error - Error code
+ */
+void eventAttentionFail(int i_error)
+{
+    // Additional data for log
+    std::map<std::string, std::string> additionalData;
+    additionalData["ERROR_CODE"] = std::to_string(i_error);
+
+    // Create log event with additional data and FFDC data
+    event(EventType::AttentionFail, additionalData,
+          createFFDCFiles(nullptr, 0));
+}
+
+/**
+ * Parse systemd journal message field
+ *
+ * Parse the journal looking for the specified field and return the journal
+ * data for that field.
+ *
+ * @param  journal - The journal to parse
+ * @param  field - Field containing the data to retrieve
+ * @return Data for the speciefied field
+ */
 std::string sdjGetFieldValue(sd_journal* journal, const char* field)
 {
     const char* data{nullptr};
@@ -167,7 +590,17 @@
     }
 }
 
-/** @brief get messages from systemd journal */
+/**
+ * Gather messages from the journal
+ *
+ * Fetch journal entry data for all entries with the specified field equal to
+ * the specified value.
+ *
+ * @param   field - Field to search on
+ * @param   fieldValue -  Value to search for
+ * @param   max - Maximum number of messages fetch
+ * @return  Vector of journal entry data
+ */
 std::vector<std::string> sdjGetMessages(const std::string& field,
                                         const std::string& fieldValue,
                                         unsigned int max)
@@ -227,78 +660,4 @@
     return messages;
 }
 
-/** @brief create a file containing FFDC data */
-util::FFDCFile createFFDCFile(const std::vector<std::string>& lines)
-{
-    // Create FFDC file of type Text
-    util::FFDCFile file{util::FFDCFormat::Text};
-    int fd = file.getFileDescriptor();
-
-    // Write FFDC lines to file
-    std::string buffer;
-    for (const std::string& line : lines)
-    {
-        // Copy line to buffer.  Add newline if necessary.
-        buffer = line;
-        if (line.empty() || (line.back() != '\n'))
-        {
-            buffer += '\n';
-        }
-
-        // write buffer to file
-        write(fd, buffer.c_str(), buffer.size());
-    }
-
-    // Seek to beginning of file so error logging system can read data
-    lseek(fd, 0, SEEK_SET);
-
-    return file;
-}
-
-/** @brief Create FDDC files from journal messages of relevant executables */
-std::vector<util::FFDCFile> createFFDCFiles()
-{
-    std::vector<util::FFDCFile> files{};
-
-    // Executables of interest
-    std::vector<std::string> executables{"openpower-hw-diags"};
-
-    for (const std::string& executable : executables)
-    {
-        try
-        {
-            // get journal messages
-            std::vector<std::string> messages =
-                sdjGetMessages("SYSLOG_IDENTIFIER", executable, 30);
-
-            // Create FFDC file containing the journal messages
-            if (!messages.empty())
-            {
-                files.emplace_back(createFFDCFile(messages));
-            }
-        }
-        catch (const std::exception& e)
-        {
-            std::stringstream ss;
-            ss << "createFFDCFiles: " << e.what();
-            trace<level::INFO>(ss.str().c_str());
-        }
-    }
-
-    return files;
-}
-
-/** create tuples of FFDC files */
-std::vector<FFDCTuple> createFFDCTuples(std::vector<util::FFDCFile>& files)
-{
-    std::vector<FFDCTuple> ffdcTuples{};
-    for (util::FFDCFile& file : files)
-    {
-        ffdcTuples.emplace_back(
-            file.getFormat(), file.getSubType(), file.getVersion(),
-            sdbusplus::message::unix_fd(file.getFileDescriptor()));
-    }
-    return ffdcTuples;
-}
-
 } // namespace attn
diff --git a/attn/attn_logging.hpp b/attn/attn_logging.hpp
index 8b5feba..0103a52 100644
--- a/attn/attn_logging.hpp
+++ b/attn/attn_logging.hpp
@@ -29,58 +29,24 @@
 /** @brief Maximum length of a single trace event message */
 static const size_t trace_msg_max_len = 255;
 
-/** @brief create trace message */
+/** @brief Create trace message template */
 template <level L>
 void trace(const char* i_message);
 
-/** @brief commit checkstop event to log */
+/** @brief Commit checkstop event to log */
 void eventCheckstop(std::map<std::string, std::string>& i_errors);
 
-/** @brief commit special attention TI event to log */
-void eventTerminate(std::map<std::string, std::string> i_additionalData);
+/** @brief Commit special attention TI event to log */
+void eventTerminate(std::map<std::string, std::string> i_additionalData,
+                    char* i_tiInfoData);
 
-/** @brief commit SBE vital event to log */
+/** @brief Commit SBE vital event to log */
 void eventVital();
 
-/** @brief commit analyzer failure event to log */
+/** @brief Commit analyzer failure event to log */
 void eventHwDiagsFail(int i_error);
 
-/** @brief commit attention handler failure event to log */
+/** @brief Commit attention handler failure event to log */
 void eventAttentionFail(int i_error);
 
-using FFDCTuple =
-    std::tuple<util::FFDCFormat, uint8_t, uint8_t, sdbusplus::message::unix_fd>;
-
-/**
- * Create an FFDCFile object containing the specified lines of text data.
- *
- * Throws an exception if an error occurs.
- *
- * @param lines lines of text data to write to file
- * @return FFDCFile object
- */
-util::FFDCFile createFFDCFile(const std::vector<std::string>& lines);
-
-/**
- * Create FFDCFile objects containing debug data to store in the error log.
- *
- * If an error occurs, the error is written to the journal but an exception
- * is not thrown.
- *
- * @param journal system journal
- * @return vector of FFDCFile objects
- */
-std::vector<util::FFDCFile> createFFDCFiles();
-
-/**
- * Create FFDCTuple objects corresponding to the specified FFDC files.
- *
- * The D-Bus method to create an error log requires a vector of tuples to
- * pass in the FFDC file information.
- *
- * @param files FFDC files
- * @return vector of FFDCTuple objects
- */
-std::vector<FFDCTuple> createFFDCTuples(std::vector<util::FFDCFile>& files);
-
 } // namespace attn
diff --git a/attn/meson.build b/attn/meson.build
index 815e038..d42289d 100644
--- a/attn/meson.build
+++ b/attn/meson.build
@@ -37,6 +37,14 @@
     'vital_handler.cpp',
 )
 
+# for custom/raw PEL creation
+pel_src = files(
+    'pel/pel_minimal.cpp',
+    'pel/private_header.cpp',
+    'pel/primary_src.cpp',
+    'pel/user_header.cpp',
+)
+
 # Library dependencies.
 attn_deps = [
     libgpiod,
@@ -48,6 +56,7 @@
 attn_lib = static_library(
     'attn_lib',
     attn_src,
+    pel_src,
     include_directories : incdir,
     dependencies : attn_deps,
     cpp_args : [boost_args, test_arg],
diff --git a/attn/pel/pel_common.hpp b/attn/pel/pel_common.hpp
new file mode 100644
index 0000000..d0f3e15
--- /dev/null
+++ b/attn/pel/pel_common.hpp
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <cstddef> // for size_t
+
+namespace attn
+{
+namespace pel
+{
+
+enum class SectionID
+{
+    privateHeader = 0x5048, // 'PH'
+    userHeader    = 0x5548, // 'UH'
+    primarySRC    = 0x5053, // 'PS'
+};
+
+enum class ComponentID
+{
+    attentionHandler = 0xd100
+};
+
+enum class CreatorID
+{
+    hostboot   = 'B',
+    hypervisor = 'H',
+    openbmc    = 'O'
+};
+
+enum class SubsystemID
+{
+    hypervisor = 0x82,
+    hostboot   = 0x8a,
+    openbmc    = 0x8d
+};
+
+enum class Severity
+{
+    information = 0x00,
+    termination = 0x51
+};
+
+enum class EventType
+{
+    na    = 0x00,
+    trace = 0x02
+};
+
+enum class ActionFlags
+{
+    service = 0x8000,
+    report  = 0x2000,
+    call    = 0x0800
+};
+
+inline ActionFlags operator|(ActionFlags a, ActionFlags b)
+{
+    return static_cast<ActionFlags>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+enum class EventScope
+{
+    platform = 0x03
+};
+
+constexpr size_t numSrcWords = 8;  // number of SRC hex words
+const size_t asciiStringSize = 32; // size of SRC ascii string
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/pel_minimal.cpp b/attn/pel/pel_minimal.cpp
new file mode 100644
index 0000000..9d7dfc1
--- /dev/null
+++ b/attn/pel/pel_minimal.cpp
@@ -0,0 +1,95 @@
+#include "pel_minimal.hpp"
+
+#include "stream.hpp"
+
+namespace attn
+{
+namespace pel
+{
+
+PelMinimal::PelMinimal(std::vector<uint8_t>& data)
+{
+    Stream pelData{data};
+
+    _ph = std::make_unique<PrivateHeader>(pelData);
+    _uh = std::make_unique<UserHeader>(pelData);
+    _ps = std::make_unique<PrimarySrc>(pelData);
+}
+
+void PelMinimal::raw(std::vector<uint8_t>& pelBuffer) const
+{
+    Stream pelData{pelBuffer};
+
+    // stream from object to buffer
+    _ph->flatten(pelData);
+    _uh->flatten(pelData);
+    _ps->flatten(pelData);
+}
+
+size_t PelMinimal::size() const
+{
+    size_t size = 0;
+
+    // size of private header section
+    if (_ph)
+    {
+        size += _ph->header().size;
+    }
+
+    // size of user header section
+    if (_uh)
+    {
+        size += _uh->header().size;
+    }
+
+    // size of primary SRC section
+    if (_ps)
+    {
+        size += _ph->header().size;
+    }
+
+    return ((size > _maxPELSize) ? _maxPELSize : size);
+}
+
+void PelMinimal::setSubsystem(uint8_t subsystem)
+{
+    _uh->setSubsystem(subsystem);
+}
+
+void PelMinimal::setSeverity(uint8_t severity)
+{
+    _uh->setSeverity(severity);
+}
+
+void PelMinimal::setType(uint8_t type)
+{
+    _uh->setType(type);
+}
+
+void PelMinimal::setAction(uint16_t action)
+{
+    _uh->setAction(action);
+}
+
+void PelMinimal::setSrcWords(std::array<uint32_t, numSrcWords> srcWords)
+{
+    _ps->setSrcWords(srcWords);
+}
+
+void PelMinimal::setAsciiString(std::array<char, asciiStringSize> asciiString)
+{
+    _ps->setAsciiString(asciiString);
+}
+
+uint8_t PelMinimal::getSectionCount()
+{
+    return _ph->getSectionCount();
+}
+
+void PelMinimal::setSectionCount(uint8_t sectionCount)
+{
+    _ph->setSectionCount(sectionCount);
+}
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/pel_minimal.hpp b/attn/pel/pel_minimal.hpp
new file mode 100644
index 0000000..cde0ead
--- /dev/null
+++ b/attn/pel/pel_minimal.hpp
@@ -0,0 +1,144 @@
+#pragma once
+
+#include "pel_common.hpp"
+#include "primary_src.hpp"
+#include "private_header.hpp"
+#include "user_header.hpp"
+
+#include <vector>
+
+namespace attn
+{
+namespace pel
+{
+
+/** @class PelMinimal
+ *
+ * @brief Class for a minimal platform event log (PEL)
+ *
+ * This class can be used to create form a PEL and create a raw PEL file. The
+ * implementation based on "Platform Event Log and SRC PLDD v1.1"
+ *
+ * This PEL consists of the following position dependent sections:
+ *
+ * |----------+------------------------------|
+ * | length   | section                      |
+ * |----------+------------------------------|
+ * | 48       | Private Header Section       |
+ * |----------+------------------------------|
+ * | 24       | User Header Section          |
+ * |----------+------------------------------|
+ * | 72       | Primary SRC Section          |
+ * |----------+------------------------------|
+ */
+class PelMinimal
+{
+  public:
+    PelMinimal()                  = delete;
+    ~PelMinimal()                 = default;
+    PelMinimal(const PelMinimal&) = delete;
+    PelMinimal& operator=(const PelMinimal&) = delete;
+    PelMinimal(PelMinimal&&)                 = delete;
+    PelMinimal& operator=(PelMinimal&&) = delete;
+
+    /**
+     * @brief Create a minimal PEL object from raw data
+     *
+     * @param[in] pelBuffer - buffer containing a raw PEL
+     */
+    PelMinimal(std::vector<uint8_t>& data);
+
+    /**
+     * @brief Stream raw PEL data to buffer
+     *
+     * @param[out] pelBuffer - What the data will be written to
+     */
+    void raw(std::vector<uint8_t>& pelBuffer) const;
+
+    /**
+     * @brief Set the User Header subsystem field
+     *
+     * @param[in] subsystem - The subsystem value
+     */
+    void setSubsystem(uint8_t subsystem);
+
+    /**
+     * @brief Set the User Header severity field
+     *
+     * @param[in] severity - The severity to set
+     */
+    void setSeverity(uint8_t severity);
+
+    /**
+     * @brief Set the User Header event type field
+     *
+     * @param[in] type - The event type
+     */
+    void setType(uint8_t type);
+
+    /**
+     * @brief Set the User Header action flags field
+     *
+     * @param[in] action - The action flags to set
+     */
+    void setAction(uint16_t action);
+
+    /**
+     * @brief Set the Primary SRC section SRC words
+     *
+     * @param[in] srcWords - The SRC words
+     */
+    void setSrcWords(std::array<uint32_t, numSrcWords> srcWords);
+
+    /**
+     * @brief Set the Primary SRC section ascii string field
+     *
+     * @param[in]  asciiString - The ascii string
+     */
+    void setAsciiString(std::array<char, asciiStringSize> asciiString);
+
+    /**
+     * @brief Get section count from the private header
+     *
+     * @return Number of sections
+     */
+    uint8_t getSectionCount();
+
+    /**
+     * @brief Set section count in private heasder
+     *
+     * @param[in] sectionCount - Number of sections
+     */
+    void setSectionCount(uint8_t sectionCount);
+
+  private:
+    /**
+     * @brief Maximum PEL size
+     */
+    static constexpr size_t _maxPELSize = 16384;
+
+    /**
+     * @brief Returns the size of the PEL
+     *
+     * @return size_t The PEL size in bytes
+     */
+    size_t size() const;
+
+    /**
+     * @brief PEL Private Header
+     */
+    std::unique_ptr<PrivateHeader> _ph;
+
+    /**
+     * @brief PEL User Header
+     */
+    std::unique_ptr<UserHeader> _uh;
+
+    /**
+     * @brief PEL Primary SRC
+     */
+    std::unique_ptr<PrimarySrc> _ps;
+};
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/pel_section.hpp b/attn/pel/pel_section.hpp
new file mode 100644
index 0000000..ca8dce4
--- /dev/null
+++ b/attn/pel/pel_section.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "section_header.hpp"
+
+namespace attn
+{
+namespace pel
+{
+/**
+ * @class Section
+ *
+ * The base class for a PEL section.  It contains the SectionHeader
+ * as all sections start with it.
+ *
+ */
+class Section
+{
+  public:
+    Section()               = default;
+    virtual ~Section()      = default;
+    Section(const Section&) = default;
+    Section& operator=(const Section&) = default;
+    Section(Section&&)                 = default;
+    Section& operator=(Section&&) = default;
+
+    /**
+     * @brief Returns a reference to the SectionHeader
+     */
+    const SectionHeader& header() const
+    {
+        return _header;
+    }
+
+    /**
+     * @brief Flatten the section into the stream
+     *
+     * @param[in] stream - The stream to write to
+     */
+    virtual void flatten(Stream& stream) const = 0;
+
+  protected:
+    /**
+     * @brief Returns the flattened size of the section header
+     */
+    static constexpr size_t flattenedSize()
+    {
+        return SectionHeader::flattenedSize();
+    }
+
+    /**
+     * @brief The section header structure.
+     *
+     * Filled in by derived classes.
+     */
+    SectionHeader _header;
+};
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/primary_src.cpp b/attn/pel/primary_src.cpp
new file mode 100644
index 0000000..0123395
--- /dev/null
+++ b/attn/pel/primary_src.cpp
@@ -0,0 +1,50 @@
+#include "primary_src.hpp"
+
+namespace attn
+{
+namespace pel
+{
+
+PrimarySrc::PrimarySrc(Stream& pel)
+{
+    unflatten(pel);
+}
+
+void PrimarySrc::flatten(Stream& stream) const
+{
+    stream << _header << _version << _flags << _reserved1B << _wordCount
+           << _reserved2B << _size;
+
+    for (auto& word : _srcWords)
+    {
+        stream << word;
+    }
+
+    stream.write(_asciiString.data(), _asciiString.size());
+}
+
+void PrimarySrc::unflatten(Stream& stream)
+{
+    stream >> _header >> _version >> _flags >> _reserved1B >> _wordCount >>
+        _reserved2B >> _size;
+
+    for (auto& word : _srcWords)
+    {
+        stream >> word;
+    }
+
+    stream.read(_asciiString.data(), _asciiString.size());
+}
+
+void PrimarySrc::setSrcWords(std::array<uint32_t, numSrcWords> srcWords)
+{
+    _srcWords = srcWords;
+}
+
+void PrimarySrc::setAsciiString(std::array<char, asciiStringSize> asciiString)
+{
+    _asciiString = asciiString;
+}
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/primary_src.hpp b/attn/pel/primary_src.hpp
new file mode 100644
index 0000000..659cb48
--- /dev/null
+++ b/attn/pel/primary_src.hpp
@@ -0,0 +1,150 @@
+#pragma once
+
+#include "pel_common.hpp"
+#include "pel_section.hpp"
+#include "stream.hpp"
+
+namespace attn
+{
+namespace pel
+{
+
+/**
+ * @class PrimarySrc
+ *
+ * @brief This class represents the primary SRC sections in the PEL.
+ *
+ * |--------+--------------------------------------------|
+ * | length | field                                      |
+ * |--------+--------------------------------------------|
+ * | 1      | Version = 0x02                             |
+ * |--------+--------------------------------------------|
+ * | 1      | Flags = 0x00 (no additional data sections) |
+ * |--------+--------------------------------------------|
+ * | 1      | reserved                                   |
+ * |--------+--------------------------------------------|
+ * | 1      | Number of words of hex data + 1 = 0x09     |
+ * |--------+--------------------------------------------|
+ * | 2      | reserved                                   |
+ * |--------+--------------------------------------------|
+ * | 2      | Total length of SRC in bytes = 0x48        |
+ * |--------+--------------------------------------------|
+ * | 4      | Hex Word 2 (word 1 intentionally skipped)  |
+ * |--------+--------------------------------------------|
+ * | 4      | Hex Word 3                                 |
+ * |--------+--------------------------------------------|
+ * | 4      | Hex Word 4                                 |
+ * |--------+--------------------------------------------|
+ * | 4      | Hex Word 5                                 |
+ * |--------+--------------------------------------------|
+ * | 4      | Hex Word 6                                 |
+ * |--------+--------------------------------------------|
+ * | 4      | Hex Word 7                                 |
+ * |--------+--------------------------------------------|
+ * | 4      | Hex Word 8                                 |
+ * |--------+--------------------------------------------|
+ * | 4      | Hex Word 9                                 |
+ * |--------+--------------------------------------------|
+ * | 32     | ASCII String                               |
+ * |--------+--------------------------------------------|
+ */
+class PrimarySrc : public Section
+{
+  public:
+    enum HeaderFlags
+    {
+        additionalSections  = 0x01,
+        powerFaultEvent     = 0x02,
+        hypDumpInit         = 0x04,
+        i5OSServiceEventBit = 0x10,
+        virtualProgressSRC  = 0x80
+    };
+
+    PrimarySrc()                  = delete;
+    ~PrimarySrc()                 = default;
+    PrimarySrc(const PrimarySrc&) = delete;
+    PrimarySrc& operator=(const PrimarySrc&) = delete;
+    PrimarySrc(PrimarySrc&&)                 = delete;
+    PrimarySrc& operator=(PrimarySrc&&) = delete;
+
+    /**
+     * @brief Constructor
+     *
+     * Fills in this class's data fields from raw data.
+     *
+     * @param[in] pel - the PEL data stream
+     */
+    explicit PrimarySrc(Stream& pel);
+
+    /**
+     * @brief Flatten the section into the stream
+     *
+     * @param[in] stream - The stream to write to
+     */
+    void flatten(Stream& stream) const override;
+
+    /**
+     * @brief Fills in the object from the stream data
+     *
+     * @param[in] stream - The stream to read from
+     */
+    void unflatten(Stream& stream);
+
+    /**
+     * @brief Set the SRC words
+     *
+     * @param[in] srcWords - The SRC words
+     */
+    void setSrcWords(std::array<uint32_t, numSrcWords> srcWords);
+
+    /**
+     * @brief Set the ascii string field
+     *
+     * @param[in]  asciiString - The ascii string
+     */
+    void setAsciiString(std::array<char, asciiStringSize> asciiString);
+
+  private:
+    /**
+     * @brief The SRC version field
+     */
+    uint8_t _version = 0x02;
+
+    /**
+     * @brief The SRC flags field
+     */
+    uint8_t _flags = 0;
+
+    /**
+     * @brief A byte of reserved data after the flags field
+     */
+    uint8_t _reserved1B = 0;
+
+    /**
+     * @brief The hex data word count.
+     */
+    uint8_t _wordCount = numSrcWords + 1; // +1 for backward compatability
+
+    /**
+     * @brief Two bytes of reserved data after the hex word count
+     */
+    uint16_t _reserved2B = 0;
+
+    /**
+     * @brief The total size of the SRC section (w/o section header)
+     */
+    uint16_t _size = 72; // 72 (bytes) = size of basic SRC section
+
+    /**
+     * @brief The SRC 'hex words'.
+     */
+    std::array<uint32_t, numSrcWords> _srcWords;
+
+    /**
+     * @brief The 32 byte ASCII character string of the SRC
+     */
+    std::array<char, asciiStringSize> _asciiString;
+};
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/private_header.cpp b/attn/pel/private_header.cpp
new file mode 100644
index 0000000..18d06d4
--- /dev/null
+++ b/attn/pel/private_header.cpp
@@ -0,0 +1,58 @@
+#include "private_header.hpp"
+
+namespace attn
+{
+namespace pel
+{
+
+PrivateHeader::PrivateHeader(Stream& pel)
+{
+    unflatten(pel);
+}
+
+void PrivateHeader::flatten(Stream& stream) const
+{
+    stream << _header << _createTimestamp << _commitTimestamp << _creatorID
+           << _reservedByte1 << _reservedByte2 << _sectionCount << _obmcLogID
+           << _creatorVersion << _plid << _id;
+}
+
+void PrivateHeader::unflatten(Stream& stream)
+{
+    stream >> _header >> _createTimestamp >> _commitTimestamp >> _creatorID >>
+        _reservedByte1 >> _reservedByte2 >> _sectionCount >> _obmcLogID >>
+        _creatorVersion >> _plid >> _id;
+}
+
+uint8_t PrivateHeader::getSectionCount()
+{
+    return _sectionCount;
+}
+
+void PrivateHeader::setSectionCount(uint8_t sectionCount)
+{
+    _sectionCount = sectionCount;
+}
+
+/*
+Stream& operator<<(Stream& s, const CreatorVersion& cv)
+{
+    for (size_t i = 0; i < sizeof(CreatorVersion); i++)
+    {
+        s << cv.version[i];
+    }
+    return s;
+}
+
+Stream& operator>>(Stream& s, CreatorVersion& cv)
+{
+    for (size_t i = 0; i < sizeof(CreatorVersion); i++)
+    {
+        s >> cv.version[i];
+    }
+    return s;
+}
+*/
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/private_header.hpp b/attn/pel/private_header.hpp
new file mode 100644
index 0000000..3b80968
--- /dev/null
+++ b/attn/pel/private_header.hpp
@@ -0,0 +1,191 @@
+#pragma once
+
+//#include "bcd_time.hpp"
+#include "pel_common.hpp"
+#include "pel_section.hpp"
+#include "stream.hpp"
+
+namespace attn
+{
+namespace pel
+{
+
+// creator version field type, init to null terminated
+// struct CreatorVersion
+//{
+//    uint8_t version[8];
+
+//    CreatorVersion()
+//    {
+//        memset(version, '\0', sizeof(version));
+//    }
+//};
+
+/**
+ * @class PrivateHeader
+ *
+ * This represents the Private Header section in a PEL.  It is required,
+ * and it is always the first section.
+ *
+ * The Section base class handles the SectionHeader structure that every
+ * PEL section has at offset zero.
+ *
+ * |--------+----------------------+----------+----------+---------------|
+ * | length | byte0                | byte1    | byte2    | byte3         |
+ * |--------+----------------------+----------+----------+---------------|
+ * | 8      | Section Header                                             |
+ * |        |                                                            |
+ * |--------+------------------------------------------------------------|
+ * | 8      | Timestamp - Creation                                       |
+ * |        |                                                            |
+ * |--------+------------------------------------------------------------|
+ * | 8      | Timestamp - Commit                                         |
+ * |        |                                                            |
+ * |--------+----------------------+----------+----------+---------------|
+ * | 4      | Creator ID           | reserved | reserved | section count |
+ * |--------+----------------------+----------+----------+---------------|
+ * | 4      | OpenBMC Event Log ID                                       |
+ * |--------+------------------------------------------------------------|
+ * | 8      | Creator Implementation                                     |
+ * |        |                                                            |
+ * |--------+------------------------------------------------------------|
+ * | 4      | Platform Log ID                                            |
+ * |--------+------------------------------------------------------------|
+ * | 4      | Log Entry ID                                               |
+ * |--------+------------------------------------------------------------|
+ */
+class PrivateHeader : public Section
+{
+  public:
+    PrivateHeader()                     = delete;
+    ~PrivateHeader()                    = default;
+    PrivateHeader(const PrivateHeader&) = default;
+    PrivateHeader& operator=(const PrivateHeader&) = default;
+    PrivateHeader(PrivateHeader&&)                 = default;
+    PrivateHeader& operator=(PrivateHeader&&) = default;
+
+    /**
+     * @brief Constructor
+     *
+     * Fills in this class's data fields from raw data.
+     *
+     * @param[in] pel - the PEL data stream
+     *
+     */
+    explicit PrivateHeader(Stream& pel);
+
+    /**
+     * @brief Flatten the section into the stream
+     *
+     * @param[in] stream - The stream to write to
+     */
+    void flatten(Stream& stream) const override;
+
+    /**
+     * @brief Fills in the object from the stream data
+     *
+     * @param[in] stream - The stream to read from
+     */
+    void unflatten(Stream& stream);
+
+    /**
+     * @brief Returns the size of this section when flattened into a PEL
+     *
+     * @return size_t - the size of the section
+     */
+    static constexpr size_t flattenedSize()
+    {
+        return Section::flattenedSize() + sizeof(_createTimestamp) +
+               sizeof(_commitTimestamp) + sizeof(_creatorID) +
+               sizeof(_reservedByte1) + sizeof(_reservedByte2) +
+               sizeof(_sectionCount) + sizeof(_obmcLogID) +
+               sizeof(_creatorVersion) + sizeof(_plid) + sizeof(_id);
+    }
+
+    /**
+     * @brief Get the total number of sections in this PEL
+     *
+     * @return Number of sections
+     */
+    uint8_t getSectionCount();
+
+    /**
+     * @brief Set the total number of sections in this PEL
+     *
+     * @param[in] sectionCount - Number of sections
+     */
+    void setSectionCount(uint8_t sectionCount);
+
+  private:
+    /**
+     * @brief The creation time timestamp
+     */
+    uint64_t _createTimestamp;
+    // BCDTime _createTimestamp;
+
+    /**
+     * @brief The commit time timestamp
+     */
+    uint64_t _commitTimestamp;
+    // BCDTime _commitTimestamp;
+
+    /**
+     * @brief The creator ID field
+     */
+    uint8_t _creatorID;
+
+    /**
+     * @brief A reserved byte.
+     */
+    uint8_t _reservedByte1 = 0;
+
+    /**
+     * @brief A reserved byte.
+     */
+    uint8_t _reservedByte2 = 0;
+
+    /**
+     * @brief Total number of sections in the PEL
+     */
+    uint8_t _sectionCount = 3; // private header, user header, primary src = 3
+
+    /**
+     * @brief The OpenBMC event log ID that corresponds to this PEL.
+     */
+    uint32_t _obmcLogID = 0;
+
+    /**
+     * @brief The creator subsystem version field
+     */
+    uint64_t _creatorVersion;
+    // CreatorVersion _creatorVersion;
+
+    /**
+     * @brief The platform log ID field
+     */
+    uint32_t _plid;
+
+    /**
+     * @brief The log entry ID field
+     */
+    uint32_t _id = 0;
+};
+
+/**
+ * @brief Stream insertion operator for the CreatorVersion
+ *
+ * @param[out] s - the stream
+ * @param[in] cv - the CreatorVersion object
+ */
+// Stream& operator<<(Stream& s, const CreatorVersion& cv);
+
+/**
+ * @brief Stream extraction operator for the CreatorVersion
+ *
+ * @param[in] s - the stream
+ * @param[out] cv - the CreatorVersion object
+ */
+// Stream& operator>>(Stream& s, CreatorVersion& cv);
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/section_header.hpp b/attn/pel/section_header.hpp
new file mode 100644
index 0000000..ae0a2c5
--- /dev/null
+++ b/attn/pel/section_header.hpp
@@ -0,0 +1,130 @@
+#pragma once
+
+#include "stream.hpp"
+
+#include <cstdint>
+
+namespace attn
+{
+namespace pel
+{
+
+/**
+ * @class SectionHeader
+ *
+ * @brief This 8-byte header is at the start of every PEL section.
+ *
+ * |--------+------------+------------+-----------+------------|
+ * | length | byte0      | byte1      | byte2     | byte3      |
+ * |--------+------------+------------+-----------+------------|
+ * | 4      | Section ID              | Section Length         |
+ * |--------+------------+-------------------------------------|
+ * | 4      | Version    | Sub-type   | Component ID           |
+ * |--------+--------------------------------------------------|
+ *
+ * Section ID:
+ * A two-ASCII character field which uniquely identifies the type of section.
+ *
+ * Section length:
+ * Length in bytes of the section, including the entire section header.
+ *
+ * Section Version:
+ * A one byte integer intended to identify differences in header structures.
+ *
+ * Section sub-type:
+ * Optional. Additional identifier describing the section.
+ *
+ * Component IDs:
+ * Optional. By convention the creator's component ID is placed in the
+ * component ID field of the Private Header and the committing component
+ * ID is placed in the component ID field of the User Header.
+ */
+struct SectionHeader
+{
+  public:
+    /**
+     * @brief Constructor
+     */
+    SectionHeader() : id(0), size(0), version(0), subType(0), componentID(0) {}
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] id - the ID field
+     * @param[in] size - the size field
+     * @param[in] version - the version field
+     * @param[in] subType - the sub-type field
+     * @param[in] componentID - the component ID field
+     */
+    SectionHeader(uint16_t id, uint16_t size, uint8_t version, uint8_t subType,
+                  uint16_t componentID) :
+        id(id),
+        size(size), version(version), subType(subType), componentID(componentID)
+    {}
+
+    /**
+     * @brief A two character ASCII field which identifies the section type.
+     */
+    uint16_t id;
+
+    /**
+     * @brief The size of the section in bytes, including this section header.
+     */
+    uint16_t size;
+
+    /**
+     * @brief The section format version.
+     */
+    uint8_t version;
+
+    /**
+     * @brief The section sub-type.
+     */
+    uint8_t subType;
+
+    /**
+     * @brief The component ID, which has various meanings depending on the
+     * section.
+     */
+    uint16_t componentID;
+
+    /**
+     * @brief Returns the size of header when flattened into a PEL.
+     *
+     * @return size_t - the size of the header
+     */
+    static constexpr size_t flattenedSize()
+    {
+        return sizeof(id) + sizeof(size) + sizeof(version) + sizeof(subType) +
+               sizeof(componentID);
+    }
+};
+
+/**
+ * @brief Stream extraction operator for the SectionHeader
+ *
+ * @param[in] s - the stream
+ * @param[out] header - the SectionHeader object
+ */
+inline Stream& operator>>(Stream& s, SectionHeader& header)
+{
+    s >> header.id >> header.size >> header.version >> header.subType >>
+        header.componentID;
+    return s;
+}
+
+/**
+ * @brief Stream insertion operator for the section header
+ *
+ * @param[out] s - the stream
+ * @param[in] header - the SectionHeader object
+ */
+inline Stream& operator<<(Stream& s, const SectionHeader& header)
+{
+    s << header.id << header.size << header.version << header.subType
+      << header.componentID;
+    return s;
+}
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/stream.hpp b/attn/pel/stream.hpp
new file mode 100644
index 0000000..a82bd8a
--- /dev/null
+++ b/attn/pel/stream.hpp
@@ -0,0 +1,372 @@
+#pragma once
+
+#include <arpa/inet.h>
+#include <byteswap.h>
+
+#include <cassert>
+#include <cstring>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace attn
+{
+namespace pel
+{
+
+namespace detail
+{
+/**
+ * @brief A host-to-network implementation for uint64_t
+ *
+ * @param[in] value - the value to convert to
+ * @return uint64_t - the byteswapped value
+ */
+inline uint64_t htonll(uint64_t value)
+{
+    return bswap_64(value);
+}
+
+/**
+ * @brief A network-to-host implementation for uint64_t
+ *
+ * @param[in] value - the value to convert to
+ * @return uint64_t - the byteswapped value
+ */
+inline uint64_t ntohll(uint64_t value)
+{
+    return bswap_64(value);
+}
+} // namespace detail
+
+/**
+ * @class Stream
+ *
+ * This class is used for getting data types into and out of a vector<uint8_t>
+ * that contains data in network byte (big endian) ordering.
+ */
+class Stream
+{
+  public:
+    Stream()              = delete;
+    ~Stream()             = default;
+    Stream(const Stream&) = default;
+    Stream& operator=(const Stream&) = default;
+    Stream(Stream&&)                 = default;
+    Stream& operator=(Stream&&) = default;
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] data - the vector of data
+     */
+    explicit Stream(std::vector<uint8_t>& data) : _data(data), _offset(0) {}
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] data - the vector of data
+     * @param[in] offset - the starting offset
+     */
+    Stream(std::vector<uint8_t>& data, std::size_t offset) :
+        _data(data), _offset(offset)
+    {
+        if (_offset >= _data.size())
+        {
+            throw std::out_of_range("Offset out of range");
+        }
+    }
+
+    /**
+     * @brief Extraction operator for a uint8_t
+     *
+     * @param[out] value - filled in with the value
+     * @return Stream&
+     */
+    Stream& operator>>(uint8_t& value)
+    {
+        read(&value, 1);
+        return *this;
+    }
+
+    /**
+     * @brief Extraction operator for a char
+     *
+     * @param[out] value -filled in with the value
+     * @return Stream&
+     */
+    Stream& operator>>(char& value)
+    {
+        read(&value, 1);
+        return *this;
+    }
+
+    /**
+     * @brief Extraction operator for a uint16_t
+     *
+     * @param[out] value -filled in with the value
+     * @return Stream&
+     */
+    Stream& operator>>(uint16_t& value)
+    {
+        read(&value, 2);
+        value = htons(value);
+        return *this;
+    }
+
+    /**
+     * @brief Extraction operator for a uint32_t
+     *
+     * @param[out] value -filled in with the value
+     * @return Stream&
+     */
+    Stream& operator>>(uint32_t& value)
+    {
+        read(&value, 4);
+        value = htonl(value);
+        return *this;
+    }
+
+    /**
+     * @brief Extraction operator for a uint64_t
+     *
+     * @param[out] value -filled in with the value
+     * @return Stream&
+     */
+    Stream& operator>>(uint64_t& value)
+    {
+        read(&value, 8);
+        value = detail::htonll(value);
+        return *this;
+    }
+
+    /**
+     * @brief Extraction operator for a std::vector<uint8_t>
+     *
+     * The vector's size is the amount extracted.
+     *
+     * @param[out] value - filled in with the value
+     * @return Stream&
+     */
+    Stream& operator>>(std::vector<uint8_t>& value)
+    {
+        if (!value.empty())
+        {
+            read(value.data(), value.size());
+        }
+        return *this;
+    }
+
+    /**
+     * @brief Extraction operator for a std::vector<char>
+     *
+     * The vector's size is the amount extracted.
+     *
+     * @param[out] value - filled in with the value
+     * @return Stream&
+     */
+    Stream& operator>>(std::vector<char>& value)
+    {
+        if (!value.empty())
+        {
+            read(value.data(), value.size());
+        }
+        return *this;
+    }
+
+    /**
+     * @brief Insert operator for a uint8_t
+     *
+     * @param[in] value - the value to write to the stream
+     * @return Stream&
+     */
+    Stream& operator<<(uint8_t value)
+    {
+        write(&value, 1);
+        return *this;
+    }
+
+    /**
+     * @brief Insert operator for a char
+     *
+     * @param[in] value - the value to write to the stream
+     * @return Stream&
+     */
+    Stream& operator<<(char value)
+    {
+        write(&value, 1);
+        return *this;
+    }
+
+    /**
+     * @brief Insert operator for a uint16_t
+     *
+     * @param[in] value - the value to write to the stream
+     * @return Stream&
+     */
+    Stream& operator<<(uint16_t value)
+    {
+        uint16_t data = ntohs(value);
+        write(&data, 2);
+        return *this;
+    }
+
+    /**
+     * @brief Insert operator for a uint32_t
+     *
+     * @param[in] value - the value to write to the stream
+     * @return Stream&
+     */
+    Stream& operator<<(uint32_t value)
+    {
+        uint32_t data = ntohl(value);
+        write(&data, 4);
+        return *this;
+    }
+
+    /**
+     * @brief Insert operator for a uint64_t
+     *
+     * @param[in] value - the value to write to the stream
+     * @return Stream&
+     */
+    Stream& operator<<(uint64_t value)
+    {
+        uint64_t data = detail::ntohll(value);
+        write(&data, 8);
+        return *this;
+    }
+
+    /**
+     * @brief Insert operator for a std::vector<uint8_t>
+     *
+     * The full vector is written to the stream.
+     *
+     * @param[in] value - the value to write to the stream
+     * @return Stream&
+     */
+    Stream& operator<<(const std::vector<uint8_t>& value)
+    {
+        if (!value.empty())
+        {
+            write(value.data(), value.size());
+        }
+        return *this;
+    }
+
+    /**
+     * @brief Insert operator for a std::vector<char>
+     *
+     * The full vector is written to the stream.
+     *
+     * @param[in] value - the value to write to the stream
+     * @return Stream&
+     */
+    Stream& operator<<(const std::vector<char>& value)
+    {
+        if (!value.empty())
+        {
+            write(value.data(), value.size());
+        }
+        return *this;
+    }
+
+    /**
+     * @brief Sets the offset of the stream
+     *
+     * @param[in] newOffset - the new offset
+     */
+    void offset(std::size_t newOffset)
+    {
+        if (newOffset >= _data.size())
+        {
+            throw std::out_of_range("new offset out of range");
+        }
+
+        _offset = newOffset;
+    }
+
+    /**
+     * @brief Returns the current offset of the stream
+     *
+     * @return size_t - the offset
+     */
+    std::size_t offset() const
+    {
+        return _offset;
+    }
+
+    /**
+     * @brief Returns the remaining bytes left between the current offset
+     *        and the data size.
+     *
+     * @return size_t - the remaining size
+     */
+    std::size_t remaining() const
+    {
+        assert(_data.size() >= _offset);
+        return _data.size() - _offset;
+    }
+
+    /**
+     * @brief Reads a specified number of bytes out of a stream
+     *
+     * @param[out] out - filled in with the data
+     * @param[in] size - the size to read
+     */
+    void read(void* out, std::size_t size)
+    {
+        rangeCheck(size);
+        memcpy(out, &_data[_offset], size);
+        _offset += size;
+    }
+
+    /**
+     * @brief Writes a specified number of bytes into the stream
+     *
+     * @param[in] in - the data to write
+     * @param[in] size - the size to write
+     */
+    void write(const void* in, std::size_t size)
+    {
+        size_t newSize = _offset + size;
+        if (newSize > _data.size())
+        {
+            _data.resize(newSize, 0);
+        }
+        memcpy(&_data[_offset], in, size);
+        _offset += size;
+    }
+
+  private:
+    /**
+     * @brief Throws an exception if the size passed in plus the current
+     *        offset is bigger than the current data size.
+     * @param[in] size - the size to check
+     */
+    void rangeCheck(std::size_t size)
+    {
+        if (_offset + size > _data.size())
+        {
+            std::string msg{"Attempted stream overflow: offset "};
+            msg += std::to_string(_offset) + " buffer size " +
+                   std::to_string(_data.size()) + " op size " +
+                   std::to_string(size);
+            throw std::out_of_range(msg.c_str());
+        }
+    }
+
+    /**
+     * @brief The data that the stream accesses.
+     */
+    std::vector<uint8_t>& _data;
+
+    /**
+     * @brief The current offset of the stream.
+     */
+    std::size_t _offset;
+};
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/user_header.cpp b/attn/pel/user_header.cpp
new file mode 100644
index 0000000..23238e8
--- /dev/null
+++ b/attn/pel/user_header.cpp
@@ -0,0 +1,48 @@
+#include "user_header.hpp"
+
+namespace attn
+{
+namespace pel
+{
+
+UserHeader::UserHeader(Stream& pel)
+{
+    unflatten(pel);
+}
+
+void UserHeader::flatten(Stream& stream) const
+{
+    stream << _header << _eventSubsystem << _eventScope << _eventSeverity
+           << _eventType << _reserved4Byte1 << _problemDomain << _problemVector
+           << _actionFlags << _reserved4Byte2;
+}
+
+void UserHeader::unflatten(Stream& stream)
+{
+    stream >> _header >> _eventSubsystem >> _eventScope >> _eventSeverity >>
+        _eventType >> _reserved4Byte1 >> _problemDomain >> _problemVector >>
+        _actionFlags >> _reserved4Byte2;
+}
+
+void UserHeader::setSubsystem(uint8_t subsystem)
+{
+    _eventSubsystem = subsystem;
+}
+
+void UserHeader::setSeverity(uint8_t severity)
+{
+    _eventSeverity = severity;
+}
+
+void UserHeader::setType(uint8_t type)
+{
+    _eventType = type;
+}
+
+void UserHeader::setAction(uint16_t action)
+{
+    _actionFlags = action;
+}
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/pel/user_header.hpp b/attn/pel/user_header.hpp
new file mode 100644
index 0000000..3be6e66
--- /dev/null
+++ b/attn/pel/user_header.hpp
@@ -0,0 +1,157 @@
+#pragma once
+
+#include "pel_common.hpp"
+#include "pel_section.hpp"
+#include "stream.hpp"
+
+namespace attn
+{
+namespace pel
+{
+
+/**
+ * @class UserHeader
+ *
+ * This represents the User Header section in a PEL.
+ *
+ * |--------+----------------+----------------+----------------+------------|
+ * | length | byte0          | byte1          | byte2          | byte3      |
+ * |--------+----------------+----------------+----------------+------------|
+ * | 8      | Section Header                                                |
+ * |        |                                                               |
+ * |--------+----------------+----------------+----------------+------------|
+ * | 4      | Subsystem ID   | Event Scope    | Event Severity | Event Type |
+ * |--------+----------------+----------------+----------------+------------|
+ * | 4      | reserved                                                      |
+ * |--------+----------------+----------------+-----------------------------|
+ * | 4      | Problem Domain | Problem Vector | Event Action Flags          |
+ * |--------+----------------+----------------+-----------------------------|
+ * | 4      | reserved                                                      |
+ * |--------+---------------------------------------------------------------|
+ *
+ */
+class UserHeader : public Section
+{
+  public:
+    UserHeader()                  = delete;
+    ~UserHeader()                 = default;
+    UserHeader(const UserHeader&) = default;
+    UserHeader& operator=(const UserHeader&) = default;
+    UserHeader(UserHeader&&)                 = default;
+    UserHeader& operator=(UserHeader&&) = default;
+
+    /**
+     * @brief Constructor
+     *
+     * Fills in this class's data fields from raw data.
+     *
+     * @param[in] pel - the PEL data stream
+     */
+    explicit UserHeader(Stream& pel);
+
+    /**
+     * @brief Flatten the section into the stream
+     *
+     * @param[in] stream - The stream to write to
+     */
+    void flatten(Stream& stream) const override;
+
+    /**
+     * @brief Fills in the object from the stream data
+     *
+     * @param[in] stream - The stream to read from
+     */
+    void unflatten(Stream& stream);
+
+    /**
+     * @brief Returns the size of this section when flattened into a PEL
+     *
+     * @return size_t - the size of the section
+     */
+    static constexpr size_t flattenedSize()
+    {
+        return Section::flattenedSize() + sizeof(_eventSubsystem) +
+               sizeof(_eventScope) + sizeof(_eventSeverity) +
+               sizeof(_eventType) + sizeof(_reserved4Byte1) +
+               sizeof(_problemDomain) + sizeof(_problemVector) +
+               sizeof(_actionFlags) + sizeof(_reserved4Byte2);
+    }
+
+    /**
+     * @brief Set the subsystem field
+     *
+     * @param[in] subsystem - The subsystem value
+     */
+    void setSubsystem(uint8_t subsystem);
+
+    /**
+     * @brief Set the severity field
+     *
+     * @param[in] severity - The severity to set
+     */
+    void setSeverity(uint8_t severity);
+
+    /**
+     * @brief Set the event type field
+     *
+     * @param[in] type - The event type
+     */
+    void setType(uint8_t type);
+
+    /**
+     * @brief Set the action flags field
+     *
+     * @param[in] action - The action flags to set
+     */
+    void setAction(uint16_t action);
+
+  private:
+    /**
+     * @brief The subsystem associated with the event.
+     */
+    uint8_t _eventSubsystem;
+
+    /**
+     * @brief The event scope field.
+     */
+    uint8_t _eventScope = static_cast<uint8_t>(EventScope::platform);
+
+    /**
+     * @brief The event severity.
+     */
+    uint8_t _eventSeverity; // set by constructor
+
+    /**
+     * @brief The event type.
+     */
+    uint8_t _eventType = static_cast<uint8_t>(EventType::trace);
+
+    /**
+     * @brief A reserved 4 byte placeholder
+     */
+    uint32_t _reserved4Byte1 = 0;
+
+    /**
+     * @brief The problem domain field.
+     */
+    uint8_t _problemDomain = 0;
+
+    /**
+     * @brief The problem vector field.
+     */
+    uint8_t _problemVector = 0;
+
+    /**
+     * @brief The action flags field.
+     */
+    uint16_t _actionFlags; // set by contructor
+
+    /**
+     * @brief A reserved 4 byte
+     * placeholder
+     */
+    uint32_t _reserved4Byte2 = 0;
+};
+
+} // namespace pel
+} // namespace attn
diff --git a/attn/ti_handler.cpp b/attn/ti_handler.cpp
index 35fb346..e1dce7c 100644
--- a/attn/ti_handler.cpp
+++ b/attn/ti_handler.cpp
@@ -1,6 +1,7 @@
 #include <attn/attn_common.hpp>
 #include <attn/attn_handler.hpp>
 #include <attn/attn_logging.hpp>
+#include <attn/pel/pel_common.hpp>
 #include <attn/ti_handler.hpp>
 #include <sdbusplus/bus.hpp>
 #include <sdbusplus/exception.hpp>
@@ -17,7 +18,7 @@
  * Use the TI info data area to determine if this is either a HB or a PHYP
  * TI event then handle the event.
  *
- * @param i_tiDataArea pointer to the TI infor data
+ * @param i_tiDataArea pointer to the TI info data
  */
 int tiHandler(TiDataArea* i_tiDataArea)
 {
@@ -84,10 +85,17 @@
     if (nullptr != i_tiDataArea)
     {
         parsePhypOpalTiInfo(tiAdditionalData, i_tiDataArea);
-        parseRawTiInfo(tiAdditionalData, i_tiDataArea);
     }
 
-    eventTerminate(tiAdditionalData); // generate PEL
+    tiAdditionalData["Subsystem"] =
+        std::to_string(static_cast<uint8_t>(pel::SubsystemID::hypervisor));
+
+    char srcChar[8];
+    memcpy(srcChar, &(i_tiDataArea->asciiData0), 4);
+    memcpy(&srcChar[4], &(i_tiDataArea->asciiData1), 4);
+    tiAdditionalData["SrcAscii"] = std::string{srcChar};
+
+    eventTerminate(tiAdditionalData, (char*)i_tiDataArea);
 }
 
 /**
@@ -207,42 +215,19 @@
     if (nullptr != i_tiDataArea)
     {
         parseHbTiInfo(tiAdditionalData, i_tiDataArea);
-        parseRawTiInfo(tiAdditionalData, i_tiDataArea);
     }
 
     if (true == generatePel)
     {
-        eventTerminate(tiAdditionalData); // generate PEL
-    }
-}
+        tiAdditionalData["Subsystem"] =
+            std::to_string(static_cast<uint8_t>(pel::SubsystemID::hostboot));
 
-/** @brief Parse the TI info data area into map as raw 32-bit fields */
-void parseRawTiInfo(std::map<std::string, std::string>& i_map,
-                    TiDataArea* i_buffer)
-{
-    if (nullptr == i_buffer)
-    {
-        return;
-    }
+        char srcChar[8];
+        memcpy(srcChar, &(i_tiDataArea->srcWord12HbWord0), 4);
+        memcpy(&srcChar[4], &(i_tiDataArea->asciiData1), 4);
+        tiAdditionalData["SrcAscii"] = std::string{srcChar};
 
-    uint32_t* tiDataArea = (uint32_t*)i_buffer;
-    std::stringstream ss;
-
-    ss << std::hex << std::setfill('0');
-    ss << "raw:";
-    while (tiDataArea <= (uint32_t*)((char*)i_buffer + sizeof(TiDataArea)))
-    {
-        ss << std::setw(8) << std::endl << be32toh(*tiDataArea);
-        tiDataArea++;
-    }
-
-    std::string key, value;
-    char delim = ':';
-
-    while (std::getline(ss, key, delim))
-    {
-        std::getline(ss, value, delim);
-        i_map[key] = value;
+        eventTerminate(tiAdditionalData, (char*)i_tiDataArea);
     }
 }
 
diff --git a/attn/ti_handler.hpp b/attn/ti_handler.hpp
index 33714a1..492868c 100644
--- a/attn/ti_handler.hpp
+++ b/attn/ti_handler.hpp
@@ -109,14 +109,6 @@
 void handleHbTi(TiDataArea* i_tiDataArea);
 
 /**
- * @brief Parse TI info data as raw 32-bit fields
- *
- * Read the TI data in as 32-bit fields and place into map.
- */
-void parseRawTiInfo(std::map<std::string, std::string>& i_map,
-                    TiDataArea* i_buffer);
-
-/**
  * @brief Parse TI info data as PHYP/OPAL data
  *
  * Read the TI data, parse as PHYP/OPAL data and place into map.
diff --git a/meson.build b/meson.build
index c2a74c7..64cf993 100644
--- a/meson.build
+++ b/meson.build
@@ -92,4 +92,3 @@
 if not build_tests.disabled()
   subdir('test')
 endif
-
diff --git a/test/end2end/logging.cpp b/test/end2end/logging.cpp
index 5163c4e..a03ad4c 100644
--- a/test/end2end/logging.cpp
+++ b/test/end2end/logging.cpp
@@ -31,7 +31,8 @@
     std::cout << "event: attention fail" << i_error << std::endl;
 }
 
-void eventTerminate(std::map<std::string, std::string> i_additionalData)
+void eventTerminate(std::map<std::string, std::string> i_additionalData,
+                    char* i_tiInfoData)
 {
     std::cout << "event: terminate" << std::endl;
 
@@ -41,6 +42,11 @@
         std::cout << '\t' << itr->first << '\t' << itr->second << '\n';
     }
     std::cout << std::endl;
+
+    if (nullptr != i_tiInfoData)
+    {
+        std::cout << "TI data present" << std::endl;
+    }
 }
 
 void eventVital()