Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions aicontext/features/integrations/native-collector/feature.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Feature: Native Collector C++ API

> Owner: integrations | Last reviewed: 2026-07-01 | Canonical: yes
> Owner: integrations | Last reviewed: 2026-07-02 | Canonical: yes
> Scope: The public, developer-facing C++ RAII API over the native collector C ABI, plus its `find_package` packaging, console example, and migration story from the C++/CLI wrapper.

---
Expand Down Expand Up @@ -81,7 +81,7 @@ The wrapper is a **thin convenience layer**: it adds no wire behavior. Every reg
- **Runtime alert attach to a built-in sensor (agent example).** The HsmAgent raises the built-in **Total CPU** sensor to **Error** on top of its default Warning: it grabs the sensor via `hsm_collector_add_default_sensor(TOTAL_CPU)` (idempotent by path — `AddAllComputerSensors` already created it), builds an `EmaMean > 50` alert with the fluent `CreateAlert(AlertKind::Bar).If(EmaMean, GreaterThan, "50").AsSensorError().ThenNotify(…).WithIcon(Error).WithConfirmationPeriod(5min)`, and `AttachAlert`s it before Start. `AttachAlert` rebuilds the sensor's registration in place, so the extra alert rides the AddOrUpdate. **No catalog/golden/managed change** — the shared default stays Warning@50%; this agent additionally Errors@50%. (`EmaMean`=213 is the bar's smoothed-mean property.) Pinned by `native_total_cpu_error_alert_attaches`.
- **Native wrapper DLL (`src/wrapper/`, drop-in replacement for the C++/CLI wrapper).** The legacy `hsm_wrapper::DataCollectorProxy` ABI is reimplemented over `hsm::collector`: `DataCollectorImpl` holds a `Collector`, lifecycle/`Initialize*Monitoring`/`Create*`/file/service-state forward to the native API, the static `AlertsBuilder` DSL accumulates plain data converted to a native alert (`Collector::CreateAlert` + `Sensor::AttachAlert`) at sensor creation, and the former managed func-sensor delegate bridge collapses (the native `Function`/`ValuesFunctionSensor` take a `std::function` directly). `Start`/`StartAsync` call `InstallWindowsMetricSources()` (live values, Windows) + `UseHttpTransport()` (real server) before the native `Start`. Built by `src/wrapper/CMakeLists.txt` (`add_subdirectory` of the native collector with `HSM_COLLECTOR_HTTP=ON`) into `HSMCppWrapper.dll`; `pch.h` force-defines `HSMWRAPPER_API=__declspec(dllexport)`. The public headers under `src/wrapper/include/` are unchanged, so a consumer relinks with zero source edits. Behavioral residue vs the managed wrapper is in `docs/native-collector-migration.md`; smoke-driven in-process by `WrapperConsole` (`src/wrapper/WrapperConsole/WrapperSmoke.cpp`), built + run in the `hsm-wrapper-build` CI lane. **Incremental-migration escape hatch:** `DataCollectorProxy::Native()` returns the wrapper's underlying `hsm::collector::Collector&`, so new sensors can be created directly against the native API on the same collector/connection while old sensors keep using the wrapper; the DLL re-exports the `extern "C"` C ABI (a CMake-generated `.def` from the frozen `hsm_collector.h`, `_t` typedefs filtered) so a consumer links only `HSMCppWrapper` — no second copy of the collector runtime. When the last wrapper call-site is gone, the proxy is swapped for a directly-owned `Collector` and the wrapper deleted (no big-bang). **Handoff:** the DLL is delivered to consumers as a versioned zip (both x64 configs + libcurl/zlib runtime + headers + a swap-recipe `MANIFEST.md`) attached to a `wrapper-v*` GitHub Release by the `hsm-wrapper-release` workflow — a stable cross-host download for the GitLab-hosted aggregator; layout mirrors their vendored tree so it unzips over the checkout. See `src/wrapper/packaging/` and the "Getting the native wrapper bundle" section of `docs/native-collector-migration.md`.
- **TLS / transport.** `CollectorOptions::allow_untrusted_server_certificate` and `allow_plaintext_transport` map to the C options of the same name; they take effect once `UseHttpTransport()` installs the libcurl transport (`HSM_COLLECTOR_HTTP` build). Default builds use the in-memory recording sender (no network), which is what lets the console example and tests run with no server.
- **Logger hookup.** `SetLogger(std::function<void(LogLevel, const std::string&)>)`. Errors pass through the deduplicator (`exception_deduplicator_window_ms` / `max_deduplicated_messages`); a window of 0 logs every message. A throwing logger is swallowed.
- **Logging (managed-parity).** Two sinks, both fed by the single `LogMessage` path: a **callback** via `SetLogger(std::function<void(LogLevel, const std::string&)>)` (C ABI `hsm_collector_set_logger`), and a **built-in rolling file logger** via `EnableFileLogging(directory, min_level = LogLevel::Info)` (C ABI `hsm_collector_enable_file_logging`) — the parity replacement for the managed NLog default sink. The file logger writes `<dir>/DataCollector_<UTC-date>.txt` (every level ≥ `min_level`) plus `<dir>/DataCollector_error_<UTC-date>.txt` (errors only), format `yyyy-MM-dd HH:mm:ss|LEVEL| message`, rolling by UTC date, on a background thread (Enqueue never blocks the caller; the destructor drains the queue before joining so shutdown logs are not lost). Both sinks receive every message; the file applies its own min-level filter. Install before Start; a second `EnableFileLogging` swaps the sink, an empty directory clears it. **Events:** lifecycle `DataCollector -> Starting/Running/Stopping/Stopped` (Info, via `NotifyLifecycle`); `Registered N sensor(s) on connect` (Info) / `Failed to register …` (Error) at the Start `/commands` POST; `Failed to send N value(s): HTTP <code> …` (Error, **deduplicated** — the dispatcher's re-enqueue retry is itself silent, matching the queue); plus the pre-existing metric-source-read / top-CPU / stop-time-drop errors. **Errors** pass through the deduplicator (`exception_deduplicator_window_ms` / `max_deduplicated_messages`; window 0 logs every message). A throwing callback is swallowed. **Parity delta vs the managed collector:** per-sensor `New sensor added {path}` (Info) and per-value `value rejected` (Debug) are deliberately *not* emitted — they would fire the user callback under the collector lock (registration) or need sensor→collector plumbing (value path); the `Registered N sensor(s)` batch line plus the sensor's own status/telemetry cover those cases.
- **Threading expectations.** `Start`/`Stop` single-threaded; sensor `AddValue` is thread-safe; callbacks run on the scheduler thread and must not re-enter a lifecycle method. `StartAsync`/`StopAsync` wrap the sync calls in `std::async` (note `std::future`'s destructor blocks until the task completes).
- **Shutdown semantics.** `Stop` is bounded (it will not hang on a dead transport — pending data is dropped rather than blocking host restart). `Dispose` is terminal and idempotent; after it, `Status()` is `Disposed` and further lifecycle/registration calls are rejected.

Expand Down Expand Up @@ -113,6 +113,8 @@ Tracked in full in `docs/native-collector-migration.md`. Headlines:
- `native_wrapper_value_path_posts_through` — the value-add path forwards 1:1.
- `native_wrapper_lifetime_move_semantics` — move + Dispose correctness.
- `native_wrapper_callbacks_bridge_std_function` — lifecycle listener, logger, function sensor, and metric-source factory all fire through their trampolines.
- `native_logging_emits_lifecycle_events` — a `SetLogger` callback receives the Info lifecycle transitions (`-> Starting/Running/Stopping/Stopped`) across Start/Stop.
- `native_file_logger_writes_to_dated_file` — `EnableFileLogging` writes a `DataCollector_<date>.txt` whose content carries the logged line + level tag (flushed deterministically by the destructor's drain+join, no sleep).
- `native_rate_options_parity` — RateOptions default cadence (1 min), M1/M5 empty-Description vs bare-null, and the full registration surface (TTL/unit/display-unit/aggregate/EMA) lowered onto a rate sensor.
- `native_file_from_path_derives_name_extension_and_content` — `SendFile(path)` overrides the creation-time name/extension with the file's own stem/extension and posts the content; `native_file_from_path_missing_file_returns_not_found_and_sends_nothing` — a missing file returns `NOT_FOUND` and enqueues nothing.
- `rate_options_contract.hsmtest` — cross-language pin: the same options through `RateSensorOptions.ToApi()` (managed) and `hsm_collector_create_rate_sensor_with_options` (native) produce byte-identical registration text; includes the unset-`unit` ⇒ ValueInSecond(3000) default case.
Expand Down
2 changes: 2 additions & 0 deletions src/native/collector/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ set(NATIVE_REGRESSION_TESTS
native_scheduler_on_error_isolates_throwing_callback
native_logger_deduplicates_repeated_errors_within_window
native_logger_zero_window_logs_every_error
native_logging_emits_lifecycle_events
native_file_logger_writes_to_dated_file
native_retry_meeting_full_queue_is_dropped_not_evicting_queued_values
native_retry_below_capacity_is_always_redelivered
cpu_select_top_n_selects_busiest_above_threshold
Expand Down
11 changes: 11 additions & 0 deletions src/native/collector/include/hsm_collector/collector.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,17 @@ namespace hsm::collector
loggers_.push_back(std::move(holder));
}

/// Install the built-in rolling file logger (parity with the managed NLog default): writes
/// `<directory>/DataCollector_<UTC-date>.txt` (levels >= `min_level`) plus a `_error_` file
/// (errors only), asynchronously, rolling by date. Complements SetLogger - both the callback
/// and the file receive every message. Call before Start(); an empty `directory` clears it.
void EnableFileLogging(const std::string& directory, LogLevel min_level = LogLevel::Info)
{
Check(
hsm_collector_enable_file_logging(handle_, directory.c_str(), static_cast<hsm_log_level_t>(min_level)),
"Failed to enable file logging.");
}

/// Install (or, with an empty std::function, clear) the metric-source factory. As with
/// SetLogger, call once before Start(): a replaced/cleared holder is retained until
/// destruction (the factory may be in use on the scheduler thread), with no reclamation.
Expand Down
9 changes: 9 additions & 0 deletions src/native/collector/include/hsm_collector/hsm_collector.h
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,15 @@ typedef enum hsm_log_level_t HSM_ENUM_INT32
typedef void (*hsm_log_callback_t)(hsm_log_level_t level, const char* message, void* user_data);
hsm_result_t hsm_collector_set_logger(hsm_collector_t* collector, hsm_log_callback_t callback, void* user_data);

/* Built-in rolling file logger (parity with the managed NLog default sink). Writes
<directory>/DataCollector_<UTC-date>.txt (every level >= min_level) and
<directory>/DataCollector_error_<UTC-date>.txt (errors only), asynchronously,
rolling when the UTC date changes; line format "yyyy-MM-dd HH:mm:ss|LEVEL| message".
Complements set_logger: both the callback (if any) and the file receive every
message (the file applies its own min_level filter). Call before Start. */
hsm_result_t hsm_collector_enable_file_logging(
hsm_collector_t* collector, const char* directory, hsm_log_level_t min_level);

hsm_result_t hsm_collector_create_int_sensor(
hsm_collector_t* collector,
const char* path,
Expand Down
Loading
Loading