Merge pull request #5 from aprilabank/feat/json-payloads

feat(main): Implement parsing of JSON payloads
This commit is contained in:
Vincent Ambo 2018-06-16 18:28:49 +02:00 committed by GitHub
commit 03a6ea742c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 130 additions and 3 deletions

View file

@ -57,6 +57,9 @@ use std::process;
use std::time::{Duration, Instant};
use systemd::journal::*;
#[cfg(test)]
mod tests;
const ENTRIES_WRITE_URL: &str = "https://logging.googleapis.com/v2/entries:write";
const METADATA_TOKEN_URL: &str = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
const METADATA_ID_URL: &str = "http://metadata.google.internal/computeMetadata/v1/instance/id";
@ -121,13 +124,57 @@ fn get_metadata(url: &str) -> Result<String> {
Ok(output.trim().into())
}
/// This structure represents the different types of payloads
/// supported by journaldriver.
///
/// Currently log entries can either contain plain text messages or
/// structured payloads in JSON-format.
#[derive(Debug, Serialize, PartialEq)]
#[serde(untagged)]
enum Payload {
TextPayload {
#[serde(rename = "textPayload")]
text_payload: String,
},
JsonPayload {
#[serde(rename = "jsonPaylaod")]
json_payload: Value,
},
}
/// Attempt to parse a log message as JSON and return it as a
/// structured payload. If parsing fails, return the entry in plain
/// text format.
fn message_to_payload(message: Option<String>) -> Payload {
match message {
None => Payload::TextPayload { text_payload: "empty log entry".into() },
Some(text_payload) => {
// Attempt to deserialize the text payload as a generic
// JSON value.
if let Ok(json_payload) = serde_json::from_str::<Value>(&text_payload) {
// If JSON-parsing succeeded on the payload, check
// whether we parsed an object (Stackdriver does not
// expect other types of JSON payload) and return it
// in that case.
if json_payload.is_object() {
return Payload::JsonPayload { json_payload }
}
}
Payload::TextPayload { text_payload }
}
}
}
/// This structure represents a log entry in the format expected by
/// the Stackdriver API.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct LogEntry {
labels: Value,
text_payload: String, // TODO: attempt to parse jsonPayloads
#[serde(flatten)]
payload: Payload,
}
impl From<JournalRecord> for LogEntry {
@ -139,7 +186,7 @@ impl From<JournalRecord> for LogEntry {
// The message field is technically just a convention, but
// journald seems to default to it when ingesting unit
// output.
let message = record.remove("MESSAGE");
let payload = message_to_payload(record.remove("MESSAGE"));
// Presumably this is always set, but who can be sure
// about anything in this world.
@ -157,7 +204,7 @@ impl From<JournalRecord> for LogEntry {
// .map();
LogEntry {
text_payload: message.unwrap_or_else(|| "empty log entry".into()),
payload,
labels: json!({
"host": hostname,
"unit": unit.unwrap_or_else(|| "syslog".into()),

80
src/tests.rs Normal file
View file

@ -0,0 +1,80 @@
use super::*;
use serde_json::to_string;
#[test]
fn test_text_entry_serialization() {
let entry = LogEntry {
labels: Value::Null,
payload: Payload::TextPayload {
text_payload: "test entry".into(),
}
};
let expected = "{\"labels\":null,\"textPayload\":\"test entry\"}";
let result = to_string(&entry).expect("serialization failed");
assert_eq!(expected, result, "Plain text payload should serialize correctly")
}
#[test]
fn test_json_entry_serialization() {
let entry = LogEntry {
labels: Value::Null,
payload: Payload::TextPayload {
text_payload: "test entry".into(),
}
};
let expected = "{\"labels\":null,\"textPayload\":\"test entry\"}";
let result = to_string(&entry).expect("serialization failed");
assert_eq!(expected, result, "Plain text payload should serialize correctly")
}
#[test]
fn test_plain_text_payload() {
let message = "plain text payload".into();
let payload = message_to_payload(Some(message));
let expected = Payload::TextPayload {
text_payload: "plain text payload".into(),
};
assert_eq!(expected, payload, "Plain text payload should be detected correctly");
}
#[test]
fn test_empty_payload() {
let payload = message_to_payload(None);
let expected = Payload::TextPayload {
text_payload: "empty log entry".into(),
};
assert_eq!(expected, payload, "Empty payload should be handled correctly");
}
#[test]
fn test_json_payload() {
let message = "{\"someKey\":\"someValue\", \"otherKey\": 42}".into();
let payload = message_to_payload(Some(message));
let expected = Payload::JsonPayload {
json_payload: json!({
"someKey": "someValue",
"otherKey": 42
})
};
assert_eq!(expected, payload, "JSON payload should be detected correctly");
}
#[test]
fn test_json_no_object() {
// This message can be parsed as valid JSON, but it is not an
// object - it should be returned as a plain-text payload.
let message = "42".into();
let payload = message_to_payload(Some(message));
let expected = Payload::TextPayload {
text_payload: "42".into(),
};
assert_eq!(expected, payload, "Non-object JSON payload should be plain text");
}