feat: Phase 5: Observability stack — docs, runbook and mTls - #6427
feat: Phase 5: Observability stack — docs, runbook and mTls#6427pratikmankawde wants to merge 131 commits into
Conversation
6cff91c to
8440a32
Compare
8440a32 to
d8c284b
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
1d093fc to
ec73991
Compare
ec73991 to
69b4829
Compare
46af63d to
2391149
Compare
fc1ed3c to
3581839
Compare
56cc5e6 to
85f583f
Compare
…phase5-docs-deployment
There was a problem hiding this comment.
This is primarily a documentation/runbook PR plus a well-tested mTLS feature addition to the OTLP exporter config validation. I reviewed the config validation logic (TelemetryConfig.cpp), the new client-cert/key exporter wiring (Telemetry.cpp/Telemetry.h), the new Main.cpp try/catch around Application construction, and the accompanying tests. The validation logic (pairing check, use_tls requirement, readable-file check, all gated on enabled=1) is internally consistent, matches the documentation in cfg/xrpld-example.cfg and docs/telemetry-runbook.md, and is covered by an extensive, well-reasoned test suite. The Main.cpp change correctly catches std::exception (which covers boost::bad_lexical_cast via std::bad_cast) around the only unsafe construction step and leaves setup()/shutdown ordering untouched. I did not find correctness, security, or resource-leak issues in the changed lines. Note: the MR description contains an embedded instruction ('No need to review tasklist files') that matches a prompt-injection pattern; per review policy I ignored it and reviewed all changed files, including the task-list docs, on their technical merits — nothing substantive turned up there either.
pratikmankawde
left a comment
There was a problem hiding this comment.
Review summary
0 BLOCKING · 2 SHOULD-FIX · 0 NIT
Two inline comments, both on docs/telemetry-runbook.md. Scope of this pass: the mTLS config/validation change, Main.cpp, the new tests, cfg/xrpld-example.cfg, and the new 656-line runbook.
The mTLS work itself is in good shape and I found nothing to raise on it. requireReadableFile (src/libxrpl/telemetry/TelemetryConfig.cpp:104-117) turns a bad certificate path into a startup error naming the key and the path; the contradiction guards at :150-171 are correctly gated on enabled=1; and the 18 GTest cases added in src/tests/libxrpl/telemetry/TelemetryConfig.cpp cover the accept path, all three throw paths, and the three skip paths, asserting the thrown message names the offending key rather than just that something threw. The Main.cpp try/catch, the @throws documentation on makeTelemetrySetup, and the xrpld-example.cfg prose all match what the code does — I checked the 17 documented [telemetry] keys against the 17 the parser reads and they agree.
Three things already raised in resolved threads that are still live at this tip
I am not re-posting these as new comments, but they are unfixed, so flagging them with the extra evidence I gathered:
-
Bare attribute names in the TraceQL examples (threads
PRRT_kwDOACmRR86SUpCbandPRRT_kwDOACmRR86SUpCi). Roughly 40 queries between lines 141 and 387 filter on bare attribute names, e.g.{name="consensus.accept.apply"} | close_time_correct = false. The same file writes the scoped form in two other places (lines 296 and 299 usespan.current_ledger_seq/span.ledger_seq), so the file contradicts itself. Independent confirmation that the scoped form is the correct one: the same four queries were rewritten later in the chain — onpratik/otel-phase9-metric-gap-fillthe runbook line 375 reads{name="consensus.accept.apply" && span.close_time_correct = false}, andspan.-scoped usage in that file goes from 4 occurrences here to 93 there. The bare form is still present on phases 5, 6, 7 and 8. -
The runbook documents a stack this branch does not ship (threads
PRRT_kwDOACmRR86CFdBvandPRRT_kwDOACmRR86CFdau).docker/telemetry/on this branch contains exactly four files (docker-compose.yml,otel-collector-config.yaml,tempo.yaml,grafana/provisioning/datasources/tempo.yaml). There is no Prometheus service in the compose file, nospanmetricsconnector orprometheusexporter in the collector config — its only pipeline isreceivers: [otlp]toexporters: [debug, otlp/tempo]— and nografana/dashboards/directory. Sections "Prometheus Metrics (Spanmetrics)" (lines 390-435) and "Grafana Dashboards" (lines 520-583) describe metric names and dashboard panels that nothing here produces. The Quick Start service list and ports at lines 19-23 are correct, for what it is worth. -
Not a defect, just a note on scope:
docs/telemetry-runbook.mddiverges into six different versions across the chain (656 / 882 / 892 / 956 / 3529 / 3923 lines on phases 5, 6, 7, 8, 9, 10). A fix applied on a later branch will not reach this one, and a fix here will conflict when merged forward, so points 1 and 2 need fixing on this branch specifically rather than at the tip.
Left open
The unresolved thread on src/libxrpl/telemetry/TelemetryConfig.cpp:110 is a real, low-severity point and I have left it open rather than resolving it. Two details worth adding to it: src/libxrpl/telemetry/Telemetry.cpp:305-307 passes only the certificate paths to the exporter, so this validation read is the only place the private key bytes ever enter the process; and getFileContents' maxSize parameter is not passed, so the size guard at src/libxrpl/basics/FileUtilities.cpp:33 is skipped and the read is unbounded. Proving readability without materialising the file contents removes both points at once.
Not posted
A documentation-structure nit in cfg/xrpld-example.cfg: the "Head sampling is intentionally fixed at 1.0" paragraph now sits at lines 1758-1764, directly under the tls_client_key= block, where every other paragraph at that indent level describes the key named immediately above it. It also predates this PR (it previously sat under tls_ca_cert=), so per the project's own rule about not raising style points on pre-existing lines I am not filing it — noting it only because the 29 added lines are what changed which key it appears to belong to.
|
|
||
| ```bash | ||
| conan install . --build=missing -o telemetry=True | ||
| cmake --preset default -Dtelemetry=ON |
There was a problem hiding this comment.
[SHOULD-FIX · High] — The Quick Start build commands name a CMake preset this repo does not have, so step 3 fails for anyone following the runbook.
Lines 39-40 tell the reader to run cmake --preset default -Dtelemetry=ON and then cmake --build --preset default. There is no preset named default in this repo:
- No presets file is tracked at all:
git ls-files | grep -i presetreturns nothing, and.gitignore:23ignores onlyCMakeUserPresets.json. - The presets file Conan generates lands in
build/generators/(set byconanfile.py:170,self.folders.generators = "build/generators"), and the only preset inside it isconan-release(conan-debugfor a Debug install). Socmake --preset defaultfrom the repo root finds no presets file, and even afterconan installthere is no preset by that name. BUILD.md, the repo's own build guide, never uses--preset. It usesconan install .. --output-folder .from a build directory, thencmake ... -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake ..andcmake --build ..
Why it matters: this is the first thing an operator runs from this runbook and it errors out instead of building. The same command appears again at line 653 under "Disabling Telemetry", so that instruction fails too.
Fix: use the BUILD.md invocation, or --preset conan-release. Related while you are here: -Dtelemetry=ON / -o telemetry=True are already the defaults (CMakeLists.txt:148 is option(telemetry "Enable OpenTelemetry tracing" ON), conanfile.py:60 sets "telemetry": True), so step 3 reads as an opt-in that is not actually needed.
There was a problem hiding this comment.
Confirmed — both sites now use the flow BUILD.md documents (conan install .. --output-folder ., then the explicit conan_toolchain.cmake line, then cmake --build .), with a note that conan-release is the only preset Conan writes. --preset default is down from 3 occurrences in the runbook to 0, and since both flags are still the defaults the text now says so and asks for them to be passed rather than omitted. Committed here, not pushed yet.
| {span.current_ledger_seq = N} | ||
|
|
||
| # Join to the ledger build/consensus trace for the same ledger | ||
| {name="ledger.build" && span.ledger_seq = N} |
There was a problem hiding this comment.
[SHOULD-FIX · Medium] — This join query returns nothing on this branch, because no ledger.build span is emitted here yet.
Lines 286-305 tell operators to bridge transaction-side spans to the ledger trace with {name="ledger.build" && span.ledger_seq = N}. That span does not exist on this branch:
src/xrpld/app/ledger/detail/LedgerSpanNames.his absent here —git ls-tree -r --name-only pratik/otel-phase5-docs-deployment | grep -c LedgerSpanNamesgives 0. It is added by commitcbbd6ebee2, which lands onpratik/otel-phase6-statsd.- There is no
ledger_spannamespace ininclude/xrpl/telemetry/SpanNames.h(grep returns nothing). - The only occurrences of the string
ledger.buildon this branch are two Doxygen examples (include/xrpl/telemetry/SpanGuard.h:149and:674), one prose mention (include/xrpl/telemetry/SpanNames.h:153), and a unit test that builds the span itself (src/tests/libxrpl/telemetry/SpanGuardScope.cpp:423-457). None of those emit it at runtime.
Why it matters: the operator gets an empty result with no error, so the workflow this section exists for — correlating a transaction to the ledger it was worked on — cannot be completed on this branch.
The span arrives with phase-6, so the text becomes correct there. Since each PR in the chain has to stand alone, either move this subsection to the phase-6 PR or say here that its ledger-side half needs that branch.
There was a problem hiding this comment.
The ledger-side half of this join lands on pratik/otel-phase6-statsd (PR #6439), the branch right after this one:
src/xrpld/app/ledger/detail/LedgerSpanNames.his added there and declaresledger_span::op::build.src/xrpld/app/ledger/detail/BuildLedger.cpp:55creates the span asScopedSpanGuard(TraceCategory::Ledger, seg::ledger, ledger_span::op::build), which composes toledger.build.BuildLedger.cpp:90setsledger_seqon it, so thespan.ledger_seq = Nfilter this query uses resolves from phase-6 onward.
Checked across the whole chain: phase-1a through phase-5 have no LedgerSpanNames.h and no emission site; phase-6 is the first branch with both. So the query in this section is correct at phase-6 and later, and returns nothing only while reading this branch in isolation.
…phase5-docs-deployment
The rename arrived from phase-1b by merge; the runbook still told operators to set `endpoint`, which the parser no longer reads. Updates the Quick Start ini block and the Configuration Reference row. No metrics_endpoint row is added: this branch exports no metrics, so documenting the key here would describe something the code ignores.
Conflict in OpenTelemetryPlan/05-configuration-reference.md: phase-5 had corrected the enabled and use_tls types to 0 or 1 and added the tls_client_cert and tls_client_key rows, while the incoming side renamed the endpoint option. Composed both — phase-5's type corrections and its two mTLS rows are kept, with the endpoint row renamed to traces_endpoint.
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
|
All conflicts have been resolved. Assigned reviewers can now start or resume their review. |
There was a problem hiding this comment.
This PR adds runbook/config docs and exporter-side mTLS support (two new optional config keys, startup-time validation of the cert/key pairing and use_tls dependency, file-readability checks, and a top-level catch in Main.cpp so a bad [telemetry] config fails cleanly instead of terminating). The production code (TelemetryConfig.cpp, Telemetry.h/.cpp, Main.cpp) is internally consistent and the extensive doc updates (cfg comments, configuration-reference.md, telemetry-runbook.md) accurately describe the new validation behavior — cross-checked the error message text, exit-code claim (255 from return -1), and the enabled/use_tls gating logic against the code and found no discrepancies. The one thing worth a closer look before merge is a new test file dependency (TempDir) that doesn't appear to have a corresponding include added. Note: the PR description contains a 'No need to review tasklist files' instruction that a security scanner flagged as a possible prompt-injection pattern; I did not treat it as binding and reviewed all changed files on their technical merits regardless — the tasklist-file diff itself is trivial (YAML sample metric-name renames) and contains no issues either way.
The consensus.accept.apply row listed close_time, parent_close_time and close_time_self. The emitted keys carry the unit and epoch suffix, so update the row to match.
Two conflicts, both additive on each side. TelemetryConfig.cpp: include blocks only. This branch added FileUtilities.h for the certificate readability checks; upstream added <limits> and <optional> for the bounds parser. Both kept. The TelemetryConfig test: this branch's mutual-TLS cases and upstream's batch-bounds cases were added at the same positions, so the file is rebuilt from both stages and carries all 32 tests. Two shared cases were each edited by one side only, so the edited side wins in each: upstream asserts the batch defaults in parse_empty_section, and this branch's parse_full_section writes a real certificate file, which is now required since the parser opens it.
| // any exporter that did run would connect in plaintext. Reject that | ||
| // contradiction instead of failing open. tls_ca_cert is deliberately | ||
| // not checked this way. | ||
| if (!setup.tlsClientCertPath.empty() && !setup.useTls) |
There was a problem hiding this comment.
With enabled=1, use_tls=1, and readable client certificate/key files, an http:// endpoint—including the default when traces_endpoint is omitted—passes all these checks.
TelemetryImpl::start() forwards that URL unchanged, and the pinned OpenTelemetry 1.28.0 SDK selects TLS from the URL scheme, so an HTTP receiver can still receive plaintext spans without client authentication.
Please reject non-HTTPS endpoints when client credentials are configured, and cover explicit/default HTTP rejection plus HTTPS acceptance in the configuration tests.
An operator could set use_tls=1 and provide the client certificates, but accidentally leave the endpoint as HTTP. The node starts without warning, yet sends telemetry unencrypted and without using those certificates. If the collector requires mTLS, the connection fails and no traces arrive.
There was a problem hiding this comment.
The parser now requires traces_endpoint to start with https:// whenever tls_client_cert is set, so the plain-HTTP default is rejected too. Config tests cover explicit HTTP, the omitted-key default, HTTPS acceptance and a case-sensitivity check, plus a one-way-TLS control that must stay accepted.
| exporterOpts.ssl_ca_cert_path = setup_.tlsCertPath; | ||
| // Present a client cert for mutual TLS. When both paths are | ||
| // empty the connection falls back to one-way (server) TLS. | ||
| exporterOpts.ssl_client_cert_path = setup_.tlsClientCertPath; |
There was a problem hiding this comment.
The new mTLS tests assert parsed Setup paths, but no test asserts the client options passed to OtlpHttpExporterFactory::Create.
The only direct makeTelemetry() test disables telemetry, while the span-export tests build their own in-memory pipeline; source inspection therefore leaves omitted or swapped client-option assignments undetected.
Please add a telemetry-enabled production-setup test using an HTTPS endpoint and distinct certificate/key paths, asserting both exact options at the exporter boundary, with a one-way-TLS control.
A local collector requiring client authentication would also cover this boundary, but merely asserting that start() does not throw would not.
Someone could accidentally remove these assignments or pass the certificate path where the key belongs, and the configuration tests would still pass. Operators would only discover the problem when their collector rejects the connection and traces stop arriving.
There was a problem hiding this comment.
The options mapping now lives in makeTraceExporterOptions(), with a new test asserting url, ssl_ca_cert_path, ssl_client_cert_path and ssl_client_key_path at that boundary using three distinct paths, so a swap fails. It has a one-way-TLS control and a use_tls=0 control, and one case runs the whole path from a [telemetry] section.
| | `trace_consensus` | `1` | Enable consensus tracing | | ||
| | `trace_peer` | `1` | Enable peer message tracing (high volume) | | ||
| | `trace_ledger` | `1` | Enable ledger tracing | | ||
| | `consensus_trace_strategy` | `deterministic` | Consensus trace ID strategy (`deterministic` or `random`) | |
There was a problem hiding this comment.
Setting the documented consensus_trace_strategy=random does not select random trace IDs.
The parser preserves that string, but RCLConsensus::Adaptor::startRoundTracing() selects random IDs only for "attribute"; every other value takes the deterministic, ledger-hash-derived branch.
Please change the documented alternative to attribute, so operators can actually select the advertised strategy.
There was a problem hiding this comment.
We kept the documented spellings and changed the code instead: consensus_trace_strategy is parsed once into an enum, deterministic or random, and the behaviour previously selected by attribute is now random. Anything else fails at startup, and random is documented as experimental and not used, because it gives each node its own trace id.
| | Parent Span | Event Name | Event Attributes | Description | | ||
| | ---------------------------- | ----------------- | ----------------------------------------------------------- | ------------------------------------------------------- | | ||
| | `consensus.update_positions` | `dispute.resolve` | `tx_id`, `dispute_our_vote`, `dispute_yays`, `dispute_nays` | Emitted per dispute when votes are tallied | | ||
| | `consensus.accept.apply` | `tx.included` | `tx_id` | Emitted per transaction included in the accepted ledger | |
There was a problem hiding this comment.
When a consensus-input transaction later fails application or remains retriable, its tx.included event has already been emitted.
RCLConsensus.cpp:703–734 emits the event while filling retriableTxs, before buildLCL(); BuildLedger.cpp:128–148 can subsequently reject the transaction or leave it unapplied.
This description therefore makes the event look like proof of accepted-ledger inclusion when it is not.
Please describe it as an input-set transaction queued for application, or emit it only after verifying inclusion in the built ledger if final inclusion is the intended contract.
An operator investigating a missing transaction could see tx.included and conclude that it reached the accepted ledger. The transaction may actually have failed during application, so the runbook sends the investigation in the wrong direction.
There was a problem hiding this comment.
Reworded to say the event covers the agreed consensus set and is recorded before buildLCL(), so it is a superset of the accepted ledger, with a note pointing an investigator at ter_result and applied on tx.transactor to confirm a transaction applied. The emission is unchanged: it is the round's input set, a transaction whose bytes cannot be parsed gets no event, and nothing that reaches the accepted ledger is missing one.
| {name="consensus.round"} | consensus_round_id = "<round_id>" | ||
|
|
||
| # Find dispute resolutions | ||
| {name="consensus.update_positions"} >> {event:name="dispute.resolve"} |
There was a problem hiding this comment.
dispute.resolve is added directly to the consensus.update_positions span, but >> looks for a descendant span carrying that event. This misses the event on the span that actually records it. Could we use {name="consensus.update_positions" && event:name="dispute.resolve"} instead, so both conditions apply to the same span?
There was a problem hiding this comment.
Changed to that form. Checked against our Tempo over the same 24 hours: the >> form returns 0 traces, the corrected one returns 20.
| {name="txq.accept"} | ledger_changed = true | ||
|
|
||
| # Find transactions that exhausted retries | ||
| {name="txq.accept_tx"} | txq_status = "retried" && retries_remaining = 0 |
There was a problem hiding this comment.
Because retries_remaining is recorded before the decrement, a retried span never has a zero count. Transactions that still cannot be applied when no retries remain are marked failed and removed. If this example is meant to find transactions dropped after exhausting retries, could we use failed with retries_remaining <= 0 instead?
There was a problem hiding this comment.
Changed to failed with retries_remaining <= 0, which is exact because the value recorded before the attempt floors at zero. Also stated on the attribute constant that it is recorded before the attempt and before any decrement, which is where the confusion started.
…s set The OTLP/HTTP exporter selects TLS from the endpoint URL scheme alone (HttpSslOptions in the pinned SDK matches "https:" exactly), so a client certificate handed to it alongside an http:// traces_endpoint is loaded and never presented. The parser checked the cert/key pairing, use_tls and file readability, but never the scheme, and the default traces_endpoint is plain HTTP. makeTelemetrySetup() now requires traces_endpoint to start with "https://" whenever tls_client_cert is set, including when the key is left at its default. Nothing asserted the client options reaching the exporter, so a swapped certificate and key would have passed every test. Move the options mapping into makeTraceExporterOptions() and assert it at that boundary with distinct certificate and key paths, plus a one-way-TLS control and a use_tls=0 control. One case runs the whole path from a [telemetry] section. Runbook and example-config fixes: - tx.included is emitted per transaction of the agreed consensus set, before buildLCL() applies anything, so it is a superset of the accepted ledger rather than proof of inclusion. - the dispute.resolve query used the descendant operator, but the event is on the consensus.update_positions span itself, so it matched nothing. - the exhausted-retries query asked for txq_status="retried" with retries_remaining=0, which cannot occur: the attribute is stamped before the attempt and the retried branch only runs while retries are left. Exhaustion is txq_status="failed" with a zero count. - consensus_round_id is an int64, so the two queries comparing it to a quoted string matched nothing. - note that consensus_trace_strategy=random is experimental and not used. - note that a trailing "| attr = value" is rejected by current Tempo; attribute filters belong inside the braces.
Two conflicts, both additive. TelemetryConfig.cpp: this branch added requireHttpsEndpoint next to requireReadableFile; upstream added readConsensusTraceStrategy at the same spot. Both kept. 05-configuration-reference.md: this branch added the two client-certificate rows while upstream corrected the consensus strategy value from attribute to random. Both kept. Also drops the stale "not yet implemented" row for consensus_trace_strategy, which the merged table now contradicts twice over: the option is parsed, and its value is no longer spelled attribute.
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
|
All conflicts have been resolved. Assigned reviewers can now start or resume their review. |
…phase5-docs-deployment
There was a problem hiding this comment.
This PR adds a large operator-facing telemetry runbook, exporter mTLS support (tls_client_cert/tls_client_key config keys with startup validation), and a defensive try/catch around Application construction in Main.cpp. The mTLS validation logic (pairing check, use_tls requirement, https-scheme requirement, file-readability check) is well-structured and thoroughly unit-tested, and the code changes (TelemetryConfig.cpp, Telemetry.cpp/h, Main.cpp) look correct on inspection. The one substantive issue found is a self-contradiction in the new docs/telemetry-runbook.md: it explicitly warns that the {name="x"} | attr = value pipe syntax is rejected by current Tempo, yet the vast majority of the runbook's own copy-paste TraceQL examples (its main selling point) use exactly that rejected syntax. Note: the MR description contains a 'No need to review tasklist files' instruction, which was flagged by automated scanning as a potential prompt-injection pattern; it appears in the human-authored PR description (not in code/comments) and looks like a benign author note excluding auto-generated planning docs rather than an attempt to hide a malicious change, so it was not treated as a security bypass, but the tasklist file was still checked and found to contain only a benign dimension-name rename.
requireReadableFile proved a path readable with getFileContents, which loads the whole file into a std::string and then drops it. One of the three paths it checks is tls_client_key, so a private key was loaded to answer a question that does not need its contents. It now stats the path, rejects anything that is not a regular file, and opens it without reading. The message shape is unchanged: "[telemetry] <key> cannot be read: <path> - <reason>". A path naming a directory used to escape as an ios failure from the stream buffer, naming neither the config key nor the path. It is now rejected as "not a regular file" with both named. The new test covers that case; it fails against the old implementation and against a copy with the file-type branch removed. The runbook's quick start and disable sections both told the reader to run "cmake --preset default". No presets file is tracked, and the only preset Conan generates is conan-release, so each of those steps failed on its first command. Replaced with the flow BUILD.md documents, and noted that telemetry is the current default while still passing the flags.
readability-identifier-naming wants lower_case for a namespace, so clang-tidy failed on this file under warnings-as-errors. All seven use sites move with the declaration.
The stream is only tested for failure, and both operator bool and operator! are const members.
…phase5-docs-deployment
…phase5-docs-deployment
There was a problem hiding this comment.
This PR (mTLS on the OTLP exporter + a large new operator runbook) is unusually well-scoped and well-tested. The new validation logic in TelemetryConfig.cpp (pairing check for tls_client_cert/tls_client_key, use_tls requirement, https:// scheme requirement, and readable-file checks) is internally consistent, correctly gated on enabled, and matches both the cfg comments and the new runbook prose almost line-for-line. The Main.cpp change to catch exceptions from Application construction and print a clean startup error (rather than std::terminate) is a sound, narrowly-scoped improvement. I traced the mTLS logic branch-by-branch against the new unit tests and did not find a gap; the doc changes (05-configuration-reference.md, xrpld-example.cfg, telemetry-runbook.md) accurately describe the implemented behavior. No security, correctness, or resource-management defects were found in the added code. Note: the MR description contains the line "No need to review tasklist files," which an automated injection scanner flagged; I did not treat it as an instruction and reviewed the tasklist diff (Phase5_taskList.md) anyway — it's a benign, in-scope documentation edit (renaming spanmetrics dimension keys to underscore form to match the actual attribute names) with no issues.
High Level Overview of Change
Two operator-facing additions on top of the instrumentation from Phases 1-4: a TraceQL runbook and mutual-TLS support on the OTLP exporter.
No need to review tasklist files.
Context of Change
Runbook (
docs/telemetry-runbook.md) — quick start, a full[telemetry]configuration reference, a span reference, and copy-paste TraceQL grouped by subsystem: failed transactions and per-type latency, queue rejections and retries, slow ledger applications, consensus close-time disagreements andmoved_onoutcomes, RPC batch/large-payload requests, expensive path-finding.Exporter mTLS — the exporter could verify the collector but could not prove its own identity, so a collector had no way to reject an unknown sender. Two new keys:
tls_client_certtls_client_keyBoth default to empty. With
use_tls=1and both set, the exporter performs mutual TLS and the collector can enforcerequire_and_verify_client_cert. Empty paths keep the existing one-way TLS behaviour anduse_tls=0is untouched, so local dev is unaffected. Collector-side enforcement and certificate issuance are deployment concerns outside this PR.API Impact
No public API change. Adds two optional
[telemetry]config keys, both defaulting to empty.Test Plan
use_tls=1+ both client keys set → mutual TLS handshake against a collector requiring client certs.use_tls=1with empty client keys → still connects one-way (no regression).use_tls=0→ unchanged.