Skip to content

feat(output): add basic opentelemetry output#971

Draft
Molter73 wants to merge 2 commits into
mauro/feat/resubscription-changefrom
mauro/feat/otel-output
Draft

feat(output): add basic opentelemetry output#971
Molter73 wants to merge 2 commits into
mauro/feat/resubscription-changefrom
mauro/feat/otel-output

Conversation

@Molter73

@Molter73 Molter73 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Description

This output allows exporting file activity events as opentelemetry logs via otlp to be collected by compatible systems.

Checklist

  • Patch has a change log entry OR does not need one.
  • Investigated and inspected CI test results
  • Updated documentation accordingly

Automated testing

  • Added unit tests
  • Added integration tests
  • Added regression tests

If any of these don't apply, please comment below.

Testing Performed

Tested pushing events to Loki and visualizing events in Grafana with the provided documentation.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: 0bde36a9-e6f8-4ebc-8e5c-e4ab396d2812

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds an optional OpenTelemetry (OTLP) logging output to fact, gated behind an otel Cargo feature, with configuration, event serialization, and a refactored subscriber-based output pipeline. It also adds otel documentation/Grafana examples, migrates integration tests to support gRPC/OTLP dual-mode testing, and refactors CI workflows into a reusable container-build workflow with new otel-aware image targets.

Changes

OpenTelemetry Output Feature

