relastic 0.4.14

Simple rust lib inspired by Serilog for application-wide logging to Elastic
Documentation
use chrono::{DateTime, Utc};
use mockito::Matcher;
use std::collections::HashMap;
use uuid::Uuid;

use crate::log::{self, LogLevel};
use crate::{log_debug, log_error, log_fatal, log_information, log_warning};

fn create_config(server_url: String) -> log::ElasticConfig {
    log::ElasticConfig {
        url: server_url,
        username: "test_user".to_string(),
        password: "test_password".to_string(),
        environment: log::LogEnvironment::Development,
        application_name: "relastic-tests".to_string(),
        log_to_console: Some(true),
    }
}

#[test]
fn test_log_macros() {
    log_debug!("Hello world {fish}", "Bass");
    log_debug!("Hello world {fish} {snake}", "Bass", "Python");
    log_debug!(
        "Hello world {fish} {snake} {owl}",
        "Bass",
        "Python",
        "Hoot hoot"
    );
    log_information!("Hello world {fish}", "Bass");
    log_information!("Hello world {fish} {snake}", "Bass", "Python");
    log_warning!("Hello world {fish}", "Bass");
    log_warning!("Hello world {fish} {snake}", "Bass", "Python");
    log_error!("Hello world {fish}", "Bass");
    log_error!("Hello world {fish} {snake}", "Bass", "Python");
    log_fatal!("Hello world {fish}", "Bass");
    log_fatal!("Hello world {fish} {snake}", "Bass", "Python");
}

#[test]
fn test_logging_displayable() {
    log_debug!("Hello world {fish}", LogLevel::Information);
    log_information!("Hello world {fish}", LogLevel::Information);
    log_warning!("Hello world {fish}", LogLevel::Information);
    log_error!("Hello world {fish}", LogLevel::Information);
    log_fatal!("Hello world {fish}", LogLevel::Information);
}

#[test]
fn it_does_not_panic_on_param_vs_template_mismatch() {
    log_debug!("Hello world {fish}", "salmon", "snacks");
    log_information!("Hello world {fish}", "salmon", "snacks");
    log_warning!("Hello world {fish}", "salmon", "snacks");
    log_error!("Hello world {fish}", "salmon", "snacks");
    log_fatal!("Hello world {fish}", "salmon", "snacks");
}

#[test]
fn it_does_not_panic_on_template_vs_param_mismatch() {
    log_debug!("Hello world {fish}{snacks}", "salmon");
    log_information!("Hello world {fish}{snacks}", "salmon");
    log_warning!("Hello world {fish}{snacks}", "salmon");
    log_error!("Hello world {fish}{snacks}", "salmon");
    log_fatal!("Hello world {fish}{snacks}", "salmon");
}

#[test]
fn test_logging() {
    let reply = r#"{
        "took": 1,
        "errors": false,
        "items": [
            {
                "index": {
                    "_index": "application-production-2021.11",
                    "_type": "_doc",
                    "_id": "abc",
                    "_version": 1,
                    "result": "created",
                    "_shards": {
                    "total": 2,
                    "successful": 1,
                    "failed": 0
                    },
                    "_seq_no": 2,
                    "_primary_term": 1,
                    "status": 201
                }
            }
        ]
    }"#;

    let my_expected_test_value = Uuid::new_v4();

    // Request a new server from the pool
    let mut server = mockito::Server::new();
    let api_mock = server
        .mock("POST", "/_bulk")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(reply)
        .match_body(Matcher::Regex(my_expected_test_value.to_string()))
        .create();

    let config = create_config(server.url());

    log::setup_elastic_log(config, 100);
    log::information(
        "This is a test {value}",
        HashMap::from([("value", my_expected_test_value.to_string())]),
    );
    log::debug(
        "This is a test {value}",
        HashMap::from([("value", my_expected_test_value.to_string())]),
    );
    log::fatal(
        "This is a test {value}",
        HashMap::from([("value", my_expected_test_value.to_string())]),
    );
    log::warning(
        "This is a test {value}",
        HashMap::from([("value", my_expected_test_value.to_string())]),
    );
    log::flush();

    assert!(api_mock.expect(4).matched());
}

