Skip to content

feat(environment:logs): read logs from the Observability API - #189

Open
pjcdawkins wants to merge 9 commits into
mainfrom
claude/log-observability-pipeline-migration-40fe23
Open

pjcdawkins wants to merge 9 commits into
mainfrom
claude/log-observability-pipeline-migration-40fe23

Conversation

@pjcdawkins

@pjcdawkins pjcdawkins commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

The log command can now read from the Observability API (the same logs API used by Console) instead of running tail over SSH.

This is behind a flag: the new api.log_protocol config option, or the UPSUN_CLI_LOG_PROTOCOL environment variable. It defaults to ssh, which keeps the current behavior. With auto, the API is used where it is available for the environment, and SSH otherwise.

With the API:

  • Log types map to API log kinds: access, app, cron, deploy, platform. error means severity ERROR or higher, across all kinds. Without a type, all kinds except platform are shown, with no interactive prompt.
  • New options: --service/-s, --severity (minimum), --since, --until, --format (text, raw, or json lines), and --fields.
  • For structured (JSON) log lines, the text format appends the line's fields as key=value pairs. Fields that are already displayed and trace/span IDs are skipped. The output is limited to 8 fields, 60 characters per value, and 200 characters in total.
  • --fields chooses the fields to display, e.g. --fields datetime,service,content,context.method,context.path,context.status.
  • --lines queries time windows of increasing size (15 minutes, growing to at most 7 days), since wide ranges can be rejected with 499 Too much data fetched. A 499 halves the window and retries.
  • --tail polls every 5 seconds, 15 seconds behind the current time, because logs take 5-12 seconds to become queryable. The API has no streaming endpoint.
  • SSH is still used if the API is not available for the environment (404, or no logs_query link), with --worker, --instance or --task, or for a type that is not a known log kind or ends in .log (e.g. error.log). Other API errors are reported. API-only options give an error when SSH is used.

Supporting changes:

  • A new Observability service fetches the entrypoint and makes requests, and the metrics commands use it too.
  • SelectorConfig::$selectRemoteContainer and Selector::withRemoteContainer() defer app selection, so the app prompt only appears when SSH is used.

Note: the API's OpenAPI spec documents the sort parameter as order, but the server reads order_by.

🤖 Generated with Claude Code

pjcdawkins and others added 3 commits September 28, 2026 00:43
The log command now reads from the Observability Pipeline logs API (as
used by Console) instead of running `tail` over SSH.

- Log types map to API log kinds: access, app, cron, deploy, platform.
  "error" filters by severity ERROR or higher. Without a type, all
  kinds except "platform" are shown, and no interactive prompt is needed.
- New options: --service, --severity (minimum), --since, --until, and
  --format (text, raw, or json lines).
- --lines queries time windows of increasing size (from the API's
  recommended default range, up to 7 days), because wide ranges can be
  rejected with "499 Too much data fetched". A 499 halves the window.
- --tail polls every 2 seconds in ascending order using the cursor,
  15 seconds behind the current time, as ingestion takes 5-12 seconds.
- SSH is still used when the API is unavailable for the environment
  (entrypoint 404 or no logs_query link), when --worker, --instance or
  --task is given, or when the type is not a known log kind.

Supporting changes:
- Add an Observability service for the entrypoint and GET requests,
  and use it in the metrics commands.
- Add SelectorConfig::$selectRemoteContainer and
  Selector::withRemoteContainer(), so the app is only selected (and
  prompted for) when falling back to SSH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For log lines with a JSON `context`, the text format now appends the
fields as dimmed key=value pairs after the message. Nested objects are
flattened with dotted keys, and lists and other non-string values are
encoded as JSON.

Top-level keys that duplicate the displayed time, severity and message
(e.g. time, level, msg) are skipped, as are trace and span IDs. The
json format still includes the full context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Filter by the app from the selection, so an app from the project URL
  or the APPLICATION_NAME environment variable is respected, as it is
  over SSH.
