Skip to content

Commit

Permalink
Extract core library
Browse files Browse the repository at this point in the history
  • Loading branch information
milandjurdjevic committed Feb 11, 2025
1 parent 6e97e16 commit 3cf6233
Show file tree
Hide file tree
Showing 18 changed files with 357 additions and 285 deletions.
42 changes: 32 additions & 10 deletions analog.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog", "src\Analog.fsproj", "{4A65E52A-645B-4AC8-9AFB-9F676E448CF9}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cli", "cli", "{5AD06B21-6AEB-4A0E-B19E-A0386D2D0621}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog.Tests", "test\Analog.Tests.fsproj", "{3BBACB6E-2AAA-433A-BF91-A40A2217FE37}"
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog.Cli", "cli\src\Analog.Cli.fsproj", "{C9093BEF-2448-439A-B089-10E9164EACD5}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog.Cli.Tests", "cli\test\Analog.Cli.Tests.fsproj", "{17BAE747-39F3-4AE2-8F14-EFAA7EAE00AF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{D78B2BF3-E8E1-4E7D-B47F-8DE6263B91AB}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog.Core", "core\src\Analog.Core.fsproj", "{8527923E-27F8-440D-9D3D-D0E15F769F47}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Analog.Core.Tests", "core\test\Analog.Core.Tests.fsproj", "{8924D0B9-0693-4402-B767-41C600A87587}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -16,13 +24,27 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4A65E52A-645B-4AC8-9AFB-9F676E448CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A65E52A-645B-4AC8-9AFB-9F676E448CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A65E52A-645B-4AC8-9AFB-9F676E448CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A65E52A-645B-4AC8-9AFB-9F676E448CF9}.Release|Any CPU.Build.0 = Release|Any CPU
{3BBACB6E-2AAA-433A-BF91-A40A2217FE37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3BBACB6E-2AAA-433A-BF91-A40A2217FE37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3BBACB6E-2AAA-433A-BF91-A40A2217FE37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BBACB6E-2AAA-433A-BF91-A40A2217FE37}.Release|Any CPU.Build.0 = Release|Any CPU
{C9093BEF-2448-439A-B089-10E9164EACD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9093BEF-2448-439A-B089-10E9164EACD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9093BEF-2448-439A-B089-10E9164EACD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9093BEF-2448-439A-B089-10E9164EACD5}.Release|Any CPU.Build.0 = Release|Any CPU
{17BAE747-39F3-4AE2-8F14-EFAA7EAE00AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17BAE747-39F3-4AE2-8F14-EFAA7EAE00AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17BAE747-39F3-4AE2-8F14-EFAA7EAE00AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17BAE747-39F3-4AE2-8F14-EFAA7EAE00AF}.Release|Any CPU.Build.0 = Release|Any CPU
{8527923E-27F8-440D-9D3D-D0E15F769F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8527923E-27F8-440D-9D3D-D0E15F769F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8527923E-27F8-440D-9D3D-D0E15F769F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8527923E-27F8-440D-9D3D-D0E15F769F47}.Release|Any CPU.Build.0 = Release|Any CPU
{8924D0B9-0693-4402-B767-41C600A87587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8924D0B9-0693-4402-B767-41C600A87587}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8924D0B9-0693-4402-B767-41C600A87587}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8924D0B9-0693-4402-B767-41C600A87587}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{C9093BEF-2448-439A-B089-10E9164EACD5} = {5AD06B21-6AEB-4A0E-B19E-A0386D2D0621}
{17BAE747-39F3-4AE2-8F14-EFAA7EAE00AF} = {5AD06B21-6AEB-4A0E-B19E-A0386D2D0621}
{8527923E-27F8-440D-9D3D-D0E15F769F47} = {D78B2BF3-E8E1-4E7D-B47F-8DE6263B91AB}
{8924D0B9-0693-4402-B767-41C600A87587} = {D78B2BF3-E8E1-4E7D-B47F-8DE6263B91AB}
EndGlobalSection
EndGlobal
9 changes: 4 additions & 5 deletions src/Analog.fsproj → cli/src/Analog.Cli.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="Log.fs" />
<Compile Include="Filter.fs" />
<Compile Include="Program.fs"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Argu" Version="6.2.5" />
<PackageReference Include="FParsec" Version="1.1.1" />
<PackageReference Include="grok.net" Version="2.0.0" />
<PackageReference Include="PCRE.NET" Version="1.1.0" />
<PackageReference Include="Spectre.Console" Version="0.49.1"/>
<PackageReference Include="Spectre.Console.Json" Version="0.49.1"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\core\src\Analog.Core.fsproj" />
</ItemGroup>

