Skip to content

Commit 1b89a2f

Browse files
authored
Show empty fields (#31)
* showEmptyFields
1 parent 7da8c32 commit 1b89a2f

File tree

18 files changed

+362
-92
lines changed

18 files changed

+362
-92
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ feeds:
216216
### Example Configuration File
217217
218218
```yaml
219+
showEmptyFields: true
219220
feeds:
220221
- name: "application-1-logs"
221222
commands:
@@ -229,6 +230,7 @@ feeds:
229230
- "DEBUG"
230231
excludeFields:
231232
- "thread_name"
233+
showEmptyFields: false
232234
- name: "application-2-logs"
233235
commands:
234236
- cat log2.txt
@@ -288,6 +290,12 @@ json-log-viewer --config-file json-log-viewer.yml
288290
json-log-viewer --timestamp-field time
289291
```
290292

293+
- **--show-empty-fields**: Display fields with empty values (null, empty strings, etc.) in output.
294+
```bash
295+
cat log.txt | json-log-viewer --show-empty-fields
296+
```
297+
298+
291299
#### Field Name Options
292300

293301
You can override the default field names to work with non-standard log formats:

frontend-laminar/src/main/scala/ru/d10xa/jsonlogviewer/ViewElement.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ object ViewElement {
2626

2727
def makeConfigYamlForInlineInput(string: String, config: Config): ConfigYaml =
2828
ConfigYaml(
29+
showEmptyFields = None,
2930
fieldNames = None,
3031
feeds = Some(
3132
List(
@@ -38,7 +39,8 @@ object ViewElement {
3839
rawInclude = None,
3940
rawExclude = None,
4041
excludeFields = None,
41-
fieldNames = None
42+
fieldNames = None,
43+
showEmptyFields = None
4244
)
4345
)
4446
)

json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala

+26-3
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,22 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
164164
case None => Validated.valid(None)
165165
}
166166

167+
private def parseOptionalBoolean(
168+
fields: Map[String, Json],
169+
fieldName: String
170+
): ValidatedNel[String, Option[Boolean]] =
171+
fields.get(fieldName) match {
172+
case Some(jsonValue) =>
173+
jsonValue
174+
.as[Boolean]
175+
.leftMap(_ =>
176+
s"Invalid '$fieldName' field format, should be a boolean"
177+
)
178+
.toValidatedNel
179+
.map(Some(_))
180+
case None => Validated.valid(None)
181+
}
182+
167183
private def parseFeed(feedJson: Json): ValidatedNel[String, Feed] =
168184
feedJson.asObject.map(_.toMap) match {
169185
case None => Validated.invalidNel("Feed entry is not a valid JSON object")
@@ -177,7 +193,7 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
177193
parseOptionalString(feedFields, "inlineInput")
178194
val filterValidated = parseOptionalQueryAST(feedFields, "filter")
179195
val formatInValidated
180-
: Validated[NonEmptyList[String], Option[FormatIn]] =
196+
: Validated[NonEmptyList[String], Option[FormatIn]] =
181197
parseOptionalFormatIn(feedFields, "formatIn")
182198
val fieldNamesValidated =
183199
parseOptionalFieldNames(feedFields, "fieldNames")
@@ -190,6 +206,9 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
190206
feedFields,
191207
"excludeFields"
192208
)
209+
val showEmptyFieldsValidated =
210+
parseOptionalBoolean(feedFields, "showEmptyFields")
211+
193212
(
194213
nameValidated,
195214
commandsValidated,
@@ -199,7 +218,8 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
199218
fieldNamesValidated,
200219
rawIncludeValidated,
201220
rawExcludeValidated,
202-
excludeFieldsValidated
221+
excludeFieldsValidated,
222+
showEmptyFieldsValidated
203223
)
204224
.mapN(Feed.apply)
205225
}
@@ -223,7 +243,10 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
223243
parseOptionalFeeds(fields, "feeds")
224244
val fieldNamesValidated =
225245
parseOptionalFieldNames(fields, "fieldNames")
226-
(fieldNamesValidated, feedsValidated).mapN(ConfigYaml.apply)
246+
val showEmptyFieldsValidated =
247+
parseOptionalBoolean(fields, "showEmptyFields")
248+
249+
(fieldNamesValidated, feedsValidated, showEmptyFieldsValidated).mapN(ConfigYaml.apply)
227250
}
228251
}
229252
}

