Splunk → SonQL
A translation guide for Splunk users. Every SonQL command name below was
checked against the shipped ANTLR grammars — Splv1Lexer.g4 / Splv1Parser.g4
(the search1 engine) and SplLexer.g4 / SplParser.g4 (the search2 engine) —
not carried over from a document. Where a command’s behavior is noted, it was
measured against a live instance in Searching logs with SonQL.
The short version. Most of the language is the same words. The traps worth knowing on day one:
head→limit,table→fields,sorttakes abyclause and sorts descending by default (Splunk ascends),ANDbinds tighter thanOR(Splunk’sa b OR cisa AND (b OR c); SonQL’s is(a AND b) OR c— parenthesise everyOR), and SonQL has two engines that disagree on a handful of commands.
Drop-in — same word, same job
Section titled “Drop-in — same word, same job”| Splunk | SonQL | note |
|---|---|---|
stats count by service | stats count() as count by service | count(), avg(), sum(), min(), max(), dc() work; an unknown function errors (no match function) rather than returning an empty column. Alias the count. Splunk names the column count; SonQL names it count(), so a migrated sort — | sort by count desc, which is what sort -count becomes — sorts on a column that does not exist and returns no rows, with no error on search2, the default engine (measured 26 and 30 Sep 2026). Splunk’s sort -count itself does not parse on either engine, so it fails loudly |
stats count min(_time) as first max(_time) as last BY host | stats count() as count, min(_time) as first, max(_time) as last by host | several aggregations are comma-separated, and as and by are lowercase — on both engines, Splunk’s space-separated list and its uppercase AS / BY each fail to parse (“unexpected symbol [‘min’]”). The same holds for eventstats, chart, timechart and streamstats. Measured 28 Oct 2026 |
eval duration = end - start | eval duration = end - start | |
where status = 500 | where status = 500 | |
rex field=_raw "…" | rex field=_raw "…" | |
fields host source | fields host, source | comma-separated on search2, the default engine: fields host source fails to parse there (“unexpected symbol … expected one of [EOF]”); search1 accepts either. Also does what Splunk’s table does (below). Measured 26 Sep 2026 |
fields - _raw / fields + host source | fields - _raw / fields + host, source | search2 only, list comma-separated: - drops the fields, + keeps only them. search1 refuses both forms. Measured 28 Oct 2026 |
fillnull value=NULL a b | fillnull value="NULL" a, b | search2 only. A text value is quoted and the fields are comma-separated; the value defaults to 0, and with no fields every field is filled. The engine is typed where Splunk is not: a number fills a number column as a number, and text filling a number column turns that column to text. A listed field that does not exist is created holding the value, as in Splunk. Measured 28 Oct 2026 |
rename src as dst | rename src as dst | |
dedup host, source | dedup host, source | |
bin _time span=1d | bin _time span="1d" | spelled bin, same as Splunk, but quote the span — unquoted, 1d is not a value the parser accepts and the query fails to parse |
timechart count() span=1d | timechart span="1d" count() | two differences, not one. The span goes before the aggregate, and it must be quoted. Splunk’s own order fails to parse |
chart avg(x) by host | chart avg(x) by host | same on both since 14 Aug 2026 |
eventstats avg(x) by host | eventstats avg(x) by host | |
transaction host maxspan=1m | transaction host maxspan="1m" | works on both engines (15 Aug 2026), but the duration is spelled differently: quoted on search2 (unquoted, 1m fails to parse there) and unquoted on search1 (quoted fails there) — measured 26 Sep 2026. maxevents=5 is bare on both. search2 carries the joined member fields as multivalue columns — the added fields are _event_count/_duration on search2, eventcount/duration_second on search1 |
lookup t a OUTPUT b | lookup "t.csv" a output b | names the lookup file, extension and all, quoted ("t" answers “lookup file t does not exist”), and output is lowercase — lookup t a OUTPUT b fails to parse, and there is no outputnew. Runs on both engines since September 2026; where the table has several rows for one key, the default engine takes the first and search1 returns them all as a multivalue field (measured 27 Sep 2026: 81 of 1,222 events enriched on both). See the SonQL reference. |
inputlookup t.csv / outputlookup t.csv | inputlookup "t.csv" | the file name is quoted; unquoted it fails to parse on search2. outputlookup replaces a table’s contents but does not create one — upload the CSV first (Configure → Lookup Tables); against a missing file it answers “lookup file … does not exist” (measured 26 Sep 2026) |
makeresults | makeresults | |
makemv delim="," field | makemv delim="," field | search1 only. On search2 — the default engine — it is refused: “makemv is not supported on search2; use search1, or split the field with eval on search2”. Until 2 Sep 2026 it parsed and was silently dropped; a wrong answer delivered quietly is worse than a refusal. Re-verified 7 Sep 2026 |
replace "a" with "b" in f | replace "a" with "b" in f | |
convert ctime(x) | convert ctime(x) | |
iplocation ip | iplocation ip | needs IP geolocation set up once, under Settings > Advanced Settings > IP Geolocation (a free MaxMind account, or an uploaded file) — Sondar ships no GeoIP data. Until then the fields are empty and the search says where to set it up. lang is quoted (lang="de"). See the SonQL reference |
accum x | accum x | search1 only. On search2 — the default engine — it is refused: “accum is not supported on search2; use search1, or compute the running total with streamstats on search1”. Until 2 Sep 2026 it parsed and silently kept the per-row value. Re-verified 7 Sep 2026 |
addtotals | addtotals | |
streamstats count() by host | streamstats count() by host | search1 only, and by is required there (without it the engine answers “expected one of ‘by’”). On search2 — the default engine — it is refused: “streamstats is not supported on search2; use search1, or aggregate with stats on search2”. Until 2 Sep 2026 the clause was silently dropped. Re-verified 7 Sep 2026 |
Renamed, or different syntax
Section titled “Renamed, or different syntax”| Splunk | SonQL | note |
|---|---|---|
head 20 | limit 20 | head is not a command — it errors (unknown command [head] … did you mean 'limit'?) |
table a b | fields a, b | there is no table; separate the fields with commas (search2) |
sort -errors | sort by errors desc | sort takes a by; defaults to descending, Splunk ascends. A size goes first: sort 10 by errors desc |
tail 20 | sort by _time desc | limit 20 | no tail; this returns newest-first |
spath input=x path=y | jsonpath input="x" path="y" | option values quoted on search2 — input=_raw fails to parse there; search1 takes either. Splunk’s spath reads _raw when no input is named; name it (measured 26 Sep 2026) |
xpath … | xmlpath input="…" path="…" | the same quoting as jsonpath |
join type=inner host [subsearch] | join type="inner" host [search …] | the subsearch is a bracketed query. On search2 (the default) the option value is quoted, type="inner" or type="left"; unquoted, type=inner fails to parse there, though search1 takes it. There is no on= keyword on either engine — the join field simply follows the options. Measured 26 Sep 2026; an earlier version of this row said on=host, which parses on neither |
append [subsearch] | append [search …] | |
search index=… | search index=… or search2 index=… | two engines (below); index= is aliased to repo= in a search predicate |
No direct command — do this instead
Section titled “No direct command — do this instead”Checked against both grammars; none of these Splunk commands exist in SonQL.
| Splunk | SonQL equivalent |
|---|---|
appendcols | not supported |
union | not supported |
map | not supported |
gentimes | makeresults (generates a row, not a time series) |
xyseries | not supported |
untable | not supported |
delta | streamstats + eval on the previous value |
trendline | not supported |
predict | ml forecast (search2) |
search2-only commands (no search1 equivalent)
Section titled “search2-only commands (no search1 equivalent)”mstats, msearch, logcluster, rate, fit, ml forecast,
ml anomalies, ml outliers, unnest, topseries, show tag/metric names,
explain, fillnull.
fit runs the built-in log-analysis algorithms — LogReduce for log-pattern
clustering, ReservoirSampling, TokenAnalyzer. There is no apply: the
algorithms fit on the data in front of them and save no models.
The traps that bite Splunk users
Section titled “The traps that bite Splunk users”The full detail, with the measurements, is in Searching logs with SonQL. The six worth internalizing before you migrate anything:
-
A
.does not break a token by default.NullPointerExceptiondoes not match an event containingjava.lang.NullPointerException. Add.to the index’s word-breaker list (affects new data), or search*NullPointerException. -
sortis descending by default, the reverse of Splunk, and it is silent — a migrated query returns the reverse order, not an error. -
top/rareneed a count on search2.top 5 service, nottop service. Withby, the limit is per group:top 3 service by hostis each host’s three commonest services. The percent is of all events withoutby, and of the group’s events that have the field with it — the same on both engines. (Before the September 2026 builds search2 refused the grouped form.)mvexpandworks on both engines, and both spellings ofmvexpand limit=<n>are accepted — Splunk’smvexpand <field> limit=<n>and the options-firstmvexpand limit=<n> <field>. -
mvcombineneeds a| fieldsreduction on search2. The bare form returns no rows against a repo holding several sourcetypes (measured 12 Sep 2026); reduce first — which is Splunk’s own advice — or usestats values(<field>) by <group>, which agrees on both with no caveat. Pin the engine in anything you save, because the global Default Search Engine setting decides it otherwise. -
ORbinds more loosely than it does in Splunk. Splunk evaluatesORbeforeAND, soa b OR cmeansa AND (b OR c). SonQL evaluatesANDfirst, on both engines, so the same text means(a AND b) OR c— silently, with no error. Parenthesise:a (b OR c). Measured 27 Sep 2026, on both engines, over one day of_internal:index="_internal" sourcetype="json" GET OR POSTreturns 1,711 events, every one of them_audit;index="_internal" sourcetype="json" (GET OR POST)— Splunk’s meaning of the first — returns 0. (Audit events have their own index,_audit, since the audit log shipped; the trap is the same.)The first silently drops the
sourcetypefilter from thePOSThalf. The converter writes Splunk’s grouping out as parentheses for you; a search you port by hand needs them added. -
A value with a hyphen, slash or colon needs no quotes after
field=.service=checkout-api,host=web-01,sourcetype=aws:cloudtrailandsourcetype=aws:*read as one value on both engines, as in Splunk, and so do the items offield IN (...). Keep the pieces together:host=web - 01is refused with a message saying why. A bare full-text term (nofield=) is still split at a hyphen, so quote it:"web-01". Measured on r1226, 30 Sep 2026. Before that image the unquoted forms failed to parse, or —host=web-01— quietly searched forhost=weband-1; the converter quotes these values, which works on every version.