Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-log

Structured event logging for Pudu: message templates, enrichment, filtering, formatting, and sinks

0.1.0Apache-2.01

InstallClose

SettingsTest.pudu

Pudu149 lines6.9 KB

GitHub ↗
1/** @Test.Settings.Suite — configurations read from settings */2module PuduLangLog.SettingsTest34import Std.Env as Env5import Std.Io as Io6import Std.List as List7import Std.Option as Option8import Std.Result as Result9import Std.Test as Test10import PuduLangLog.Clock as Clock11import PuduLangLog.Configuration as Configuration12import PuduLangLog.Event as Event13import PuduLangLog.LevelSwitch as LevelSwitch14import PuduLangLog as Log15import PuduLangLog.Logger as Logger16import PuduLangLog.Settings.Registry as Registry17import PuduLangLog.Settings as Settings18import PuduLangLog.Sink as Sink19import PuduLangLog.Sinks.Memory as Memory20import PuduLangLog.Value as Value2122/// A registry adding a `Memory` sink writing to the store, whose `refuse` argument makes the23/// factory fail.24fn registryWith(memory: &Memory.Memory) -> Registry.Registry {25  let store = *memory26  Registry.standard().withSink("Memory", fn(arguments: &Array[(Str, Str)]) -> Result[Sink.Sink, Str] {27      if Settings.argument(arguments, "refuse") != None { return Err("refused") }28      Ok(Memory.sink(&store))29    })30}3132/// The loaded configuration of settings over a registry writing to the store.33fn load(memory: &Memory.Memory, pairs: Array[(Str, Str)]) -> Result[Settings.Loaded, Array[Str]] {34  Settings.apply(&Configuration.create().clock(Clock.fixed(Log.Timestamp{millis: 0, offset: 0})), &pairs, &registryWith(memory))35}3637/// The problems of settings that fail to load.38fn problemsOf(pairs: Array[(Str, Str)]) -> Array[Str] {39  match load(&Memory.create(), pairs) {40    case Ok(_) => []41    case Err(problems) => problems42  }43}4445/// Runs the suite.46fn main() -> Int {47  let memory = Memory.create()48  let loaded = load(&memory, [49      ("MinimumLevel", "Debug"),50      ("MinimumLevel:Override:App.Noisy", "Error"),51      ("MinimumLevel:Override:App.Jobs", "$jobs"),52      ("LevelSwitches:$jobs", "Warning"),53      ("Properties:Application", "Shop"),54      ("Enrich:0:Name", "EnvironmentName"),55      ("Enrich:0:Args:variable", "PUDU_LANG_LOG_UNSET_VARIABLE"),56      ("Filter:0:ByExcluding", "RequestPath like '/health%'"),57      ("Destructure:MaximumStringLength", "8"),58      ("Destructure:AsScalar:0", "Money"),59      ("WriteTo:0:Name", "Memory"),60      ("WriteTo:0:Args:restrictedToMinimumLevel", "Information"),61      ("WriteTo:1:Name", "Console"),62      ("WriteTo:1:Args:theme", "none"),63      ("WriteTo:1:Args:restrictedToMinimumLevel", "Fatal")64    ])65  let logger = match &loaded {66    case Ok(found) => found.configuration.createLogger()67    case Err(_) => Logger.none()68  }69  logger.debug("hidden by the sink's minimum", [])70  logger.information("Paid \{Amount\} for \{Item\}", [Value.structure("Money", [("Units", Value.int(5))]), Value.text("teapots")])71  logger.information("Health \{RequestPath\}", [Value.text("/health/live")])72  logger.forSource("App.Noisy.Parts").warning("noisy warning", [])73  logger.forSource("App.Jobs").warning("job warning", [])74  let jobs = Option.andThen(Result.ok(loaded), |found: Settings.Loaded| Settings.switchNamed(&found, "$jobs"))75  if let Some(control) = jobs { LevelSwitch.set(&control, Log.Error) }76  logger.forSource("App.Jobs").warning("job warning after", [])77  let written = Memory.events(&memory)78  let path = Env.temporaryDirectory() + "/pudu-lang-log-settings-test.json"79  let _saved = Io.write(path, "\{\"Log\":\{\"MinimumLevel\":\"Warning\",\"WriteTo\":[\{\"Name\":\"Memory\"\}]\}\}")80  let fromFile = Settings.fromJsonFile(path, "log")81  let _removed = Io.remove(path)82  let fromJsonFile = Result.map(fromFile, |pairs: Array[(Str, Str)]| Result.isOk(&load(&Memory.create(), pairs)))83  let checks = Test.suite("Settings", &[84      Test.that("the settings load", Result.isOk(&loaded)),85      Test.equals("messages the sink kept", &Memory.messages(&memory), &["Paid \"Money \{…\" for \"teapots\"", "job warning"]),86      Test.equals("properties and enrichers apply", &Option.map(List.first(&written), |event: Log.Event| (Event.property(&event, "Application"), Event.property(&event, "EnvironmentName"))), &Some((Some(Value.text("Shop")), Some(Value.text("Product…"))))),87      Test.that("the declared switch is handed back", jobs != None && Option.andThen(Result.ok(loaded), |found: Settings.Loaded| Settings.switchNamed(&found, "other")) == None),88      Test.equals("a JSON file", &fromJsonFile, &Ok(true)),89      Test.that("a missing JSON file is refused", Result.isErr(&Settings.fromJsonFile(path, "Log"))),90      Test.equals("problems of every kind are reported together", &problemsOf([91            ("MinimumLevel", "Loud"),92            ("Enrich:0", "Nobody"),93            ("Enrich:1:Name", "EnvironmentVariable"),94            ("Filter:0:ByExcluding", "Count >"),95            ("WriteTo:0", "Nowhere"),96            ("WriteTo:1:Name", "Memory"),97            ("WriteTo:1:Args:refuse", "yes"),98            ("WriteTo:2:Name", "Memory"),99            ("WriteTo:2:Args:restrictedToMinimumLevel", "Loud"),100            ("AuditTo:0:Name", "File")101          ]), &[102          "Unknown or malformed setting `MinimumLevel`",103          "No enricher is registered as `Nobody`",104          "The enricher `EnvironmentVariable`: The argument `variable` is required",105          "The filter `Count >`: the expression ends too soon",106          "No sink is registered as `Nowhere`",107          "The sink `Memory`: refused",108          "The sink `Memory`: `Loud` is not a level",109          "The sink `File`: The argument `path` is required"110        ]),111      Test.equals("standard sinks refuse bad arguments", &problemsOf([112            ("WriteTo:0:Name", "Console"),113            ("WriteTo:0:Args:theme", "neon"),114            ("WriteTo:1:Name", "Console"),115            ("WriteTo:1:Args:formatter", "xml"),116            ("WriteTo:2:Name", "File"),117            ("WriteTo:2:Args:path", "x.log"),118            ("WriteTo:2:Args:rollingInterval", "week"),119            ("WriteTo:3:Name", "File"),120            ("WriteTo:3:Args:path", "x.log"),121            ("WriteTo:3:Args:buffered", "maybe"),122            ("WriteTo:4:Name", "Http"),123            ("WriteTo:4:Args:endpoint", " "),124            ("WriteTo:5:Name", "Http"),125            ("WriteTo:5:Args:endpoint", "http://localhost:1"),126            ("WriteTo:5:Args:batchSizeLimit", "many"),127            ("WriteTo:6:Name", "Console"),128            ("WriteTo:6:Args:standardErrorFromLevel", "Loud"),129            ("WriteTo:7:Name", "File"),130            ("WriteTo:7:Args:path", "x.log"),131            ("WriteTo:7:Args:fileSizeLimitBytes", "-5")132          ]), &[133          "The sink `Console`: Unknown theme `neon`",134          "The sink `Console`: Unknown formatter `xml`",135          "The sink `File`: `week` is not a rolling interval",136          "The sink `File`: `maybe` is not a flag",137          "The sink `Http`: The argument `endpoint` is empty",138          "The sink `Http`: `many` is not a count",139          "The sink `Console`: `Loud` is not a level",140          "The sink `File`: `-5` is not a count"141        ])142    ])143  let ran = Test.run(&checks)144  for failure in Test.failuresOf(&ran) {145    let _reported = Io.writeErrorLine(failure)146  }147  Test.report(&ran)148}149