Add UUIDs for both the logger and parts

These aren't required to be used, but they help track which parts should
go together, and which parts came from the same logger.  The former
being more interesting than the latter, but if we are here, why not
track both?

Follow-up review will add a "sort bunch of files into corresponding
parts" function.

Change-Id: I1e17ae8617b267bc6c5d90ff9d7feb351c0c4b47
diff --git a/aos/events/logging/BUILD b/aos/events/logging/BUILD
index b4a9b00..81ab90f 100644
--- a/aos/events/logging/BUILD
+++ b/aos/events/logging/BUILD
@@ -47,6 +47,7 @@
     deps = [
         ":logfile_utils",
         ":logger_fbs",
+        ":uuid",
         "//aos/events:event_loop",
         "//aos/events:simulated_event_loop",
         "//aos/network:message_bridge_server_fbs",
@@ -158,3 +159,18 @@
         "//aos/testing:googletest",
     ],
 )
+
+cc_library(
+    name = "uuid",
+    srcs = ["uuid.cc"],
+    hdrs = ["uuid.h"],
+)
+
+cc_test(
+    name = "uuid_test",
+    srcs = ["uuid_test.cc"],
+    deps = [
+        ":uuid",
+        "//aos/testing:googletest",
+    ],
+)
diff --git a/aos/events/logging/logfile_utils.cc b/aos/events/logging/logfile_utils.cc
index 05ee4e0..214fa2b 100644
--- a/aos/events/logging/logfile_utils.cc
+++ b/aos/events/logging/logfile_utils.cc
@@ -377,12 +377,44 @@
       }
     }
 
+    // We don't have a good way to set the realtime start time on remote nodes.
+    // Confirm it remains consistent.
+    CHECK_EQ(log_file_header_.mutable_message()->has_realtime_start_time(),
+             message_reader.log_file_header()->has_realtime_start_time());
+
+    // Parts index will *not* match unless we set them to match.  We only want
+    // to accept the start time and parts mismatching, so set them.
+    log_file_header_.mutable_message()->mutate_parts_index(
+        message_reader.log_file_header()->parts_index());
+
     // Now compare that the headers match.
-    CHECK(CompareFlatBuffer(message_reader.raw_log_file_header(),
-                            log_file_header_))
-        << ": Header is different between log file chunks " << filenames_[0]
-        << " and " << filenames_[i] << ", this is not supported.";
+    if (!CompareFlatBuffer(message_reader.raw_log_file_header(),
+                           log_file_header_)) {
+      if (message_reader.log_file_header()->has_logger_uuid() &&
+          log_file_header_.message().has_logger_uuid() &&
+          message_reader.log_file_header()->logger_uuid()->string_view() !=
+              log_file_header_.message().logger_uuid()->string_view()) {
+        LOG(FATAL) << "Logger UUIDs don't match between log file chunks "
+                   << filenames_[0] << " and " << filenames_[i]
+                   << ", this is not supported.";
+      }
+      if (message_reader.log_file_header()->has_parts_uuid() &&
+          log_file_header_.message().has_parts_uuid() &&
+          message_reader.log_file_header()->parts_uuid()->string_view() !=
+              log_file_header_.message().parts_uuid()->string_view()) {
+        LOG(FATAL) << "Parts UUIDs don't match between log file chunks "
+                   << filenames_[0] << " and " << filenames_[i]
+                   << ", this is not supported.";
+      }
+
+      LOG(FATAL) << "Header is different between log file chunks "
+                 << filenames_[0] << " and " << filenames_[i]
+                 << ", this is not supported.";
+    }
   }
+  // Put the parts index back to the first log file chunk.
+  log_file_header_.mutable_message()->mutate_parts_index(
+      message_reader_->log_file_header()->parts_index());
 
   // Setup per channel state.
   channels_.resize(configuration()->channels()->size());