</Project>
24 changes: 12 additions & 12 deletions src/Program.fs → cli/src/Program.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
open System
open System.IO
open System.Text.Json
open Analog
open Analog.Core
open Argu
open Microsoft.FSharp.Core
open Spectre.Console
Expand All @@ -25,35 +25,35 @@ let import files =

let parse pattern text =
pattern
|> Option.map Log.createGrok
|> Option.defaultValue (Log.defaultGrok |> Result.Ok)
|> Result.bind (Log.parseGrok text)
|> Option.map Grok.create
|> Option.defaultValue (Grok.pattern |> Result.Ok)
|> Result.bind (Grok.extract text)
|> Result.map (Grok.group >> Grok.transform)

let filter expression entries =
expression
|> Option.map Filter.parse
|> Option.map (fun exp -> Parser.expression |> Parser.parse exp)
|> Option.map (fun res ->
res
|> Result.bind (fun filter -> entries |> Result.map (fun entries -> filter, entries)))
|> Option.map (fun result ->
result
|> Result.map (fun (filter, entries) ->
entries |> List.filter (fun entry -> Filter.evaluate entry filter)))
|> Result.map (fun (expression, entries) -> entries |> List.filter (Filter.eval expression)))
|> Option.defaultValue entries

let handle (args: ParseResults<Command>) =
import (args.GetResults Command.File)
|> parse (args.TryGetResult Command.Pattern)
|> filter (args.TryGetResult Command.Filter)

let normalize (entry: Log.Entry) =
let normalize (Log entry) =
entry
|> Map.map (fun _ value ->
match value with
| Log.StringLiteral value -> box value
| Log.NumberLiteral value -> box value
| Log.BooleanLiteral value -> box value
| Log.TimestampLiteral value -> box value)
| StringLiteral value -> box value
| NumberLiteral value -> box value
| BooleanLiteral value -> box value
| TimestampLiteral value -> box value)

let print entries =
entries
Expand Down
7 changes: 0 additions & 7 deletions test/Analog.Tests.fsproj → cli/test/Analog.Cli.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="EntryParserTest.fs" />
<Compile Include="FilterParserTest.fs" />
<Compile Include="FilterEvaluatorTest.fs" />
<Compile Include="Program.fs"/>
</ItemGroup>

Expand All @@ -28,10 +25,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\src\Analog.fsproj" />
</ItemGroup>



Expand Down
2 changes: 2 additions & 0 deletions cli/test/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[<EntryPoint>]
let main _ = 0
21 changes: 21 additions & 0 deletions core/src/Analog.Core.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<Compile Include="Log.fs" />
<Compile Include="Filter.fs" />
<Compile Include="Parser.fs" />
<Compile Include="Grok.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FParsec" Version="1.1.1" />
<PackageReference Include="grok.net" Version="2.0.0" />
<PackageReference Include="PCRE.NET" Version="1.1.0" />
</ItemGroup>

</Project>
83 changes: 83 additions & 0 deletions core/src/Filter.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace Analog.Core

type Operator =
| EqualOperator
| NotEqualOperator
| GreaterThanOperator
| GreaterThanOrEqualOperator
| LessThanOperator
| LessThanOrEqualOperator
| AndOperator
| OrOperator

