| #include "sse_plugin/event.h" |
| |
| #include <cstdint> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/algorithm/container.h" |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/numbers.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/str_replace.h" |
| #include "absl/strings/str_split.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/time/time.h" |
| #include "nlohmann/json.hpp" |
| #include "nlohmann/json_fwd.hpp" |
| #include "proxy_config.pb.h" |
| #include "events.pb.h" |
| #include "vendor_events.pb.h" |
| #include "utils/status_macros.h" |
| |
| namespace milotic { |
| |
| namespace { |
| const int kVlogVerbosity = 1; |
| |
| using ::platforms_vbmc::OtsEvent; |
| using ::platforms_vbmc::OtsUpdate; |
| |
| // Finds MessageId from parsed_event, if exists, and parses it based Redfish |
| // format. |
| // <MessageRegistryPrefix>.<MajorVersion>.<MinorVersion>.<MessageKey> |
| absl::StatusOr<EventMessageId> FindEventMessageId( |
| const nlohmann::json& parsed_event) { |
| if (!parsed_event.is_object()) { |
| return absl::NotFoundError("event json is not object"); |
| } |
| const auto message_id_it = parsed_event.find("MessageId"); |
| if (message_id_it == parsed_event.end()) { |
| VLOG(kVlogVerbosity) << "event json does not contain MessageId" |
| << parsed_event.dump(); |
| return absl::NotFoundError("event json does not contain MessageId"); |
| } |
| const auto* message_id = message_id_it.value().get_ptr<const std::string*>(); |
| if (message_id == nullptr) { |
| return absl::InvalidArgumentError("MessageId is not string"); |
| } |
| |
| std::vector<std::string> message_id_parts = absl::StrSplit(*message_id, '.'); |
| if (message_id_parts.size() != 4) { |
| return absl::InvalidArgumentError("MessageId not in correct format"); |
| } |
| |
| return EventMessageId{ |
| .message_registry_prefix = std::move(message_id_parts[0]), |
| .major_version = std::move(message_id_parts[1]), |
| .minor_version = std::move(message_id_parts[2]), |
| .message_key = std::move(message_id_parts[3]), |
| }; |
| } |
| |
| // Finds ReasonId from OemPath whose value should be an array each of which |
| // contains `Reasons` which is an array each of which contains a `ReasonId` and |
| // `DateTime`, we select the `ReasonId` which has the max `DateTime`. |
| // Eg {"OemField": [{"Reasons": [{"ReasonId": xxx, "DateTime": yyy}]}]} |
| absl::StatusOr<int> FindReasonIdFromOemPath( |
| const nlohmann::json& parsed_event, |
| const nlohmann::json::json_pointer& oem_path) { |
| absl::TimeZone tz = absl::LocalTimeZone(); |
| if (!parsed_event.is_object()) { |
| return absl::NotFoundError("event json is not object"); |
| } |
| |
| if (!parsed_event.contains(oem_path)) { |
| return absl::NotFoundError("event json does not Oem field"); |
| } |
| |
| const nlohmann::json& faults = parsed_event[oem_path]; |
| if (!faults.is_array()) |
| return absl::NotFoundError("faults in event json is null/not array"); |
| |
| int reason_id = -1; |
| for (auto fault : faults) { |
| if (!fault.is_object()) continue; |
| auto it = fault.find("Reasons"); |
| if (it == fault.end()) continue; |
| const nlohmann::json& reasons = it.value(); |
| if (!reasons.is_array()) continue; |
| |
| absl::Time max_time; |
| for (auto reason : reasons) { |
| absl::Time reason_time; |
| std::string error_string; |
| std::string* time_value = reason["DateTime"].get_ptr<std::string*>(); |
| std::int64_t* reason_value = reason["ReasonId"].get_ptr<std::int64_t*>(); |
| if (time_value == nullptr || reason_value == nullptr) { |
| return absl::InvalidArgumentError(absl::StrFormat( |
| "Failed to parse time/reasonID from reason: %s", reason.dump())); |
| } |
| absl::ParseTime("%Y-%m-%dT%H:%M:%SZ", *time_value, tz, &reason_time, |
| &error_string); |
| if (!error_string.empty()) { |
| return absl::InvalidArgumentError(absl::StrFormat( |
| "Failed to parse time from value: %s", *time_value)); |
| } |
| if (reason_time > max_time) { |
| max_time = reason_time; |
| reason_id = static_cast<int>(*reason_value); |
| } |
| } |
| } |
| |
| if (reason_id == -1) |
| return absl::NotFoundError("No reason ID found in event"); |
| |
| return reason_id; |
| } |
| |
| // Finds ReasonId from MessageArgs which is any array of strings and |
| // ReasonIdMessageArgsIndex which has 2 values MessageKey and MessageArgIndex, |
| // if MessageKey matches the event message key, then reason id is the |
| // present in MessageArgs at the MessageArgIndex position. |
| // Eg ReasonIdMessageArgsIndex: {ResourceIndicted, 1} |
| // {"MessageId":"H3Alert.1.1.ResourceIndicted", "MessageArgs":["0", |
| // "1349"]} |
| // In this case 1349 is at index 1 which should be converted to int. |
| absl::StatusOr<int> FindReasonIdFromMessageArg( |
| const nlohmann::json& parsed_event, absl::string_view message_key, |
| const milotic_grpc_proxy::EventReasonIdParse::ReasonIdMessageArgsIndex& |
| message_args_config) { |
| if (message_key != message_args_config.message_key()) { |
| return absl::NotFoundError("MessageKey mismatch"); |
| } |
| |
| auto it = parsed_event.find("MessageArgs"); |
| if (it == parsed_event.end()) { |
| return absl::NotFoundError("MessageArgs not found"); |
| } |
| |
| const nlohmann::json& message_args = it.value(); |
| if (!message_args.is_array()) |
| return absl::NotFoundError("message_args in event json is null/not array"); |
| if (message_args_config.message_arg_index() >= |
| static_cast<int>(message_args.size())) |
| return absl::NotFoundError("MessageArgIndex out of bounds"); |
| |
| const auto* reason_id_from_message_arg = |
| message_args.at(message_args_config.message_arg_index()) |
| .get_ptr<const std::string*>(); |
| |
| // ReasonID is present in string which has to be converted to int. |
| int reason_id = -1; |
| if (reason_id_from_message_arg != nullptr && |
| absl::SimpleAtoi(*reason_id_from_message_arg, &reason_id)) { |
| VLOG(kVlogVerbosity) << "reason id " << reason_id |
| << " found in MessageArgs"; |
| return reason_id; |
| } |
| return absl::NotFoundError("No reason ID found in MessageArgs"); |
| } |
| |
| absl::StatusOr<EventSeverity> FindReasonSeverity( |
| const absl::flat_hash_map<int, platforms_vbmc::VendorEvent>& registry, |
| int reason_id) { |
| auto it = registry.find(reason_id); |
| if (it != registry.end()) { |
| switch (it->second.severity()) { |
| case platforms_vbmc::OtsEvent::SEVERITY_OK: |
| return EventSeverity::kOk; |
| case platforms_vbmc::OtsEvent::SEVERITY_LOG: |
| return EventSeverity::kLog; |
| case platforms_vbmc::OtsEvent::SEVERITY_WARNING: |
| return EventSeverity::kWarning; |
| case platforms_vbmc::OtsEvent::SEVERITY_CRITICAL: |
| return EventSeverity::kCritical; |
| default: |
| return absl::InvalidArgumentError("Invalid event severity"); |
| } |
| } |
| |
| return absl::NotFoundError( |
| absl::StrFormat("Reason ID %d not found in registry", reason_id)); |
| } |
| |
| absl::string_view GetValueOrDefault(const nlohmann::json& json, |
| absl::string_view key) { |
| if (!json.is_object()) return ""; |
| |
| auto it = json.find(key); |
| if (it == json.end()) return ""; |
| |
| const auto* value = it.value().get_ptr<const std::string*>(); |
| if (value == nullptr) return ""; |
| return *value; |
| } |
| |
| absl::StatusOr<Event> CreateEvent(const EventParseConfig& parse_config, |
| nlohmann::json parsed_event) { |
| // Validate and parse event ID. |
| int64_t event_id; |
| int reason_id = kInvalidReasonId; |
| auto event_id_it = parsed_event.find("EventId"); |
| if (event_id_it == parsed_event.end()) { |
| return absl::InvalidArgumentError("EventId not found"); |
| } |
| if (!absl::SimpleAtoi(absl::StrReplaceAll(event_id_it->dump(), {{"\"", ""}}), |
| &event_id)) { |
| return absl::InvalidArgumentError("EventId is not a number"); |
| } |
| VLOG(kVlogVerbosity) << "event id " << event_id; |
| |
| if (GetValueOrDefault(parsed_event, "MessageSeverity") == "OK") { |
| // No parsing required for OK events |
| return Event(parsed_event.dump(), event_id, EventSeverity::kOk); |
| } |
| |
| absl::string_view message_severity = |
| GetValueOrDefault(parsed_event, "MessageSeverity"); |
| |
| // Log Non OK events |
| // EventId, MessageSeverity, MessageId, Message, EventTimestamp |
| LOG(INFO) << "Creating event with EventId: " << event_id |
| << ", MessageSeverity: " << message_severity |
| << ", MessageId: " << GetValueOrDefault(parsed_event, "MessageId") |
| << ", Message: " << GetValueOrDefault(parsed_event, "Message") |
| << ", Timestamp: " |
| << GetValueOrDefault(parsed_event, "EventTimestamp"); |
| |
| absl::StatusOr<EventMessageId> message_id = FindEventMessageId(parsed_event); |
| |
| // TODO: b/323233763 - Break into smaller functions |
| // Check if MessageKey is ignored. |
| if (parse_config.event_parse.ignore_event_sev_message_keys_size() > 0) { |
| if (!message_id.ok()) return message_id.status(); |
| |
| VLOG(kVlogVerbosity) << "Searching MessageKey in ignore list: " |
| << message_id->message_key; |
| if (absl::c_linear_search( |
| parse_config.event_parse.ignore_event_sev_message_keys(), |
| message_id->message_key)) { |
| LOG(INFO) << "EventId: " << event_id |
| << " created with OK Severity, message key " |
| << message_id->message_key << " in ignore list"; |
| return Event(parsed_event.dump(), event_id, EventSeverity::kOk); |
| } |
| } |
| |
| // Check if MessageRegistryPrefix is ignored. |
| if (parse_config.event_parse.ignore_event_sev_message_prefix_size() > 0) { |
| if (!message_id.ok()) return message_id.status(); |
| |
| VLOG(kVlogVerbosity) << "Searching MessageRegistryPrefix in ignore list: " |
| << message_id->message_registry_prefix; |
| if (absl::c_linear_search( |
| parse_config.event_parse.ignore_event_sev_message_prefix(), |
| message_id->message_registry_prefix)) { |
| LOG(INFO) << "EventId: " << event_id |
| << " created with OK Severity, message prefix " |
| << message_id->message_registry_prefix << " in ignore list"; |
| return Event(parsed_event.dump(), event_id, EventSeverity::kOk); |
| } |
| } |
| |
| // Check if ReasonId parsing is present. |
| if (parse_config.event_parse.reason_id_parse_size() > 0) { |
| for (const auto& reason_id_parse : |
| parse_config.event_parse.reason_id_parse()) { |
| // Check ReasonId in Oem Field. |
| if (reason_id_parse.has_oem_field_path()) { |
| VLOG(kVlogVerbosity) << "Checking ReasonId in Oem field"; |
| nlohmann::json::json_pointer oem_path(reason_id_parse.oem_field_path()); |
| if (absl::StatusOr<int> from_oem_path = |
| FindReasonIdFromOemPath(parsed_event, oem_path); |
| from_oem_path.ok()) { |
| reason_id = *from_oem_path; |
| VLOG(kVlogVerbosity) |
| << "reason id " << reason_id << " found in Oem field"; |
| break; |
| } |
| } |
| |
| // Check ReasonId in MessageArgs. |
| if (reason_id_parse.has_message_args_index()) { |
| if (!message_id.ok()) return message_id.status(); |
| |
| if (absl::StatusOr<int> reason_id_from_message_arg = |
| FindReasonIdFromMessageArg( |
| parsed_event, message_id->message_key, |
| reason_id_parse.message_args_index()); |
| reason_id_from_message_arg.ok()) { |
| reason_id = *reason_id_from_message_arg; |
| VLOG(kVlogVerbosity) |
| << "reason id " << reason_id << " found in MessageArgs"; |
| break; |
| } |
| } |
| |
| // Check ReasonId in Message string using regex. |
| if (reason_id_parse.has_message_string_regex()) { |
| // TODO: b/323233763 - Add support for Message string regex. |
| } |
| } |
| } |
| |
| // If ReasonId is found, get severity from registry |
| if (!parse_config.registry.empty() && reason_id != kInvalidReasonId) { |
| if (absl::StatusOr<EventSeverity> severity = |
| FindReasonSeverity(parse_config.registry, reason_id); |
| severity.ok()) { |
| LOG(INFO) << "EventId: " << event_id << " created with " |
| << EventSeverityToString(*severity) |
| << " Severity, reasonId: " << reason_id; |
| return Event(parsed_event.dump(), event_id, *severity, reason_id); |
| } |
| } |
| |
| // If ignore_event_sev_wo_reason_id is set and we can't find ReasonId |
| if (reason_id == kInvalidReasonId && |
| parse_config.event_parse.ignore_event_sev_wo_reason_id()) { |
| LOG(INFO) << "EventId: " << event_id |
| << " created with OK Severity, missing reasonId"; |
| return Event(parsed_event.dump(), event_id, EventSeverity::kOk); |
| } |
| |
| // Finally use MessageSeverity for event severity. |
| ASSIGN_OR_RETURN(EventSeverity severity, |
| StringToEventSeverity(message_severity)); |
| LOG(INFO) << "EventId: " << event_id << " created with " |
| << EventSeverityToString(severity) |
| << " Severity, using MessageSeverity"; |
| return Event(parsed_event.dump(), event_id, severity, reason_id); |
| } |
| |
| } // namespace |
| |
| std::string EventSeverityToString(EventSeverity severity) { |
| switch (severity) { |
| case EventSeverity::kOk: |
| return "OK"; |
| case EventSeverity::kLog: |
| return "Log"; |
| case EventSeverity::kWarning: |
| return "Warning"; |
| case EventSeverity::kCritical: |
| return "Critical"; |
| default: |
| return ""; |
| } |
| } |
| |
| absl::StatusOr<EventSeverity> StringToEventSeverity( |
| absl::string_view severity) { |
| if (severity == "OK") { |
| return EventSeverity::kOk; |
| } |
| if (severity == "Warning") { |
| return EventSeverity::kWarning; |
| } |
| if (severity == "Critical") { |
| return EventSeverity::kCritical; |
| } |
| return absl::InvalidArgumentError("Invalid message severity"); |
| } |
| |
| OtsUpdate Event::ToProto() const { |
| OtsUpdate ots_update; |
| OtsEvent event_proto; |
| |
| switch (severity_) { |
| case EventSeverity::kOk: |
| event_proto.set_severity(OtsEvent::SEVERITY_OK); |
| break; |
| case EventSeverity::kLog: |
| event_proto.set_severity(OtsEvent::SEVERITY_LOG); |
| break; |
| case EventSeverity::kWarning: |
| event_proto.set_severity(OtsEvent::SEVERITY_WARNING); |
| break; |
| case EventSeverity::kCritical: |
| event_proto.set_severity(OtsEvent::SEVERITY_CRITICAL); |
| break; |
| } |
| event_proto.mutable_event_json()->assign(event_json_); |
| *ots_update.mutable_event() = event_proto; |
| return ots_update; |
| } |
| |
| absl::StatusOr<std::vector<Event>> Event::ParseRedfishEvent( |
| const EventParseConfig& parse_config, absl::string_view event_json) { |
| nlohmann::json parsed = nlohmann::json::parse(event_json, /*cb=*/nullptr, |
| /*allow_exceptions=*/false); |
| if (parsed.is_discarded()) { |
| return absl::InvalidArgumentError("Failed to parse json"); |
| } |
| // parsed json should be similar to |
| // {"Id": 123, "Events": [{"EventId": 123, ...}]}. |
| if (!parsed.is_object()) { |
| return absl::InvalidArgumentError( |
| "Invalid event json, json is not an object"); |
| } |
| |
| // Validated `Id` is present. |
| auto id_it = parsed.find("Id"); |
| if (id_it == parsed.end()) { |
| return absl::InvalidArgumentError("Invalid event json, Id is not present"); |
| } |
| std::string* id_ptr = id_it->get_ptr<std::string*>(); |
| if (id_ptr == nullptr) { |
| return absl::InvalidArgumentError("Id is not a string"); |
| } |
| |
| // Validate `Events` is present. |
| auto events_it = parsed.find("Events"); |
| if (events_it == parsed.end()) { |
| return absl::InvalidArgumentError( |
| "Invalid event json, Events is not present"); |
| } |
| |
| // Validate `Events` json is an array and contains at least one event. |
| if (!events_it->is_array() || events_it->size() != 1) { |
| return absl::InvalidArgumentError( |
| "Invalid event json, Events should be an array with single element"); |
| } |
| |
| if (!events_it->at(0).is_object()) { |
| return absl::InvalidArgumentError( |
| absl::StrFormat("Invalid event json, Events[0] is not an object")); |
| } |
| |
| if (!events_it->at(0).contains("EventId")) { |
| return absl::InvalidArgumentError(absl::StrFormat( |
| "Invalid event json, EventId is not present in Events[0]")); |
| } |
| |
| std::string* event_id_ptr = |
| events_it->at(0)["EventId"].get_ptr<std::string*>(); |
| if (event_id_ptr == nullptr) { |
| return absl::InvalidArgumentError("EventId is not a string"); |
| } |
| |
| if (*id_ptr != *event_id_ptr) { |
| return absl::InvalidArgumentError( |
| absl::StrFormat("EventId mismatch, Id: %s, Events[0][EventId]: %s", |
| *id_ptr, *event_id_ptr)); |
| } |
| |
| absl::StatusOr<Event> event = CreateEvent(parse_config, events_it->at(0)); |
| if (!event.ok()) { |
| return event.status(); |
| } |
| return std::vector<Event>{std::move(*event)}; |
| } |
| |
| absl::StatusOr<std::vector<Event>> Event::ParseReplayEvents( |
| const EventParseConfig& parse_config, absl::string_view event_json) { |
| std::vector<Event> events; |
| if (event_json.empty()) { |
| return events; |
| } |
| nlohmann::json parsed = nlohmann::json::parse(event_json, /*cb=*/nullptr, |
| /*allow_exceptions=*/false); |
| if (parsed.is_discarded()) { |
| return absl::InvalidArgumentError("Failed to parse json"); |
| } |
| if (!parsed.is_array()) { |
| return absl::InvalidArgumentError("Invalid json"); |
| } |
| LOG(INFO) << "Got " << parsed.size() << " events"; |
| |
| events.reserve(parsed.size()); |
| for (const auto& parsed_event : parsed) { |
| if (absl::StatusOr<Event> event = CreateEvent(parse_config, parsed_event); |
| event.ok()) { |
| events.push_back(*event); |
| } else { |
| LOG(ERROR) << "Failed to create event: " << event.status(); |
| return event.status(); |
| } |
| } |
| LOG(INFO) << "Parsed " << events.size() << " events"; |
| return events; |
| } |
| |
| } // namespace milotic |