@@ -429,11 +461,22 @@
   // We can't support the config diverging between two log file headers.  See if
   // they are the same.
   if (next_filename_index_ != 0) {
+    // In order for the headers to identically compare, they need to have the
+    // same parts_index.  Rewrite the saved header with the new parts_index,
+    // compare, and then restore.
+    const int32_t original_parts_index =
+        log_file_header_.message().parts_index();
+    log_file_header_.mutable_message()->mutate_parts_index(
+        message_reader_->log_file_header()->parts_index());
+
     CHECK(CompareFlatBuffer(message_reader_->raw_log_file_header(),
                             log_file_header_))
         << ": Header is different between log file chunks "
         << filenames_[next_filename_index_] << " and "
         << filenames_[next_filename_index_ - 1] << ", this is not supported.";
+
+    log_file_header_.mutable_message()->mutate_parts_index(
+        original_parts_index);
   }
 
   ++next_filename_index_;
diff --git a/aos/events/logging/logger.cc b/aos/events/logging/logger.cc
index 89839ec..85809b1 100644
--- a/aos/events/logging/logger.cc
+++ b/aos/events/logging/logger.cc
@@ -12,6 +12,7 @@
 #include "absl/types/span.h"
 #include "aos/events/event_loop.h"
 #include "aos/events/logging/logger_generated.h"
+#include "aos/events/logging/uuid.h"
 #include "aos/flatbuffer_merge.h"
 #include "aos/network/team_number.h"
 #include "aos/time/time.h"
@@ -34,16 +35,30 @@
 namespace logger {
 namespace chrono = std::chrono;
 
+void LogNamer::UpdateHeader(
+    aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header,
+    const UUID &uuid, int parts_index) {
+  header->mutable_message()->mutate_parts_index(parts_index);
+  CHECK_EQ(uuid.string_view().size(),
+           header->mutable_message()->mutable_parts_uuid()->size());
+  std::copy(uuid.string_view().begin(), uuid.string_view().end(),
+            reinterpret_cast<char *>(
+                header->mutable_message()->mutable_parts_uuid()->Data()));
+}
+
 void MultiNodeLogNamer::WriteHeader(
-    const aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> &header,
+    aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header,
     const Node *node) {
   if (node == this->node()) {
-    data_writer_->WriteSizedFlatbuffer(header.full_span());
+    UpdateHeader(header, uuid_, part_number_);
+    data_writer_->WriteSizedFlatbuffer(header->full_span());
   } else {
     for (std::pair<const Channel *const, DataWriter> &data_writer :
          data_writers_) {
       if (node == data_writer.second.node) {
-        data_writer.second.writer->WriteSizedFlatbuffer(header.full_span());
+        UpdateHeader(header, data_writer.second.uuid,
+                     data_writer.second.part_number);
+        data_writer.second.writer->WriteSizedFlatbuffer(header->full_span());
       }
     }
   }
@@ -51,18 +66,21 @@
 
 void MultiNodeLogNamer::Rotate(
     const Node *node,
-    const aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> &header) {
+    aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header) {
   if (node == this->node()) {
     ++part_number_;
     *data_writer_ = std::move(*OpenDataWriter());
-    data_writer_->WriteSizedFlatbuffer(header.full_span());
+    UpdateHeader(header, uuid_, part_number_);
+    data_writer_->WriteSizedFlatbuffer(header->full_span());
   } else {
     for (std::pair<const Channel *const, DataWriter> &data_writer :
          data_writers_) {
       if (node == data_writer.second.node) {
         ++data_writer.second.part_number;
         data_writer.second.rotate(data_writer.first, &data_writer.second);
-        data_writer.second.writer->WriteSizedFlatbuffer(header.full_span());
+        UpdateHeader(header, data_writer.second.uuid,
+                     data_writer.second.part_number);
+        data_writer.second.writer->WriteSizedFlatbuffer(header->full_span());
       }
     }
   }
@@ -76,6 +94,7 @@
 Logger::Logger(std::unique_ptr<LogNamer> log_namer, EventLoop *event_loop,
                std::chrono::milliseconds polling_period)
     : event_loop_(event_loop),
+      uuid_(UUID::Random()),
       log_namer_(std::move(log_namer)),
       timer_handler_(event_loop_->AddTimer([this]() { DoLogData(); })),
       polling_period_(polling_period),
@@ -246,7 +265,7 @@
         configuration::GetNodeIndex(event_loop_->configuration(), node);
     MaybeUpdateTimestamp(node, node_index, monotonic_start_time,
                          realtime_start_time);
-    log_namer_->WriteHeader(node_state_[node_index].log_file_header, node);
+    log_namer_->WriteHeader(&node_state_[node_index].log_file_header, node);
   }
 }
 
@@ -268,7 +287,7 @@
             node, node_index,
             server_statistics_fetcher_.context().monotonic_event_time,
             server_statistics_fetcher_.context().realtime_event_time)) {
-      log_namer_->Rotate(node, node_state_[node_index].log_file_header);
+      log_namer_->Rotate(node, &node_state_[node_index].log_file_header);
     }
   }
 }
