Skip to content

JEP-0013 Phase 3 - operator image - #1061

Open
RoddieKieley wants to merge 3 commits into
jumpstarter-dev:mainfrom
RoddieKieley:jep-0013-phase3-operator-image
Open

RoddieKieley wants to merge 3 commits into
jumpstarter-dev:mainfrom
RoddieKieley:jep-0013-phase3-operator-image

Conversation

@RoddieKieley

@RoddieKieley RoddieKieley commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

JEP-0013 Phase 3 PR B**: make the MetricsStream hub from PR A runnable in-cluster.

Depends on PR A: #1060

Do not merge until A is on main, then rebase this branch onto main. Related PRs C–E will be linked here as they are opened.

  • Build and copy /telemetry in controller/Containerfile and make build / docker-build-ci (operator already Command: ["/telemetry"]; main still only ships manager + router).
  • Expose telemetry HTTP metrics on Service/container port 8080; /healthz and /readyz probes on that port (replacing TCP probes on gRPC :9093).
  • CR: spec.telemetry.metrics.scrapeTimeout, driverTypeEnum, exemplarKeys. Defaults match JEP-0013 (7s, the driver-type enum, client,lease_id). No ServiceMonitor (Phase 5).
  • Set GRPC_TELEMETRY_ENDPOINT so the telemetry process advertises the in-cluster Service DNS.

TLS: #1023 already landed telemetry TLS on main. This PR keeps that path (cert-manager or manual spec.telemetry.grpc.tls.certSecret, rolling-restart hash, non-fatal missing CA). It does not reimplement TLS.

Lab: in-cluster reverse-scrape of GET /metrics on Service port 8080 worked (port-forward). After driver ops, Python OpenMetrics exemplars still fail Go parseMetricFamilies; the hub from A omits that snapshot and increments jumpstarter_metrics_parse_errors_total. spec.telemetry.metrics.exemplarKeys only allowlists keys on the merge path after a successful parse — it does not fix exemplar decode. That remains A / JEP DD-3, not this PR.

DEMO

The lab walkthrough for the stacked Phase 3 work (A+B+C+D+E plus lab-only Route) is on the fork demo branch, not this PR:

See MetricsStream during the lease for the DD-3 exemplar limitation observed on :8080.

How this PR fits the series

flowchart TB
  A["PR A #1060: proto + hub"]
  B["PR B this PR: image + metrics port + scrape CR"]
  C["PR C: exporter MetricsStream client"]
  D["PR D: Loki HTTP push"]
  E["PR E: jmp PushLogs"]
  A --> B
  A --> C
  B --> D
  D --> E
Loading
PR Branch Status
A jep-0013-phase3-metricsstream #1060
B jep-0013-phase3-operator-image This PR
C jep-0013-phase3-exporter-metricsstream #1062
D jep-0013-phase3-loki-push #1063
E jep-0013-phase3-client-pushlogs #1064

Unique work vs PR A: the operator/image commit on this branch (435eebee). If GitHub shows A's commits, that is stacking against main; review that unique commit only.

Out of scope

  • MetricsStream protocol and parse-error omit (A / JEP-0013 Phase 3 - MetricsStream #1060)
  • Exporter MetricsStream client and Register identity (C) — after #1058, identity must be exporter_name, not Metadata.name
  • Loki / PushLogs (D, E), including exporter TFTP errors that only show up on the log path
  • Advertising a Route/hostname for out-of-cluster exporters (lab-only on the demo branch)
  • ServiceMonitor (Phase 5)

NOTE

Open #1027 also touches telemetry operator files (log-ingest e2e).

RoddieKieley and others added 3 commits September 2, 2026 10:39
Add the MetricsStream protocol and Go hub so Prometheus can scrape
merged exporter OpenMetrics from telemetry without an exporter client
yet. Generated Python stubs are included for proto consistency.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stop silently dropping unparseable exporter snapshots. Log the exporter and
error, and increment jumpstarter_metrics_parse_errors_total so reverse-scrape
omissions are visible on the same /metrics response.

Co-authored-by: Cursor <cursoragent@cursor.com>
…e 3)

Build /telemetry into the controller image and have the operator mount
cert-manager TLS, advertise the CA, and expose scrape flags plus the
HTTP metrics port so reverse-scrape can run in-cluster.
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 54 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: e8a9202e-3561-4b2c-9e70-c2cd15edac03

📥 Commits

Reviewing files that changed from the base of the PR and between f1b7f24 and 435eebe.

