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

Registry.pudu

Pudu208 lines9.4 KB

GitHub ↗
1/** @Log.Settings.Registry.Catalog — sinks and enrichers settings may name */2module PuduLangLog.Settings.Registry34import Std.List as List5import PuduLangLog.Domain.Settings as Plan6import PuduLangLog.Enricher as Enricher7import PuduLangLog.Enrichers.Environment as Environment8import PuduLangLog.Formatting.Compact as Compact9import PuduLangLog.Formatting.Json as Json10import PuduLangLog.Formatting.Text as Text11import PuduLangLog as Log12import PuduLangLog.Sink as Sink13import PuduLangLog.Sinks.Batching as Batching14import PuduLangLog.Sinks.Console as Console15import PuduLangLog.Sinks.File as File16import PuduLangLog.Sinks.Http as Http17import PuduLangLog.Sinks.Theme as Theme1819/** @Log.Settings.Registry.SinkFactory — a sink built from its arguments */20export type SinkFactory = fn(&Array[(Str, Str)]) -> Result[Sink.Sink, Str]2122/** @Log.Settings.Registry.EnricherFactory — an enricher built from its arguments */23export type EnricherFactory = fn(&Array[(Str, Str)]) -> Result[Enricher.Enricher, Str]2425/** @Log.Settings.Registry.Catalog — factories by name */26export type Registry = { sinks: Array[(Str, SinkFactory)], enrichers: Array[(Str, EnricherFactory)] }2728/// A registry naming nothing.29export fn empty() -> Registry { Registry{sinks: [], enrichers: []} }3031/// A registry naming the sinks `Console`, `File`, and `Http`, and the enrichers `MachineName`,32/// `EnvironmentUserName`, `EnvironmentName`, `EnvironmentVariable`, `ProcessId`, `ProcessName`, and33/// `FailureDetail`.34export fn standard() -> Registry {35  empty()36    .withSink("Console", console)37    .withSink("File", file)38    .withSink("Http", http)39    .withEnricher("MachineName", fixed(Environment.machineName))40    .withEnricher("EnvironmentUserName", fixed(Environment.environmentUserName))41    .withEnricher("EnvironmentName", fn(arguments: &Array[(Str, Str)]) -> Result[Enricher.Enricher, Str] { Ok(Environment.environmentName(textOr(arguments, "variable", Environment.ENVIRONMENT_VARIABLE))) })42    .withEnricher("EnvironmentVariable", fn(arguments: &Array[(Str, Str)]) -> Result[Enricher.Enricher, Str] { Ok(Environment.environmentVariable(required(arguments, "variable") ?)) })43    .withEnricher("ProcessId", fixed(Environment.processId))44    .withEnricher("ProcessName", fixed(Environment.processName))45    .withEnricher("FailureDetail", fixed(Environment.failureDetail))46}4748/** @Log.Settings.Registry.Registering — adding factories under names */49export trait Registering {50  fn withSink(self: &Self, name: Str, factory: SinkFactory) -> Self51  fn withEnricher(self: &Self, name: Str, factory: EnricherFactory) -> Self52}5354impl Registering for Registry {55  /// The registry with a sink factory under a name, replacing one of the same name ignoring case.56  fn withSink(self: &Self, name: Str, factory: SinkFactory) -> Self {57    Registry{..*self, sinks: self.sinks.filter(|entry: (Str, SinkFactory)| entry[0].toLower() != name.toLower()).push((name, factory))}58  }5960  /// The registry with an enricher factory under a name, replacing one of the same name ignoring61  /// case.62  fn withEnricher(self: &Self, name: Str, factory: EnricherFactory) -> Self {63    Registry{..*self, enrichers: self.enrichers.filter(|entry: (Str, EnricherFactory)| entry[0].toLower() != name.toLower()).push((name, factory))}64  }65}6667/// The sink factory registered under a name, ignoring case.68export fn sinkNamed(registry: &Registry, name: Str) -> Option[SinkFactory] {69  let found = List.find(&registry.sinks, |entry: (Str, SinkFactory)| entry[0].toLower() == name.toLower()) ?70  Some(found[1])71}7273/// The enricher factory registered under a name, ignoring case.74export fn enricherNamed(registry: &Registry, name: Str) -> Option[EnricherFactory] {75  let found = List.find(&registry.enrichers, |entry: (Str, EnricherFactory)| entry[0].toLower() == name.toLower()) ?76  Some(found[1])77}7879/// A factory ignoring its arguments.80fn fixed(build: fn() -> Enricher.Enricher) -> EnricherFactory {81  fn(_arguments: &Array[(Str, Str)]) -> Result[Enricher.Enricher, Str] { Ok(build()) }82}8384/// The console sink: `outputTemplate`, `theme` (`none`, `literate`, `grayscale`, `code`, or85/// `sixteen`), `standardErrorFromLevel`, and `formatter` (`compact`, `rendered`, or `json`).86fn console(arguments: &Array[(Str, Str)]) -> Result[Sink.Sink, Str] {87  let base = Console.defaults()88  let theme = match lookup(arguments, "theme") {89    case Some(name) => themeOf(name) ?90    case None => base.theme91  }92  let standardError = match lookup(arguments, "standardErrorFromLevel") {93    case Some(text) => Some(levelOf(text) ?)94    case None => None95  }96  Ok(Console.sink(Console.Options{..base, outputTemplate: textOr(arguments, "outputTemplate", base.outputTemplate), theme: theme, standardErrorFromLevel: standardError, formatter: namedFormatter(arguments) ?}))97}9899/// The file sink: `path` (required), `outputTemplate`, `formatter`, `rollingInterval`,100/// `fileSizeLimitBytes`, `rollOnFileSizeLimit`, `retainedFileCountLimit`, and `buffered`.101fn file(arguments: &Array[(Str, Str)]) -> Result[Sink.Sink, Str] {102  let base = File.defaults(required(arguments, "path") ?)103  let formatter = match (namedFormatter(arguments) ?, lookup(arguments, "outputTemplate")) {104    case (Some(named), _) => named105    case (None, Some(output)) => Text.template(output)106    case _ => base.formatter107  }108  let interval = match lookup(arguments, "rollingInterval") {109    case Some(text) => optionOf(Plan.intervalOf(text), "rolling interval", text) ?110    case None => base.rollingInterval111  }112  Ok(File.sink(File.Options {113        ..base,114        formatter: formatter,115        rollingInterval: interval,116        fileSizeLimitBytes: countOr(arguments, "fileSizeLimitBytes", base.fileSizeLimitBytes) ?,117        rollOnFileSizeLimit: flagOr(arguments, "rollOnFileSizeLimit", base.rollOnFileSizeLimit) ?,118        retainedFileCountLimit: countOr(arguments, "retainedFileCountLimit", base.retainedFileCountLimit) ?,119        buffered: flagOr(arguments, "buffered", base.buffered) ?120      }))121}122123/// The HTTP sink: `endpoint` (required), `formatter`, `batchSizeLimit`, and `queueLimit`.124fn http(arguments: &Array[(Str, Str)]) -> Result[Sink.Sink, Str] {125  let base = Http.defaults(required(arguments, "endpoint") ?)126  let formatter = match namedFormatter(arguments) ? {127    case Some(named) => named128    case None => base.formatter129  }130  let batchSize = match lookup(arguments, "batchSizeLimit") {131    case Some(text) => optionOf(Plan.count(text), "count", text) ?132    case None => base.batching.batchSizeLimit133  }134  let batching = Batching.Options{..base.batching, batchSizeLimit: batchSize, queueLimit: countOr(arguments, "queueLimit", base.batching.queueLimit) ?}135  Ok(Http.sink(Http.Options{..base, formatter: formatter, batching: batching}))136}137138/// The formatter an argument names, if any.139fn namedFormatter(arguments: &Array[(Str, Str)]) -> Result[Option[Log.Formatter], Str] {140  let name = match lookup(arguments, "formatter") {141    case Some(text) => text.trim().toLower()142    case None => { return Ok(None) }143  }144  let found = List.find(&formatters(), |entry: (Str, Log.Formatter)| entry[0] == name)145  match found {146    case Some(entry) => Ok(Some(entry[1]))147    case None => Err("Unknown formatter `" + name + "`")148  }149}150151/// The formatters settings may name.152fn formatters() -> Array[(Str, Log.Formatter)] { [("compact", Compact.compact()), ("rendered", Compact.rendered()), ("json", Json.formatter(Json.defaults()))] }153154/// The console theme a name denotes, ignoring case.155fn themeOf(name: Str) -> Result[Theme.Theme, Str] {156  let themes = [("none", Theme.none()), ("literate", Theme.literate()), ("grayscale", Theme.grayscale()), ("code", Theme.code()), ("sixteen", Theme.sixteen())]157  match List.find(&themes, |entry: (Str, Theme.Theme)| entry[0] == name.trim().toLower()) {158    case Some(entry) => Ok(entry[1])159    case None => Err("Unknown theme `" + name + "`")160  }161}162163/// An argument's text, its name ignoring case.164fn lookup(arguments: &Array[(Str, Str)], name: Str) -> Option[Str] { Plan.argument(&Plan.Target{name: "", arguments: *arguments}, name) }165166/// An argument's text, or a fallback.167fn textOr(arguments: &Array[(Str, Str)], name: Str, fallback: Str) -> Str {168  match lookup(arguments, name) {169    case Some(text) => text170    case None => fallback171  }172}173174/// An argument's text, or an error naming it.175fn required(arguments: &Array[(Str, Str)], name: Str) -> Result[Str, Str] {176  match lookup(arguments, name) {177    case Some(text) => if text.trim().isEmpty() { Err("The argument `" + name + "` is empty") } else { Ok(text) }178    case None => Err("The argument `" + name + "` is required")179  }180}181182/// A level argument.183fn levelOf(text: Str) -> Result[Log.Level, Str] { optionOf(Plan.levelOf(text), "level", text) }184185/// A count argument, `none` for no limit, or a fallback when absent.186fn countOr(arguments: &Array[(Str, Str)], name: Str, fallback: Option[Int]) -> Result[Option[Int], Str] {187  match lookup(arguments, name) {188    case Some(text) => if text.trim().toLower() == "none" { Ok(None) } else { Ok(Some(optionOf(Plan.count(text), "count", text) ?)) }189    case None => Ok(fallback)190  }191}192193/// A `true` or `false` argument, or a fallback when absent.194fn flagOr(arguments: &Array[(Str, Str)], name: Str, fallback: Bool) -> Result[Bool, Str] {195  match lookup(arguments, name) {196    case Some(text) => optionOf(Plan.flag(text), "flag", text)197    case None => Ok(fallback)198  }199}200201/// A parsed value, or an error naming what the text is not.202fn optionOf[T](parsed: Option[T], what: Str, text: Str) -> Result[T, Str] {203  match parsed {204    case Some(found) => Ok(found)205    case None => Err("`" + text + "` is not a " + what)206  }207}208