Usage

1) Overview

Endpoints

  • GET /lce/api/collectors

  • POST /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.


2) Authentication & Authorization

Authentication

Requires a valid Skylar One user session.

Ownership is enforced on top of authentication. A queued request is attributed to the user that dispatched it, and only that user can read its output. The output table is shared by every Remote Toolbox tool, so a read that cannot be shown to belong to the caller is refused.

Access Hooks

To call GET /lce/api/collectors you must have the following Access Hook:

  • DEV_COLLECTIONS_VIEW

To call POST /lce/api/collector_logs/list you must have the following Access Hooks:

  • DEV_TOOLS_RUN

  • DEV_COLLECTIONS_VIEW

To call GET /lce/api/collector_logs/list/{request_id} you must have the following Access Hook:

  • DEV_TOOLS_RUN

To call POST /lce/api/collector_logs/fetch you must have the following Access Hooks:

  • DEV_TOOLS_RUN

  • DEV_COLLECTIONS_VIEW

To call GET /lce/api/collector_logs/fetch/{request_id} you must have the following Access Hook:

  • DEV_TOOLS_RUN

A local run, with run_local set to true, needs the same hooks as the POST it is sent to.


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

directory_path

Contents

sl1

/var/log/sl1

The current service logs, written by rsyslog. This is the default on the two POST endpoints.

em7

/var/log/em7

The logdir recorded in silo.conf.

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 the directory of the next request, or as the directory query 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 removed

  • rotated — whether this is a rotated generation rather than the current log

  • compressed — whether it is gzip compressed

  • generation — the rotation suffix (<date>-<epoch>), or null for 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.

  1. POST the request. You get 202 and a request_id. No output exists yet.

  2. GET the matching poll endpoint with that request_id.

  3. If terminated is false, call again, passing the previous response’s last_id as after_id.

  4. Stop when terminated is true.

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 truncated is set if rows were left behind. Continue from last_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.

  • phase is queued, running, finished or unknown. It is derived from the platform’s spool row and the output together, because the Remote Toolbox can lose either signal. unknown means neither the spool row nor any output could be read; a job with no spool row is otherwise reported as not found.

  • state and returncode are the spool row’s own fields as found. state is 1 scheduled, 2 launched, 3 running, 0 complete, 4 failed or 5 canceled, but each update reaches the database separately and they can land out of order, so a finished job can still read 2. returncode is the command’s exit status as the platform stores it, null for 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 as 0 too.

  • seconds_since_queued and seconds_since_last_output say 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_id is 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, lines and max_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 what max_bytes allows; output past it is discarded and truncated is then true. Otherwise truncated is false.

  • The request waits for the run. A local run that has not finished after 90 seconds is stopped, and the POST returns 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 toolbox object describes a finished run. phase is finished, returncode is the command’s exit status, seconds_since_queued is how long the run took and seconds_since_last_output how long before it ended it last wrote anything. state is 0, or 4 when 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

collector

yes, for a queued request

The id or name of the collector to list logs on. Ignored by a local run.

directory

no

sl1 or em7. Defaults to sl1.

run_local

no

true to list the directory on this host and return the listing, false to queue it. Left out, true on an All-In-One and false otherwise. See 6) Running a tool locally.

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

directory

yes

Which log directory the request was queued for, sl1 or em7. There is no default here: it must match the directory the listing was queued with. Take the directory of the queueing response and send it straight back.

after_id

no

Read only output produced after this cursor. Defaults to the beginning.

page_size

no

Maximum number of output rows to read. Defaults to 1000 and is capped at 5000. 0 is rejected rather than read as “use the default”.

wait_seconds

no

Wait up to this long for the collector to finish. Defaults to no wait; capped at 90.

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

collector

yes, for a queued request

The id or name of the collector to read from. Ignored by a local run.

run_local

no

true to read the log on this host and return the content, false to queue the read. Left out, true on an All-In-One and false otherwise. See 6) Running a tool locally.

log_name

yes

The log file to read, within the chosen directory. Rotated generations are accepted.

directory

no

sl1 or em7. Defaults to sl1.

lines

no

Return only this many trailing lines. An integer from 1 to 100000. Omitting it does not remove the line cap; it applies 100000.

pattern

no

An extended regular expression to match records against. See 9) Filtering.

since

no

Inclusive lower bound on the record timestamp. Use T between the date and the time.

until

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.

max_bytes

no

Ceiling on the bytes the collector will return for this read. A positive integer, defaulting to and capped at 5242880 (5 MiB). See max_bytes, and the caps that always apply.

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

after_id

Read only output produced after this cursor. Defaults to the beginning.

page_size

Maximum number of output rows to return. Defaults to 1000 and is capped at 5000. 0 is rejected rather than read as “use the default”.

wait_seconds

Wait up to this long for the collector to finish. Defaults to no wait; capped at 90.

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 — lines when supplied, otherwise 100000 trailing lines

  • a byte cap — max_bytes when supplied, otherwise 5242880 bytes (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 collector for a queued request, a missing log_name, a run_local that is not true, false, 1 or 0, an invalid combine_streams on a local fetch, an unknown directory, a missing directory on GET /lce/api/collector_logs/list/{request_id}, a rejected pattern, since, until, lines or max_bytes, or an after_id, page_size or wait_seconds that is not a finite non-negative number. page_size=0 is rejected rather than read as “use the default”, and nan, inf and -nan are rejected for all three poll parameters.

  • 401 Unauthorized: missing or invalid authentication.

  • 403 Forbidden: access hooks insufficient.

  • 404 Not Found: the tool_name in the path is neither list nor fetch; on a POST, no collecting appliance matches the collector; on a GET, the request_id is 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.