Layer / File(s) Summary
OTelConfig, CLI, and reload wiring
fact/Cargo.toml, fact/src/config/mod.rs, fact/src/config/reloader.rs, fact/src/config/tests.rs
Adds optional OTel dependencies/feature, OTelConfig with YAML parsing/merging, CLI --otel-endpoint, FactCli::into_config, a Reloader otel() subscription channel, and expanded config tests.
Event/Process AnyValue conversions
fact/src/event/mod.rs, fact/src/event/process.rs
Adds From<...> for AnyValue conversions for Event, FileData variants, Process, and Lineage behind the otel feature.
Subscriber-based output pipeline and OTel client
fact/src/output/mod.rs, fact/src/output/grpc.rs, fact/src/output/stdout.rs, fact/src/output/otel.rs, fact/src/metrics/mod.rs, fact/src/lib.rs
Refactors gRPC/stdout clients to request event receivers via an mpsc/oneshot subscription channel, orchestrates workers with a JoinSet, adds a new OTel Client forwarding events to an OTLP HTTP exporter, and adds an otel events metric.
OTel documentation and Grafana/Loki examples
docs/otel/*
Adds otel.md documentation and example Grafana datasource/dashboard/provider and Loki config files.
Dual-mode test server and fixtures
tests/server.py, tests/conftest.py, tests/event.py, tests/Makefile, tests/requirements.txt
Replaces FileActivityService with an EventServer abstraction plus GrpcServer/OtlpServer, parametrizes tests over output modes, and reworks event diffing to compare model objects.
Test suite EventServer type updates
tests/test_*.py, tests/test_editors/*, tests/utils.py
Updates server fixture annotations from FileActivityService to EventServer across test modules and fixes a quoting edge case in rust_style_quote.

CI/CD and Build Infrastructure

Layer / File(s) Summary
Reusable container-build workflow
.github/workflows/container-build.yml, .github/workflows/ci.yml
Adds a new reusable workflow computing tags, building multi-arch images, and pushing manifests; rewires ci.yml to call it for standard and otel variants.
Integration tests VM/output-type parameterization
.github/workflows/integration-tests.yml, .github/workflows/konflux-tests.yml
Adds vms/output-type inputs, derives test matrix from inputs, injects output type into vars.yml, and updates artifact naming.
otel image build target and pytest wiring
Containerfile, Makefile, ansible/run-tests.yml
Adds CARGO_ARGS build arg, a Makefile image-otel target, and OUTPUT_TYPE passthrough in ansible pytest execution.

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

Sequence Diagram(s)

sequenceDiagram
  participant EventLoop as output::start
  participant OtelClient as otel::Client
  participant Exporter as OTLP LogExporter
  participant Collector as OTLP/Loki Endpoint

  EventLoop->>OtelClient: subscribe via oneshot handshake
  OtelClient->>OtelClient: receive Event from broadcast
  OtelClient->>OtelClient: convert Event to AnyValue LogRecord
  OtelClient->>Exporter: emit log record
  Exporter->>Collector: POST /v1/logs (protobuf)
Loading
sequenceDiagram
  participant CI as ci.yml
  participant Build as container-build.yml
  participant Registry as Quay Registries

  CI->>Build: workflow_call (tag-suffix, make-target)
  Build->>Build: vars job computes image tags
  Build->>Registry: push stackrox-io image (amd64/arm64)
  Build->>Registry: push rhacs-eng retagged image
  Build->>Registry: create/push multi-arch manifests
  Build-->>CI: output tag
Loading

Suggested reviewers: ovalenti

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding basic OpenTelemetry output.
Description check ✅ Passed The description includes the required sections and provides a clear summary, checklist, automated testing, and testing performed notes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mauro/feat/otel-output

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

@codecov-commenter

codecov-commenter commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.05556% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 34.13%. Comparing base (c98bda1) to head (b915ce8).

Files with missing lines Patch % Lines
fact/src/output/mod.rs 0.00% 19 Missing ⚠️
fact/src/config/reloader.rs 0.00% 12 Missing ⚠️
fact/src/metrics/mod.rs 0.00% 4 Missing ⚠️
fact/src/output/grpc.rs 0.00% 3 Missing ⚠️
fact/src/config/mod.rs 93.93% 2 Missing ⚠️
fact/src/lib.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@                         Coverage Diff                          @@
##           mauro/feat/resubscription-change     #971      +/-   ##
====================================================================
+ Coverage                             32.64%   34.13%   +1.48%     
====================================================================
  Files                                    21       21              
  Lines                                  2916     2868      -48     
  Branches                               2916     2868      -48     
====================================================================
+ Hits                                    952      979      +27     
+ Misses                                 1961     1886      -75     
  Partials                                  3        3              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Molter73 Molter73 force-pushed the mauro/feat/otel-output branch 5 times, most recently from 229cd7d to 8bb3067 Compare July 6, 2026 14:30

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
tests/event.py (1)

298-317: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

.match() doesn't anchor at the end of the string.

expected.match(actual) only checks that actual starts with a match for expected; a longer actual value with unexpected trailing characters would still be considered a match. Use fullmatch() for exact validation of regex-based expectations.

🐛 Proposed fix
-            if not isinstance(actual, str) or not expected.match(actual):
+            if not isinstance(actual, str) or not expected.fullmatch(actual):
🤖 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 `@tests/event.py` around lines 298 - 317, The regex check in _diff_path
currently uses expected.match(actual), which allows extra trailing characters to
slip through. Update the Pattern branch in _diff_path to use fullmatch() instead
so regex-based expectations must match the entire actual path, while keeping the
existing non-regex equality behavior unchanged.
🧹 Nitpick comments (5)
.github/workflows/container-build.yml (2)

25-28: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider persist-credentials: false on checkout steps.

zizmor's artipacked finding notes these actions/checkout@v4 steps don't set persist-credentials: false, leaving the ambient GITHUB_TOKEN in .git/config for the remainder of the job — including during cargo build/podman build, which execute third-party build scripts.

🔒 Suggested fix
     - uses: actions/checkout@v4
       with:
         submodules: true
         fetch-depth: 0
+        persist-credentials: false

Also applies to: 54-56

🤖 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 @.github/workflows/container-build.yml around lines 25 - 28, The checkout
steps using actions/checkout@v4 currently leave the ambient GITHUB_TOKEN
persisted in .git/config, which can leak into later cargo build and podman build
steps. Update each checkout block in the workflow to set persist-credentials to
false, including the other checkout occurrence referenced by the review, while
keeping the existing submodules and fetch-depth settings in place.

Source: Linters/SAST tools


33-35: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Hoist interpolated expressions into env: to avoid template injection.

zizmor flags direct ${{ inputs.tag-suffix }}, ${{ inputs.make-target }}, and ${{ needs.vars.outputs.* }} interpolation inside run: blocks (lines 33-35, 59, 98-102, 120-124) as template-injection risk. Even though these values currently originate from static strings in ci.yml, the canonical mitigation is to pass them through a step-level env: block and reference them as shell variables, so the pattern remains safe if these inputs are ever driven by less-trusted values.

🔒 Example fix for the `vars` step
     - id: vars
+      env:
+        TAG_SUFFIX: ${{ inputs.tag-suffix }}
       run: |
         cat << EOF >> "$GITHUB_OUTPUT"
-        tag=$(make tag)${{ inputs.tag-suffix }}
-        stackrox-io-image=$(make image-name)${{ inputs.tag-suffix }}
-        rhacs-eng-image=$(FACT_REGISTRY="quay.io/rhacs-eng/fact" make image-name)${{ inputs.tag-suffix }}
+        tag=$(make tag)${TAG_SUFFIX}
+        stackrox-io-image=$(make image-name)${TAG_SUFFIX}
+        rhacs-eng-image=$(FACT_REGISTRY="quay.io/rhacs-eng/fact" make image-name)${TAG_SUFFIX}
         EOF

Similarly for line 59 (env: MAKE_TARGET: ${{ inputs.make-target }} then run: DOCKER=podman make "${MAKE_TARGET}") and the manifest steps (98-102, 120-124), which interpolate needs.vars.outputs.* directly.

Also applies to: 59-59, 98-102, 120-124

🤖 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 @.github/workflows/container-build.yml around lines 33 - 35, The workflow
steps that build image tags and run make targets are interpolating GitHub
expressions directly inside run blocks, which is the template-injection risk
flagged by zizmor. Move the affected values from inputs.tag-suffix,
inputs.make-target, and needs.vars.outputs.* into step-level env entries, then
reference those shell variables inside the run commands. Apply this pattern in
the tag/image-name step, the MAKE_TARGET step, and the manifest-related steps so
the logic stays the same but the expressions are no longer expanded directly in
shell scripts.

Source: Linters/SAST tools

tests/conftest.py (1)

70-100: 🩺 Stability & Availability | 🔵 Trivial

Fixed OTLP port may conflict under parallel test execution.

OtlpServer.serve() defaults to a hardcoded port (4318), mirroring the existing fixed-port pattern for GrpcServer (9999). If these tests are ever run with pytest-xdist or otherwise in parallel across workers, both fixed ports could collide across concurrently-running test processes. Not a regression introduced by this diff, but doubling the fixed-port surface (grpc + otlp) is worth tracking if parallelization is a goal.

🤖 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 `@tests/conftest.py` around lines 70 - 100, The server fixture currently
hardcodes OTLP to a fixed port via OtlpServer.serve(), which can collide when
tests run in parallel. Update the server setup in pytest_generate_tests/server
so OtlpServer can be started with a worker-unique or free port (similar to how
GrpcServer is selected by mode), and make the fixture pass that port through
instead of relying on the default 4318. Ensure the change is localized around
_get_output_modes, pytest_generate_tests, and server so each test process gets
an isolated OTLP endpoint.
tests/server.py (1)

41-58: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

ThreadPoolExecutor is never shut down.

Neither GrpcServer.stop() nor OtlpServer.stop() calls self.executor.shutdown(). Each test using the server fixture creates a fresh EventServer subclass instance with its own 2-worker pool that's never explicitly released, leaking idle threads across the test session (they'll only terminate at interpreter exit).

♻️ Proposed fix
     def stop(self):
         """Stop the gRPC server."""
         self.server.stop(1)
         self.running.clear()
+        self.executor.shutdown(wait=False)

and similarly for OtlpServer.stop().

🤖 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 `@tests/server.py` around lines 41 - 58, `EventServer` creates a
`ThreadPoolExecutor` in its constructor but the pool is never released, so
update the shutdown path in both `GrpcServer.stop()` and `OtlpServer.stop()` to
call `self.executor.shutdown()` after stopping the server work. Make the cleanup
part of the existing `stop` methods so each `EventServer` subclass instance
properly tears down its worker threads when the test fixture finishes.
fact/src/event/process.rs (1)

45-57: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Inconsistent attribute key naming: exec_path vs exe_path.

Lineage's exe_path field is emitted under the key "exec_path", while the sibling Process conversion (line 259-261 in this same file) emits the analogous field under "exe_path". Since these are exported as OTLP log attributes consumed by external tooling (Grafana/Loki dashboards per this PR's docs), inconsistent naming for the same semantic field across nested maps will confuse queries/dashboards built against the OTel schema.

🔧 Suggested fix
 impl From<Lineage> for opentelemetry::logs::AnyValue {
     fn from(value: Lineage) -> Self {
         AnyValue::Map(Box::new(HashMap::from([
             ("uid".into(), value.uid.into()),
             (
-                "exec_path".into(),
+                "exe_path".into(),
                 value.exe_path.to_string_lossy().to_string().into(),
             ),
         ])))
     }
 }

This isn't exercised by the current Python OTLP test server (which doesn't parse lineage), so it wouldn't be caught by CI as-is.

🤖 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 `@fact/src/event/process.rs` around lines 45 - 57, The `Lineage` OTEL
conversion is exporting the `exe_path` field under a different attribute name
than the sibling `Process` conversion, causing inconsistent log schema. Update
the `From<Lineage> for opentelemetry::logs::AnyValue` implementation in
`process.rs` so the map key matches the existing `Process` export naming
(`exe_path`) and keep the attribute naming consistent across both conversions
for downstream queries and dashboards.
🤖 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 `@fact/src/output/otel.rs`:
- Around line 65-70: The LogExporter setup in otel initialization is panicking
on build failure via expect, which should be converted to normal error handling.
Update the LogExporter::builder() chain in the otel initialization path to
propagate the ExporterBuildError with ? or a context-wrapped error instead of
expect, so failures flow through the existing Result-based path rather than
crashing.

In `@fact/src/output/stdout.rs`:
- Around line 27-29: The subscription handshake in the stdout output path can
panic if the orchestrator is shutting down and the oneshot/channel is already
closed. In the task that uses std::sync::oneshot::channel and
self.subscriber.send(tx), replace both unwrap() calls with graceful error
handling: if sending the subscription request fails or rx.await returns an
error, log the failure and return early instead of panicking. Use the existing
stdout subscription flow around the send/receive handshake to locate the fix.

In `@tests/utils.py`:
- Around line 61-68: The string-quoting logic in the helper that builds
shell-safe literals has an unsafe fallback in the single-quote path for values
containing backslashes or quotes plus backticks or dollar signs. Update the
quoting branch in the relevant utility function to route those cases through the
double-quoted form instead, and make that branch escape backslash, double quote,
backtick, and dollar-sign characters so the value is always treated literally.
Keep the single-quoted fallback only for strings that do not need those extra
escapes.

---

Outside diff comments:
In `@tests/event.py`:
- Around line 298-317: The regex check in _diff_path currently uses
expected.match(actual), which allows extra trailing characters to slip through.
Update the Pattern branch in _diff_path to use fullmatch() instead so
regex-based expectations must match the entire actual path, while keeping the
existing non-regex equality behavior unchanged.

---

Nitpick comments:
In @.github/workflows/container-build.yml:
- Around line 25-28: The checkout steps using actions/checkout@v4 currently
leave the ambient GITHUB_TOKEN persisted in .git/config, which can leak into
later cargo build and podman build steps. Update each checkout block in the
workflow to set persist-credentials to false, including the other checkout
occurrence referenced by the review, while keeping the existing submodules and
fetch-depth settings in place.
- Around line 33-35: The workflow steps that build image tags and run make
targets are interpolating GitHub expressions directly inside run blocks, which
is the template-injection risk flagged by zizmor. Move the affected values from
inputs.tag-suffix, inputs.make-target, and needs.vars.outputs.* into step-level
env entries, then reference those shell variables inside the run commands. Apply
this pattern in the tag/image-name step, the MAKE_TARGET step, and the
manifest-related steps so the logic stays the same but the expressions are no
longer expanded directly in shell scripts.

In `@fact/src/event/process.rs`:
- Around line 45-57: The `Lineage` OTEL conversion is exporting the `exe_path`
field under a different attribute name than the sibling `Process` conversion,
causing inconsistent log schema. Update the `From<Lineage> for
opentelemetry::logs::AnyValue` implementation in `process.rs` so the map key
matches the existing `Process` export naming (`exe_path`) and keep the attribute
naming consistent across both conversions for downstream queries and dashboards.

In `@tests/conftest.py`:
- Around line 70-100: The server fixture currently hardcodes OTLP to a fixed
port via OtlpServer.serve(), which can collide when tests run in parallel.
Update the server setup in pytest_generate_tests/server so OtlpServer can be
started with a worker-unique or free port (similar to how GrpcServer is selected
by mode), and make the fixture pass that port through instead of relying on the
default 4318. Ensure the change is localized around _get_output_modes,
pytest_generate_tests, and server so each test process gets an isolated OTLP
endpoint.

In `@tests/server.py`:
- Around line 41-58: `EventServer` creates a `ThreadPoolExecutor` in its
constructor but the pool is never released, so update the shutdown path in both
`GrpcServer.stop()` and `OtlpServer.stop()` to call `self.executor.shutdown()`
after stopping the server work. Make the cleanup part of the existing `stop`
methods so each `EventServer` subclass instance properly tears down its worker
threads when the test fixture finishes.
🪄 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: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: 6070caf3-6f12-4fe2-99cd-d0c9f8b532da

📥 Commits

Reviewing files that changed from the base of the PR and between e4718f9 and cda03b6.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (46)
  • .github/workflows/ci.yml
  • .github/workflows/container-build.yml
  • .github/workflows/integration-tests.yml
  • .github/workflows/konflux-tests.yml
  • Containerfile
  • Makefile
  • ansible/run-tests.yml
  • docs/otel/grafana-dashboard-provider.yaml
  • docs/otel/grafana-datasource.yaml
  • docs/otel/grafana-fact-dashboard.json
  • docs/otel/loki-config.yaml
  • docs/otel/otel.md
  • fact/Cargo.toml
  • fact/src/config/mod.rs
  • fact/src/config/reloader.rs
  • fact/src/config/tests.rs
  • fact/src/event/mod.rs
  • fact/src/event/process.rs
  • fact/src/lib.rs
  • fact/src/metrics/mod.rs
  • fact/src/output/grpc.rs
  • fact/src/output/mod.rs
  • fact/src/output/otel.rs
  • fact/src/output/stdout.rs
  • tests/Makefile
  • tests/conftest.py
  • tests/event.py
  • tests/requirements.txt
  • tests/server.py
  • tests/test_config_hotreload.py
  • tests/test_editors/test_nvim.py
  • tests/test_editors/test_sed.py
  • tests/test_editors/test_vi.py
  • tests/test_editors/test_vim.py
  • tests/test_file_open.py
  • tests/test_misc.py
  • tests/test_path_chmod.py
  • tests/test_path_chown.py
  • tests/test_path_mkdir.py
  • tests/test_path_rename.py
  • tests/test_path_rmdir.py
  • tests/test_path_unlink.py
  • tests/test_rate_limit.py
  • tests/test_wildcard.py
  • tests/test_xattr.py
  • tests/utils.py

Comment thread fact/src/output/otel.rs Outdated
Comment on lines +65 to +70
let exporter_otlp = LogExporter::builder()
.with_http()
.with_protocol(opentelemetry_otlp::Protocol::HttpBinary)
.with_endpoint(endpoint)
.build()
.expect("Failed to create log exporter");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

opentelemetry-otlp 0.32 LogExporter builder build() return type error

💡 Result:

In opentelemetry-otlp version 0.32.0 (Rust), the build() method on LogExporterBuilder returns a Result<LogExporter, ExporterBuildError> [1][2]. If you are encountering a type error, it is likely related to the breaking changes introduced in the 0.32.0 release [2][3]. Specifically, the build() method's return type was updated to return Result<LogExporter, ExporterBuildError>, replacing the previous implementation that returned signal-specific errors (such as LogError) from the opentelemetry_sdk [2]. To resolve this: 1. Update any code matching on the error type to use ExporterBuildError instead of previous error enums, which have been removed [2]. 2. If your code uses unwrap() or expect(), it should continue to work without modification, provided your logic is compatible with the new Result signature [2]. 3. Ensure you are aware that build() now auto-selects the transport (gRPC or HTTP) based on the OTEL_EXPORTER_OTLP_PROTOCOL environment variable or enabled feature flags [2][3][4]. If you need specific transport configurations, continue using .with_tonic() or .with_http() before calling build() [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files fact/src/output/otel.rs Cargo.toml Cargo.lock '**/Cargo.toml' | sed 's#^`#-` #'

