duperq
duperq is a fast filter and processor of Duper files and logs, which also works with JSON.
Installation
duperq is currently only available as a Crates.IO binary. To compile from source:
cargo install --locked duperqBasic usage
As an example, we'll assume the following data from a log format:
{
traceId: UUID("a2ce2f29-84cf-47c9-a877-381855d59e77"),
spanId: UUID("78ee3c78-c090-43f2-8568-f4542dc10ea5"),
timestamp: "2025-11-29T22:21:45.133Z",
level: "INFO",
service: "store-webapp",
development: false,
http: {
method: "GET",
url: "/shopping-cart",
statusCode: 200,
address: ("192.168.1.100", 14567),
userAgent: "Mozilla/5.0",
duration: Duration('PT0.14567S'),
history: ["/", "/search?q=headphones+", "/products/42"],
},
}You can read files by passing them after the duperq filter:
duperq "filter ." path/to/**/*.duperYou can also read lines of Duper values from stdin.
tail -f path/to/app.log | duperq "filter ."Filtering
To filter results, use the filter param in the query. You can bypass filtering by passing an empty query to duperq.
duperq "" log.duperTo access fields in objects or arrays of objects, use .fieldName. For complex keys, you can use quotes and Duper escaping, i.e. ."special key".
duperq "filter .level == \"INFO\"" log.duper
# ... equivalent to ...
duperq "filter .\"level\" == \"INFO\"" log.duperYou can concatenate fields to access nested objects:
duperq "filter .http.method == \"GET\"" log.duperYou can also combine filters with and/&& or or/||. To check if a field simply exists, use exists(...).
duperq "filter (.http.url = \"/admin\" || .level = \"INFO\") && exists(.spanId)" log.duperYou can use comparison operators (== or = for equality; != or <> for inequality; <, <=, >, >=) as you'd expect. For sized values (objects, arrays, tuples, strings and bytes), you can use the len(...) function.
duperq "filter .http.statusCode >= 400" log.duper
duperq "filter len(.http.history) > 2" log.duper
duperq "filter .http.duration < Duration('PT1S')" log.duperYou can also match strings/bytes with Rust regexes via =~ "regex", or access the current element with a sole .. To filter elements in an array, add a [selector operator value] to the fields. For example, we can filter history values that contain "headphones" by putting all of these together:
duperq "filter .http.history[. =~ \"headphones\"]" log.duperTo check if a value is truthy, simply use the selector without an operator. You can also negate the result of a filter with !:
duperq "filter !.development" log.duperTo validate that a value is of a certain type, use the is operator. The valid right-handside operands are:
ObjectArrayTupleStringBytesInstantZonedDateTimePlainDatePlainTimePlainDateTimePlainYearMonthPlainMonthDayDurationTemporalIntegerFloatNumberBooleanNull
duperq "filter .http.address is Tuple" log.duperYou can index into an array/tuple with [index]. Negative indexes also work, but they do not wrap around. To filter over an identifier, use identifier(...).
duperq "filter identifier(.http.address[0]) == \"IPv4Address\"" log.duper
# We can use a regex instead
duperq "filter identifier(.http.address[0]) =~ \"(?i)^ipv\\\\daddress\$\"" log.duper
# To check if there is NO identifier
duperq "filter identifier(.http.userAgent) == null" log.duper
# To check if there is ANY identifier
duperq "filter identifier(.traceId) <> null" log.duperYou can use ranges over array values.
duperq "filter .http.history[..2] == \"/\"" log.duperLast but not least, you can cast values into different types with cast(..., type), with the same possible types from the is operator. This can be useful when dealing with JSON data, where there are no tuples or Temporal values, or to treat a tuple as an array. In our example, we can transform the string-only timestamp into a filterable value:
duperq "filter cast(.timestamp, Instant) > Instant('2025-11-01T00:00:00-03:00')" log.duperManipulation
Other than filtering data, you may also skip the first values with skip X, or limit the number of filtered values you take with take X. These operations can be combined with pipes |:
duperq "filter .development | skip 3 | filter .level = \"ERROR\" | take 10" path/to/**/*.duperOutput
By default, duperq serializes output data into a single-line format. You can change this by piping the output of your query to:
| ansi: Prints with ANSI colors.| pretty-print: Pretty-prints values over multiple lines with indentation.| format: Allows you to print arbitrary strings, replacing${...}blocks with the selector inside. String values will have their quotes stripped. Missing values will be printed as<MISSING>.
duperq "filter . | format \"[\${.level}] \${.http.statusCode} - \${.http.method} \${.http.url}\"" log.duperFormats must always be the last block in your query workflow.