@@ -361,9 +380,15 @@
   flatbuffers::Offset<aos::Configuration> configuration_offset =
       CopyFlatBuffer(event_loop_->configuration(), &fbb);
 
-  flatbuffers::Offset<flatbuffers::String> string_offset =
+  flatbuffers::Offset<flatbuffers::String> name_offset =
       fbb.CreateString(network::GetHostname());
 
+  flatbuffers::Offset<flatbuffers::String> logger_uuid_offset =
+      fbb.CreateString(uuid_.string_view());
+
+  flatbuffers::Offset<flatbuffers::String> parts_uuid_offset =
+      fbb.CreateString("00000000-0000-4000-8000-000000000000");
+
   flatbuffers::Offset<Node> node_offset;
 
   if (configuration::MultiNode(event_loop_->configuration())) {
@@ -372,7 +397,7 @@
 
   aos::logger::LogFileHeader::Builder log_file_header_builder(fbb);
 
-  log_file_header_builder.add_name(string_offset);
+  log_file_header_builder.add_name(name_offset);
 
   // Only add the node if we are running in a multinode configuration.
   if (node != nullptr) {
@@ -400,6 +425,11 @@
             .count());
   }
 
+  log_file_header_builder.add_logger_uuid(logger_uuid_offset);
+
+  log_file_header_builder.add_parts_uuid(parts_uuid_offset);
+  log_file_header_builder.add_parts_index(0);
+
   fbb.FinishSizePrefixed(log_file_header_builder.Finish());
   return fbb.Release();
 }
@@ -408,7 +438,7 @@
   for (const Node *node : log_namer_->nodes()) {
     const int node_index =
         configuration::GetNodeIndex(event_loop_->configuration(), node);
-    log_namer_->Rotate(node, node_state_[node_index].log_file_header);
+    log_namer_->Rotate(node, &node_state_[node_index].log_file_header);
   }
 }
 
