Il logo del progetto open source Boolli

Boolli

L'interprete di espressioni booleane in C# che ti permette di implementare sistemi di regole avanzati

Indice6 min di lettura

Boolli è un interprete di espressioni booleane open-source scritto in C# e disponibile come libreria su NuGet.

Cosa può fare Boolli? Rispondere true o false data un'espressione booleana!

Per Boolli, un'espressione booleana è una stringa generata secondo questa grammatica EBNF:

expr: factor((and | or) factor)*
factor: (not)* factor | boolean | LPAR expr RPAR
boolean: true | false | 0 | 1

Come software Boolli è composto da tre moduli:

  • Un lexer
  • Un parser
  • Un interprete

...e la struttura dati alla base del suo funzionamento è l'Abstract Syntax Tree.

Perché Boolli può esserti utile?

  • A scopo didattico: per capire come funziona un semplice interprete. Noi informatici li diamo per scontati, ma sono molto interessanti!
  • Se sostituisci, via codice, i token booleani con delle Func<bool> (o anche delle Func<Task<bool>>). Uno use case è quello di definire un semplice sistema di regole.

Valutare un'espressione booleana

var boolli = new Evaluator();
string booleanExpression = "not true and false or (true and false)";
bool result = boolli.EvaluateBooleanExpression(booleanExpression);

...puoi anche usare 0 al posto di false e 1 al posto di true

var boolli = new Evaluator();
string booleanExpression = "not 1 and 0 or (true and false)";
bool result = boolli.EvaluateBooleanExpression(booleanExpression);

Valutare un'espressione di Func<bool>

var boolli = new Evaluator();
bool result = boolli.EvaluateFuncOfBoolExpression(
    "f1 and f2",
    new NamedBooleanFunction[]
    {
        new NamedBooleanFunction("f1", () => true),
        new NamedBooleanFunction("f2", () => false),
    });

...anche in versione Async, con un'espressione di Func<Task<bool>

var boolli = new Evaluator();
bool result = await boolli.EvaluateFuncOfBoolExpressionAsync(
    "f3 and f4",
    new NamedAsyncBooleanFunction[]
    {
        new NamedAsyncBooleanFunction(
            "f3",
            async () => { await Task.Delay(100); return true; }),
        new NamedAsyncBooleanFunction(
            "f4",
            async () => { await Task.Delay(100); return true; })
    });

Scenario reale

Ho sviluppato un semplicissimo scenario a scopo dimostrativo (se vuoi lo puoi migliorare tramite GitHub, fa sempre piacere!): un sistema di creazione di alert basato su regole configurabili interpretate da Boolli.

Qui su GitHub puoi trovare il codice sorgente dell'esempio specifico che sto per approfondire.

boolli-sample-scenario-output

L'idea è quella di generare degli alert testuali basati su dati di monitoraggio hardware, come ad esempio la percentuali di CPU utilizzata in un dato momento. Ogni alert è generato da una regola che può essere scritta come una stringa - e quindi configurabile anche da una GUI - come ad esempio: CPUPercentage and UsedRAMGigaBytes.

In questo caso CPUPercentage è una funzione booleana che valuta la percentuale di CPU di un dato record secondo una soglia. Ad esempio può ritornare true se il valore supera il 90%.

Per completare l'esempio, ho cercato di immaginare uno scenario il più possibile reale. C'è bisogno di questi elementi per far funzionare tutto:

  • Una lista di regole (Rules)
  • Dei dati di monitoraggio (MonitoringData) che in questo caso ho generato casualmente con FakeMonitoringDataRepository
  • Un generatore di alert che utilizzi un interprete di espressioni booleane (Boolli)

L'anatomia di una regola

  new Rule()
  {
      RuleName = "RAM full and CPU busy last minute",
      BooleanExpression = $"{Metrics.CPUPercentage} and {Metrics.UsedRAMGigaBytes}",
      ResultingAlertGravity = AlertGravity.Critical,
      MessageFormat = "Attention! Last minute CPU ({0}%) and RAM ({1} GB) are very critical! (Last value collection time: {2})",
      TimeFrameMinutes = 1,
      DataEvaluationFunctionDescription  = new DataEvaluationFunctionDescription[]
      {
          new DataEvaluationFunctionDescription()
          {
              Metric = Metrics.CPUPercentage,
              Threshold = 90
          },
          new DataEvaluationFunctionDescription()
          {
              Metric = Metrics.UsedRAMGigaBytes,
              Threshold = 7
          }
      }
  }