type Expression =
| LiteralExpression of Literal
| MemberExpression of string
| BinaryExpression of Expression * Operator * Expression

[<RequireQualifiedAccess>]
module Filter =
type private Evaluation =
| TemporaryEvaluation of Literal option
| FinalEvaluation of bool

let eval expression entry =
let compareLiteral (left: Literal option) (right: Literal option) comparer =
match left, right with
| Some left, Some right ->
match left, right with
| StringLiteral _, StringLiteral _ -> comparer left right
| NumberLiteral _, NumberLiteral _ -> comparer left right
| BooleanLiteral _, BooleanLiteral _ -> comparer left right
| TimestampLiteral _, TimestampLiteral _ -> comparer left right
| _ -> false
| _ -> false

let combineLiteral (left: Literal option) (right: Literal option) combiner =
match left, right with
| Some left, Some right ->
match left, right with
| BooleanLiteral left, BooleanLiteral right -> combiner left right
| _ -> false
| _ -> false

let wrapFinal = BooleanLiteral >> Some

let compareEvaluation (left: Evaluation) (right: Evaluation) comparer =
match left, right with
| TemporaryEvaluation left, TemporaryEvaluation right -> compareLiteral left right comparer
| TemporaryEvaluation left, FinalEvaluation right -> compareLiteral left (wrapFinal right) comparer
| FinalEvaluation left, TemporaryEvaluation right -> compareLiteral (wrapFinal left) right comparer
| FinalEvaluation left, FinalEvaluation right -> compareLiteral (wrapFinal left) (wrapFinal right) comparer

let combineEvaluation (left: Evaluation) (right: Evaluation) combiner =
match left, right with
| TemporaryEvaluation left, TemporaryEvaluation right -> combineLiteral left right combiner
| TemporaryEvaluation left, FinalEvaluation right -> combineLiteral left (wrapFinal right) combiner
| FinalEvaluation left, TemporaryEvaluation right -> combineLiteral (wrapFinal left) right combiner
| FinalEvaluation left, FinalEvaluation right -> combineLiteral (wrapFinal left) (wrapFinal right) combiner

let evaluateOperator (left: Evaluation) (operator: Operator) (right: Evaluation) =
match operator with
| EqualOperator -> compareEvaluation left right (=)
| NotEqualOperator -> compareEvaluation left right (<>)
| GreaterThanOperator -> compareEvaluation left right (>)
| GreaterThanOrEqualOperator -> compareEvaluation left right (>=)
| LessThanOperator -> compareEvaluation left right (<)
| LessThanOrEqualOperator -> compareEvaluation left right (<=)
| AndOperator -> combineEvaluation left right (&&)
| OrOperator -> combineEvaluation left right (||)

let rec loop (expression: Expression) (entry: Log) : Evaluation =
match expression with
| LiteralExpression right -> right |> Option.Some |> TemporaryEvaluation
| MemberExpression field -> entry |> Log.get field |> TemporaryEvaluation
| BinaryExpression(left, operator, right) ->
let left = loop left entry
let right = loop right entry
evaluateOperator left operator right |> FinalEvaluation

match loop expression entry with
| TemporaryEvaluation temp -> temp.IsSome
| FinalEvaluation final -> final

47 changes: 47 additions & 0 deletions core/src/Grok.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace Analog.Core

type GrokGroup = GrokGroup of Map<string, string>

[<RequireQualifiedAccess>]
module Grok =
open GrokNet

let create (pattern: string) : Result<Grok, string> =
try
Grok(pattern) |> Result.Ok
with err ->
$"Grok initialization failed with error: {err.Message}" |> Result.Error

let extract (text: string) (pattern: Grok) : Result<GrokResult, string> =
try
pattern.Parse text |> Result.Ok
with err ->
$"Grok extraction failed with error: {err.Message}" |> Result.Error

let pattern: Grok =
Grok("\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{LOGLEVEL:loglevel}\] %{GREEDYDATA:message}")