json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/config/ResolvedConfig.scala

+11-5
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ final case class ResolvedConfig(
3737
timestampBefore: Option[ZonedDateTime],
3838

3939
// Other settings
40-
grep: List[ConfigGrep]
40+
grep: List[ConfigGrep],
41+
showEmptyFields: Boolean
4142
)
4243

4344
/** Resolves configuration by merging global and feed-specific settings into a
@@ -75,7 +76,9 @@ object ConfigResolver {
7576
// For each feed, merge its field names with global field names
7677
val feedFieldNames =
7778
mergeFieldNames(globalFieldNames, feed.fieldNames)
78-
79+
val feedShowEmptyFields = feed.showEmptyFields
80+
.orElse(yaml.showEmptyFields)
81+
.getOrElse(config.showEmptyFields)
7982
ResolvedConfig(
8083
feedName = feed.name,
8184
commands = feed.commands,
@@ -89,7 +92,8 @@ object ConfigResolver {
8992
excludeFields = feed.excludeFields,
9093
timestampAfter = config.timestamp.after,
9194
timestampBefore = config.timestamp.before,
92-
grep = config.grep
95+
grep = config.grep,
96+
showEmptyFields = feedShowEmptyFields
9397
)
9498
}
9599
case _ =>
@@ -108,7 +112,8 @@ object ConfigResolver {
108112
excludeFields = None,
109113
timestampAfter = config.timestamp.after,
110114
timestampBefore = config.timestamp.before,
111-
grep = config.grep
115+
grep = config.grep,
116+
showEmptyFields = config.showEmptyFields
112117
)
113118
)
114119
}
@@ -128,7 +133,8 @@ object ConfigResolver {
128133
excludeFields = None,
129134
timestampAfter = config.timestamp.after,
130135
timestampBefore = config.timestamp.before,
131-
grep = config.grep
136+
grep = config.grep,
137+
showEmptyFields = config.showEmptyFields
132138
)
133139
)
134140
}

json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/decline/Config.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ final case class Config(
1212
grep: List[ConfigGrep],
1313
filter: Option[QueryAST],
1414
formatIn: Option[Config.FormatIn],
15-
formatOut: Option[Config.FormatOut]
15+
formatOut: Option[Config.FormatOut],
16+
showEmptyFields: Boolean
1617
)
1718

1819
object Config:

json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/decline/DeclineOpts.scala

+17-10
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ object DeclineOpts {
114114
.map(ConfigFile.apply)
115115
.orNone
116116

117+
val showEmptyFields: Opts[Boolean] = Opts
118+
.flag("show-empty-fields", help = "Show fields with empty values in output")
119+
.orFalse
120+
117121
val config: Opts[Config] =
118122
(
119123
configFile,
@@ -122,25 +126,28 @@ object DeclineOpts {
122126
grepConfig,
123127
filterConfig,
124128
formatIn,
125-
formatOut
129+
formatOut,
130+
showEmptyFields
126131
).mapN {
127132
case (
128-
configFile,
129-
fieldNamesConfig,
130-
timestampConfig,
131-
grepConfig,
132-
filterConfig,
133-
formatIn,
134-
formatOut
135-
) =>
133+
configFile,
134+
fieldNamesConfig,
135+
timestampConfig,
136+
grepConfig,
137+
filterConfig,
138+
formatIn,
139+
formatOut,
140+
showEmptyFields
141+
) =>
136142
Config(
137143
configFile = configFile,
138144
fieldNames = fieldNamesConfig,
139145
timestamp = timestampConfig,
140146
grep = grepConfig,
141147
filter = filterConfig,
142148
formatIn = formatIn,
143-
formatOut = formatOut
149+
formatOut = formatOut,
150+
showEmptyFields = showEmptyFields
144151
)
145152
}
146153

json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYaml.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package ru.d10xa.jsonlogviewer.decline.yaml
22

33
case class ConfigYaml(
44
fieldNames: Option[FieldNames],
5-
feeds: Option[List[Feed]]
5+
feeds: Option[List[Feed]],
6+
showEmptyFields: Option[Boolean]
67
)
78

89
object ConfigYaml:
9-
val empty: ConfigYaml = ConfigYaml(None, None)
10+
val empty: ConfigYaml = ConfigYaml(None, None, None)

json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/Feed.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ case class Feed(
1212
fieldNames: Option[FieldNames],
1313
rawInclude: Option[List[String]],
1414
rawExclude: Option[List[String]],
15-
excludeFields: Option[List[String]]
15+
excludeFields: Option[List[String]],
16+
showEmptyFields: Option[Boolean]
1617
)

json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/formatout/ColorLineFormatter.scala

+10-3
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,13 @@ class ColorLineFormatter(
8989
otherAttributes: Map[String, String],
9090
needNewLine: Boolean
9191
): Seq[Str] =
92-
val filteredAttributes = otherAttributes.filterNot { case (key, _) =>
93-
shouldExcludeField(key)
94-
}
92+
val filteredAttributes = otherAttributes
93+
.filterNot { case (key, _) =>
94+
shouldExcludeField(key)
95+
}
96+
.filterNot { case (_, value) =>
97+
!config.showEmptyFields && isEmptyValue(value)
98+
}
9599

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

117+
private def isEmptyValue(value: String): Boolean =
118+
value.trim.isEmpty || value == "null" || value == "\"\"" || value == "{}" || value == "[]"
119+
113120
def strPrefix(s: Option[String]): Seq[Str] =
114121
if (shouldExcludeField("prefix")) Nil
115122
else

json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/LogViewerStreamIntegrationTest.scala

+9-3
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
4040
grep = List.empty,
4141
filter = None,
4242
formatIn = Some(Config.FormatIn.Json),
43-
formatOut = Some(Config.FormatOut.Raw)
43+
formatOut = Some(Config.FormatOut.Raw),
44+
showEmptyFields = false
4445
)
4546

4647
test("config filters should update during live reload") {
4748
// Create config with INFO filter
4849
val infoFilter = QueryCompiler("level = 'INFO'").toOption
4950
val initialConfig = ConfigYaml(
51+
showEmptyFields = None,
5052
fieldNames = None,
5153
feeds = Some(
5254
List(
@@ -59,7 +61,8 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
5961
fieldNames = None,
6062
rawInclude = None,
6163
rawExclude = None,
62-
excludeFields = None
64+
excludeFields = None,
65+
showEmptyFields = None
6366
)
6467
)
6568
)
@@ -155,6 +158,7 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
155158
test("field mappings should update during live reload") {
156159
// Initial configuration with standard field names
157160
val initialConfig = ConfigYaml(
161+
showEmptyFields = None,
158162
fieldNames = None,
159163
feeds = Some(
160164
List(
@@ -167,7 +171,8 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
167171
fieldNames = None,
168172
rawInclude = None,
169173
rawExclude = None,
170-
excludeFields = None
174+
excludeFields = None,
175+
showEmptyFields = None
171176
)
172177
)
173178
)
@@ -180,6 +185,7 @@ class LogViewerStreamIntegrationTest extends CatsEffectSuite {
180185

181186
// Updated config with custom field names mapping
182187
val updatedConfig = ConfigYaml(
188+
showEmptyFields = None,
183189
fieldNames = Some(
184190
FieldNames(
185191
timestamp = Some("ts"),

0 commit comments

Comments
 (0)