⛔ Files ignored due to path filters (2)
  • controller/internal/protocol/jumpstarter/v1/telemetry.pb.go is excluded by !**/*.pb.go
  • controller/internal/protocol/jumpstarter/v1/telemetry_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (23)
  • controller/Containerfile
  • controller/Makefile
  • controller/cmd/telemetry/main.go
  • controller/deploy/operator/api/v1alpha1/jumpstarter_types.go
  • controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go
  • controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml
  • controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go
  • controller/deploy/operator/internal/controller/jumpstarter/telemetry.go
  • controller/deploy/operator/internal/controller/jumpstarter/telemetry_phase3_test.go
  • controller/deploy/operator/internal/controller/jumpstarter/telemetry_test.go
  • controller/image_layout_test.go
  • controller/internal/service/metrics_merge.go
  • controller/internal/service/metrics_merge_test.go
  • controller/internal/service/metrics_stream.go
  • controller/internal/service/metrics_stream_test.go
  • controller/internal/service/telemetry_http.go
  • controller/internal/service/telemetry_identity.go
  • controller/internal/service/telemetry_service.go
  • protocol/proto/jumpstarter/v1/telemetry.proto
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/telemetry_pb2.py
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/telemetry_pb2.pyi
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/telemetry_pb2_grpc.py
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/telemetry_pb2_grpc.pyi

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.

@raballew raballew left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a few comments for now. Waiting for merge of the first PR.

Comment on lines +326 to +332
return []string{
fmt.Sprintf("--grpc-bind=:%d", telemetryPort),
fmt.Sprintf("-metrics-bind-address=:%d", telemetryMetricsPort),
fmt.Sprintf("-scrape-timeout=%s", timeout),
fmt.Sprintf("-driver-type-enum=%s", strings.Join(enum, ",")),
fmt.Sprintf("-exemplar-keys=%s", strings.Join(keys, ",")),
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Mixed use of dash and double-dash

Comment on lines +342 to +345
// Max wait for parallel exporter MetricsStream responses during a /metrics fan-out.
// Should be lower than the Prometheus scrape_timeout.
// +kubebuilder:default="7s"
ScrapeTimeout *metav1.Duration `json:"scrapeTimeout,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also set an upper limit to avoid keeping connections open for very long times.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

2 minutes max? (and sounds excessive)

Comment on lines 280 to 284
srv := grpc.NewServer(
grpc.Creds(creds),
grpc.ChainUnaryInterceptor(recovery.UnaryServerInterceptor()),
grpc.ChainStreamInterceptor(recovery.StreamServerInterceptor()),
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Adding grpc.MaxRecvMsgSize to the server options would provide an explicit limit especially if you are running 1000s of exporters that are scraped concurrently (each with up to 4 MB per message). Otherwise this might cause some OOM killed processes.

Comment on lines 107 to 116
func (s *TelemetryService) PushLogs(ctx context.Context, req *pb.PushLogsRequest) (*pb.PushLogsResponse, error) {
token, err := authentication.BearerTokenFromContext(ctx)
id, err := s.authenticateExporter(ctx)
if err != nil {
return nil, err
}

// Validate token and extract the subject (format: exporter:namespace:name:uid).
subject, err := s.Signer.ParseSubject(token)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}

// Only exporter tokens are allowed to push logs. Any other validly-signed
// token (e.g. a client token) is rejected immediately so that the identity
// checks below always have a non-empty claimedName/claimedNamespace.
parts := strings.SplitN(subject, ":", 4)
if len(parts) != 4 || parts[0] != "exporter" {
return nil, status.Errorf(codes.PermissionDenied, "token is not an exporter token")
}
claimedNamespace := parts[1]
claimedName := parts[2]
if claimedNamespace == "" || claimedName == "" {
return nil, status.Errorf(codes.PermissionDenied, "token has incomplete exporter identity")
}
claimedNamespace := id.namespace
claimedName := id.name

// Use context-based logger so tests can inject their own via logf.IntoContext.
logger := log.FromContext(ctx).WithName("telemetry")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

entry.Message, entry.Component, entry.Severity, entry.Lease, entry.Client, entry.Operation, entry.Result, entry.DriverType have no size bound. Should we truncate them too?

Comment on lines +334 to +341
// Allowlist of keys to include in Prometheus exemplars. Unlisted keys are omitted.
// +kubebuilder:default={"client","lease_id"}
ExemplarKeys []string `json:"exemplarKeys,omitempty"`

// Allowed driver_type label values. Unlisted types are remapped to "other".
// +kubebuilder:default={"power","storage","network","serial","console","video","composite"}
DriverTypeEnum []string `json:"driverTypeEnum,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A user with CR write access can set either list to an arbitrarily large array for ExemplarKeys []string and DriverTypeEnum []string. Adding // +kubebuilder:validation:MaxItems=<insert a good limit here> and // +kubebuilder:validation:MaxLength=<insert a good limit here> per item would enforce a bound.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could be worth limiting to 16 for example? /8 default + 8 custom, more than enough.

@bennyz
bennyz requested a review from bkhizgiy September 16, 2026 12:09
maboras-rh pushed a commit to maboras-rh/jumpstarter that referenced this pull request Sep 22, 2026
## Summary

JEP-0013 Phase 3 PR A**: the `MetricsStream` contract and the telemetry
reverse-scrape hub. This is the merge-base for the rest of the Phase 3
series.

Related PRs will be linked here as they are opened (B, C, D, E). This PR
stays draft until those links are filled in.

- Add `MetricsStream` to `telemetry.proto` (register + scrape
request/response) and regenerate Go + Python stubs.
- Telemetry fans out scrapes over connected streams, merges OpenMetrics,
remaps unknown `driver_type` to `other`, and serves `GET /metrics` on a
dedicated HTTP port (not gRPC `:9093`).
- `/healthz` and `/readyz` as in DD-7.
`jumpstarter_scrape_timeouts_total` on scrape timeout.
- **Lab follow-up in the same PR:** stop silently dropping unparseable
exporter snapshots. Log `exporter metrics snapshot omitted` and
increment `jumpstarter_metrics_parse_errors_total{exporter}` on the same
`/metrics` response. Python `prometheus_client` OpenMetrics exemplars
(`# {lease_id=...}`) still fail `parseMetricFamilies`; the snapshot is
omitted so one exporter cannot 500 the hub. Fixing OpenMetrics/exemplar
parse is a later pass — not this PR.

No exporter client, no operator/image/CRD, no Loki in this PR.

## DEMO

An asciinema demo for this combined Phase 3 work is available in the[
jep-0013-phase3-demo
branch](https://github.com/RoddieKieley/jumpstarter/tree/jep-0013-phase3-demo)
in my repository. Best to [read the description in the DEMO.md
there](https://github.com/RoddieKieley/jumpstarter/blob/jep-0013-phase3-demo/DEMO.md)
and then watch (manually for now) the asciinema file contained at the
end -
[jep-0013-phase3-demo.cast](https://github.com/RoddieKieley/jumpstarter/blob/jep-0013-phase3-demo/jep-0013-phase3-demo.cast)

### IMPORTANT NOTES

* See [MetricsStream during the
lease](https://github.com/RoddieKieley/jumpstarter/blob/jep-0013-phase3-demo/DEMO.md#metricsstream-during-the-lease)
for an important caveat about the implementation about a potential
limitation pointed out in the original JEP-0013 DD-3 regarding potential
limitations for exemplar support.
* Register identity must be exporter_name after jumpstarter-dev#1058 (not Metadata.name
/ "unknown"). That is in PR C.

## How this PR fits the series

```mermaid
flowchart TB
  A["PR A this PR: proto + hub"]
  B["PR B: image + metrics port + scrape CR"]
  C["PR C: exporter MetricsStream client"]
  D["PR D: Loki HTTP push"]
  E["PR E: jmp PushLogs"]
  A --> B
  A --> C
  B --> D
  D --> E
```

| PR | Branch | Status |
|----|--------|--------|
| **A** | `jep-0013-phase3-metricsstream` | **This PR** |
| B | `jep-0013-phase3-operator-image` | jumpstarter-dev#1061  |
| C | `jep-0013-phase3-exporter-metricsstream` | jumpstarter-dev#1062  |
| D | `jep-0013-phase3-loki-push` | jumpstarter-dev#1063  |
| E | `jep-0013-phase3-client-pushlogs` | jumpstarter-dev#1064  |

Reverse-scrape is useful only after **A+B+C**. This PR is still
reviewable alone with mock streams.

## Data flow (this PR)

```mermaid
sequenceDiagram
  participant Prom as Prometheus
  participant Tel as jumpstarter-telemetry
  participant Exp as Exporter stream (PR C)
  Prom->>Tel: GET /metrics
  Tel->>Exp: MetricsScrapeRequest
  Exp-->>Tel: OpenMetrics snapshot
  alt parse OK
    Tel-->>Prom: merged families + scrape_timeouts
  else OpenMetrics exemplar parse fail
    Tel-->>Prom: snapshot omitted + parse_errors_total
  end
```

## Out of scope / later passes

- Exporter client (C), operator image/metrics port (B), Loki (D), `jmp`
logs (E)
- OpenMetrics exemplar parse (hub still omits those snapshots; now
visible)
- `ServiceMonitor` (Phase 5), driver telemetry API (Phase 4),
multi-replica sticky streams (DD-8)
- Out-of-cluster telemetry Route

## NOTES
### Known merge work (not new features)

* jumpstarter-dev#1062 conflicts with main on exporter.py because of
[jumpstarter-dev#1059](jumpstarter-dev#1059). When
C rebases, keep identity=exporter_name, _session_labels(), and jumpstarter-dev#1059’s
exporter_name= on Session(...).
* [jumpstarter-dev#1027](jumpstarter-dev#1027)
still overlaps operator telemetry files with B/D.
The series is still based on jumpstarter-dev#1058. After A lands, rebase the rest onto
current main (jumpstarter-dev#1059) as a set.

### Explicitly later (already called out as out of scope)

* OpenMetrics exemplar parse (A omits those snapshots; JEP DD-3)
* ServiceMonitor (Phase 5), driver telemetry API (Phase 4)
* Out-of-cluster telemetry Route (demo-only)
* Changing PushLogs logger.Error(nil, …) for exporter TFTP errors
* Cosmetic only: jumpstarter-dev#1060 still has the[ jep-0013-phase3-demo.

---------

Signed-off-by: Roddie Kieley <rkieley@redhat.com>
Signed-off-by: Miguel Angel Ajo Pelayo <miguelangel@ajo.es>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Miguel Angel Ajo Pelayo <miguelangel@ajo.es>

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.

3 participants