// Copyright 2021 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

syntax = "proto3";

package pw.log;

import "pw_protobuf_protos/common.proto";
import "pw_tokenizer_proto/options.proto";

option java_outer_classname = "Log";

// A log message and metadata. Logs come in a few different forms:
//
//  1. A tokenized log message (recommended for production)
//  2. A non-tokenized log message (good for development)
//  3. A "log missed" tombstone, indicating that some logs were dropped
//
// Size analysis for tokenized log messages, including each field's proto tag:
//
//  - message     - 6-12 bytes; depending on number and value of arguments
//  - line_level  - 3 bytes; 4 bytes if line > 2048 (uncommon)
//  - timestamp   - 3 bytes; assuming delta encoding
//  - thread      - 2-6 bytes; depending on whether value is a token or string
//
// Adding the fields gives the total proto message size:
//
//    6-12 bytes - log
//    9-15 bytes - log + level + line
//   12-18 bytes - log + level + line + timestamp
//
// An analysis of a project's log token database revealed the following
// distribution of the number of arguments to log messages:
//
//   # args   # messages
//     0         2,700
//     1         2,400
//     2         1,200
//     3+        1,000
//
// Note: The below proto makes some compromises compared to what one might
// expect for a "clean" proto design, in order to shave bytes off of the
// messages. It is critical that the log messages are as small as possible to
// enable storing more logs in limited memory. This is why, for example, there
// is no separate "DroppedLog" type, or a "TokenizedLog" and "StringLog", which
// would add at least 2 extra bytes per message
message LogEntry {
  // The log message, which may be tokenized.
  //
  // If tokenized logging is used, implementations may encode metadata in the
  // log message rather than as separate proto fields. This reduces the size of
  // the protobuf with no overhead.
  //
  // The standard format for encoding metadata in the log message is defined by
  // the pw_log_tokenized module. The message and metadata are encoded as
  // key-value pairs using ■ and ♦ as delimiters. For example:
  //
  //  ■msg♦This is the log message: %d■module♦wifi■file♦../path/to/file.cc
  //
  // See http://pigweed.dev/pw_log_tokenized for full details. When
  // pw_log_tokenized is used, this metadata is automatically included as
  // described.
  //
  // The level and flags are not included since they may be runtime values and
  // thus cannot always be tokenized. The line number is not included because
  // line numbers change frequently and a new token is created for each line.
  //
  // Size analysis when tokenized:
  //
  //   tag+wire = 1 byte
  //   size     = 1 byte; payload will almost always be < 127 bytes
  //   payload  = N bytes; typically 4-10 in practice
  //
  // Total: 2 + N ~= 6-12 bytes
  optional bytes message = 1 [(tokenizer.format) = TOKENIZATION_OPTIONAL];

  // Packed log level and line number. Structure:
  //
  //   Level: Bottom 3 bits; level = line_level & 0x7
  //   Line: Remaining bits; line = (line_level >> 3)
  //
  // Note: This packing saves two bytes per log message in most cases compared
  // to having line and level separately; and is zero-cost if the log backend
  // omits the line number.
  uint32 line_level = 2;

  // Some log messages have flags to indicate attributes such as whether they
  // are from an assert or if they contain PII. The particular flags are
  // product- and implementation-dependent.
  uint32 flags = 3;

  // Timestamps are either specified with an absolute timestamp or relative to
  // the previous log entry.
  oneof time {
    // The absolute timestamp in implementation-defined ticks. Applications
    // determine how to interpret this on the receiving end. In the simplest
    // case, these ticks might be milliseconds or microseconds since boot.
    // Applications could also access clock information out-of-band with a
    // ClockParameters protobuf.
    int64 timestamp = 4;

    // Time since the last entry in implementation-defined ticks, as for the
    // timestamp field. This enables delta encoding when batching entries
    // together.
    //
    // Size analysis for this field including tag and varint, assuming 1 kHz
    // ticks:
    //
    //           < 127 ms gap == 127 ms      ==  7 bits == 2 bytes
    //        < 16,000 ms gap ==  16 seconds == 14 bits == 3 bytes
    //     < 2,000,000 ms gap ==  35 minutes == 21 bits == 4 bytes
    //   < 300,000,000 ms gap ==  74 hours   == 28 bits == 5 bytes
    //
    // Log bursts will thus consume just 2 bytes (tag + up to 127ms delta) for
    // the timestamp, which is a good improvement over an absolute timestamp.
    int64 time_since_last_entry = 5;
  }

  // When the log buffers are full but more logs come in, the logs are counted
  // and a special log message is omitted with only counts for the number of
  // messages dropped.
  uint32 dropped = 6;

  // The PW_LOG_MODULE_NAME for this log message.
  bytes module = 7 [(tokenizer.format) = TOKENIZATION_OPTIONAL];

  // The file path where this log was created, if not encoded in the message.
  bytes file = 8 [(tokenizer.format) = TOKENIZATION_OPTIONAL];

  // The task or thread name that created the log message. If the log was not
  // created on a thread, it should use a name appropriate to that context.
  bytes thread = 9 [(tokenizer.format) = TOKENIZATION_OPTIONAL];

  // The following fields are planned but will not be added until they are
  // needed. Protobuf field numbers over 15 use an extra byte, so these fields
  // are left out for now to avoid reserving field numbers unnecessarily.

  // Represents the device from which the log originated. The meaning of this
  // field is implementation defined
  // uint32 source_id = ?;

  // Some messages are associated with trace events, which may carry additional
  // contextual data. This is a tuple of a data format string which could be
  // used by the decoder to identify the data (e.g. printf-style tokens) and the
  // data itself in bytes.
  // bytes data_format = ?
  //     [(tokenizer.format) = TOKENIZATION_OPTIONAL];
  // bytes data = ?;
}

