Skip to content

Emit forwarder version on request (hubmanager version-check) - #164

Open
erics-ddog wants to merge 2 commits into
masterfrom
eric/forwarder-cold-start-version-metric
Open

Emit forwarder version on request (hubmanager version-check)#164
erics-ddog wants to merge 2 commits into
masterfrom
eric/forwarder-cold-start-version-metric

Conversation

@erics-ddog

@erics-ddog erics-ddog commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

  • Each forwarder recognizes a new control message {"version_check":"true"} (mirroring the existing {"backfill_mode":"true"} pattern) and responds with its build-stamped Version directly in the invoke response — no Vault fetch, no HTTP call to Datadog, nothing that touches the customer's Datadog org.
  • Version is still stamped into the binary at build time via -ldflags "-X main.Version=<tag>" (Dockerfile-events, Dockerfile-logs, Dockerfile-metrics, docker-images.sh), so it reflects what's actually baked into the image, not what OCI's function record claims should be running.
  • fnResponse gains an optional version field; a new writeVersionResponse answers the check.
  • README's manual build instructions now pass --build-arg TAG=<tag> so a locally built image doesn't silently report version:unknown.

Why (redesign from the original approach)

This PR originally had each forwarder push a datadog.oci.forwarder.cold_start metric to Datadog's intake on cold start, using the same client/credentials the forwarder already uses to send the customer's own telemetry. Review feedback (see thread below) correctly caught two real problems with that:

  1. It would submit a new custom metric under the customer's Datadog org/API key — billable to them, and invisible to Datadog engineers in aggregate unless someone went looking inside a specific customer's org.
  2. It ran synchronously before fdk.Handle, chained through Vault's 3-minute timeout, so a slow/unavailable Vault or Datadog could delay or fail a cold start over a best-effort observability signal.
  3. (Separately) it tagged region from HOME_REGION, which is the Vault's home region, not necessarily the function's actual execution region in multi-region deployments — collapsing the very rollout signal the metric existed to produce.

This redesign flips the direction: instead of the forwarder pushing to Datadog, hubmanager — which already talks to these functions over OCI's control/invoke plane using Datadog's own credentials, and already does exactly this for backfill triggers — pulls the version by invoking the function synchronously and reading the response. See the companion PR in dd-source: https://github.com/ddoghq/dd-source/pull/58292

This resolves all three points above as a side effect: nothing here ever touches customer credentials or the customer's Datadog org (point 1), there is no network/Vault call anywhere on this path so nothing can block fdk.Handle (point 2), and region tagging moves entirely to hubmanager's side, using its own known-correct region rather than a self-reported env var (point 3).

