Skip to content

feat: structured log gathering from exporters (JEP-0013) - #930

Open
bkhizgiy wants to merge 7 commits into
jumpstarter-dev:mainfrom
bkhizgiy:exporter-logs
Open

feat: structured log gathering from exporters (JEP-0013)#930
bkhizgiy wants to merge 7 commits into
jumpstarter-dev:mainfrom
bkhizgiy:exporter-logs

Conversation

@bkhizgiy

Copy link
Copy Markdown
Member

Exporters now push structured logs to an optional jumpstarter-telemetry service, giving operators a single place to query what happened during any lease , which pipeline ran it, what driver operation failed, and with what result.

What changed:
New proto RPCs - GetServiceEndpoints lets exporters discover the telemetry service after registration. TelemetryService.PushLogs receives batched structured log entries.

Controller config - A new telemetry section in the controller ConfigMap (endpoint, certificate, min_severity) tells authenticated exporters where to send logs.

jumpstarter-telemetry service - A standalone Go binary that receives log entries and writes them to structured stdout. Ready to forward to Loki in a future phase without any protocol changes.

TelemetryLogHandler (Python) - A logging.Handler attached to the root logger after registration. It pulls correlation fields from structlog contextvars (lease_id, client, exporter) and spec.context fields (build_id, pipeline, etc.), batches them, and flushes every 500ms via PushLogs. Telemetry failures are silently dropped — device operations are never blocked.

What you can do with it:
A single log query like {lease="telemetry-test-4"} returns every event from that lease's full lifecycle. {pipeline="ci-main"} finds all leases across any run tagged with that pipeline. The correlation data answers questions like "which pipeline was running on this lease?" and "what driver operation failed?" without joining multiple sources.

bkhizgiy added 3 commits July 30, 2026 12:20
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Bella Khizgiyaev <bkhizgiy@redhat.com>
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Bella Khizgiyaev <bkhizgiy@redhat.com>
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Bella Khizgiyaev <bkhizgiy@redhat.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds telemetry protobuf APIs, authenticated endpoint discovery, a standalone telemetry gRPC process, exporter log delivery, telemetry configuration wiring, generated Python bindings, and a stricter ExporterSet driver schema.

Changes

Telemetry collection

