Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ru.d10xa.jsonlogviewer

import ru.d10xa.jsonlogviewer.config.ResolvedConfig
import ru.d10xa.jsonlogviewer.decline.Config
import ru.d10xa.jsonlogviewer.formatout.{ColorLineFormatter, RawFormatter}

final case class FilterComponents(
timestampFilter: TimestampFilter,
parseResultKeys: ParseResultKeys,
logLineFilter: LogLineFilter,
fuzzyFilter: FuzzyFilter,
outputLineFormatter: OutputLineFormatter
)

object FilterComponents {

def fromConfig(resolvedConfig: ResolvedConfig): FilterComponents = {
val timestampFilter = TimestampFilter()
val parseResultKeys = ParseResultKeys(resolvedConfig)
val logLineFilter = LogLineFilter(resolvedConfig, parseResultKeys)
val fuzzyFilter = new FuzzyFilter(resolvedConfig)

val outputLineFormatter = resolvedConfig.formatOut match {
case Some(Config.FormatOut.Raw) => RawFormatter()
case Some(Config.FormatOut.Pretty) | None =>
ColorLineFormatter(
resolvedConfig,
resolvedConfig.feedName,
resolvedConfig.excludeFields
)
}

FilterComponents(
timestampFilter,
parseResultKeys,
logLineFilter,
fuzzyFilter,
outputLineFormatter
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ru.d10xa.jsonlogviewer

import cats.effect.IO
import fs2.Stream
import ru.d10xa.jsonlogviewer.config.ResolvedConfig
import scala.util.{Failure, Success, Try}

object FilterPipeline {

// Filter order: rawFilter -> parse -> grep -> query -> fuzzy -> timestamp -> format
def applyFilters(
stream: Stream[IO, String],
parser: LogLineParser,
components: FilterComponents,
resolvedConfig: ResolvedConfig
): Stream[IO, String] = {
stream
.filter(
rawFilter(_, resolvedConfig.rawInclude, resolvedConfig.rawExclude)
)
.map(parser.parse)
.filter(components.logLineFilter.grep)
.filter(components.logLineFilter.logLineQueryPredicate)
.filter(components.fuzzyFilter.test)
.through(
components.timestampFilter.filterTimestampAfter(resolvedConfig.timestampAfter)
)
.through(
components.timestampFilter.filterTimestampBefore(
resolvedConfig.timestampBefore
)
)
.map(formatWithSafety(_, components.outputLineFormatter))
}

private def rawFilter(
str: String,
include: Option[List[String]],
exclude: Option[List[String]]
): Boolean = {
import scala.util.matching.Regex
val includeRegexes: List[Regex] = include.getOrElse(Nil).map(_.r)
val excludeRegexes: List[Regex] = exclude.getOrElse(Nil).map(_.r)
val includeMatches = includeRegexes.isEmpty || includeRegexes.exists(
_.findFirstIn(str).isDefined
)
val excludeMatches = excludeRegexes.forall(_.findFirstIn(str).isEmpty)
includeMatches && excludeMatches
}

private def formatWithSafety(
parseResult: ParseResult,
formatter: OutputLineFormatter
): String =
Try(formatter.formatLine(parseResult)) match {
case Success(formatted) => formatted.toString
case Failure(_) => parseResult.raw
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ru.d10xa.jsonlogviewer

import ru.d10xa.jsonlogviewer.config.ResolvedConfig
import ru.d10xa.jsonlogviewer.csv.CsvLogLineParser
import ru.d10xa.jsonlogviewer.decline.Config.FormatIn
import ru.d10xa.jsonlogviewer.logfmt.LogfmtLogLineParser

object LogLineParserFactory {

def createNonCsvParser(resolvedConfig: ResolvedConfig): LogLineParser = {
val jsonPrefixPostfix = JsonPrefixPostfix(JsonDetector())
resolvedConfig.formatIn match {
case Some(FormatIn.Logfmt) =>
LogfmtLogLineParser(resolvedConfig)
case Some(FormatIn.Csv) =>
throw new IllegalStateException(
"CSV format requires header line, use createCsvParser instead"
)
case Some(FormatIn.Json) | None =>
JsonLogLineParser(resolvedConfig, jsonPrefixPostfix)
}
}

def createCsvParser(
resolvedConfig: ResolvedConfig,
headerLine: String
): LogLineParser = {
CsvLogLineParser(resolvedConfig, headerLine)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,11 @@ import fs2.*
import fs2.Pull
import ru.d10xa.jsonlogviewer.config.ConfigResolver
import ru.d10xa.jsonlogviewer.config.ResolvedConfig
import ru.d10xa.jsonlogviewer.csv.CsvLogLineParser
import ru.d10xa.jsonlogviewer.decline.yaml.ConfigYaml
import ru.d10xa.jsonlogviewer.decline.Config
import ru.d10xa.jsonlogviewer.decline.Config.FormatIn
import ru.d10xa.jsonlogviewer.formatout.ColorLineFormatter
import ru.d10xa.jsonlogviewer.formatout.RawFormatter
import ru.d10xa.jsonlogviewer.logfmt.LogfmtLogLineParser
import ru.d10xa.jsonlogviewer.shell.Shell
import ru.d10xa.jsonlogviewer.shell.ShellImpl
import scala.util.matching.Regex
import scala.util.Failure
import scala.util.Success
import scala.util.Try

object LogViewerStream {

Expand Down Expand Up @@ -116,43 +108,18 @@ object LogViewerStream {
)
)
} else {
IO.pure(makeNonCsvLogLineParser(resolvedConfig))
IO.pure(LogLineParserFactory.createNonCsvParser(resolvedConfig))
}

Stream.eval(getParser).flatMap { parser =>
val timestampFilter = TimestampFilter()
val parseResultKeys = ParseResultKeys(resolvedConfig)
val logLineFilter = LogLineFilter(resolvedConfig, parseResultKeys)
val fuzzyFilter = new FuzzyFilter(resolvedConfig)

val outputLineFormatter = resolvedConfig.formatOut match {
case Some(Config.FormatOut.Raw) => RawFormatter()
case Some(Config.FormatOut.Pretty) | None =>
ColorLineFormatter(
resolvedConfig,
resolvedConfig.feedName,
resolvedConfig.excludeFields
)
}

Stream
.emit(line)
.filter(
rawFilter(_, resolvedConfig.rawInclude, resolvedConfig.rawExclude)
)
.map(parser.parse)
.filter(logLineFilter.grep)
.filter(logLineFilter.logLineQueryPredicate)
.filter(fuzzyFilter.test)
.through(
timestampFilter.filterTimestampAfter(resolvedConfig.timestampAfter)
)
.through(
timestampFilter.filterTimestampBefore(
resolvedConfig.timestampBefore
)
)
.map(formatWithSafety(_, outputLineFormatter))
val components = FilterComponents.fromConfig(resolvedConfig)

FilterPipeline.applyFilters(
Stream.emit(line),
parser,
components,
resolvedConfig
)
}
}

Expand All @@ -162,80 +129,14 @@ object LogViewerStream {
): Stream[IO, String] =
lines.pull.uncons1.flatMap {
case Some((headerLine, rest)) =>
val csvHeaderParser = CsvLogLineParser(resolvedConfig, headerLine)

val timestampFilter = TimestampFilter()
val parseResultKeys = ParseResultKeys(resolvedConfig)
val logLineFilter = LogLineFilter(resolvedConfig, parseResultKeys)
val fuzzyFilter = new FuzzyFilter(resolvedConfig)

val outputLineFormatter = resolvedConfig.formatOut match {
case Some(Config.FormatOut.Raw) => RawFormatter()
case Some(Config.FormatOut.Pretty) | None =>
ColorLineFormatter(
resolvedConfig,
resolvedConfig.feedName,
resolvedConfig.excludeFields
)
}
val csvHeaderParser = LogLineParserFactory.createCsvParser(resolvedConfig, headerLine)
val components = FilterComponents.fromConfig(resolvedConfig)

rest
.filter(
rawFilter(_, resolvedConfig.rawInclude, resolvedConfig.rawExclude)
)
.map(csvHeaderParser.parse)
.filter(logLineFilter.grep)
.filter(logLineFilter.logLineQueryPredicate)
.filter(fuzzyFilter.test)
.through(
timestampFilter.filterTimestampAfter(resolvedConfig.timestampAfter)
)
.through(
timestampFilter.filterTimestampBefore(
resolvedConfig.timestampBefore
)
)
.map(formatWithSafety(_, outputLineFormatter))
FilterPipeline
.applyFilters(rest, csvHeaderParser, components, resolvedConfig)
.pull
.echo
case None =>
Pull.done
}.stream

private def formatWithSafety(
parseResult: ParseResult,
formatter: OutputLineFormatter
): String =
Try(formatter.formatLine(parseResult)) match {
case Success(formatted) => formatted.toString
case Failure(_) => parseResult.raw
}

def makeNonCsvLogLineParser(
resolvedConfig: ResolvedConfig
): LogLineParser = {
val jsonPrefixPostfix = JsonPrefixPostfix(JsonDetector())
resolvedConfig.formatIn match {
case Some(FormatIn.Logfmt) => LogfmtLogLineParser(resolvedConfig)
case Some(FormatIn.Csv) =>
throw new IllegalStateException(
"method makeNonCsvLogLineParser does not support csv"
)
case _ => JsonLogLineParser(resolvedConfig, jsonPrefixPostfix)
}
}

def rawFilter(
str: String,
include: Option[List[String]],
exclude: Option[List[String]]
): Boolean = {
val includeRegexes: List[Regex] = include.getOrElse(Nil).map(_.r)
val excludeRegexes: List[Regex] = exclude.getOrElse(Nil).map(_.r)
val includeMatches = includeRegexes.isEmpty || includeRegexes.exists(
_.findFirstIn(str).isDefined
)
val excludeMatches = excludeRegexes.forall(_.findFirstIn(str).isEmpty)
includeMatches && excludeMatches
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package ru.d10xa.jsonlogviewer

import munit.FunSuite
import ru.d10xa.jsonlogviewer.decline.{Config, FieldNamesConfig}
import ru.d10xa.jsonlogviewer.formatout.{ColorLineFormatter, RawFormatter}

class FilterComponentsTest extends FunSuite with TestResolvedConfig {

override def baseResolvedConfig = super.baseResolvedConfig.copy(
feedName = Some("test-feed")
)

test("fromConfig should create all filter components") {
val components = FilterComponents.fromConfig(baseResolvedConfig)

assertNotEquals(components.timestampFilter, null, "TimestampFilter should be created")
assertNotEquals(components.parseResultKeys, null, "ParseResultKeys should be created")
assertNotEquals(components.logLineFilter, null, "LogLineFilter should be created")
assertNotEquals(components.fuzzyFilter, null, "FuzzyFilter should be created")
assertNotEquals(components.outputLineFormatter, null, "OutputLineFormatter should be created")
}

test("fromConfig should create RawFormatter when formatOut is Raw") {
val config = baseResolvedConfig.copy(formatOut = Some(Config.FormatOut.Raw))
val components = FilterComponents.fromConfig(config)

assert(
components.outputLineFormatter.isInstanceOf[RawFormatter],
"Should create RawFormatter for Raw format"
)
}

test("fromConfig should create ColorLineFormatter when formatOut is Pretty") {
val config = baseResolvedConfig.copy(formatOut = Some(Config.FormatOut.Pretty))
val components = FilterComponents.fromConfig(config)

assert(
components.outputLineFormatter.isInstanceOf[ColorLineFormatter],
"Should create ColorLineFormatter for Pretty format"
)
}

test("fromConfig should create ColorLineFormatter when formatOut is None") {
val config = baseResolvedConfig.copy(formatOut = None)
val components = FilterComponents.fromConfig(config)

assert(
components.outputLineFormatter.isInstanceOf[ColorLineFormatter],
"Should create ColorLineFormatter when formatOut is None (default)"
)
}

test("fromConfig should initialize ParseResultKeys with correct config") {
val config = baseResolvedConfig.copy(
fieldNames = FieldNamesConfig(
timestampFieldName = "custom_ts",
levelFieldName = "custom_level",
messageFieldName = "custom_msg",
stackTraceFieldName = "custom_stack",
loggerNameFieldName = "custom_logger",
threadNameFieldName = "custom_thread"
)
)
val components = FilterComponents.fromConfig(config)

// Verify that ParseResultKeys was created (opaque object, can't test internals)
assertNotEquals(
components.parseResultKeys,
null,
"ParseResultKeys should be created with custom config"
)
}

test("fromConfig should create components multiple times with same config") {
val components1 = FilterComponents.fromConfig(baseResolvedConfig)
val components2 = FilterComponents.fromConfig(baseResolvedConfig)

// Components should be created each time (not cached)
assert(
components1 ne components2,
"Each call should create new FilterComponents"
)
}
}
Loading