Testing

  • go build ./... / go vet ./... clean for all three forwarders and lib.
  • go test ./... passes for all three forwarders and lib, including new TestMyHandler_VersionCheck tests per forwarder that assert the check succeeds with no environment variables set at all (proving it can't depend on Vault/Datadog config).
  • gofmt -l clean on all changed files.
  • Not yet verified against a live OCI region — this is still a draft pending the companion hubmanager PR.

Notes

hubmanager reconciles each forwarder's OCI Function image against Balto's
desired tag, but registry/image-cache propagation lag means the control
plane reporting "updated" doesn't guarantee a given container is actually
running the new code. Stamp the image tag into the binary at build time and
emit it as a gauge tagged by forwarder/version/region once per cold start
(not per invocation, since the version is fixed for a container's lifetime),
so rollout propagation is directly observable in Datadog.
@erics-ddog
erics-ddog marked this pull request as ready for review August 14, 2026 13:35
@erics-ddog
erics-ddog requested a review from a team as a code owner August 14, 2026 13:35

@EamonBrady1 EamonBrady1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This would be a pretty notable change and is sending a custom metric to Datadog from the customer account (which would charge them). We currently don't have any behavior in the forwarding functions that emits monitoring data directly to Datadog.
Additionally, if we are sending it with this client, it would appear as monitoring data in their account, invisible to us unless we partlow into user accounts every time we want to check this, which we wouldn't want to do.
I think the solution to this issue is a bit more complicated than just emitting a metric from the forwarding functions and is part of bigger picture OCI monitoring that we've thought about in the past. It might require a new endpoint to send this monitoring data to or something like that, but I think we need to think about this one a little more

var Version = "unknown"

func main() {
client.EmitColdStart("logs", Version)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given this is called synchronously, it could take up to 3 minutes and 10 seconds to run which would delay the fdk.Handle call and maybe put it at risk of hitting timeouts elsewhere? It might be good to call the EmitColdStart in a go routine async

@bengillmandd

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56d31eeced

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

var Version = "unknown"

func main() {
client.EmitColdStart("events", Version)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit the metric without blocking FDK startup

When Vault or Datadog is slow or unavailable, this synchronous call runs before fdk.Handle in all three forwarders, so the container cannot begin serving. In particular, NewDatadogClientWithSite performs the initial Vault fetch under a three-minute timeout before the later ten-second metric-send timeout is even created; swallowing its eventual error therefore does not make emission non-blocking and can turn a transient observability failure into a failed or severely delayed cold start. Start the FDK first or move the best-effort emission off the startup path.

Useful? React with 👍 / 👎.

"tags": []string{
"forwarder:" + forwarder,
"version:" + version,
"region:" + os.Getenv("HOME_REGION"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Tag the metric with the function's execution region

When a regional stack falls back to the home-region Vault, HOME_REGION is explicitly set to local.vault_region (datadog-integration/modules/regional-stacks/locals.tf:24-32), not to the region where this function executes. Consequently, forwarders running in multiple regions can all report region:<home-region>, collapsing the rollout signal this metric is intended to measure; use an execution-region value or pass the stack's var.region separately.

Useful? React with 👍 / 👎.

COPY lib/ lib/
COPY events-forwarder/ events-forwarder/
ARG TARGETARCH
ARG TAG=unknown

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the version in documented Docker builds

The repository's documented local commands in datadog-functions/README.md:10-12 set only the output image tag and do not pass --build-arg TAG=...; Docker does not propagate --tag into this build argument. Anyone following those commands therefore gets a valid tagged image whose binary reports version:unknown, undermining the new metric's claim to identify the running image. Update those build paths to supply TAG or arrange for the version to be derived without a separate required argument.

Useful? React with 👍 / 👎.

Review feedback on the original approach (emitting a metric to Datadog's
intake using the customer's own DD_SITE/API key) correctly flagged that it
would bill the customer for a new custom metric and land in their org,
invisible to Datadog engineers in aggregate. It also ran synchronously before
fdk.Handle, risking cold-start delays if Vault/Datadog were slow.

This replaces the push with a passive echo: forwarders now recognize a
{"version_check":"true"} control message (mirroring the existing
{"backfill_mode":"true"} pattern) and respond with their build-stamped
Version directly from memory — no Vault fetch, no HTTP call, nothing that can
block startup or touch customer credentials. hubmanager (companion change in
dd-source) pulls this by invoking the function synchronously with its own OCI
credentials and emits the comparison as an internal-only metric.

Also fixes the README's local-build instructions to pass --build-arg TAG, so
the manual build path doesn't silently produce a binary reporting
version:unknown.
@erics-ddog erics-ddog changed the title Emit forwarder version metric on cold start Emit forwarder version on request (hubmanager version-check) Aug 17, 2026
@erics-ddog

Copy link
Copy Markdown
Contributor Author

@EamonBrady1 you were right — I've pushed a redesign that removes the push-to-Datadog-intake entirely. Forwarders no longer touch Vault or send anything to the customer's Datadog org for this. Instead they just echo their build-stamped version back when hubmanager invokes them with {"version_check":"true"} (same pattern as the existing backfill trigger), and hubmanager itself — using its own OCI credentials — is the one that emits the comparison as an internal metric. Companion PR in dd-source coming shortly.

@bengillmandd this also addresses your synchronous-call concern from a different angle than "make it async": there's no longer any Vault/HTTP call on the cold-start path at all, so nothing can block fdk.Handle.

Also fixed the two Codex findings: region is no longer self-reported via HOME_REGION (that move to hubmanager's side entirely), and the README's manual build instructions now pass --build-arg TAG.

Keeping this as a draft until the hubmanager-side PR is up so they can be reviewed together.

@erics-ddog

Copy link
Copy Markdown
Contributor Author

Companion PR is up: https://github.com/ddoghq/dd-source/pull/58292 (hubmanager side — invokes {"version_check":"true"} synchronously via the OCI invoke plane and emits the desired-vs-actual comparison as an internal dd.oci.hubmanager.function_version_check metric). Both are ready for review together.

echo "Building and pushing multi-arch image ${IMAGE_PATH} to $REGISTRY..."
docker buildx build -f ${DOCKER_FILE} \
--platform linux/amd64,linux/arm64 \
--build-arg TAG="${TAG}" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is this doing?

out.Write([]byte(`{"status":"error","error":"Internal server error"}`))
return
}
fdk.WriteStatus(out, 200)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do these statuses actually make it to hubmanager? The idea seems right, but I'm curious if this is actually what's surfaced as the function response when we invoke it

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.

3 participants