Le property salienti sono:

  • BooleanExpression: con questa property specifichi un'espressione booleana che deve essere parsata ed eseguita da Boolli utilizzando i nomi di funzione specificati in DataEvaluationFunctionDescription
  • MessageFormat: per comporre il messaggio testuale di alert utilizzando come parametri l'output delle funzioni di valutazione dei record

In questo esempio specifico ho definito la classe DataEvaluationFunctionDescription con lo scopo di parametrizzare ogni tipo di metrica da valutare e la sua soglia specifica.

Il generatore di alert

Questa è la parte più complessa di questo esempio, perché qui si mette tutto insieme: le regole, Boolli e i dati di monitoraggio.

public List<Alert> GenerateAlerts(MonitoringData[] monitoringData)
{
  var alerts = new List<Alert>();
  var boolli = new Evaluator();
 
  foreach (var rule in _alertGenerationRules)
  {
    // I need object for String.Format
    var lastValues = new Dictionary<Metrics, MonitoringData>();
 
    var namedBooleanFunctions = rule.DataEvaluationFunctionDescription.Select(d => new NamedBooleanFunction(
      d.Metric.ToString(),
      () =>
      {
        (bool IsCritical, MonitoringData data) = ValueCritical(
          monitoringData,
          DateTime.Now.AddMinutes(-rule.TimeFrameMinutes),
          d.Metric,
          d.Threshold);
 
        if (IsCritical && data != null)
        {
          lastValues[d.Metric] = data;
        }
 
        return IsCritical;
      })).ToArray();
 
    bool alertShouldBeGenerated = boolli.EvaluateFuncOfBoolExpression(
      rule.BooleanExpression,
      namedBooleanFunctions);
 
    if (alertShouldBeGenerated)
    {
      var stringFormatParameters = new List<object>();
      foreach (var lastValue in lastValues)
      {
        stringFormatParameters.Add((int)lastValue.Value.MetricValue);
      }
 
      if (stringFormatParameters.Count < Enum.GetNames(typeof(Metrics)).Length)
      {
        for (int i = 0; i < Enum.GetNames(typeof(Metrics)).Length; ++i)
          stringFormatParameters.Add("-");
      }
 
      stringFormatParameters.Add(lastValues.Values.OrderByDescending(v => v.CollectionTime).FirstOrDefault().CollectionTime);
 
      alerts.Add(new Alert()
      {
        GeneratingRuleName = rule.RuleName,
        AlertGravity = rule.ResultingAlertGravity,
        GenerationDate = DateTime.Now,
        Message = string.Format(rule.MessageFormat, stringFormatParameters.ToArray())
      });
    }
  }
 
  return alerts;
}

In questo frangente c'è un ciclo che valuta ogni regola ed il punto di giunzione con Boolli è proprio qui: le DataEvaluationFunctionDescription diventano NamedBooleanFunction per poter essere utilizzate dalla libreria.

bool alertShouldBeGenerated = boolli.EvaluateFuncOfBoolExpression(
    rule.BooleanExpression,
    namedBooleanFunctions);

La funzione concreta dietro le quinte

Dopo aver letto tutto questo potrete pensare: ok, ma chi è che materialmente valuta quando i dati di monitoraggio devono generare un alert oppure no?

private (bool IsCritical, MonitoringData Data) ValueCritical(
    MonitoringData[] monitoringData,
    DateTime startingDate,
    Metrics metric,
    double threshold)
{
  foreach (var data in monitoringData
    .Where(d => d.CollectionTime > startingDate)
    .OrderByDescending(d => d.CollectionTime))
  {
    if (data.Metric == metric && data.MetricValue > threshold)
    {
      return (true, data);
    }
  }
 
  return (false, null);
}

Il codice sopra è l'If sostanziale che sta alla base di tutto.

Contribuire

Il mondo dell'open source è meraviglioso proprio perché ognuno può dare il proprio contributo ed aiutare chissà chi nel mondo!

Il codice di Boolli è interamente open source su GitHub ed è aperta alla collaborazione da parte di tutti!

Altri progetti open-source su cui ho lavorato

Hai domande o vuoi contribuire?

Il codice di Boolli è interamente open source su GitHub ed è aperto alla collaborazione da parte di tutti!

Codice sorgente