#[test]
fn it_returns_error_on_inner_error_replies() {
    let reply = r#"{
        "took": 3,
        "errors": true,
        "items": [
          {
            "index": {
              "_index": "application-production-2021.11",
              "_type": "_doc",
              "_id": "321",
              "status": 400,
              "error": {
                "type": "mapper_parsing_exception",
                "reason": "failed to parse",
                "caused_by": {
                  "type": "json_parse_exception",
                  "reason": "Unexpected character ('H' (code 72)): was expecting comma to separate Object entries\n at [Source: org.elasticsearch.common.bytes.AbstractBytesReference$MarkSupportingStreamInputWrapper@7bc49eab; line: 1, column: 137]"
                }
              }
            }
          },
          {
            "index": {
              "_index": "application-production-2021.11",
              "_type": "_doc",
              "_id": "1234",
              "status": 400,
              "error": {
                "type": "mapper_parsing_exception",
                "reason": "failed to parse",
                "caused_by": {
                  "type": "json_parse_exception",
                  "reason": "Unexpected character ('H' (code 72)): was expecting comma to separate Object entries\n at [Source: org.elasticsearch.common.bytes.AbstractBytesReference$MarkSupportingStreamInputWrapper@d19c3fd; line: 1, column: 137]"
                }
              }
            }
          },
          {
            "index": {
              "_index": "application-production-2021.11",
              "_type": "_doc",
              "_id": "123",
              "_version": 1,
              "result": "created",
              "_shards": {
                "total": 2,
                "successful": 1,
                "failed": 0
              },
              "_seq_no": 2,
              "_primary_term": 1,
              "status": 201
            }
          }
        ]
    }"#;

    let mut server = mockito::Server::new();
    let api_mock = server
        .mock("POST", "/_bulk")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(reply)
        .create();

    let config = create_config(server.url());

    // We are not testing the input. That's why we send an empty string here.
    let result = log::test_post_to_elastic(config, "".to_string());

    assert!(api_mock.expect(1).matched());
    assert!(result.is_err());
    let err = result.expect_err("Expected error here");

    match err {
        log::Error::SomeLogsWereNotAccepted { errors } => {
            assert_ne!(errors, "".to_string())
        }
        _ => {
            panic!(
                "Did not get the expected error here. Expected 'SomeLogsWereNotAccepted', got {}",
                err
            )
        }
    }
}

#[test]
fn it_returns_error_on_api_error_reply() {
    let reply = r#"{
      "error": {
        "root_cause": [
            {
                "type": "illegal_argument_exception",
                "reason": "The bulk request must be terminated by a newline [\\n]"
            }
        ],
        "type": "illegal_argument_exception",
        "reason": "The bulk request must be terminated by a newline [\\n]"
      },
      "status": 400
    }"#;

    // Request a new server from the pool
    let mut server = mockito::Server::new();
    let api_mock = server
        .mock("POST", "/_bulk")
        .with_status(400)
        .with_header("content-type", "application/json")
        .with_body(reply)
        .create();

    let config = create_config(server.url());

    // We are not testing the input. That's why we send an empty string here.
    let result = log::test_post_to_elastic(config, "".to_string());
    assert!(api_mock.expect(1).matched());
    assert!(result.is_err());

    let err = result.expect_err("Expected error here");

    match err {
        log::Error::ApiRejectedLogPayload { errors } => assert_ne!(errors, "".to_string()),
        _ => {
            panic!(
                "Did not get the expected error here. Expected 'ApiRejectedPayload', got: {}",
                err
            )
        }
    }
}

#[test]
fn it_does_not_publish_debug_in_production() {
    let reply = r#"{
        "took": 1,
        "errors": false,
        "items": [
            {
                "index": {
                    "_index": "application-production-2021.11",
                    "_type": "_doc",
                    "_id": "abc",
                    "_version": 1,
                    "result": "created",
                    "_shards": {
                    "total": 2,
                    "successful": 1,
                    "failed": 0
                    },
                    "_seq_no": 2,
                    "_primary_term": 1,
                    "status": 201
                }
            }
        ]
    }"#;

    let my_expected_test_value = Uuid::new_v4();

    let mut server = mockito::Server::new();
    let api_mock = server
        .mock("POST", "/_bulk")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(reply)
        .match_body(Matcher::Regex(my_expected_test_value.to_string()))
        .create();

    // Request a new server from the pool
    let config = log::ElasticConfig {
        url: server.url(),
        username: "test_user".to_string(),
        password: "test_password".to_string(),
        environment: log::LogEnvironment::Production,
        application_name: "relastic-tests".to_string(),
        log_to_console: Some(true),
    };

    log::setup_elastic_log(config, 100);
    log::debug(
        "This is a test {value}",
        HashMap::from([("value", my_expected_test_value.to_string())]),
    );
    log::flush();

    assert!(api_mock.expect(0).matched());
}

#[test]
fn it_formats_console_logs_with_timestamp() {
    let time = "2014-11-28T21:00:09Z".parse::<DateTime<Utc>>().unwrap();
    let log_string = log::create_console_message(&LogLevel::Information, "Hello!", &time);
    assert_eq!("[21:00:09 INF]: Hello!", log_string);
}