diff --git a/aos/events/logging/logger.fbs b/aos/events/logging/logger.fbs
index 00becfa..2905dfa 100644
--- a/aos/events/logging/logger.fbs
+++ b/aos/events/logging/logger.fbs
@@ -15,9 +15,11 @@
 
 table LogFileHeader {
   // Time this log file started on the monotonic clock in nanoseconds.
-  monotonic_start_time:long = -9223372036854775808;
+  // If this isn't known (the log file is being recorded from another node
+  // where we don't know the time offset), both timestamps will be min_time.
+  monotonic_start_time:int64 = -9223372036854775808;
   // Time this log file started on the realtime clock in nanoseconds.
-  realtime_start_time:long = -9223372036854775808;
+  realtime_start_time:int64 = -9223372036854775808;
 
   // Messages are not written in order to disk.  They will be out of order by
   // at most this duration (in nanoseconds).  If the log reader buffers until
@@ -33,6 +35,36 @@
 
   // The current node, if known and running in a multi-node configuration.
   node:Node;
+
+  // All UUIDs are uuid4.
+
+  // A log file is made up of a bunch of log files and parts.  These build up
+  // a tree.  Every .bfbs file has a LogFileHeader at the start.
+  //
+  //  /-- basename_pi1_data.part0.bfbs, basename_pi1_data.part1.bfbs, etc.
+  // ---- basename_timestamps/pi1/aos/remote_timestamps/pi2/aos.logger.MessageHeader.part0.bfbs, etc.
+  //  \-- basename_pi2_data/pi2/aos/aos.message_bridge.Timestamp.part0.bfbs, etc.
+
+  // All log files and parts from a single run of a logger executable will have
+  // the same uuid.  This should be all the files generated on a single node.
+  // Used to correlate files recorded together.
+  logger_uuid:string;
+
+  // Part files which go together all have headers.  When creating a log file
+  // with multiple parts, the logger should stop writing to part n-1 as soon
+  // as it starts writing to part n, and write messages as though there was
+  // just 1 big file.  Therefore, part files won't be self standing, since
+  // they won't have data fetched at the beginning.
+
+  // If data is logged before the time offset can be established to the other
+  // node, the start time will be monotonic_clock::min_time, and a new part file
+  // will be created when the start time is known.
+
+  // All the parts which go together have the same uuid.
+  parts_uuid:string;
+  // And the parts_index corresponds to which part this is in the sequence.  The
+  // index should start at 0.
+  parts_index:int32;
 }
 
 // Table holding a message.
@@ -58,12 +90,12 @@
 
   // Time this message was sent on the monotonic clock of the remote node in
   // nanoseconds.
-  monotonic_remote_time:long = -9223372036854775808;
+  monotonic_remote_time:int64 = -9223372036854775808;
   // Time this message was sent on the realtime clock of the remote node in
   // nanoseconds.
-  realtime_remote_time:long = -9223372036854775808;
+  realtime_remote_time:int64 = -9223372036854775808;
   // Queue index of this message on the remote node.
-  remote_queue_index:uint = 4294967295;
+  remote_queue_index:uint32 = 4294967295;
 }
 
 root_type MessageHeader;
diff --git a/aos/events/logging/logger.h b/aos/events/logging/logger.h
index e599091..d4ed786 100644
--- a/aos/events/logging/logger.h
+++ b/aos/events/logging/logger.h
@@ -14,6 +14,7 @@
 #include "aos/events/logging/eigen_mpq.h"
 #include "aos/events/logging/logfile_utils.h"
 #include "aos/events/logging/logger_generated.h"
+#include "aos/events/logging/uuid.h"
 #include "aos/events/simulated_event_loop.h"
 #include "aos/network/message_bridge_server_generated.h"
 #include "aos/network/timestamp_filter.h"
@@ -30,7 +31,7 @@
   virtual ~LogNamer() {}
 
   virtual void WriteHeader(
-      const aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> &header,
+      aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header,
       const Node *node) = 0;
   virtual DetachedBufferWriter *MakeWriter(const Channel *channel) = 0;
 
@@ -39,13 +40,16 @@
       const Channel *channel, const Node *node) = 0;
   virtual void Rotate(
       const Node *node,
-      const aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader>
-          &header) = 0;
+      aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header) = 0;
   const std::vector<const Node *> &nodes() const { return nodes_; }
 
   const Node *node() const { return node_; }
 
  protected:
+  void UpdateHeader(
+      aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header,
+      const UUID &uuid, int part_id);
+
   const Node *const node_;
   std::vector<const Node *> nodes_;
 };