printf '\n== fact/src/output/otel.rs (relevant region) ==\n'
cat -n fact/src/output/otel.rs | sed -n '1,220p'

printf '\n== dependency versions mentioning opentelemetry-otlp/opentelemetry-sdk ==\n'
rg -n 'opentelemetry-(otlp|sdk)|opentelemetry' Cargo.toml Cargo.lock fact/Cargo.toml -S

Repository: stackrox/fact

Length of output: 6548


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

# Inspect Cargo.lock entries for opentelemetry-otlp and related crates.
text = Path('Cargo.lock').read_text() if Path('Cargo.lock').exists() else ''
for crate in ['opentelemetry-otlp', 'opentelemetry-sdk', 'opentelemetry']:
    print(f'== {crate} ==')
    for m in re.finditer(rf'name = "{re.escape(crate)}"\nversion = "([^"]+)"(?:\n.*?)+?(?=\n\s*\[\[package\]\]|\Z)', text, re.S):
        block = m.group(0)
        print(block.splitlines()[0])
        for line in block.splitlines()[1:8]:
            print(line)
        print('...')
PY

Repository: stackrox/fact

Length of output: 732


🌐 Web query:

opentelemetry-otlp 0.32 ExporterBuildError std::error::Error anyhow context

💡 Result:

In opentelemetry-otlp version 0.32.0, the ExporterBuilder::build() method was updated to return a specific ExporterBuildError enum instead of generic or signal-specific errors [1][2]. This change was introduced to improve error handling clarity and consistency [2][3]. The ExporterBuildError enum is designed to be non_exhaustive and includes the following variants [4][5]: - ThreadSpawnFailed: Occurs when spawning a new thread fails. - NoHttpClient: Occurs when no HTTP client is specified. - UnsupportedCompressionAlgorithm(String): Indicates the use of a compression algorithm that is not supported. - InvalidUri(String, String): Indicates an invalid URI (available when using grpc-tonic, http-proto, or http-json features). - InvalidConfig { name: String, reason: String }: Used for invalid configurations. - InternalFailure(String): Used for implementation-specific internal errors, intended for logging rather than programmatic decision-making [4][5]. Because ExporterBuildError implements the standard std::error::Error trait, it is compatible with error handling crates like anyhow [4][5]. You can typically use the ? operator to propagate these errors in functions that return anyhow::Result<T> [4]. If you were previously calling unwrap() or expect() on the result of a builder, this will still work, but you are encouraged to handle the ExporterBuildError variants specifically to provide better diagnostics [1][2].