message LogRequest {}

message LogEntries {
  repeated LogEntry entries = 1;
  uint32 first_entry_sequence_id = 2;
}

// RPC service for accessing logs.
service Logs {
  rpc Listen(LogRequest) returns (stream LogEntries);
}

message FilterRule {
  // Log level values match pw_log/levels.h. Enum names avoid collissions with
  // possible macros.
  enum Level {
    ANY_LEVEL = 0;
    DEBUG_LEVEL = 1;
    INFO_LEVEL = 2;
    WARN_LEVEL = 3;
    ERROR_LEVEL = 4;
    CRITICAL_LEVEL = 5;
    FATAL_LEVEL = 7;
  };
  // Condition 1: log.level >= level_greater_than_or_equal.
  Level level_greater_than_or_equal = 1;

  // Condition 2: (module_equals.size() == 0) || (log.module == module_equals);
  bytes module_equals = 2 [(tokenizer.format) = TOKENIZATION_OPTIONAL];

  // Condition 3: (any_flags_set == 0) || (log.flags & any_flags_set) != 0))
  uint32 any_flags_set = 3;

  // Action to take if all conditions are met and rule is not inactive.
  enum Action {
    INACTIVE = 0;  // Ignore the rule entirely.
    KEEP = 1;      // Keep the log entry if all conditions are met.
    DROP = 2;      // Drop the log entry if all conditions are met
  };
  Action action = 4;

  // Condition 4: (thread_equals.size() == 0 || log.thread == thread_equals).
  bytes thread_equals = 5 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
}

// A filter is a series of rules. First matching rule wins.
message Filter {
  repeated FilterRule rule = 1;
}

message SetFilterRequest {
  // A filter can be identified by a human readable string, token, or number.
  bytes filter_id = 1 [(tokenizer.format) = TOKENIZATION_OPTIONAL];

  Filter filter = 2;
}

message GetFilterRequest {
  bytes filter_id = 1 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
}

message FilterIdListRequest {}

message FilterIdListResponse {
  repeated bytes filter_id = 1 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
}

// RPC service for retrieving and modifying log filters.
service Filters {
  rpc SetFilter(SetFilterRequest) returns (pw.protobuf.Empty);
  rpc GetFilter(GetFilterRequest) returns (Filter);
  rpc ListFilterIds(FilterIdListRequest) returns (FilterIdListResponse);
}