@@ -53,13 +57,17 @@
 class LocalLogNamer : public LogNamer {
  public:
   LocalLogNamer(std::string_view base_name, const Node *node)
-      : LogNamer(node), base_name_(base_name), data_writer_(OpenDataWriter()) {}
+      : LogNamer(node),
+        base_name_(base_name),
+        uuid_(UUID::Random()),
+        data_writer_(OpenDataWriter()) {}
 
   void WriteHeader(
-      const aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> &header,
+      aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header,
       const Node *node) override {
     CHECK_EQ(node, this->node());
-    data_writer_->WriteSizedFlatbuffer(header.full_span());
+    UpdateHeader(header, uuid_, part_number_);
+    data_writer_->WriteSizedFlatbuffer(header->full_span());
   }
 
   DetachedBufferWriter *MakeWriter(const Channel *channel) override {
@@ -68,12 +76,13 @@
   }
 
   void Rotate(const Node *node,
-              const aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader>
-                  &header) override {
+              aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header)
+      override {
     CHECK(node == this->node());
     ++part_number_;
     *data_writer_ = std::move(*OpenDataWriter());
-    data_writer_->WriteSizedFlatbuffer(header.full_span());
+    UpdateHeader(header, uuid_, part_number_);
+    data_writer_->WriteSizedFlatbuffer(header->full_span());
   }
 
   DetachedBufferWriter *MakeTimestampWriter(const Channel *channel) override {
@@ -98,6 +107,7 @@
         absl::StrCat(base_name_, ".part", part_number_, ".bfbs"));
   }
   const std::string base_name_;
+  const UUID uuid_;
   size_t part_number_ = 0;
   std::unique_ptr<DetachedBufferWriter> data_writer_;
 };
@@ -111,17 +121,18 @@
       : LogNamer(node),
         base_name_(base_name),
         configuration_(configuration),
+        uuid_(UUID::Random()),
         data_writer_(OpenDataWriter()) {}
 
   // Writes the header to all log files for a specific node.  This function
   // needs to be called after all the writers are created.
   void WriteHeader(
-      const aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> &header,
+      aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header,
       const Node *node) override;
 
   void Rotate(const Node *node,
-              const aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader>
-                  &header) override;
+              aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> *header)
+      override;
 
   // Makes a data logger for a specific channel.
   DetachedBufferWriter *MakeWriter(const Channel *channel) override {
@@ -221,6 +232,7 @@
     std::unique_ptr<DetachedBufferWriter> writer = nullptr;
     const Node *node;
     size_t part_number = 0;
+    UUID uuid = UUID::Random();
     std::function<void(const Channel *, DataWriter *)> rotate;
   };
 
@@ -258,6 +270,7 @@
 
   const std::string base_name_;
   const Configuration *const configuration_;
+  const UUID uuid_;
 
   size_t part_number_ = 0;
 
@@ -303,6 +316,7 @@
   void LogUntil(monotonic_clock::time_point t);
 
   EventLoop *event_loop_;
+  const UUID uuid_;
   std::unique_ptr<LogNamer> log_namer_;
 
   // Structure to track both a fetcher, and if the data fetched has been
diff --git a/aos/events/logging/logger_test.cc b/aos/events/logging/logger_test.cc
index ab5f02c..bf062b8 100644
--- a/aos/events/logging/logger_test.cc
+++ b/aos/events/logging/logger_test.cc
@@ -125,6 +125,23 @@
     event_loop_factory_.RunFor(chrono::milliseconds(10000));
   }
 
