Skip to content

Conversation

@neubig
Copy link
Contributor

@neubig neubig commented Jan 15, 2026

Summary

This PR fixes incomplete JSON logging for uvicorn access logs in the agent-server.

Problem

Currently, when LOG_JSON=true or in CI environments, uvicorn access logs appear in Datadog with a raw message string like:

10.2.66.141:47584 - "POST /api/conversations HTTP/1.1" 201

While the logs are technically JSON (they have asctime, levelname, etc.), the HTTP fields are not extracted into separate attributes, making it difficult to:

  • Filter logs by HTTP method, status code, or URL
  • Create dashboards based on endpoint performance
  • Set up alerts on specific HTTP errors

Solution

Added a custom AccessLogJsonFormatter that extracts HTTP fields from uvicorn's access log records into structured JSON attributes:

{
  "asctime": "2026-01-15 15:24:08,131",
  "levelname": "INFO", 
  "name": "uvicorn.access",
  "message": "10.2.66.141:47584 - \"POST /api/conversations HTTP/1.1\" 201",
  "http.client_ip": "10.2.66.141:47584",
  "http.method": "POST",
  "http.url": "/api/conversations",
  "http.version": "1.1",
  "http.status_code": 201
}

Changes

  • Created AccessLogJsonFormatter class that extends JsonFormatter to extract HTTP fields from record.args
  • Updated get_uvicorn_logging_config() to use the custom formatter when LOG_JSON=true or CI=true
  • Access logs now use a dedicated handler to avoid double-logging

Testing

Verified locally that the formatter correctly extracts HTTP fields:

LOG_JSON=true python3 -c "
from openhands.agent_server.logging_config import AccessLogJsonFormatter
import logging

formatter = AccessLogJsonFormatter(fmt='%(asctime)s %(levelname)s %(name)s %(message)s')
record = logging.LogRecord(
    name='uvicorn.access', level=logging.INFO, pathname='', lineno=0,
    msg='%s - \"%s %s HTTP/%s\" %d',
    args=('10.2.66.141:47584', 'POST', '/api/conversations', '1.1', 201),
    exc_info=None,
)
print(formatter.format(record))
"

Output:

{"asctime": "2026-01-15 15:24:08,131", "levelname": "INFO", "name": "uvicorn.access", "message": "10.2.66.141:47584 - \"POST /api/conversations HTTP/1.1\" 201", "http.client_ip": "10.2.66.141:47584", "http.method": "POST", "http.url": "/api/conversations", "http.version": "1.1", "http.status_code": 201}

@neubig can click here to continue refining the PR


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:58aa049-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-58aa049-python \
  ghcr.io/openhands/agent-server:58aa049-python

All tags pushed for this build

ghcr.io/openhands/agent-server:58aa049-golang-amd64
ghcr.io/openhands/agent-server:58aa049-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:58aa049-golang-arm64
ghcr.io/openhands/agent-server:58aa049-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:58aa049-java-amd64
ghcr.io/openhands/agent-server:58aa049-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:58aa049-java-arm64
ghcr.io/openhands/agent-server:58aa049-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:58aa049-python-amd64
ghcr.io/openhands/agent-server:58aa049-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:58aa049-python-arm64
ghcr.io/openhands/agent-server:58aa049-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:58aa049-golang
ghcr.io/openhands/agent-server:58aa049-java
ghcr.io/openhands/agent-server:58aa049-python

About Multi-Architecture Support

  • Each variant tag (e.g., 58aa049-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 58aa049-python-amd64) are also available if needed

@openhands-ai
Copy link

openhands-ai bot commented Jan 15, 2026

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • Pre-commit checks

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #1733 at branch `fix/json-access-logs`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

Copy link
Contributor Author

neubig commented Jan 15, 2026

Related PR for V0 legacy server (OpenHands main repo): OpenHands/OpenHands#12442

@neubig neubig force-pushed the fix/json-access-logs branch 2 times, most recently from eed63aa to 429c700 Compare January 15, 2026 15:50
The current uvicorn access logs pass HTTP data in record.args as a tuple,
but the standard JsonFormatter doesn't extract these into structured fields.
This results in the HTTP information only being available in the message string.

This commit adds UvicornAccessJsonFormatter that properly extracts HTTP
fields from record.args into structured JSON attributes:
- http.client_ip
- http.method
- http.url
- http.version
- http.status_code

Before:
{"message": "10.2.66.141:47584 - \"POST /api/conversations HTTP/1.1\" 201"}

After:
{"message": "...", "http.client_ip": "10.2.66.141:47584",
 "http.method": "POST", "http.url": "/api/conversations",
 "http.version": "1.1", "http.status_code": 201}

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig force-pushed the fix/json-access-logs branch from 429c700 to 64a4cb9 Compare January 15, 2026 15:53
@neubig neubig marked this pull request as ready for review January 15, 2026 17:43
@neubig neubig requested a review from tofarr January 15, 2026 17:46
Copy link
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

Overall the PR looks good and solves the stated problem effectively. I have a couple of minor suggestions for code quality improvements.

log_data["http.method"] = method
log_data["http.url"] = full_path
log_data["http.version"] = http_version
log_data["http.status_code"] = int(status_code) # type: ignore[call-overload]
Copy link
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: The type: ignore comment suppresses a type checking warning for the int conversion. Since status_code comes from uvicorn's log args tuple, it should already be an int. Consider either:

  1. Removing the conversion if it's guaranteed to be an int
  2. Adding explicit validation to handle edge cases
  3. Adding a try-except to handle potential conversion errors gracefully

This would make the code more defensive and easier to maintain if uvicorn's logging format changes.

else:
# Non-JSON mode: propagate all to root (uses Rich handler)
config["loggers"] = {
"uvicorn": {
Copy link
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: The uvicorn and uvicorn.error logger configurations here (lines 100-109) are identical to lines 80-89. Consider extracting the common configuration outside the if/else block to reduce duplication and make the code more maintainable.

@neubig
Copy link
Contributor Author

neubig commented Jan 15, 2026

@OpenHands the above comments are good, reflect them

@openhands-ai
Copy link

openhands-ai bot commented Jan 15, 2026

I'm on it! neubig can track my progress at all-hands.dev

- Replace type: ignore with explicit type checking for status_code
  to handle edge cases gracefully (int, str, or fallback to original)
- Extract common uvicorn/uvicorn.error logger configs outside if/else
  block to reduce code duplication
- Store log_level in variable to avoid repeated getLevelName calls

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 15, 2026

I've addressed the review comments on PR #1733 and pushed the changes. Here's a summary of what was done:

Changes Made

1. Fixed status_code type handling (addressing comment on line 39)

  • Replaced int(status_code) # type: ignore[call-overload] with explicit type checking
  • Now handles three cases:
    • If status_code is already an int, use it directly
    • If it's a string of digits, convert to int
    • Otherwise, use the original value as fallback
  • This makes the code more defensive and type-safe

2. Reduced code duplication (addressing comment on line 100)

  • Extracted common uvicorn and uvicorn.error logger configurations outside the if/else block
  • Only the uvicorn.access logger configuration differs between JSON and non-JSON modes
  • Also stored log_level in a variable to avoid repeated getLevelName() calls

Verification

  • All pre-commit checks pass (Ruff format, Ruff lint, pycodestyle, pyright, import rules)
  • Tested the formatter manually and confirmed it correctly extracts HTTP fields

The changes have been pushed to the fix/json-access-logs branch: #1733

View full conversation

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.

4 participants