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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ feeds:
### Example Configuration File

```yaml
showEmptyFields: true
feeds:
- name: "application-1-logs"
commands:
Expand All @@ -229,6 +230,7 @@ feeds:
- "DEBUG"
excludeFields:
- "thread_name"
showEmptyFields: false
- name: "application-2-logs"
commands:
- cat log2.txt
Expand Down Expand Up @@ -288,6 +290,12 @@ json-log-viewer --config-file json-log-viewer.yml
json-log-viewer --timestamp-field time
```

- **--show-empty-fields**: Display fields with empty values (null, empty strings, etc.) in output.
```bash
cat log.txt | json-log-viewer --show-empty-fields
```


#### Field Name Options

You can override the default field names to work with non-standard log formats:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ object ViewElement {

def makeConfigYamlForInlineInput(string: String, config: Config): ConfigYaml =
ConfigYaml(
showEmptyFields = None,
fieldNames = None,
feeds = Some(
List(
Expand All @@ -38,7 +39,8 @@ object ViewElement {
rawInclude = None,
rawExclude = None,
excludeFields = None,
fieldNames = None
fieldNames = None,
showEmptyFields = None
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
case None => Validated.valid(None)
}

private def parseOptionalBoolean(
fields: Map[String, Json],
fieldName: String
): ValidatedNel[String, Option[Boolean]] =
fields.get(fieldName) match {
case Some(jsonValue) =>
jsonValue
.as[Boolean]
.leftMap(_ =>
s"Invalid '$fieldName' field format, should be a boolean"
)
.toValidatedNel
.map(Some(_))
case None => Validated.valid(None)
}

private def parseFeed(feedJson: Json): ValidatedNel[String, Feed] =
feedJson.asObject.map(_.toMap) match {
case None => Validated.invalidNel("Feed entry is not a valid JSON object")
Expand All @@ -177,7 +193,7 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
parseOptionalString(feedFields, "inlineInput")
val filterValidated = parseOptionalQueryAST(feedFields, "filter")
val formatInValidated
: Validated[NonEmptyList[String], Option[FormatIn]] =
: Validated[NonEmptyList[String], Option[FormatIn]] =
parseOptionalFormatIn(feedFields, "formatIn")
val fieldNamesValidated =
parseOptionalFieldNames(feedFields, "fieldNames")
Expand All @@ -190,6 +206,9 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
feedFields,
"excludeFields"
)
val showEmptyFieldsValidated =
parseOptionalBoolean(feedFields, "showEmptyFields")

(
nameValidated,
commandsValidated,
Expand All @@ -199,7 +218,8 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
fieldNamesValidated,
rawIncludeValidated,
rawExcludeValidated,
excludeFieldsValidated
excludeFieldsValidated,
showEmptyFieldsValidated
)
.mapN(Feed.apply)
}
Expand All @@ -223,7 +243,10 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
parseOptionalFeeds(fields, "feeds")
val fieldNamesValidated =
parseOptionalFieldNames(fields, "fieldNames")
(fieldNamesValidated, feedsValidated).mapN(ConfigYaml.apply)
val showEmptyFieldsValidated =
parseOptionalBoolean(fields, "showEmptyFields")

(fieldNamesValidated, feedsValidated, showEmptyFieldsValidated).mapN(ConfigYaml.apply)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ final case class ResolvedConfig(
timestampBefore: Option[ZonedDateTime],

// Other settings
grep: List[ConfigGrep]
grep: List[ConfigGrep],
showEmptyFields: Boolean
)

/** Resolves configuration by merging global and feed-specific settings into a
Expand Down Expand Up @@ -75,7 +76,9 @@ object ConfigResolver {
// For each feed, merge its field names with global field names
val feedFieldNames =
mergeFieldNames(globalFieldNames, feed.fieldNames)

val feedShowEmptyFields = feed.showEmptyFields
.orElse(yaml.showEmptyFields)
.getOrElse(config.showEmptyFields)
ResolvedConfig(
feedName = feed.name,
commands = feed.commands,
Expand All @@ -89,7 +92,8 @@ object ConfigResolver {
excludeFields = feed.excludeFields,
timestampAfter = config.timestamp.after,
timestampBefore = config.timestamp.before,
grep = config.grep
grep = config.grep,
showEmptyFields = feedShowEmptyFields
)
}
case _ =>
Expand All @@ -108,7 +112,8 @@ object ConfigResolver {
excludeFields = None,
timestampAfter = config.timestamp.after,
timestampBefore = config.timestamp.before,
grep = config.grep
grep = config.grep,
showEmptyFields = config.showEmptyFields
)
)
}
Expand All @@ -128,7 +133,8 @@ object ConfigResolver {
excludeFields = None,
timestampAfter = config.timestamp.after,
timestampBefore = config.timestamp.before,
grep = config.grep
grep = config.grep,
showEmptyFields = config.showEmptyFields
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ final case class Config(
grep: List[ConfigGrep],
filter: Option[QueryAST],
formatIn: Option[Config.FormatIn],
formatOut: Option[Config.FormatOut]
formatOut: Option[Config.FormatOut],
showEmptyFields: Boolean
)

object Config:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ object DeclineOpts {
.map(ConfigFile.apply)
.orNone

val showEmptyFields: Opts[Boolean] = Opts
.flag("show-empty-fields", help = "Show fields with empty values in output")
.orFalse

val config: Opts[Config] =
(
configFile,
Expand All @@ -122,25 +126,28 @@ object DeclineOpts {
grepConfig,
filterConfig,
formatIn,
formatOut
formatOut,
showEmptyFields
).mapN {
case (
configFile,
fieldNamesConfig,
timestampConfig,
grepConfig,
filterConfig,
formatIn,
formatOut
) =>
configFile,
fieldNamesConfig,
timestampConfig,
grepConfig,
filterConfig,
formatIn,
formatOut,
showEmptyFields
) =>
Config(
configFile = configFile,
fieldNames = fieldNamesConfig,
timestamp = timestampConfig,
grep = grepConfig,
filter = filterConfig,
formatIn = formatIn,
formatOut = formatOut
formatOut = formatOut,
showEmptyFields = showEmptyFields
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package ru.d10xa.jsonlogviewer.decline.yaml

case class ConfigYaml(
fieldNames: Option[FieldNames],
feeds: Option[List[Feed]]
feeds: Option[List[Feed]],
showEmptyFields: Option[Boolean]
)

object ConfigYaml:
val empty: ConfigYaml = ConfigYaml(None, None)
val empty: ConfigYaml = ConfigYaml(None, None, None)
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ case class Feed(
fieldNames: Option[FieldNames],
rawInclude: Option[List[String]],
rawExclude: Option[List[String]],
excludeFields: Option[List[String]]
excludeFields: Option[List[String]],
showEmptyFields: Option[Boolean]
)
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,13 @@ class ColorLineFormatter(
otherAttributes: Map[String, String],
needNewLine: Boolean
): Seq[Str] =
val filteredAttributes = otherAttributes.filterNot { case (key, _) =>
shouldExcludeField(key)
}
val filteredAttributes = otherAttributes
.filterNot { case (key, _) =>
shouldExcludeField(key)
}
.filterNot { case (_, value) =>
!config.showEmptyFields && isEmptyValue(value)
}

filteredAttributes match
case m if m.isEmpty => Nil
Expand All @@ -110,6 +114,9 @@ class ColorLineFormatter(
)
(if (needNewLine) strNewLine else strEmpty) :: s :: Nil

private def isEmptyValue(value: String): Boolean =
value.trim.isEmpty || value == "null" || value == "\"\"" || value == "{}" || value == "[]"

def strPrefix(s: Option[String]): Seq[Str] =
if (shouldExcludeField("prefix")) Nil
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
grep = List.empty,
filter = None,
formatIn = Some(Config.FormatIn.Json),
formatOut = Some(Config.FormatOut.Raw)
formatOut = Some(Config.FormatOut.Raw),
showEmptyFields = false
)

test("config filters should update during live reload") {
// Create config with INFO filter
val infoFilter = QueryCompiler("level = 'INFO'").toOption
val initialConfig = ConfigYaml(
showEmptyFields = None,
fieldNames = None,
feeds = Some(
List(
Expand All @@ -59,7 +61,8 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
fieldNames = None,
rawInclude = None,
rawExclude = None,
excludeFields = None
excludeFields = None,
showEmptyFields = None
)
)
)
Expand Down Expand Up @@ -155,6 +158,7 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
test("field mappings should update during live reload") {
// Initial configuration with standard field names
val initialConfig = ConfigYaml(
showEmptyFields = None,
fieldNames = None,
feeds = Some(
List(
Expand All @@ -167,7 +171,8 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
fieldNames = None,
rawInclude = None,
rawExclude = None,
excludeFields = None
excludeFields = None,
showEmptyFields = None
)
)
)
Expand All @@ -180,6 +185,7 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {

// Updated config with custom field names mapping
val updatedConfig = ConfigYaml(
showEmptyFields = None,
fieldNames = Some(
FieldNames(
timestamp = Some("ts"),
Expand Down
Loading
Loading