+  {
+    // Confirm that the UUIDs match for both the parts and the logger, and the
+    // parts_index increments.
+    std::vector<FlatbufferVector<LogFileHeader>> log_header;
+    for (std::string_view f : {logfile0, logfile1}) {
+      log_header.emplace_back(ReadHeader(f));
+    }
+
+    EXPECT_EQ(log_header[0].message().logger_uuid()->string_view(),
+              log_header[1].message().logger_uuid()->string_view());
+    EXPECT_EQ(log_header[0].message().parts_uuid()->string_view(),
+              log_header[1].message().parts_uuid()->string_view());
+
+    EXPECT_EQ(log_header[0].message().parts_index(), 0);
+    EXPECT_EQ(log_header[1].message().parts_index(), 1);
+  }
+
   // Even though it doesn't make any difference here, exercise the logic for
   // passing in a separate config.
   LogReader reader(std::vector<std::string>{logfile0, logfile1},
@@ -375,16 +392,50 @@
   }
 
   {
-    // Confirm that the headers are all for the correct nodes.
-    FlatbufferVector<LogFileHeader> logheader1 = ReadHeader(logfiles_[0]);
-    EXPECT_EQ(logheader1.message().node()->name()->string_view(), "pi1");
-    FlatbufferVector<LogFileHeader> logheader2 = ReadHeader(logfiles_[1]);
-    EXPECT_EQ(logheader2.message().node()->name()->string_view(), "pi2");
-    FlatbufferVector<LogFileHeader> logheader3 = ReadHeader(logfiles_[2]);
-    EXPECT_EQ(logheader3.message().node()->name()->string_view(), "pi2");
-    FlatbufferVector<LogFileHeader> logheader4 = ReadHeader(logfiles_[3]);
-    EXPECT_EQ(logheader4.message().node()->name()->string_view(), "pi2");
+    std::set<std::string> logfile_uuids;
+    std::set<std::string> parts_uuids;
+    // Confirm that we have the expected number of UUIDs for both the logfile
+    // UUIDs and parts UUIDs.
+    std::vector<FlatbufferVector<LogFileHeader>> log_header;
+    for (std::string_view f : logfiles_) {
+      log_header.emplace_back(ReadHeader(f));
+      logfile_uuids.insert(log_header.back().message().logger_uuid()->str());
+      parts_uuids.insert(log_header.back().message().parts_uuid()->str());
+    }
 
+    EXPECT_EQ(logfile_uuids.size(), 2u);
+    EXPECT_EQ(parts_uuids.size(), 7u);
+
+    // And confirm everything is on the correct node.
+    EXPECT_EQ(log_header[0].message().node()->name()->string_view(), "pi1");
+    EXPECT_EQ(log_header[1].message().node()->name()->string_view(), "pi2");
+    EXPECT_EQ(log_header[2].message().node()->name()->string_view(), "pi2");
+    EXPECT_EQ(log_header[3].message().node()->name()->string_view(), "pi2");
+    EXPECT_EQ(log_header[4].message().node()->name()->string_view(), "pi2");
+    EXPECT_EQ(log_header[5].message().node()->name()->string_view(), "pi2");
+    EXPECT_EQ(log_header[6].message().node()->name()->string_view(), "pi1");
+    EXPECT_EQ(log_header[7].message().node()->name()->string_view(), "pi1");
+    EXPECT_EQ(log_header[8].message().node()->name()->string_view(), "pi1");
+    EXPECT_EQ(log_header[9].message().node()->name()->string_view(), "pi1");
+    EXPECT_EQ(log_header[10].message().node()->name()->string_view(), "pi2");
+    EXPECT_EQ(log_header[11].message().node()->name()->string_view(), "pi2");
+
+    // And the parts index matches.
+    EXPECT_EQ(log_header[0].message().parts_index(), 0);
+    EXPECT_EQ(log_header[1].message().parts_index(), 0);
+    EXPECT_EQ(log_header[2].message().parts_index(), 1);
+    EXPECT_EQ(log_header[3].message().parts_index(), 0);
+    EXPECT_EQ(log_header[4].message().parts_index(), 0);
+    EXPECT_EQ(log_header[5].message().parts_index(), 1);
+    EXPECT_EQ(log_header[6].message().parts_index(), 0);
+    EXPECT_EQ(log_header[7].message().parts_index(), 1);
+    EXPECT_EQ(log_header[8].message().parts_index(), 0);
+    EXPECT_EQ(log_header[9].message().parts_index(), 1);
+    EXPECT_EQ(log_header[10].message().parts_index(), 0);
+    EXPECT_EQ(log_header[11].message().parts_index(), 1);
+  }
+
+  {
     using ::testing::UnorderedElementsAre;
 
     // Timing reports, pings
diff --git a/aos/events/logging/uuid.cc b/aos/events/logging/uuid.cc
new file mode 100644
index 0000000..9298c8b
--- /dev/null
+++ b/aos/events/logging/uuid.cc
@@ -0,0 +1,60 @@
+#include "aos/events/logging/uuid.h"
+
+#include <array>
+#include <random>
+#include <string_view>
+
+namespace aos {
+namespace {
+char ToHex(int val) {
+  if (val < 10) {
+    return val + '0';
+  } else {
+    return val - 10 + 'a';
+  }
+}
+}  // namespace
+
+UUID UUID::Random() {
+  std::random_device rd;
+  std::mt19937 gen(rd());
+
+  std::uniform_int_distribution<> dis(0, 15);
+  std::uniform_int_distribution<> dis2(8, 11);
+
+  UUID result;
+
+  // UUID4 is implemented per https://www.cryptosys.net/pki/uuid-rfc4122.html
+  int i;
+  for (i = 0; i < 8; i++) {
+    result.data_[i] = ToHex(dis(gen));
+  }
+  result.data_[i] = '-';
+  ++i;
+  for (; i < 13; i++) {
+    result.data_[i] = ToHex(dis(gen));
+  }
+  result.data_[i] = '-';
+  ++i;
+  result.data_[i] = '4';
+  ++i;
+  for (; i < 18; i++) {
+    result.data_[i] = ToHex(dis(gen));
+  }
+  result.data_[i] = '-';
+  ++i;
+  result.data_[i] = ToHex(dis2(gen));
+  ++i;
+  for (; i < 23; i++) {
+    result.data_[i] = ToHex(dis(gen));
+  }
+  result.data_[i] = '-';
+  ++i;
+  for (; i < 36; i++) {
+    result.data_[i] = ToHex(dis(gen));
+  }
+
+  return result;
+}
+
+}  // namespace aos
diff --git a/aos/events/logging/uuid.h b/aos/events/logging/uuid.h
new file mode 100644
index 0000000..b81b811
--- /dev/null
+++ b/aos/events/logging/uuid.h
@@ -0,0 +1,36 @@
+#ifndef AOS_EVENTS_LOGGING_UUID_H_
+#define AOS_EVENTS_LOGGING_UUID_H_
+
+#include <array>
+#include <random>
+#include <string_view>
+
+namespace aos {
+
+// Class to generate and hold a UUID.
+class UUID {
+ public:
+  // Returns a randomly generated UUID.  This is known as a UUID4.
+  static UUID Random();
+
+  std::string_view string_view() const {
+    return std::string_view(data_.data(), data_.size());
+  }
+
+  bool operator==(const UUID &other) const {
+    return other.string_view() == string_view();
+  }
+  bool operator!=(const UUID &other) const {
+    return other.string_view() != string_view();
+  }
+
+ private:
+  UUID() {}
+
+  // Fixed size storage for the data.  Non-null terminated.
+  std::array<char, 36> data_;
+};
+
+}  // namespace aos
+
+#endif  // AOS_EVENTS_LOGGING_UUID_H_
diff --git a/aos/events/logging/uuid_test.cc b/aos/events/logging/uuid_test.cc
new file mode 100644
index 0000000..d0320de
--- /dev/null
+++ b/aos/events/logging/uuid_test.cc
@@ -0,0 +1,18 @@
+#include "aos/events/logging/uuid.h"
+
+#include "glog/logging.h"
+#include "gtest/gtest.h"
+
+namespace aos {
+namespace testing {
+
+// Tests that random UUIDs are actually random, and we can convert them to a
+// string.  Not very exhaustive, but it is a good smoke test.
+TEST(UUIDTest, GetOne) {
+  LOG(INFO) << UUID::Random().string_view();
+
+  EXPECT_NE(UUID::Random(), UUID::Random());
+}
+
+}  // namespace testing
+}  // namespace aos