Layer / File(s) Summary
Telemetry and endpoint RPC contracts
protocol/proto/jumpstarter/v1/*, python/packages/jumpstarter-protocol/...
Adds telemetry ingestion and optional endpoint-discovery contracts with generated Go/Python bindings.
Controller telemetry configuration and discovery
controller/internal/config/*, controller/internal/service/controller_service.go, controller/cmd/main.go, controller/internal/service/telemetry_service_test.go
Loads telemetry settings, wires them into ControllerService, authenticates discovery requests, and applies endpoint defaults.
Telemetry service process and log ingestion
controller/internal/service/telemetry_service.go, controller/cmd/telemetry/main.go, controller/internal/service/telemetry_service_test.go
Adds the gRPC server, bounded log ingestion, structured metadata, shutdown handling, and tests.
Exporter telemetry batching and lifecycle
python/packages/jumpstarter/jumpstarter/exporter/*
Discovers endpoints, configures TLS or insecure transport, attaches a logging handler, batches logs, flushes them, and closes telemetry resources.
Required driver name in ExporterSet CRD
controller/deploy/operator/config/crd/bases/virtualtarget.jumpstarter.dev_exportersets.yaml
Requires drivers[].name alongside drivers[].type.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Exporter
  participant ControllerService
  participant TelemetryLogHandler
  participant TelemetryService
  Exporter->>ControllerService: GetServiceEndpoints
  ControllerService-->>Exporter: TelemetryEndpoint
  Exporter->>TelemetryLogHandler: configure logging handler
  TelemetryLogHandler->>TelemetryService: PushLogs batch
  TelemetryService-->>Exporter: accepted and dropped counts
Loading

Possibly related PRs

Suggested reviewers: mangelajo

Poem

I’m a rabbit with logs in a queue,
Hopping telemetry channels anew.
Endpoints bloom, batches take flight,
Structured fields glow in the night.
One final flush, and I’m through!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: structured log gathering from exporters.
Description check ✅ Passed The description matches the changeset and accurately describes the new telemetry and logging flow.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Bella Khizgiyaev <bkhizgiy@redhat.com>
@bkhizgiy

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Actionable comments posted: 9

🧹 Nitpick comments (1)
python/packages/jumpstarter/jumpstarter/exporter/telemetry_test.py (1)

1-267: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an end-to-end test alongside the mocked unit tests.

All tests here mock TelemetryServiceStub.PushLogs. Per repo guidelines for *_test.py, end-to-end tests that start a real server and client should be prioritized, with mocks reserved for cases where e2e is impractical. Since TelemetryServiceServicer can be run in-process with a local gRPC server, an e2e test validating a full TelemetryLogHandler → real stub → real servicer round trip would strengthen coverage of the wire contract (serialization, field mapping) beyond what mocks can verify.

As per coding guidelines, "Provide comprehensive package test coverage, prioritizing end-to-end tests that start a server and client; use mocks when system tools, services, or platform compatibility make end-to-end testing impractical."

🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/telemetry_test.py` around
lines 1 - 267, Add an end-to-end test in TestFlush that starts an in-process
local gRPC server with TelemetryServiceServicer, creates a real
TelemetryServiceStub and TelemetryLogHandler, emits a record, and flushes it.
Assert the servicer receives the serialized entry with the expected message,
severity, and mapped fields, while keeping mocked tests for failure and queue
behavior.

Source: Coding guidelines

🤖 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 `@controller/cmd/telemetry/main.go`:
- Around line 78-86: Update the signal-handling branch in main’s select to wait
for the telemetry service shutdown after calling cancel(). Receive from errCh
before returning so TelemetryService.Start() can execute GracefulStop() and
drain active RPCs; preserve the existing error logging and exit behavior for
service failures.
- Around line 66-72: Update the telemetry service startup around
TelemetryService and svc.Start to configure TLS for the ingress listener,
require and validate client certificates, and authorize authenticated clients
before accepting batches. Ensure discovery publishes the matching client
credentials and TLS settings so exporters connect securely without insecure
mode; preserve the existing service lifecycle and error propagation.

In `@controller/internal/service/telemetry_service.go`:
- Around line 85-111: Update TelemetryService.Start to configure grpc.NewServer
with TLS credentials derived from TelemetryConfig.Certificate and its
corresponding private key before registering the service, ensuring the server
uses the same certificate advertised by GetServiceEndpoints. Preserve the
existing lifecycle and graceful-shutdown behavior, and propagate
certificate-loading or server-setup errors.
- Around line 43-82: Bound unauthenticated batches in TelemetryService.PushLogs
by enforcing a server-side maximum number of entries before iterating and
logging them. Process only entries within the limit, report the remainder
through the response’s Dropped field, and preserve Accepted for processed
entries; use an established configuration or constant for the limit if
available.

In `@protocol/proto/jumpstarter/v1/telemetry.proto`:
- Around line 30-40: Update TelemetryService.PushLogs to preserve each log
entry’s Timestamp and Severity in the sink output. Use entry.Timestamp as the
emitted event timestamp rather than ingestion time, and retain entry.Severity as
a structured field so warning and debug entries remain distinguishable.

In
`@python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py`:
- Around line 33-34: Regenerate client_pb2.py and the related protobuf classes
using one consistent protoc/grpcio-tools configuration, ensuring its
kubernetes_pb2 and common_pb2 imports match the relative ..v1 imports used by
the other generated pb2 files. Do not hand-edit the generated output; update the
generation process or invocation if needed, then regenerate the affected files
consistently.

In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 1181-1188: Update the shutdown flow around
_telemetry_handler.close_async() to drain all queued telemetry batches before
closing _telemetry_channel. Repeat flushing until the handler’s queue is empty,
then proceed with the existing handler removal and channel closure.
- Around line 408-434: Update the telemetry setup around TelemetryLogHandler so
it receives ep.min_severity when constructed, and enforce that threshold when
processing exporter records. Preserve the existing channel selection and handler
registration, ensuring records below the controller-advertised minimum severity
are filtered out.
- Around line 393-397: Bound both best-effort telemetry RPCs with _RPC_TIMEOUT:
pass the deadline when awaiting GetServiceEndpoints in the exporter registration
flow, and apply the same deadline to PushLogs in TelemetryLogHandler._flush().
Preserve existing error handling while ensuring registration, periodic flushing,
and shutdown cannot wait indefinitely.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/telemetry_test.py`:
- Around line 1-267: Add an end-to-end test in TestFlush that starts an
in-process local gRPC server with TelemetryServiceServicer, creates a real
TelemetryServiceStub and TelemetryLogHandler, emits a record, and flushes it.
Assert the servicer receives the serialized entry with the expected message,
severity, and mapped fields, while keeping mocked tests for failure and queue
behavior.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7210c4a5-f9a9-4494-a887-2484a984d27d

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb0364 and df77e4d.

⛔ Files ignored due to path filters (4)
  • controller/internal/protocol/jumpstarter/v1/jumpstarter.pb.go is excluded by !**/*.pb.go
  • controller/internal/protocol/jumpstarter/v1/jumpstarter_grpc.pb.go is excluded by !**/*.pb.go
  • 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/cmd/main.go
  • controller/cmd/telemetry/main.go
  • controller/deploy/operator/config/crd/bases/virtualtarget.jumpstarter.dev_exportersets.yaml
  • controller/internal/config/config.go
  • controller/internal/config/types.go
  • controller/internal/service/controller_service.go
  • controller/internal/service/telemetry_service.go
  • controller/internal/service/telemetry_service_test.go
  • protocol/proto/jumpstarter/v1/jumpstarter.proto
  • protocol/proto/jumpstarter/v1/telemetry.proto
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/__init__.py
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.pyi
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2_grpc.py
  • python/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2_grpc.pyi
  • 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
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • python/packages/jumpstarter/jumpstarter/exporter/telemetry.py
  • python/packages/jumpstarter/jumpstarter/exporter/telemetry_test.py

Comment thread controller/cmd/telemetry/main.go
Comment thread controller/cmd/telemetry/main.go
Comment thread controller/internal/service/telemetry_service.go
Comment thread controller/internal/service/telemetry_service.go
Comment thread protocol/proto/jumpstarter/v1/telemetry.proto
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Bella Khizgiyaev <bkhizgiy@redhat.com>
@bkhizgiy

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

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)
python/packages/jumpstarter/jumpstarter/exporter/exporter.py (1)

404-414: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep all optional telemetry setup failures non-fatal.

The handler catches only grpc.aio.AioRpcError from endpoint discovery. channel_factory() and the subsequent telemetry channel/handler setup can raise other exceptions; since _register_with_controller() awaits this after registration, those failures can abort exporter startup despite telemetry being optional. Wrap the complete setup in best-effort handling and clean up any partially created telemetry resources.

Also applies to: 429-447

🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py` around lines
404 - 414, Update _register_with_controller around endpoint discovery and
telemetry setup so every optional telemetry failure, including channel_factory
and channel/handler construction errors, is caught and cannot abort exporter
startup. Wrap the complete setup in best-effort exception handling and clean up
any partially created telemetry resources before returning on failure, while
preserving successful registration behavior.
🤖 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 `@controller/internal/service/telemetry_service.go`:
- Around line 120-122: Update the context-cancellation branch around
srv.GracefulStop() to normalize grpc.ErrServerStopped from errCh as a successful
shutdown when ctx is canceled. Preserve and return other Serve errors unchanged.

---

Outside diff comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 404-414: Update _register_with_controller around endpoint
discovery and telemetry setup so every optional telemetry failure, including
channel_factory and channel/handler construction errors, is caught and cannot
abort exporter startup. Wrap the complete setup in best-effort exception
handling and clean up any partially created telemetry resources before returning
on failure, while preserving successful registration behavior.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fea4c4f-3b1c-4e0a-818d-db584454eee2

📥 Commits

Reviewing files that changed from the base of the PR and between df77e4d and 2c998b6.

📒 Files selected for processing (6)
  • controller/cmd/telemetry/main.go
  • controller/internal/config/config.go
  • controller/internal/config/types.go
  • controller/internal/service/telemetry_service.go
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • python/packages/jumpstarter/jumpstarter/exporter/telemetry.py

Comment thread controller/internal/service/telemetry_service.go Outdated
bkhizgiy added 2 commits July 30, 2026 15:36
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Bella Khizgiyaev <bkhizgiy@redhat.com>
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Bella Khizgiyaev <bkhizgiy@redhat.com>
@bkhizgiy

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
python/packages/jumpstarter/jumpstarter/exporter/exporter.py (1)

404-414: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Keep optional telemetry setup non-fatal.

Only AioRpcError is handled. An ordinary exception from _controller_stub() can escape after registration and abort the exporter, despite telemetry being best-effort. Catch Exception here as well, while allowing cancellation (BaseException) through.

Proposed fix
         except grpc.aio.AioRpcError as e:
             logger.debug("GetServiceEndpoints unavailable: %s", e.code())
             return
+        except Exception:
+            logger.debug("GetServiceEndpoints setup failed", exc_info=True)
+            return
🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py` around lines
404 - 414, Update the GetServiceEndpoints setup around _controller_stub() to
catch ordinary Exception failures in addition to grpc.aio.AioRpcError, log the
failure at debug level, and return without aborting exporter registration. Keep
cancellation and other BaseException subclasses propagating, and preserve the
existing successful response flow.
🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter_telemetry_test.py`:
- Around line 33-42: Update the telemetry setup tests using make_bare_exporter
to track the root logger handler created by each test, then add teardown cleanup
that removes and closes that handler after every test. Ensure cleanup runs even
when a test fails and does not affect unrelated root logger handlers.

In `@python/packages/jumpstarter/jumpstarter/exporter/telemetry.py`:
- Around line 134-137: Update close_async so shutdown does not retry every
queued batch after telemetry becomes unavailable. Track whether _flush()
successfully pushed the batch and stop draining immediately after the first
failure, or enforce a single overall shutdown deadline while preserving the
existing flush behavior.

---

Outside diff comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 404-414: Update the GetServiceEndpoints setup around
_controller_stub() to catch ordinary Exception failures in addition to
grpc.aio.AioRpcError, log the failure at debug level, and return without
aborting exporter registration. Keep cancellation and other BaseException
subclasses propagating, and preserve the existing successful response flow.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f51c6ad4-021b-4305-b619-e4e28a24c309

📥 Commits

Reviewing files that changed from the base of the PR and between 2c998b6 and 264e112.

📒 Files selected for processing (6)
  • controller/cmd/telemetry/main.go
  • controller/internal/service/telemetry_service.go
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_telemetry_test.py
  • python/packages/jumpstarter/jumpstarter/exporter/telemetry.py
  • python/packages/jumpstarter/jumpstarter/exporter/telemetry_test.py

Comment on lines +33 to +42
def make_bare_exporter():
"""Minimal Exporter instance for testing _setup_telemetry in isolation."""
exp = Exporter.__new__(Exporter)
exp._telemetry_handler = None
exp._telemetry_channel = None

tls = MagicMock()
tls.insecure = False
exp.tls = tls
return exp

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clean up root handlers after each setup test.

Successful setup tests register a handler on the global root logger but never remove it. Handlers accumulate across the module and can capture later tests’ logs. Add teardown that removes and closes the handler created by each test.

🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter_telemetry_test.py`
around lines 33 - 42, Update the telemetry setup tests using make_bare_exporter
to track the root logger handler created by each test, then add teardown cleanup
that removes and closes that handler after every test. Ensure cleanup runs even
when a test fails and does not affect unrelated root logger handlers.

Comment on lines +134 to +137
async def close_async(self) -> None:
"""Flush all remaining entries and close the handler."""
while self._queue:
await self._flush()

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not multiply shutdown time by queued batches.

_flush() drops a failed batch but hides the failure. With telemetry unavailable, this loop waits up to 10 seconds for each queued batch before exit. Stop draining after the first failed push, or apply one total shutdown deadline.

Proposed fix
-    async def _flush(self) -> None:
+    async def _flush(self) -> bool:
         ...
         try:
             await self._stub.PushLogs(
                 telemetry_pb2.PushLogsRequest(entries=batch),
                 timeout=_PUSH_TIMEOUT,
             )
         except Exception as exc:
             print(f"[telemetry] PushLogs failed, {len(batch)} entries dropped: {exc}", file=sys.stderr)
+            return False
+        return True

     async def close_async(self) -> None:
         """Flush all remaining entries and close the handler."""
         while self._queue:
-            await self._flush()
+            if not await self._flush():
+                break
         self.close()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def close_async(self) -> None:
"""Flush all remaining entries and close the handler."""
while self._queue:
await self._flush()
async def close_async(self) -> None:
"""Flush all remaining entries and close the handler."""
while self._queue:
if not await self._flush():
break
self.close()
🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/telemetry.py` around lines
134 - 137, Update close_async so shutdown does not retry every queued batch
after telemetry becomes unavailable. Track whether _flush() successfully pushed
the batch and stop draining immediately after the first failure, or enforce a
single overall shutdown deadline while preserving the existing flush behavior.

@bkhizgiy
bkhizgiy marked this pull request as ready for review July 30, 2026 14:40

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

Mostly nit-pick type comments on minor details for the moment. May have more detailed comments after building the branch and testing it in-cluster.

@@ -0,0 +1,42 @@
// Copyright 2024 The Jumpstarter Authors

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Am guessing that if this is a new protobuf definition file then this should be 2026?

"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
Copyright 2024 The Jumpstarter Authors"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

s/2024/2026 from the proto before generation perhaps?

"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
Copyright 2024 The Jumpstarter Authors"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

s/2024/2026 from the proto generation perhaps?

var configmap corev1.ConfigMap
if err := client.Get(ctx, key, &configmap); err != nil {
return nil, "", nil, nil, nil, nil, nil, err
return nil, "", nil, nil, nil, nil, nil, nil, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I've noticed that the LLM's don't mind not being dry, an in this case there's quite a few of this particular return value repeated. However prior to this code change it was only 1 nil different so for the moment it's probably fine without the refactor. This is probably one of those areas where adopting some clean code type rules to guide the coding agent up front could help.

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.

Yes, a few PRs ago we had the same conversation with HiddenLabels.

It could be worth to return all this as a config structure, and then just pass the structure to the controller later in https://github.com/jumpstarter-dev/jumpstarter/pull/930/changes#diff-032eafab48326b32dad354dd24c3da532106bde78b3589793bd8fdc47be0bb65R283 as a single thing, we could also return "nil" config :) single nil

@@ -0,0 +1,88 @@
/*
Copyright 2024.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

s/2024/2026?

@@ -0,0 +1,111 @@
/*
Copyright 2024.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

s/2024/2026?

@@ -0,0 +1,200 @@
/*
Copyright 2024.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

s/2024/2026?


_BATCH_SIZE = 50
_FLUSH_INTERVAL = 0.5 # seconds between automatic flushes
_MAX_QUEUE_SIZE = 10_000 # entries; oldest are dropped when full

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Any particular reason these magic numbers were chosen? Could be useful to leave a comment relating to the why if there were any particular reason they were chosen. Otherwise the existing two comments are helpful.


_MAX_EXTRA_FIELDS = 16
_MAX_KEY_LEN = 64
_MAX_VAL_LEN = 256

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same as above regarding number choices. The LLM can likely connect the dots but a new human to the codebase might wonder.

Certificate string `json:"certificate,omitempty" yaml:"certificate,omitempty"`

// MinSeverity is the minimum log severity to forward to the telemetry service.
// Accepted values: debug, info, warning, error. Defaults to "info" when empty.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this correct? Is there also a critical that is mentioned elsewhere in the code?
"
"""Normalise Python logging level names to JEP-0013 severity strings."""
return {
"DEBUG": "debug",
"INFO": "info",
"WARNING": "warning",
"ERROR": "error",
"CRITICAL": "critical",
"

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.

def _severity_to_level(severity: str) -> int:

@RoddieKieley

Copy link
Copy Markdown

@bkhizgiy I ran up your exporter-logs branch on top of my local setup here and confirm local exporter logging output:
Screenshot From 2026-07-31 08-42-05

matches the log output as viewed via LogQL inside the local openshift sno cluster:
Screenshot From 2026-07-31 08-42-15

I did have to manually configure and expose a Service to connect to the jumpstarter-telemetry pods endpoint but I think that's expected at this point of the phased implementation of JEP-0013.

var configmap corev1.ConfigMap
if err := client.Get(ctx, key, &configmap); err != nil {
return nil, "", nil, nil, nil, nil, nil, err
return nil, "", nil, nil, nil, nil, nil, nil, err

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.

Yes, a few PRs ago we had the same conversation with HiddenLabels.

It could be worth to return all this as a config structure, and then just pass the structure to the controller later in https://github.com/jumpstarter-dev/jumpstarter/pull/930/changes#diff-032eafab48326b32dad354dd24c3da532106bde78b3589793bd8fdc47be0bb65R283 as a single thing, we could also return "nil" config :) single nil

Certificate string `json:"certificate,omitempty" yaml:"certificate,omitempty"`

// MinSeverity is the minimum log severity to forward to the telemetry service.
// Accepted values: debug, info, warning, error. Defaults to "info" when empty.

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.

def _severity_to_level(severity: str) -> int:

Name is the key used in the ExporterConfig export map.
If omitted, derived from the Type (last segment after the last dot).
description: Name is the key used in the ExporterConfig
export map.

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.

this change sneaked in? :)

// controller-runtime logger (structured JSON to stdout).
// Future phase: forward to Loki push API.
func (s *TelemetryService) PushLogs(ctx context.Context, req *pb.PushLogsRequest) (*pb.PushLogsResponse, error) {
logger := ctrl.Log.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.

we need to authenticate the exporter from the call.

LeasePolicy LeasePolicy `json:"leasePolicy,omitempty" yaml:"leasePolicy,omitempty"`
HiddenLabels HiddenLabels `json:"hiddenLabels,omitempty" yaml:"hiddenLabels,omitempty"`
Telemetry *Telemetry `json:"telemetry,omitempty" yaml:"telemetry,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.

The proposed JEP configuration is quite different: https://jumpstarter.dev/main/contributing/jeps/JEP-0013-observability-telemetry-logs.html#operator-configuration

At least the logging related should match.

Also, the JEP was missing endpoint config, but we should re-use the structs from the router/controller/etc. And make it auto-calculate the endpoint name as we do with the others.

enabled/disabled must be controlled by the telemetry.enabled flag.

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