Il logo di Imagine Software

Attualmente sono impegnato full-time come CTO per Tuduu e non valuto altri incarichi

Boolli

L'interprete di espressioni booleane

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:

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

Perché Boolli può esserti utile?

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. Abbiamo bisogno di questi elementi per far funzionare tutto:

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:

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!