Skip to content

otel: honor standard env vars + load /etc/http-proxy/otel.env - #679

Merged
jay-418 merged 5 commits into
mainfrom
jay/http-proxy-otel
Aug 4, 2026
Merged

otel: honor standard env vars + load /etc/http-proxy/otel.env#679
jay-418 merged 5 commits into
mainfrom
jay/http-proxy-otel

Conversation

@jay-418

@jay-418 jay-418 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

relates to:

problem

http-proxy still ships directly to teleport, not routing through ops like we want.

why

Systemd unit had no EnvironmentFile= so cloudinit-supplied OTLP env vars never reached the process. Every deployment that thought it was pointing http-proxy-lantern at a local otelcol was in fact getting the hardcoded telemetry.iantem.io:443 fallback with no resource attribution.

fixes

  • internal/http-proxy{,@}.service: EnvironmentFile=-/etc/http-proxy/otel.env (leading - = optional).
  • http_proxy.go:getTelemetryEndpoint: check OTEL_EXPORTER_OTLP_ENDPOINT first; CUSTOM_OTLP_ENDPOINT kept as fallback for backwards compat.
  • otel/otel.go: accept URL-form endpoints via WithEndpointURL; merge resource with resource.WithFromEnv() + WithHost(), env-wins order so deployment identity (service.name, host.name, etc. supplied via OTEL_RESOURCE_ATTRIBUTES) lands while runtime attrs from code (proxy.protocol, is_pro, legacy, ...) are still authoritative for the fields env doesn't set.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • OpenTelemetry settings can now be loaded from an optional system configuration file.
    • Telemetry endpoints, schemes, and TLS options use standard OpenTelemetry environment variables.
  • Bug Fixes

    • Improved telemetry resource handling for more reliable tracing and metrics configuration.
    • Services continue starting normally when the optional telemetry configuration file is unavailable.

Systemd unit had no EnvironmentFile= so cloudinit-supplied OTLP config
never reached the process. Also: honor OTEL_EXPORTER_OTLP_ENDPOINT
(CUSTOM_OTLP_ENDPOINT still works as a fallback), accept URL-form
endpoints via WithEndpointURL, and merge resource with WithFromEnv +
WithHost so OTEL_RESOURCE_ATTRIBUTES lands (env wins over the
hardcoded fallbacks so deployment identity like service.name is set
by the launcher).
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jay-418, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e063d070-48c4-485d-bb15-c33e11294bf2

📥 Commits

Reviewing files that changed from the base of the PR and between e94d5a8 and 7e45bfa.

📒 Files selected for processing (1)
  • otel/otel.go
📝 Walkthrough

Walkthrough

OpenTelemetry endpoint configuration moved from proxy-provided options to standard environment variables. Systemd services optionally load /etc/http-proxy/otel.env. Resource detection and provider shutdown behavior remain supported.

Changes

OpenTelemetry configuration

Layer / File(s) Summary
Proxy and service configuration
http_proxy.go, internal/http-proxy*.service
The proxy no longer resolves or passes a telemetry endpoint. Both systemd services optionally load /etc/http-proxy/otel.env.
OpenTelemetry resource and exporter setup
otel/otel.go
Opts.Endpoint was removed. Resource creation merges explicit, host, and environment attributes with fallback handling. Trace and metric exporters now use standard OpenTelemetry environment variables. Provider creation and shutdown remain supported.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: reflog

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: standard OpenTelemetry environment variables and optional loading of /etc/http-proxy/otel.env.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jay/http-proxy-otel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI 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.

Pull request overview

This PR improves OpenTelemetry configuration for http-proxy deployments by ensuring standard OTEL environment variables and deployment-provided resource attributes reliably reach the running systemd service and are applied by the SDK.

Changes:

  • Add an optional systemd EnvironmentFile=-/etc/http-proxy/otel.env to both unit variants so cloud-init/deploy tooling can inject OTEL env vars.
  • Prefer the standard OTEL_EXPORTER_OTLP_ENDPOINT over legacy CUSTOM_OTLP_ENDPOINT when selecting the OTLP endpoint.
  • Enhance OTel setup to (a) accept URL-form endpoints via WithEndpointURL and (b) merge env/host-derived resource attributes with code-defined attributes.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