Citations:


Avoid panicking on exporter build failure
LogExporter::builder().build() returns an ExporterBuildError, so this can be propagated with ?/.context()? instead of expect(). That keeps the worker on the normal error path instead of crashing on recoverable init failures.

🤖 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 `@fact/src/output/otel.rs` around lines 65 - 70, The LogExporter setup in otel
initialization is panicking on build failure via expect, which should be
converted to normal error handling. Update the LogExporter::builder() chain in
the otel initialization path to propagate the ExporterBuildError with ? or a
context-wrapped error instead of expect, so failures flow through the existing
Result-based path rather than crashing.

Comment thread fact/src/output/stdout.rs Outdated
Comment on lines +27 to +29
let (tx, rx) = oneshot::channel();
self.subscriber.send(tx).await.unwrap();
let mut rx = rx.await.unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle subscription handshake failure gracefully instead of unwrap().

If the output orchestrator's loop has already exited (e.g. during shutdown), subs_rx/the oneshot sender may be dropped, making these .unwrap()s panic in the spawned task. Prefer logging and returning, matching the graceful termination used elsewhere.

🛡️ Proposed fix
-            let (tx, rx) = oneshot::channel();
-            self.subscriber.send(tx).await.unwrap();
-            let mut rx = rx.await.unwrap();
+            let (tx, rx) = oneshot::channel();
+            if self.subscriber.send(tx).await.is_err() {
+                info!("Output orchestrator gone, stopping stdout output...");
+                return;
+            }
+            let mut rx = match rx.await {
+                Ok(rx) => rx,
+                Err(_) => {
+                    info!("Failed to obtain event receiver, stopping stdout output...");
+                    return;
+                }
+            };
📝 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
let (tx, rx) = oneshot::channel();
self.subscriber.send(tx).await.unwrap();
let mut rx = rx.await.unwrap();
let (tx, rx) = oneshot::channel();
if self.subscriber.send(tx).await.is_err() {
info!("Output orchestrator gone, stopping stdout output...");
return;
}
let mut rx = match rx.await {
Ok(rx) => rx,
Err(_) => {
info!("Failed to obtain event receiver, stopping stdout output...");
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 `@fact/src/output/stdout.rs` around lines 27 - 29, The subscription handshake
in the stdout output path can panic if the orchestrator is shutting down and the
oneshot/channel is already closed. In the task that uses
std::sync::oneshot::channel and self.subscriber.send(tx), replace both unwrap()
calls with graceful error handling: if sending the subscription request fails or
rx.await returns an error, log the failure and return early instead of
panicking. Use the existing stdout subscription flow around the send/receive
handshake to locate the fix.

Comment thread tests/utils.py Outdated
Comment on lines 61 to 68
if re.search(r'[^a-zA-Z0-9_.:\-/+@\]]', s):
# Backslash and single-quote cannot appear in single-quoted
# strings in Rust's shlex, use double-quoting instead.
if ('\\' in s or "'" in s) and '`' not in s and '$' not in s:
escaped = s.replace('\\', '\\\\').replace('"', '\\"')
return f'"{escaped}"'
escaped = s.replace("'", "\\'")
return f"'{escaped}'"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Broken single-quote fallback for strings with both a quote and backtick/$.

When a string contains a backslash or ' and also contains ` or $, the code falls through to the single-quote branch (s.replace("'", "\\'")). This contradicts the function's own comment: backslash has no escaping power inside single-quoted shell strings, so \' does not produce a literal quote — it just embeds a literal backslash and then the string is prematurely terminated by the unescaped '. The rest of the string (containing the backtick/$) ends up unquoted, exposing it to command substitution/expansion instead of being treated literally.

The double-quote branch already handles \ and "; extending it to also escape ` and $ removes the need for this unsafe fallback entirely.

🐛 Proposed fix
     if re.search(r'[^a-zA-Z0-9_.:\-/+@\]]', s):
-        # Backslash and single-quote cannot appear in single-quoted
-        # strings in Rust's shlex, use double-quoting instead.
-        if ('\\' in s or "'" in s) and '`' not in s and '$' not in s:
-            escaped = s.replace('\\', '\\\\').replace('"', '\\"')
+        # Backslash and single-quote cannot appear in single-quoted
+        # strings in Rust's shlex, use double-quoting instead.
+        if '\\' in s or "'" in s:
+            escaped = (
+                s.replace('\\', '\\\\')
+                .replace('"', '\\"')
+                .replace('`', '\\`')
+                .replace('$', '\\$')
+            )
             return f'"{escaped}"'
         escaped = s.replace("'", "\\'")
         return f"'{escaped}'"
📝 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
if re.search(r'[^a-zA-Z0-9_.:\-/+@\]]', s):
# Backslash and single-quote cannot appear in single-quoted
# strings in Rust's shlex, use double-quoting instead.
if ('\\' in s or "'" in s) and '`' not in s and '$' not in s:
escaped = s.replace('\\', '\\\\').replace('"', '\\"')
return f'"{escaped}"'
escaped = s.replace("'", "\\'")
return f"'{escaped}'"
if re.search(r'[^a-zA-Z0-9_.:\-/+@\]]', s):
# Backslash and single-quote cannot appear in single-quoted
# strings in Rust's shlex, use double-quoting instead.
if '\\' in s or "'" in s:
escaped = (
s.replace('\\', '\\\\')
.replace('"', '\\"')
.replace('`', '\\`')
.replace('$', '\\$')
)
return f'"{escaped}"'
escaped = s.replace("'", "\\'")
return f"'{escaped}'"
🤖 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 `@tests/utils.py` around lines 61 - 68, The string-quoting logic in the helper
that builds shell-safe literals has an unsafe fallback in the single-quote path
for values containing backslashes or quotes plus backticks or dollar signs.
Update the quoting branch in the relevant utility function to route those cases
through the double-quoted form instead, and make that branch escape backslash,
double quote, backtick, and dollar-sign characters so the value is always
treated literally. Keep the single-quoted fallback only for strings that do not
need those extra escapes.

@Molter73 Molter73 force-pushed the mauro/feat/otel-output branch from f4dda9f to a52c44e Compare July 7, 2026 15:55
@Molter73 Molter73 changed the base branch from main to mauro/feat/resubscription-change July 7, 2026 15:55
Molter73 added 2 commits July 7, 2026 18:02
This output allows exporting file activity events as opentelemetry logs
via otlp to be collected by compatible systems.

The output is gated behind the "otel" feature, which allows regular fact
builds to keep using just the gRPC and stodout outputs, no additional
dependencies.

WIP
@Molter73 Molter73 force-pushed the mauro/feat/otel-output branch from a52c44e to b915ce8 Compare July 7, 2026 16:02
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.

2 participants