- When tailing, start each poll a second before the previous one ended
  (the cursor prevents duplicates), rather than at the last matching
  line. Quiet streams previously queried ever-growing windows. Each
  poll is also capped at 7 days, to catch up gradually after a suspend.
- Poll every 5 seconds instead of 2, to reduce API load. The added
  latency is small next to the 15 second ingestion delay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Changes suggested — 🟡 3 warnings · 🔵 1 minor point

🔍 Full review · 7 files reviewed

Verification
  • In fetchRecent, a 499 retry only happens before any cursor is used ($cursor !== null rethrows), so a page can never be retried in the middle of pagination.
  • queryPage clears _has_more_results when the returned cursor is unchanged, so the do/while loops cannot spin on the same query.
  • The imports left in MetricsCommandBase (Request, BadResponseException, ApiResponseException, Environment) are still used, so no_unused_imports is satisfied.
  • Selector only skips remote-container selection when selectRemoteContainer is false, and withRemoteContainer builds a new Selection with the same config, project, environment and app name.

The new Go integration tests in integration-tests/environment_log_test.go cover the API path against a mocked query endpoint: default and larger --lines, the error type, JSON output, filters, and the app env var. They also cover the SSH fallback on a 404 and the rejection of API-only options there. Nothing tests the 499 window-halving, --tail polling, or entrypoint errors other than 404.

Review details
  • Commit: ff24d5f
  • Model: claude-opus-5-5

Review 1 of 10 for this pull request · View the full run

Comment thread legacy/src/Command/Environment/EnvironmentLogCommand.php
Comment thread legacy/src/Service/Observability.php
Comment thread legacy/src/Command/Environment/EnvironmentLogCommand.php Outdated
Comment thread legacy/src/Command/Environment/EnvironmentLogCommand.php Outdated
- Read a type ending in ".log" (e.g. "error.log") as a file over SSH,
  even when the logs API is available, so the error log file can still
  be read. "error" still filters by severity via the API.
- Fall back to SSH, with a warning, if the observability entrypoint
  fails for reasons other than a 404 (e.g. 403, 5xx or a connection
  error). A 401 is still an error.
- Keep rows without a cursor instead of deduplicating them into one.
- With --tail, a --since time later than the tail delay now starts
  tailing instead of failing. Without --tail, the error no longer
  mentions --until unless it was given.
- Test the 499 window halving, the entrypoint error fallback, file
  names over SSH, and invalid time ranges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Changes suggested — 🟡 2 warnings

🔁 Incremental · 2 files reviewed

Verification
  • log error.log now makes sshReason() return 'a log file name was given' before .log is stripped, so SSH reads /var/log/error.log.
  • A 401 from the entrypoint is still rethrown, while 403, 5xx and ConnectException now fall back to SSH with a warning.
  • Rows with an empty cursor now get unique no-cursor-N keys in fetchRecent(), so they are no longer collapsed into one row.
  • Without --tail, the $from >= $to error names --until only when --until was actually passed.

Integration tests in integration-tests/environment_log_test.go now cover the 499 window halving, the two invalid-range messages, the 403→SSH fallback and error.log going over SSH. No test covers --tail with a recent or future --since, or a non-JSON entrypoint response.

Review details

Review 2 of 10 for this pull request · View the full run

Comment thread legacy/src/Command/Environment/EnvironmentLogCommand.php Outdated
Comment thread legacy/src/Command/Environment/EnvironmentLogCommand.php Outdated
…d entrypoint responses

- With --tail, a future --since is now an error, as without --tail. A
  --since time within the tail delay skips the initial fetch and polls
  from that time, instead of from slightly earlier.
- Fall back to SSH if the observability entrypoint response cannot be
  decoded (e.g. an HTML page from a proxy), as for other API errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@upsun-dispatch
upsun-dispatch Bot dismissed stale reviews from themself September 28, 2026 00:51

