Skip to content

feat(s3): identify Metaflow in the S3 client user agent - #3323

Draft
goanpeca wants to merge 1 commit into
Netflix:masterfrom
goanpeca:feat/s3-user-agent
Draft

feat(s3): identify Metaflow in the S3 client user agent#3323
goanpeca wants to merge 1 commit into
Netflix:masterfrom
goanpeca:feat/s3-user-agent

Conversation

@goanpeca

Copy link
Copy Markdown

PR Type

  • Bug fix
  • New feature
  • Core Runtime change (higher bar -- see CONTRIBUTING.md)
  • Docs / tooling
  • Refactoring

Summary

S3 clients built by Boto3ClientProvider now carry a metaflow/<version> token in their User-Agent, so Metaflow traffic is identifiable in server-side access logs. Nothing else about the request changes: no new config, no credential or endpoint handling, no change to the existing retry config block.

Issue

No issue yet, which is why this is a draft. metaflow/plugins/aws/aws_client.py is Core Runtime, and CONTRIBUTING.md asks for a maintainer-acknowledged issue and an agreed approach before that kind of change. I am opening this to show the concrete 14-line shape of the idea. Happy to file an issue and close this if you prefer that ordering, and equally happy to close it if you do not want the client identifying itself.

Reproduction

Not a bug fix, so this is a before/after observation rather than a failing repro.

Runtime: local (client construction only, no requests are sent)

Commands to run:

python - <<'PY'
from botocore.config import Config
from metaflow.plugins.aws.aws_client import Boto3ClientProvider

print("default s3 :", Boto3ClientProvider.get_client("s3").meta.config.user_agent_extra)
print("caller UA  :", Boto3ClientProvider.get_client(
    "s3", client_params={"config": Config(user_agent_extra="my-app/1.0")}
).meta.config.user_agent_extra)
print("non-s3     :", Boto3ClientProvider.get_client("sts").meta.config.user_agent_extra)
PY

Where evidence shows up: parent console for the snippet above, and the User-Agent header on the wire (server access logs) for real traffic.

Before (on master)
default s3 : None
caller UA  : my-app/1.0
non-s3     : None

full UA: Boto3/1.43.31 md/Botocore#1.43.31 ua/2.1 os/macos#25.6.0 md/arch#arm64 lang/python#3.11.15 md/pyimpl#CPython cfg/retry-mode#adaptive Botocore/1.43.31
After (this branch)
default s3 : metaflow/2.19.23
caller UA  : my-app/1.0 metaflow/2.19.23
non-s3     : None

full UA: Boto3/1.43.31 md/Botocore#1.43.31 ua/2.1 os/macos#25.6.0 md/arch#arm64 lang/python#3.11.15 md/pyimpl#CPython cfg/retry-mode#adaptive Botocore/1.43.31 metaflow/2.19.23

Root Cause

Not a defect, so there is no violated invariant. The gap is that Boto3ClientProvider.get_client never sets user_agent_extra, so on the wire a Metaflow S3 request is indistinguishable from any other boto3 caller in the same account. That makes it hard for an operator to attribute request volume, throttling, or a cost spike to Metaflow when looking at Amazon S3 server access logs, or at the logs of any other S3-compatible endpoint Metaflow is pointed at (Backblaze B2, Cloudflare R2, MinIO, which devtools/ already uses for local testing). Most data tools set a client identifier for exactly this reason. botocore's user_agent_extra is the supported hook for it.

Why This Fix Is Correct

The block sits right after the existing retry-config block, reuses the same client_params.get("config", Config()) pattern, and runs only when module == "s3". It appends rather than assigns, so a caller-supplied user_agent_extra survives, and it skips the append when the token is already present, so a Config object reused across two get_client calls does not accumulate duplicates. metaflow.version is imported inside the function, matching how the rest of this function defers its imports.

The change is header-only: user_agent_extra is not part of the signed header set, so signing and presigned URLs are unaffected (verified generate_presigned_url output is identical apart from its expiry timestamp).

Failure Modes Considered

  1. Caller already set a user agent. Overwriting would break anyone who identifies their own application, so the token is appended to the existing value. Both the Config object and the dict form of client_params["config"] are covered, since the dict is normalized to a Config a few lines above.
  2. Repeated application. Config.user_agent_extra is mutable state on an object the caller owns, so the same Config passed to get_client twice would grow a duplicate token. The ua not in existing_ua.split() guard keeps it to one. Token matching is on whitespace-split words so metaflow/2.19.23 does not falsely match a longer token that happens to contain it as a substring.
  3. Non-S3 clients. STS, Batch, Step Functions, and the rest are untouched, confirmed above (non-s3 : None). Keeping the scope narrow means the sandbox STS path and the AWS Batch client behave exactly as before.
  4. Backward compatibility. No config knob is added and no default changes, so nothing to migrate. The only observable difference is a longer User-Agent string, which no Metaflow code reads.

Tests

  • Unit tests added/updated
  • Reproduction script provided (required for Core Runtime)
  • CI passes
  • If tests are impractical: explain why below and provide manual evidence above

There is no unit test for Boto3ClientProvider today, so this branch adds none. A pytest-mock test asserting the token lands in client.meta.config.user_agent_extra, and that a caller-supplied value is preserved rather than replaced, would be straightforward. Tell me if you want it and I will add it here rather than guess at the shape you would accept for a new test/unit/test_aws_client.py. Manual before/after evidence is above.

Non-Goals

No config knob to disable or customize the token. No change to the retry config, endpoint resolution, credential or role-assumption paths, or the sandbox path. No change to non-S3 clients. No change to metaflow/plugins/datatools/s3/.

AI Tool Usage

  • No AI tools were used in this contribution
  • AI tools were used (describe below)

Claude Code was used to write the patch and to capture the before/after client output above. I reviewed the diff, ran the checks shown here against both master and this branch, and can defend the append-not-replace and idempotency choices without going back to the tool.

Signed-off-by: Gonzalo Peña-Castellanos <goanpeca@gmail.com>
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