let group: GrokResult -> GrokGroup list =
Seq.fold
(fun list item ->
match list with
| [] -> [ Map([ item.Key, string item.Value ]) ]
| head :: tail ->
if head |> Map.containsKey item.Key then
[ Map([ item.Key, string item.Value ]); head ] @ tail
else
(head |> Map.add item.Key (string item.Value)) :: tail)
List.empty
>> List.rev
>> List.map GrokGroup

let transform: GrokGroup list -> Log list =
List.map (fun (GrokGroup group) ->
group
|> Map.toSeq
|> Seq.choose (fun (key, value) ->
match Parser.literal |> Parser.parse value with
| Ok value -> Some(key, value)
| Error _ -> None)
|> Map.ofSeq
|> Log)
16 changes: 16 additions & 0 deletions core/src/Log.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Analog.Core

open System

type Literal =
| StringLiteral of string
| NumberLiteral of float
| BooleanLiteral of bool
| TimestampLiteral of DateTimeOffset

type Log = Log of Map<string, Literal>

[<RequireQualifiedAccess>]
module Log =
let get key (Log entry) = entry |> Map.tryFind key
let empty = Map.empty |> Log
67 changes: 67 additions & 0 deletions core/src/Parser.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[<RequireQualifiedAccess>]
module Analog.Core.Parser

open System
open FParsec

let parse input parser =
run parser input
|> function
| Success(value, _, _) -> Result.Ok value
| Failure(error, _, _) -> Result.Error error

let dateTimeOffset: Parser<_, unit> =
restOfLine false
>>= fun input ->
try
DateTimeOffset.Parse input |> preturn
with err ->
fail err.Message
|> attempt

let floatFinite: Parser<_, unit> =
pfloat
>>= fun res ->
if Double.IsInfinity res || Double.IsNaN res then
fail "Number cannot be infinite or NaN"
else
preturn res
|> attempt

let boolCI: Parser<_, unit> =
choice [ pstringCI "true" >>% true; pstringCI "false" >>% false ]

let stringQuoted: Parser<string, unit> =
skipChar '\'' >>. manyCharsTill anyChar (skipChar '\'')

let literal: Parser<_, unit> =
choice
[ dateTimeOffset |>> TimestampLiteral
floatFinite |>> Literal.NumberLiteral
boolCI |>> BooleanLiteral
restOfLine true |>> StringLiteral ]

let expression: Parser<_, unit> =
let literalExpression: Parser<_, unit> =
choice
[ dateTimeOffset |>> TimestampLiteral .>> spaces
floatFinite |>> Literal.NumberLiteral .>> spaces
boolCI |>> BooleanLiteral .>> spaces
stringQuoted |>> StringLiteral .>> spaces ]
|>> LiteralExpression

let memberExpression: Parser<_, unit> =
many1Chars (letter <|> digit) |>> MemberExpression .>> spaces

let builder = OperatorPrecedenceParser<Expression, _, _>()
builder.TermParser <- choice [ literalExpression; memberExpression ]
let bin op left right = BinaryExpression(left, op, right)
builder.AddOperator(InfixOperator("&", spaces, 1, Associativity.Left, bin AndOperator))
builder.AddOperator(InfixOperator("|", spaces, 2, Associativity.Left, bin OrOperator))
builder.AddOperator(InfixOperator(">", spaces, 3, Associativity.None, bin GreaterThanOperator))
builder.AddOperator(InfixOperator(">=", spaces, 4, Associativity.None, bin GreaterThanOrEqualOperator))
builder.AddOperator(InfixOperator("<", spaces, 5, Associativity.None, bin LessThanOperator))
builder.AddOperator(InfixOperator("<=", spaces, 6, Associativity.None, bin LessThanOrEqualOperator))
builder.AddOperator(InfixOperator("=", spaces, 7, Associativity.None, bin EqualOperator))
builder.AddOperator(InfixOperator("<>", spaces, 8, Associativity.None, bin NotEqualOperator))
builder.ExpressionParser
Loading

0 comments on commit 3cf6233

Please sign in to comment.