Superseded: the latest Upsun Dispatch review no longer requests changes.

pjcdawkins and others added 3 commits September 28, 2026 02:07
… available

Fall back to SSH only if the observability entrypoint is not found
(404) or has no logs_query link. Other errors, such as a 403, a 5xx or
an invalid response, are now reported instead of being hidden behind
the SSH fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…terminals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add the api.log_protocol config option (and the <PREFIX>LOG_PROTOCOL
  environment variable). It defaults to "ssh", which keeps the previous
  behavior. "auto" uses the Observability API where it is available.
  API-only options give an error with the "ssh" protocol, saying how to
  enable the API.
- Add --fields, to choose the fields shown in the text format, including
  "context.<key>" for fields of structured log lines. Colors match the
  default format.
- Limit the "context" fields in the text format to 8 fields, 60
  characters per value and 200 characters in total, noting how many
  were omitted. Fields chosen with --fields are not truncated.
- List the valid formats in the error for an invalid --format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@upsun-dispatch

upsun-dispatch Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

📋 PR Summary

Adds an opt-in way for environment:log to read logs from the Observability API instead of running tail over SSH. It is controlled by api.log_protocol or UPSUN_CLI_LOG_PROTOCOL, which accept ssh (the default), api or auto. With the API, the command gets service, severity, time-range, format and field options, and it uses windowed queries for --lines and polling for --tail. The latest push fixes structured log fields with numeric keys, which PHP turns into integers, by converting those keys back to strings before formatting.

Changes
Layer / File(s) Summary
Log command
legacy/src/Command/Environment/EnvironmentLogCommand.php Adds API-based log reading with fallback to SSH, new options, and formatting of structured fields. Numeric keys in structured fields are now converted to strings before they are filtered and formatted.
Tests
integration-tests/environment_log_test.go Adds integration tests against a mock API. The test for field limits now includes a numeric key (404).
Observability service
legacy/src/Service/Observability.php New service that fetches the Observability API entrypoint and makes requests to it.
legacy/src/Command/Metrics/MetricsCommandBase.php The metrics commands now use the shared Observability service.
Selector
legacy/src/Selector/Selector.php Adds withRemoteContainer() so app selection can wait until SSH is actually used.
legacy/src/Selector/SelectorConfig.php Adds the selectRemoteContainer option.
Config
legacy/config-defaults.yaml Adds the api.log_protocol default, set to ssh.
legacy/src/Service/Config.php Maps the environment variable to the new config option.
legacy/phpstan-baseline.neon Removes baseline entries that are no longer needed.

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Changes suggested — 🟡 1 warning

🔁 Incremental · 4 files reviewed

Verification
  • sshReason() checks for a .log suffix before execute() strips it, so log error.log still reads /var/log/error.log over SSH.
  • LOG_PROTOCOL is added to the override map in Config.php and defaults to ssh in config-defaults.yaml. execute() rejects any value other than ssh or auto.
  • With the ssh protocol, --fields falls under API_OPTIONS and gives a "cannot be used" error instead of being silently ignored.
  • In formatContext, the length check only runs once $parts is non-empty, so at least one context field is always shown. After the first omitted field, every later field is omitted too, and they all go into the (+N more) count.

The diff extends the integration tests TestEnvironmentLogAPI and TestEnvironmentLogSSHFallback. They cover the protocol default, --fields, and the context limits, and they run in the integration-test job in ci.yml. No test covers context keys that are numeric.

Review details

Review 6 of 10 for this pull request · View the full run

Comment thread legacy/src/Command/Environment/EnvironmentLogCommand.php
PHP converts numeric array keys such as "404" to integers, which caused
a TypeError under strict types when displaying context fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@upsun-dispatch
upsun-dispatch Bot dismissed their stale review September 28, 2026 07:26

Superseded: the latest Upsun Dispatch review no longer requests changes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant