Usage
1) Overview
Endpoints
GET /lce/api/collectorsPOST /lce/api/collector_logs/list(queues a listing and returns a receipt, or lists locally and returns the listing)GET /lce/api/collector_logs/list/{request_id}(reads a queued listing back)POST /lce/api/collector_logs/fetch(queues a read and returns a receipt, or reads locally and returns the content)GET /lce/api/collector_logs/fetch/{request_id}(reads queued content back)
The log tools are served by two generic routes, POST /lce/api/collector_logs/{tool_name} and
GET /lce/api/collector_logs/{tool_name}/{request_id}, where tool_name is list or
fetch. These are the same URLs as above, so existing callers are unaffected. Any other
tool_name returns 404.
The Collector Logs API lists and reads Data Collector log files remotely, without shell access to the collector appliance. It supports two things:
Discovering which log files exist on a collector, including rotated generations
Reading one of those files, optionally filtered by pattern, timestamp window, line count and size
Nothing is installed on the collector to support this. Every command is composed from binaries already present in the appliance image, and all filtering runs there rather than in this service.
Important
Retrieval is two calls, not one. The read is queued for the Remote Toolbox on the
collector and reaches it through a database update, so nothing is available immediately. The
POST endpoints return a request_id receipt with status 202; the matching GET
endpoint is what returns the result, once the collector has produced it.
Measured pickup latency is 12 to 27 seconds, and can be minutes on a busy collector.
With run_local set to true on either POST, or left out on an All-In-One, the tool
instead runs on the host this service runs on and the POST answers with the result
directly. Send "run_local": false to queue it. See 6) Running a tool locally.
3) Collectors
Only appliances that actually collect hold collector logs, so only those are addressable. An All-In-One and a Collector Unit collect; a Database Server does not and is excluded, because a log read queued against one would never run.
GET /lce/api/collectors lists the appliances you can target:
{
"collectors": [
{ "id": 2, "name": "cu-01", "ip": "10.64.23.11" },
{ "id": 3, "name": "cu-02", "ip": "10.64.23.12" }
]
}
Either the id or the name of an entry is accepted wherever a collector is required.
A numeric value is treated as an appliance id and anything else as an appliance name. A
collector that resolves to no collecting appliance returns 404.
Note
Call this endpoint first. It is the only way to learn which names and ids are addressable: a 404 from a dispatch does not suggest alternatives, so there is nothing to guess from if the name is wrong.
4) Log directories
The directory field is a key, never a path. The service maps the key to an absolute
directory server side, so a path traversal cannot be expressed in the first place.
Key |
|
Contents |
|---|---|---|
|
|
The current service logs, written by rsyslog. This is the default on the two |
|
|
The |
Any other value is rejected with 400.
Responses report both halves, so nothing has to be mapped back by the caller:
directory— the key you named, such as"sl1". This round-trips: take the value from a queueing response and send it straight back as thedirectoryof the next request, or as thedirectoryquery parameter when polling a listing, where it is required rather than defaulted.directory_path— the absolute path it resolves to on the collector, such as"/var/log/sl1". This is where the log actually lives, which is what you want when quoting a location in a ticket or correlating with something read on the appliance itself.
Note
silo.log is 0 bytes on current releases, so it is not a useful target. List the directory
first and use size_bytes to find the files that actually carry content; the sl1
directory is where the current service logs are.
Log file names
A log_name must have the shape the platform actually produces: a current log such as
sl1.log, or a rotated generation such as sl1.log.20260913-1789312001.gz. Rotated
generations are accepted for reading, and a compressed generation is read in place rather than
decompressed first.
Each entry in a listing is self describing on this point:
base_name— the log the file belongs to, with any rotation suffix removedrotated— whether this is a rotated generation rather than the current logcompressed— whether it is gzip compressedgeneration— the rotation suffix (<date>-<epoch>), ornullfor the current log
Each entry also carries directory and directory_path, so an entry can be turned into the
next fetch request without consulting anything else: send the entry’s directory and name
as the directory and log_name of a POST /lce/api/collector_logs/fetch.
5) The dispatch-then-poll cycle
Both retrieval flows work the same way.
POSTthe request. You get 202 and arequest_id. No output exists yet.GETthe matching poll endpoint with thatrequest_id.If
terminatedisfalse, call again, passing the previous response’slast_idasafter_id.Stop when
terminatedistrue.
after_id and last_id
Output arrives as rows, in the order the collector emitted them. last_id is the highest row
id in the page you just read; passing it back as after_id on the next call continues where
the last one stopped and avoids re-reading what you already have. Start with after_id
omitted, or 0, to read from the beginning.
Important
An empty page with terminated false means “not yet”, not “nothing there”.
The queued command has to reach the collector through a database update before anything is written at all, so early polls legitimately return no rows. Treat an empty page as a reason to poll again, never as an empty log or an empty directory.
truncated
truncated is true when the page could not carry everything that was waiting. Two limits
produce it, and either one, or both together, sets the flag:
Row exhaustion — more rows were waiting than the page size allowed.
Character exhaustion — the rows already returned reached the per-response character ceiling before the page size was reached, with rows still left behind. A page whose last available row is the one that reached the ceiling has carried everything, so it is not truncated.
Request the next page rather than assuming the output ended — a truncated page is returned even
when terminated is false and even when a wait was requested. last_id is the cursor of
the last row actually returned whichever limit stopped the page, so either kind of truncation
resumes cleanly.
Both budgets are cumulative across a wait: a poll with wait_seconds spends one row budget
and one character budget over the whole wait rather than one of each per internal read. A page
from such a poll therefore holds at most page_size rows as well, and reports truncated
when either budget ran out with output still waiting.
Response size limits
Two ceilings bound a page independently of page_size:
1,048,576 characters per response. Rows stop being accumulated once the text returned reaches 1,048,576 characters, and
truncatedis set if rows were left behind. Continue fromlast_id. The limit counts decoded characters rather than encoded bytes, so a response carrying heavily multibyte text can be larger than 1 MiB on the wire — bounded in magnitude, which is the point, but not exactly a megabyte.4,096 characters per row. A single pathological record — a dumped payload, one enormous frame of a traceback — is cut to 4,096 characters and the marker
... [truncated]is appended to it. The row is still returned and still counts as one row.
The two are different failures. The per-response cap ends the page and is resumable by polling
again from last_id; the per-record marker means that one record’s text is incomplete and no
amount of polling will produce the rest of it, so narrow the read on the collector instead.
wait_seconds
wait_seconds is opt-in server-side waiting. With no wait_seconds, a poll returns
immediately with whatever the collector has written so far. That is the right default: pickup
latency is measured in tens of seconds, so a caller that waits a short time anyway pays the wait
and still gets an unfinished page.
With wait_seconds, the poll repeats internally until the collector reports termination, the
job settles after losing its completion marker, the page fills, or the budget runs out.
Important
The wait budget is capped at 90 seconds. The cap is deliberately below the 120-second read timeout the reverse proxy applies to this service, so that a wait does not turn a request that is merely still running into a gateway timeout the caller cannot distinguish from a failure. The 30 seconds left over cover opening a connection for each read and the reads that follow the wait; only a database too slow to answer within them would exhaust it. A larger value is silently reduced to the cap rather than rejected.
A poll with no wait uses one database connection. A wait holds a worker for its whole duration, but not a connection: each internal read opens its own and closes it before sleeping, so a wait costs the database what the same number of short polls would. Prefer a short wait with client-side polling over a single long wait.
terminated does not mean success
Warning
terminated reports termination only. The collector writes the same completion marker
for:
a command that worked,
a command that ran and failed, and
a payload the Remote Toolbox refused before running it at all.
A terminated page is therefore not a successful one. Judge the outcome from the content —
whether there are stdout rows, and what any stderr rows say — together with the
toolbox fields.
terminated is also true once a job the platform records as finished has been read to the
end, even if its completion marker never arrived. The Remote Toolbox does not retry a write that
fails, so the marker can be lost, and a caller waiting for it alone would poll a finished job
forever. Without the marker, the job must also have had no output for 90 seconds, so a batch
still on its way from the collector is not cut off.
Output streams
The output comes back as text per stream, in output: {"stdout": ..., "stderr": ...}. For
a fetch, stdout is the content of the log file, and stderr anything the collector
reported while reading it.
Each record is one line ending in a newline; a record without one is given one, so the next
page’s first line never runs onto it. The toolbox’s own “Process Started” and “Process Completed”
rows are left out, since toolbox.phase and terminated carry the same news.
line_counts still counts every row of the page, those included. An unrecognised
collector-side stream is kept in output under its own name, msg_type_<n>, rather than
being folded into a known one.
A page ends on a line, so build the whole output by appending each stream’s text from each page to what you already have.
The toolbox object
toolbox reports how far the queued command has got. Both read endpoints return it.
phaseisqueued,running,finishedorunknown. It is derived from the platform’s spool row and the output together, because the Remote Toolbox can lose either signal.unknownmeans neither the spool row nor any output could be read; a job with no spool row is otherwise reported as not found.stateandreturncodeare the spool row’s own fields as found.stateis1scheduled,2launched,3running,0complete,4failed or5canceled, but each update reaches the database separately and they can land out of order, so a finished job can still read2.returncodeis the command’s exit status as the platform stores it,nullfor as long as it is running, and is only ever written when the command ends. The column is unsigned and the platform does not run in strict mode, so a command killed by a signal, whose status is negative, is stored as0too.seconds_since_queuedandseconds_since_last_outputsay how long ago the request was queued and how long since the collector last wrote anything for it.
There is no “stuck” phase. A request the collector never picks up, because it is offline, stays
queued; one the Remote Toolbox abandons stays running. Neither ever terminates, so decide
from the two ages when to stop waiting. Pickup normally takes tens of seconds, and longer on a
busy collector or one far from the database.
6) Running a tool locally
Both POST endpoints accept run_local. false queues the tool for a collector’s Remote
Toolbox as described above. Left out, it is true on an All-In-One, which is its own
collector, and false everywhere else. Whether this host is an All-In-One is read from
model_type in silo.conf, as the platform reads it. A collector named for a local run
is accepted and ignored, since the run always reads this host.
Set to true, the tool runs on the host this service runs on, an All-In-One or a
Database Server, and the POST answers with the result directly: there is no
request_id, no receipt and nothing to poll.
run_local accepts true or false, or 1 or 0. Any other value, including the
strings "true" and "false", is rejected with 400.
A local run executes the same command a queued request would, and runs it the way the Remote
Toolbox does on a collector: split without a shell, line buffered, with each line of standard
output and standard error recorded as a row of its own. Its output is then read by the same rules
as a queued request’s, so the listing, the output text and the toolbox object have the
same meaning in both modes.
Important
A local run reads this host, not a collector. On an All-In-One that is the appliance’s own
collector logs; on a Database Server it is the Database Server’s own logs. collector is
therefore not required for a local run, and is ignored if given. A malformed one, such as
true or "", is still refused with 400.
Every response, local or queued, says which happened in mode: "local" for a local run and
"remote" for a queued request’s receipt and for every poll of it.
A local response differs from the final poll of a queued request in three ways:
The whole output comes back at once. There is no paging, so
last_idis the id of the last row, and the per-response ceiling in Response size limits does not apply. The per-record cap still does: a record longer than 4,096 characters arrives cut, with... [truncated]appended. The caps the collector side of a fetch applies,linesandmax_bytes, are part of the command and apply to a local run unchanged. A local run keeps at most 32 MiB of output, well above whatmax_bytesallows; output past it is discarded andtruncatedis thentrue. Otherwisetruncatedisfalse.The request waits for the run. A local run that has not finished after 90 seconds is stopped, and the
POSTreturns 504 with what the run produced before then. See Timeouts. At most five local runs execute at once, shared with the Snippet Framework tools; one asked for while five are running returns 429 and nothing runs.The
toolboxobject describes a finished run.phaseisfinished,returncodeis the command’s exit status,seconds_since_queuedis how long the run took andseconds_since_last_outputhow long before it ended it last wrote anything.stateis0, or4when the command could not be started or ran past its timeout.
Audit entries for a local run say locally in place of a collector.
Timeouts
A local run that runs past 90 seconds is killed. The response is 504, carrying the usual
error fields on top of the body the run would have returned, with terminated false and
whatever was written before the run was stopped:
{
"success": false,
"error": "Local Run Timed Out",
"message": "The log read did not finish within 90 seconds and was stopped. The output it wrote before then is included",
"mode": "local",
"directory": "sl1",
"directory_path": "/var/log/sl1",
"log_name": "sl1.log",
"max_bytes": 5242880,
"terminated": false,
"truncated": false,
"last_id": 2,
"line_counts": { "started": 1, "stdout": 1 },
"output": {
"stdout": "2026-09-23T18:40:02.118Z cu-01 sl1: AID: 907 collection complete\n",
"stderr": ""
},
"toolbox": {
"phase": "finished",
"state": 4,
"returncode": null,
"seconds_since_queued": 90,
"seconds_since_last_output": 88
}
}
7) Listing log files
POST /lce/api/collector_logs/list
Queues a request to enumerate the log files in one directory on one collector, or, with
run_local, enumerates them on this host and returns the listing.
Request body:
Field |
Required |
Description |
|---|---|---|
|
yes, for a queued request |
The |
|
no |
|
|
no |
|
Returns 202:
{
"mode": "remote",
"request_id": 8842,
"collector": { "id": 2, "name": "cu-01", "ip": "10.64.23.11" },
"directory": "sl1",
"directory_path": "/var/log/sl1",
"note": "The listing is not available yet. Read it from /api/collector_logs/list/<request_id> until terminated is true."
}
With run_local set to true, returns 200 with the listing instead, in the shape of the
final poll below without request_id, or 504 if the run timed out:
{
"mode": "local",
"directory": "sl1",
"directory_path": "/var/log/sl1",
"terminated": true,
"truncated": false,
"last_id": 4,
"files": [
{
"name": "sl1.log",
"base_name": "sl1.log",
"directory": "sl1",
"directory_path": "/var/log/sl1",
"size_bytes": 48211993,
"modified": "2026-09-23T18:41:07.512000+00:00",
"rotated": false,
"compressed": false,
"generation": null
}
],
"toolbox": {
"phase": "finished",
"state": 0,
"returncode": 0,
"seconds_since_queued": 0,
"seconds_since_last_output": 0
}
}
GET /lce/api/collector_logs/list/{request_id}
Reads the listing back.
Query parameters:
Parameter |
Required |
Description |
|---|---|---|
|
yes |
Which log directory the request was queued for, |
|
no |
Read only output produced after this cursor. Defaults to the beginning. |
|
no |
Maximum number of output rows to read. Defaults to |
|
no |
Wait up to this long for the collector to finish. Defaults to no wait; capped at |
Important
directory is required on this endpoint, unlike the two POST endpoints, where it
defaults to sl1. A missing directory is rejected with 400.
It has to match what was queued because the listing does not carry the directory back: the
collector reports basenames only, and the service stamps each entry with the key given here.
Polling an em7 listing without the parameter would return entries labelled sl1 and
/var/log/sl1, and feeding one of those entries straight back to a fetch — the documented
workflow — would then target a file that does not exist, or, for a basename that exists in
both trees, silently a different file. The mismatch cannot be detected server side, which
is why the parameter is demanded rather than guessed.
Response:
{
"mode": "remote",
"request_id": 8842,
"directory": "sl1",
"directory_path": "/var/log/sl1",
"terminated": true,
"truncated": false,
"last_id": 41207,
"toolbox": {
"phase": "finished",
"state": 2,
"returncode": 0,
"seconds_since_queued": 31,
"seconds_since_last_output": 2
},
"files": [
{
"name": "sl1.log",
"base_name": "sl1.log",
"directory": "sl1",
"directory_path": "/var/log/sl1",
"size_bytes": 48211993,
"modified": "2026-09-23T18:41:07.512000+00:00",
"rotated": false,
"compressed": false,
"generation": null
},
{
"name": "sl1.log.20260913-1789312001.gz",
"base_name": "sl1.log",
"directory": "sl1",
"directory_path": "/var/log/sl1",
"size_bytes": 2411882,
"modified": "2026-09-13T15:06:41+00:00",
"rotated": true,
"compressed": true,
"generation": "20260913-1789312001"
}
]
}
modified is an ISO 8601 timestamp in UTC. size_bytes is the file size on the collector,
which is worth reading before fetching: an active collector log reaches tens of megabytes.
An entry that cannot be parsed is skipped rather than failing the whole listing, because returning the files that were readable is more useful than refusing the lot over one malformed record or a page that ended mid record.
8) Fetching log content
POST /lce/api/collector_logs/fetch
Queues a request to read one log file on one collector, or, with run_local, reads it on
this host and returns the content.
Request body:
Field |
Required |
Description |
|---|---|---|
|
yes, for a queued request |
The |
|
no |
|
|
yes |
The log file to read, within the chosen directory. Rotated generations are accepted. |
|
no |
|
|
no |
Return only this many trailing lines. An integer from |
|
no |
An extended regular expression to match records against. See 9) Filtering. |
|
no |
Inclusive lower bound on the record timestamp. Use |
|
no |
Inclusive upper bound on the record timestamp, and genuinely inclusive: a record stamped anywhere inside the day, minute or year the bound names is returned. |
|
no |
Ceiling on the bytes the collector will return for this read. A positive integer,
defaulting to and capped at |
Returns 202:
{
"mode": "remote",
"request_id": 8851,
"collector": { "id": 2, "name": "cu-01", "ip": "10.64.23.11" },
"directory": "sl1",
"directory_path": "/var/log/sl1",
"log_name": "sl1.log",
"max_bytes": 5242880,
"note": "The log content is not available yet. Read it from /api/collector_logs/fetch/<request_id> until terminated is true."
}
With run_local set to true, returns 200 with the content instead, or 504 if the
run timed out. The body joins the receipt’s description of the read to the final poll’s output,
without request_id or collector. note is present only when the read has a limitation
worth stating, such as a time-bearing bound against em7. The combine_streams query
parameter is accepted on the POST for a local run only, true to return the output as one
stream, combined, in the order it was written; the poll of a queued request does not offer it:
{
"mode": "local",
"directory": "sl1",
"directory_path": "/var/log/sl1",
"log_name": "sl1.log",
"max_bytes": 5242880,
"terminated": true,
"truncated": false,
"last_id": 4,
"line_counts": { "started": 1, "stdout": 2, "completed": 1 },
"output": {
"stdout": "2026-09-23T18:40:02.118Z cu-01 sl1: AID: 907 collection complete\n2026-09-23T18:41:07.512Z cu-01 sl1: AID: 907 collection complete\n",
"stderr": ""
},
"toolbox": {
"phase": "finished",
"state": 0,
"returncode": 0,
"seconds_since_queued": 0,
"seconds_since_last_output": 0
}
}
GET /lce/api/collector_logs/fetch/{request_id}
Reads the content back.
Query parameters:
Parameter |
Description |
|---|---|
|
Read only output produced after this cursor. Defaults to the beginning. |
|
Maximum number of output rows to return. Defaults to |
|
Wait up to this long for the collector to finish. Defaults to no wait; capped at |
There is no directory parameter here. The directory was fixed when the request was queued and
is reported on the 202 receipt as directory and directory_path.
Response:
{
"mode": "remote",
"request_id": 8851,
"terminated": true,
"truncated": false,
"last_id": 41395,
"line_counts": { "started": 1, "stdout": 2, "completed": 1 },
"output": {
"stdout": "2026-09-23T18:40:02.118Z cu-01 sl1: AID: 907 collection complete\n2026-09-23T18:41:07.512Z cu-01 sl1: AID: 907 collection complete\n",
"stderr": ""
},
"toolbox": {
"phase": "finished",
"state": 2,
"returncode": 0,
"seconds_since_queued": 27,
"seconds_since_last_output": 1
}
}
Rows are written in batches as the collector produces them, but each row carries one log record:
the collector side accumulates a whole line before writing it, so a page boundary is also a
record boundary, and the text of consecutive pages joins cleanly. A record longer than 4,096
characters arrives cut, with ... [truncated] appended.
Every fetch is capped, whether or not it is filtered: see max_bytes, and the caps that always apply.
9) Filtering
Filtering runs on the collector, not in this service. The output travels back through a shared platform log table in chunked rows, and an active collector log reaches tens of megabytes, so narrowing the result at the source is what keeps a fetch usable. Every filter contributes one stage of a pipeline that the collector runs.
pattern
pattern is an extended regular expression, matched by grep -E (zgrep -E for a
compressed generation).
Warning
A pattern may not contain whitespace, quotes or backslashes. It is limited to 1 to 256
characters drawn from letters, digits and _ . : @ / = , + ^ $ * ? ( ) | { } [ ] -.
Express whitespace with a POSIX character class such as [[:space:]] instead. A literal
space would split the pattern into a second argument on the collector, and a backslash would
be consumed by the transport before the pattern ever reached grep.
Because the pattern is an extended regular expression, the POSIX classes and the usual ERE
metacharacters are all available. To match an application id of 907 in records that write it as
AID: 907, with an unknown amount of space after the colon:
AID:[[:space:]]*907
Other useful shapes, all expressible within the allowed characters:
(ERROR|CRITICAL) alternation
^2026-09-23T18:4 anchored timestamp prefix
Traceback|Exception either word
did=(101|102|103) a small set of device ids
An invalid pattern is rejected at dispatch time with 400 and a message naming what was rejected.
since and until
Records in the rsyslog-written directory begin with an ISO 8601 timestamp, which sorts correctly as text, so a window is applied by comparing that leading field against the bound rather than by parsing dates on the collector. A bound may be any leading portion of a timestamp, so a whole year, a day and a single minute are all expressible:
2026
2026-09
2026-09-23
2026-09-23T14:05
Both bounds are inclusive, and until is inclusive of everything the bound names rather
than of the single instant it names. The bound is compared against the leading portion of each
record’s timestamp, so until=2026-09-24 keeps every record stamped on the 24th, and
since=2026-09-23&until=2026-09-24
is the whole of the 23rd together with the whole of the 24th. A longer bound still cuts exactly
where it says: until=2026-09-24T09:30 stops at the end of that minute.
Warning
Use T between the date and the time. 2026-09-23T14:05 is accepted;
2026-09-23 14:05 is not, and is rejected with 400. A space cannot survive the way the
filter is handed to the collector, so a space-bearing bound is refused at dispatch rather than
accepted and then quietly matching nothing.
An ill-formed bound is rejected with 400, and the message names which of the two was rejected.
Important
In the em7 directory a window is day-granular only. Records in
/var/log/em7 separate the date from the time with a space, so the leading field a bound is
compared against is the date alone. since=2026-09-23 and until=2026-09-24 behave
exactly as they do for sl1, but since=2026-09-23T14:05 matches nothing at all, because
no record’s leading field ever carries a time.
A time-bearing bound is not rejected — a day bound against em7 is legitimate, and the
request does not say which was meant — so the 202 receipt from
POST /lce/api/collector_logs/fetch says so in its note when the directory is em7
and a bound carries a time component. Drop the time component, or narrow within the day with
pattern instead.
lines
lines returns only the trailing n lines, applied after pattern and after the
timestamp window, so lines is the last n matching records rather than the last n records
in the file. Valid values are 1 to 100000.
max_bytes, and the caps that always apply
max_bytes is a ceiling on how many bytes the collector will return for one read. It is a
positive integer, it defaults to 5242880 (5 MiB), and that is also its maximum: a larger
value is reduced to the cap rather than rejected. 0, a negative number, a boolean and a
non-numeric string are rejected with 400. The value that will actually be enforced is echoed
as max_bytes on the 202 receipt.
Important
Every read is capped twice, whatever else it asks for. The collector applies both:
a line cap —
lineswhen supplied, otherwise100000trailing linesa byte cap —
max_byteswhen supplied, otherwise5242880bytes (5 MiB)
Both are appended after pattern and after the timestamp window, so they bound the
result of the filters rather than the file, and neither depends on which filters were asked
for. A plain read, a compressed generation, a pattern-only read and a window-only read are all
bounded by both.
The line cap runs first and the byte cap second, and both keep the trailing end of what they are given, so a read that the byte cap actually cuts returns the most recent records of the last n lines and drops the oldest of that window — which is what a trailing read asked for.
The cut lands at a byte offset rather than on a record boundary, so the first record of
such a read can be a partial line. Expect to discard a leading fragment. The
... [truncated] marker is not involved here: that is the separate per-record limit
applied as the output is read back, not on the collector.
The byte cap is the one that bounds the work, because the line cap cannot: 100,000 records of an unknown width is not a size. These two caps are collector side and bound the whole read; the per-response ceiling in Response size limits is a separate, service-side limit on one page of what the collector has already produced.
The caps exist to stop a read being unbounded, not to make reading a whole log pleasant.
10) Log retention
Important
Collector log output ages out of the platform’s log tables after about 31 days. The row
that describes the request is pruned independently of the rows that carry its output, so
the two do not disappear together: a request_id can outlive its own description.
In practice this means an old request_id is not a durable handle. Once the describing row
is gone, the poll endpoints can no longer establish that the request is a collector log
retrieval belonging to you, and the read is refused with 404 even if output rows remain.
Read a request’s output while you are working with it; re-dispatch rather than returning to a
request_id from days ago.
11) Examples
Both examples use the same Skylar One session as every other endpoint in this service. Replace
$SL1_HOST and $SL1_COOKIE with your host and session cookie.
Listing the log files on a collector
Dispatch the listing:
curl -sS -X POST "https://$SL1_HOST/lce/api/collector_logs/list" \
-H "Content-Type: application/json" \
-b "$SL1_COOKIE" \
-d '{"collector": "cu-01", "directory": "sl1"}'
{
"mode": "remote",
"request_id": 8842,
"collector": { "id": 2, "name": "cu-01", "ip": "10.64.23.11" },
"directory": "sl1",
"directory_path": "/var/log/sl1",
"note": "The listing is not available yet. Read it from /api/collector_logs/list/<request_id> until terminated is true."
}
Poll for it. Pickup takes 12 to 27 seconds in the measured case, so the first poll asks the server to wait up to 30 seconds:
curl -sS -b "$SL1_COOKIE" \
"https://$SL1_HOST/lce/api/collector_logs/list/8842?directory=sl1&wait_seconds=30"
If that returns "terminated": false, poll again from the cursor it gave you:
curl -sS -b "$SL1_COOKIE" \
"https://$SL1_HOST/lce/api/collector_logs/list/8842?directory=sl1&after_id=41180&wait_seconds=30"
Repeat until terminated is true, each time passing the previous response’s last_id
as after_id. Remember that an empty files list with terminated false means the
collector has not produced the listing yet, not that the directory is empty.
Fetching matching lines from a log
Dispatch a read of sl1.log for records mentioning application id 907 on 23 and 24 September,
keeping only the last 200 matches. Note [[:space:]] in place of a literal space, and that
until is inclusive, so 2026-09-24 returns the whole of the 24th rather than stopping at
its first instant:
curl -sS -X POST "https://$SL1_HOST/lce/api/collector_logs/fetch" \
-H "Content-Type: application/json" \
-b "$SL1_COOKIE" \
-d '{
"collector": "cu-01",
"directory": "sl1",
"log_name": "sl1.log",
"pattern": "AID:[[:space:]]*907",
"since": "2026-09-23",
"until": "2026-09-24",
"lines": 200
}'
{
"mode": "remote",
"request_id": 8863,
"collector": { "id": 2, "name": "cu-01", "ip": "10.64.23.11" },
"directory": "sl1",
"directory_path": "/var/log/sl1",
"log_name": "sl1.log",
"max_bytes": 5242880,
"note": "The log content is not available yet. Read it from /api/collector_logs/fetch/<request_id> until terminated is true."
}
Poll for the content:
curl -sS -b "$SL1_COOKIE" \
"https://$SL1_HOST/lce/api/collector_logs/fetch/8863?page_size=2000&wait_seconds=30"
This page came back before the collector had finished, so terminated is false, the return
code is still null, and last_id is the cursor to resume from:
{
"mode": "remote",
"request_id": 8863,
"terminated": false,
"truncated": false,
"last_id": 52212,
"line_counts": { "started": 1, "stdout": 2 },
"output": {
"stdout": "2026-09-23T18:40:02.118Z cu-01 sl1: AID: 907 collection complete\n2026-09-23T18:41:07.512Z cu-01 sl1: AID: 907 collection complete\n",
"stderr": ""
},
"toolbox": {
"phase": "running",
"state": 2,
"returncode": null,
"seconds_since_queued": 22,
"seconds_since_last_output": 1
}
}
Continue from that cursor until termination:
curl -sS -b "$SL1_COOKIE" \
"https://$SL1_HOST/lce/api/collector_logs/fetch/8863?after_id=52212&page_size=2000&wait_seconds=30"
{
"mode": "remote",
"request_id": 8863,
"terminated": true,
"truncated": false,
"last_id": 52213,
"line_counts": { "completed": 1 },
"output": {
"stdout": "",
"stderr": ""
},
"toolbox": {
"phase": "finished",
"state": 2,
"returncode": 0,
"seconds_since_queued": 27,
"seconds_since_last_output": 1
}
}
terminated is now true, so the read is over. It is not a statement that the read
succeeded: check that stdout rows were returned across the pages you read, and look at any
stderr rows and at toolbox before concluding anything about the outcome.
Rotated generations are fetched the same way, by name. A compressed generation is read in place, and both caps still apply — here the byte cap is lowered to 1 MiB rather than left at its 5 MiB default:
curl -sS -X POST "https://$SL1_HOST/lce/api/collector_logs/fetch" \
-H "Content-Type: application/json" \
-b "$SL1_COOKIE" \
-d '{
"collector": "cu-01",
"log_name": "sl1.log.20260913-1789312001.gz",
"pattern": "(ERROR|CRITICAL)",
"lines": 500,
"max_bytes": 1048576
}'
Reading a log on this host
With run_local, the same read runs on the host this service runs on and is answered in one
call. No collector is named, and there is nothing to poll:
curl -sS -X POST "https://$SL1_HOST/lce/api/collector_logs/fetch" \
-H "Content-Type: application/json" \
-b "$SL1_COOKIE" \
-d '{
"run_local": true,
"log_name": "sl1.log",
"pattern": "AID:[[:space:]]*907",
"lines": 200
}'
The response is 200 with "mode": "local", the whole output and a finished toolbox,
as in 6) Running a tool locally.
12) Validation & error responses
Common status codes
200 OK: a local run finished, and the response holds its result.
202 Accepted: the request was queued. The result is not available yet; poll for it.
400 Bad Request: a missing
collectorfor a queued request, a missinglog_name, arun_localthat is nottrue,false,1or0, an invalidcombine_streamson a local fetch, an unknowndirectory, a missingdirectoryonGET /lce/api/collector_logs/list/{request_id}, a rejectedpattern,since,until,linesormax_bytes, or anafter_id,page_sizeorwait_secondsthat is not a finite non-negative number.page_size=0is rejected rather than read as “use the default”, andnan,infand-nanare rejected for all three poll parameters.401 Unauthorized: missing or invalid authentication.
403 Forbidden: access hooks insufficient.
404 Not Found: the
tool_namein the path is neitherlistnorfetch; on aPOST, no collecting appliance matches thecollector; on aGET, therequest_idis not a collector log request belonging to you.429 Too Many Requests: a local run was refused because five are already in progress. Try again once one has finished.
500 Internal Server Error: the request could not be queued or run, or the output could not be read.
504 Gateway Timeout: a local run did not finish within 90 seconds and was stopped. The body holds what it produced before then; see Timeouts.
Error body
{
"success": false,
"error": "Invalid Collector Log Request",
"message": "Invalid log pattern. A pattern must be 1 to 256 characters and may not contain whitespace, quotes or backslashes. Express whitespace with a POSIX class such as [[:space:]]"
}
Every error body has the same three fields. A 404 from a POST suggests no alternatives,
because a collector is looked up by exact id or name and there are no near misses to offer, so
use GET /lce/api/collectors to find the right collector:
{
"success": false,
"error": "Collector Not Found",
"message": "No collecting appliance found for 'cu-99'. Collector logs can only be retrieved from an All-In-One or a Collector Unit"
}
An unknown tool name in the path is reported the same way:
{
"success": false,
"error": "Not Found",
"message": "Unknown collector log tool 'tail'. Expected one of fetch, list"
}
Note
A 404 from a poll endpoint is deliberately the same for every reason a request is not
readable: the request_id was never issued, it belongs to a different Remote Toolbox tool,
it was dispatched by another user, or it has aged out of the platform’s retention window.
Distinguishing those cases would turn the poll endpoint into an oracle for which request ids
exist, and the rows it guards are shared with every other Remote Toolbox tool.