otel/otel.go Adds resource merging with env/host detection and supports URL-form OTLP endpoints for traces/metrics exporters.
http_proxy.go Updates telemetry endpoint precedence to honor OTEL_EXPORTER_OTLP_ENDPOINT first.
internal/http-proxy.service Loads optional /etc/http-proxy/otel.env so deploy-time OTEL env vars reach the service.
internal/http-proxy@.service Loads optional /etc/http-proxy/otel.env for templated instances as well.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread otel/otel.go Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@otel/otel.go`:
- Around line 109-116: Replace the substring-based port check in both the trace
and metric exporter branches with exact endpoint port parsing using
net.SplitHostPort or URL parsing, so only port 443 is treated as secure and
values such as 4430 or IPv6 addresses are handled correctly. Update the logic
around the trace client options and metric exporter setup while preserving the
existing insecure fallback for non-443 endpoints.
- Around line 88-91: Update the resource creation error path around resource.New
to preserve and continue using a non-nil partial base resource, including its
valid environment and detected attributes. Fall back to
resource.NewWithAttributes only when resource detection returns no resource,
while retaining the existing error logging.
- Around line 105-108: Validate opts.Endpoint before calling
otlptracehttp.WithEndpointURL: parse the URL, require the scheme to be exactly
http or https and require a non-empty host, and return the validation error from
the surrounding setup flow instead of appending options or allowing exporter
defaults. Keep valid URL-form endpoints using WithEndpointURL unchanged.
- Around line 105-108: Update the URL-form endpoint handling in the trace and
metric exporter setup to append each signal’s required path (/v1/traces or
/v1/metrics) before passing the result to WithEndpointURL. Preserve the
configured base path, avoid applying the same unsuffixed endpoint to both
exporters, and make the corresponding change in both URL-handling blocks.
- Around line 76-87: Update the detector order in resource.New so
resource.WithHost() runs before resource.WithFromEnv(). Preserve the existing
detectors and ensure environment-derived attributes, including
OTEL_RESOURCE_ATTRIBUTES host.name values, are applied last and override host
detection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c1b686ac-a45f-4612-a47b-bfb5bae61c4f

📥 Commits

Reviewing files that changed from the base of the PR and between fe93122 and cfd6005.

📒 Files selected for processing (4)
  • http_proxy.go
  • internal/http-proxy.service
  • internal/http-proxy@.service
  • otel/otel.go

Comment thread otel/otel.go Outdated
Comment thread otel/otel.go
Comment thread otel/otel.go Outdated
Comment thread otel/otel.go Outdated
@jay-418

jay-418 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

verification

On success, we should see all http-proxy-lantern be subsumed by service.name=vps-proxy (retaining only the proxy.binary differentiator between this and lantern-box).

query

WITH sub AS (
  SELECT
    service_name,
    COALESCE(NULLIF(host_name, ''), proxy_track) AS host_id,
    COUNTIF(via != '') AS via_rows,
    MAX(ts) AS last_ts
  FROM `lantern-cloud.teleport.base_table`
  WHERE ts >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 6 HOUR)
    AND name = 'proxy.io'
  GROUP BY service_name, host_id
)
SELECT
  service_name,
  COUNTIF(via_rows > 0) AS converted,
  COUNT(*) AS total_seen_6h,
  COUNTIF(last_ts >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 MINUTE))
    AS live_now,
  COUNTIF(via_rows > 0 AND last_ts >=
    TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 MINUTE)) AS converted_live,
  ROUND(COUNTIF(via_rows > 0 AND last_ts >=
    TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 MINUTE)) /
    NULLIF(COUNTIF(last_ts >=
      TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 MINUTE)), 0), 2) AS pct_live
FROM sub
GROUP BY service_name
ORDER BY total_seen_6h DESC

result

[{
  "service_name": "vps-proxy",
  "converted": "895",
  "total_seen_6h": "1058",
  "live_now": "801",
  "converted_live": "800",
  "pct_live": "1.0"
}, {
  "service_name": "http-proxy-lantern",
  "converted": "0",
  "total_seen_6h": "48",
  "live_now": "45",
  "converted_live": "0",
  "pct_live": "0.0"
}]

Warning

service.name flips to vps-proxy with proxy.binary='http-proxy-lantern' so the above query isn't an ideal check.

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (2)

otel/otel.go:121

  • endpointURL always appends the OTLP signal path to any URL-form endpoint. If the provided endpoint already includes /v1/traces or /v1/metrics (a plausible config), this will produce a doubled path like /v1/traces/v1/traces and break exports. Consider making endpointURL idempotent by detecting an existing signalPath suffix before appending.
// endpointURL parses a URL-form endpoint and appends the OTLP signal path
// (e.g. "v1/traces") since WithEndpointURL uses the URL path as-is. Callers
// can pass a base URL like https://collector/otlp and end up hitting
// https://collector/otlp/v1/traces. On parse failure, returns the endpoint
// unchanged.
func endpointURL(endpoint, signalPath string) string {
	u, err := url.Parse(endpoint)
	if err != nil {
		log.Errorf("failed to parse OTEL endpoint %q: %v", endpoint, err)
		return endpoint
	}
	return u.JoinPath(signalPath).String()
}

otel/otel.go:86

  • The merge comment says the runtime attrs "don't overlap with any env-supplied key today", but attrs includes service.name, which can be set via OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES and will be overridden by the env resource in the merge. Updating the comment avoids misleading future readers about the overlap/precedence.
	// over the OS host detector, and the merge below places base after the
	// runtime attrs so env identity also wins over the code's built-in
	// fallbacks. The runtime attrs above (proxy.protocol, is_pro, track, ...)
	// don't overlap with any env-supplied key today.

@jay-418
jay-418 requested a review from Crosse August 3, 2026 21:14

@Crosse Crosse 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.

I'd lean towards making this a little more opinionated than it is now, honestly. If we require that OTEL_EXPORTER_OTLP_ENDPOINT be set in the environment as a full URL, most of this code collapses into just the resource merging bit in buildResource():

  • If there's no scheme, it's an error. (No need for endpointURL() or isSecureHostPort(), and both InitGlobalMeterProvider()'s and BuildTracerProvider()'s diff become subtractive-only.)
  • No need to divine security based on hard-coded port. (No need for isSecureHostPort().)
  • No more manually constructing metrics/trace/logs endpoints; we let the OTel SDK handle constructing them. If endpoints must be set explicitly, the SDK already has it covered via OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT env vars.
  • Get rid of CUSTOM_OTLP_ENDPOINT entirely. We're the only consumers of this code, so we can eliminate any uses of that env var in the same PR.

But I'm not going to hold the PR hostage by requesting changes. 🙂 If you think the idea has merit, great. If not, you're free to merge.

@jay-418

jay-418 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

I'd lean towards making this a little more opinionated than it is now, honestly. If we require that OTEL_EXPORTER_OTLP_ENDPOINT be set in the environment as a full URL, most of this code collapses into just the resource merging bit in buildResource():

  • If there's no scheme, it's an error. (No need for endpointURL() or isSecureHostPort(), and both InitGlobalMeterProvider()'s and BuildTracerProvider()'s diff become subtractive-only.)
  • No need to divine security based on hard-coded port. (No need for isSecureHostPort().)
  • No more manually constructing metrics/trace/logs endpoints; we let the OTel SDK handle constructing them. If endpoints must be set explicitly, the SDK already has it covered via OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT env vars.
  • Get rid of CUSTOM_OTLP_ENDPOINT entirely. We're the only consumers of this code, so we can eliminate any uses of that env var in the same PR.

But I'm not going to hold the PR hostage by requesting changes. 🙂 If you think the idea has merit, great. If not, you're free to merge.

No honestly this is great. I preserved backwards compatibility only from apprehension and deference.

I'd love to strip this down to be normal idiomatic otel setup and I'll do that.

Deployment sets OTEL_EXPORTER_OTLP_ENDPOINT via /etc/http-proxy/otel.env
(loaded by the systemd unit), and the OTel SDK reads that env var
natively — deriving scheme, TLS, and per-signal endpoints without our
help. So the endpoint parsing, TLS-by-port heuristic, and CUSTOM_OTLP_ENDPOINT
fallback all go away.

- Drop Opts.Endpoint, endpointURL(), isSecureHostPort() from otel/.
- Drop getTelemetryEndpoint(), defaultTeleportHost, CUSTOM_OTLP_ENDPOINT
  from http_proxy.go.

Without the env var the SDK falls back to localhost:4318 and the
exporter fails at runtime — the implicit telemetry.iantem.io:443
default is gone.

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (3)

otel/otel.go:150

  • otlpmetrichttp.WithHeaders(opts.Headers) has the same precedence behavior as traces: providing it (even as nil) overrides OTEL_EXPORTER_OTLP_HEADERS / OTEL_EXPORTER_OTLP_METRICS_HEADERS. As-is, env-provided headers from /etc/http-proxy/otel.env won’t be applied for metrics exports. Gate WithHeaders on a non-empty map.
	exp, err := otlpmetrichttp.New(context.Background(),
		otlpmetrichttp.WithHeaders(opts.Headers),
		otlpmetrichttp.WithTemporalitySelector(func(kind sdkmetric.InstrumentKind) metricdata.Temporality {
			switch kind {
			case

otel/otel.go:110

  • otlptracehttp.WithHeaders(opts.Headers) overrides OTEL_EXPORTER_OTLP_HEADERS / OTEL_EXPORTER_OTLP_TRACES_HEADERS from the environment; since opts.Headers is never set (nil), this effectively disables env-provided auth headers loaded from /etc/http-proxy/otel.env. Only apply WithHeaders when the map is non-empty so standard env var headers still work.

This issue also appears on line 146 of the same file.

func BuildTracerProvider(opts *Opts) (*sdktrace.TracerProvider, func()) {
	client := otlptracehttp.NewClient(otlptracehttp.WithHeaders(opts.Headers))

	exporter, err := otlptrace.New(context.Background(), client)

http_proxy.go:679

  • The PR description says CUSTOM_OTLP_ENDPOINT is kept as a fallback for backwards compatibility, but the current changes remove all handling of it (and no longer set/choose any endpoint in code). This means deployments still using CUSTOM_OTLP_ENDPOINT (or using host:port values) will silently stop exporting. Consider mapping CUSTOM_OTLP_ENDPOINT into the standard OTEL_EXPORTER_OTLP_ENDPOINT env var when the standard vars are unset, normalizing host:port into a URL.
func (p *Proxy) buildOTELOpts(includeProxyName bool) *otel.Opts {
	proxyName, provider, dc := p.ProxyName, p.Provider, p.DC
	if dc == "" {
		// This proxy is running on the old infrastructure, parse the name to get the dc
		proxyName, dc = proxyNameAndDC(p.ProxyName)
	}
	opts := &otel.Opts{

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
http_proxy.go (1)

679-688: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve environment-derived OTLP headers.

buildOTELOpts leaves otel.Opts.Headers nil. Both otel/otel.go callers pass this map explicitly via WithHeaders, so if OTEL_EXPORTER_OTLP_HEADERS or signal-specific headers supply required metadata, those headers are dropped for trace and metric exports. Only call WithHeaders when opts.Headers is non-nil, or merge explicit headers with the environment-derived header map.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http_proxy.go` around lines 679 - 688, Update buildOTELOpts to preserve
environment-derived OTLP headers by populating or merging opts.Headers with the
headers used by the otel/otel.go callers; ensure WithHeaders is invoked only
when opts.Headers is non-nil so trace and metric exports retain environment and
signal-specific metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@http_proxy.go`:
- Line 640: Update the OTEL initialization paths around BuildTracerProvider and
configureOTELMetrics to require OTEL_EXPORTER_OTLP_ENDPOINT before starting
http-proxy. Validate the value from the systemd environment and surface a clear
deployment error when it is absent, ensuring the exporter cannot silently fall
back to https://localhost:4318.

---

Outside diff comments:
In `@http_proxy.go`:
- Around line 679-688: Update buildOTELOpts to preserve environment-derived OTLP
headers by populating or merging opts.Headers with the headers used by the
otel/otel.go callers; ensure WithHeaders is invoked only when opts.Headers is
non-nil so trace and metric exports retain environment and signal-specific
metadata.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7d1126c-ceb3-47b6-8754-ba293b46f501

📥 Commits

Reviewing files that changed from the base of the PR and between cfd6005 and e94d5a8.

📒 Files selected for processing (2)
  • http_proxy.go
  • otel/otel.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • otel/otel.go

Comment thread http_proxy.go
jay-418 added 2 commits August 4, 2026 12:51
Removing the telemetry.iantem.io:443 default without an assertion turned
a misconfiguration into a silent telemetry outage — otlptracehttp /
otlpmetrichttp default to localhost:4318 and construction never fails.
Log the resolved endpoint at startup, and log a loud error if neither
the per-signal nor the generic OTEL_EXPORTER_OTLP_ENDPOINT env var is
set.

Also drop the unused Opts.Headers field. Headers now come from the
standard OTEL_EXPORTER_OTLP_HEADERS env var, same as the endpoint.
The prior comment claimed the runtime attrs "don't overlap with any
env-supplied key today," but service.name is exactly such an overlap
(and env winning is intentional). Reword to be honest about the one
overlap and clarify which attrs actually can't collide.
@jay-418
jay-418 requested a review from Crosse August 4, 2026 19:07
@jay-418
jay-418 merged commit c6c51e6 into main Aug 4, 2026
2 checks passed
@jay-418
jay-418 deleted the jay/http-proxy-otel branch August 4, 2026 19:24
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