diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index d3a44aa9e0..fd460480a9 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -288,6 +288,13 @@ jobs: with: recipe: "actionlint" + - name: "markdownlint" + id: "markdownlint" + continue-on-error: true + uses: *just + with: + recipe: "markdownlint" + - name: "license-headers" id: "license-headers" continue-on-error: true @@ -305,6 +312,7 @@ jobs: zizmor=${{ steps.zizmor.outcome }} pinact=${{ steps.pinact.outcome }} actionlint=${{ steps.actionlint.outcome }} + markdownlint=${{ steps.markdownlint.outcome }} license-headers=${{ steps.license-headers.outcome }} run: | set -euo pipefail diff --git a/.markdownlint.json b/.markdownlint.json index 95390fc9a4..11ffabfd71 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -6,12 +6,11 @@ "style": "backtick" }, "emphasis-style": { - "allow_asterisk": true, - "allow_underscore": true + "style": "underscore" }, "line-length": { "strict": false, - "line_length": 120, + "line_length": 160, "code_blocks": false }, "link-image-style": { diff --git a/Cargo.lock b/Cargo.lock index 859e51db0d..f749e2afd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,7 +900,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2210,7 +2210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2590,9 +2590,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", @@ -4928,7 +4928,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4985,7 +4985,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5624,7 +5624,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6337,7 +6337,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/README.md b/README.md index 5639c08d08..8130a8005c 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,10 @@ of the Fabric. [nix]: https://nixos.org/download/#nix-install-linux [just]: https://github.com/casey/just - > [!NOTE] -> Ensure flakes are enabled (e.g. `experimental-features = nix-command flakes` in `~/.config/nix/nix.conf` for single-user installs, or `/etc/nix/nix.conf` for multi-user installs), or some commands will fail. +> Ensure flakes are enabled +> (e.g. `experimental-features = nix-command flakes` in `~/.config/nix/nix.conf` for single-user installs, +> or `/etc/nix/nix.conf` for multi-user installs), or some commands will fail. ### Step 0. Clone the repository diff --git a/default.nix b/default.nix index 8088e8c04b..4aeeda88eb 100644 --- a/default.nix +++ b/default.nix @@ -170,6 +170,7 @@ let kopium llvmPackages'.clang # you need the host compiler in order to link proc macros llvmPackages'.llvm # needed for coverage + markdownlint-cli2 npins opengrep openssl diff --git a/development/documentation/README.md b/development/documentation/README.md index 7d1a89f768..b0b79d7796 100644 --- a/development/documentation/README.md +++ b/development/documentation/README.md @@ -11,14 +11,18 @@ Design your documentation to be maintainable. If your code is incomprehensible then documentation won't fix it. - **Line breaks in between sentences** - This makes diffs easier to understand and discuss. - **Try to satisfy linting rules** - Ensure documentation is free of linting errors. + - Run `just markdownlint` to check, or `just markdownlint --fix` to repair the mechanical violations. + CI runs the same recipe. - The `.markdownlint.json` file may be adjusted if the rules proves overly restrictive, but try to avoid deactivating too many lints. + - The line length limit is deliberately loose (160) because it is a backstop, not the primary rule: + break lines between sentences first and let the limit catch only the rare over-long one. - **Prefer markdown shortcut links to inline links** - Use markdown shortcut syntax for links and images to make the documentation more readable and maintainable for humans. Inline links are disruptive to humans reading or editing the documentation as raw text. It is also easier to maintain the links when external resources change. - *Example*: + _Example_: ```markdown - This is an [inline markdown link example](https://www.markdownguide.org/basic-syntax/#links) diff --git a/flow-filter/README.md b/flow-filter/README.md index 3a7c9858fb..85e0a6aa96 100644 --- a/flow-filter/README.md +++ b/flow-filter/README.md @@ -62,15 +62,15 @@ Rules are protocol-aware by encoding the packet's L4 protocol as the first ACL k - TCP/UDP rules match exactly (`mask 0xff`). - `L4Protocol::Any` wildcards the proto (`mask 0x00`), so non-TCP/UDP packets only match `Any` rules. -Stateful-NAT exposes get special treatment while building, because only the *forward* +Stateful-NAT exposes get special treatment while building, because only the _forward_ direction of their sessions can be answered from config alone: - **local end** excludes port-forwarding exposes (`can_init_connection()`): a port-forwarding source cannot initiate, and a covering expose (e.g. masquerade) must - answer for connection initiation from that range. Reply traffic *from* a + answer for connection initiation from that range. Reply traffic _from_ a port-forwarding-only source surfaces as a distinct stage-2 miss (`SourceMiss`), which the NF resolves against the packet's established flow. -- **remote end** keeps masquerade exposes, but only as *markers*: a masquerade +- **remote end** keeps masquerade exposes, but only as _markers_: a masquerade destination cannot accept new connections, so the NF lets a masquerade verdict through only when the packet rides an established masquerade flow (reply traffic); otherwise it drops. The marker is what distinguishes that reply traffic from a destination no diff --git a/interface-manager/src/tc/README.md b/interface-manager/src/tc/README.md index 710a1966f1..e3e43ab0cc 100644 --- a/interface-manager/src/tc/README.md +++ b/interface-manager/src/tc/README.md @@ -22,10 +22,12 @@ erDiagram ### qdisc **A `qdisc` is short for queuing discipline**. -There are many different algorithms for queueing packets, but the non-trivial case pretty much exclusively focuses on _the order in which packets **egress**_ a network card. +There are many different algorithms for queueing packets, +but the non-trivial case pretty much exclusively focuses on _the order in which packets **egress**_ a network card. `qdisc` also applies to ingress traffic, but in a much more trivial sense. -You can't really control the order or timing in which packets arrive, so the ingress `qdisc` is mostly just a place to attach [filters], [chains], and (optionally) [block]s. +You can't really control the order or timing in which packets arrive, +so the ingress `qdisc` is mostly just a place to attach [filters], [chains], and (optionally) [block]s. In general, `qdisc`s are quite complex and powerful, but we only need the "trivial" case (ingress) for our current design. Our ingress qdisc of choice is called clsact; basically FIFO but with the ability to attach (potentially offloaded) [filter]s rules. @@ -48,24 +50,36 @@ In such cases, a packet must match all listed criteria to trigger the [action]s. An action is some type of manipulation or event that may occur when a packet matches on a [filter] criteria. -Examples of action include +Examples of actions include -* dropping packets, -* editing source or destination ip addresses, -* pushing or popping VLAN headers, -* redirecting the packet to the ingress or egress pipeline of another network device, -* encapsulating the packet in a VXLAN packet, -* or mirroring (copying) the packet to another network device. +- dropping packets, +- editing source or destination ip addresses, +- pushing or popping VLAN headers, +- redirecting the packet to the ingress or egress pipeline of another network device, +- encapsulating the packet in a VXLAN packet, +- or mirroring (copying) the packet to another network device. Actions are reference counted and **may be attached to more than one filter.** More specifically, each action has four important variables associated with it -1. The action `kind`. `kind` is a static string which identifies the family of the action. Examples include `mirred`, which covers packet redirect and mirroring; `gact`, which includes actions like `drop` or `jump`; and `tunnel_key`, which includes encapsulation and decapsulation actions. Many other families of actions exist in the linux `tc` subsystem. -2. The action `index`. `index`, is a unique (_per `kind`_) identifier to track the specific incidence of a given action. For example, if you might create a `gact` dro action with index 17. You may then attach that drop command to several different filters by referencing that index on filter creation. If you recycle an action in this way then all the filters associated with that action will update the hit counters for that action. Reuse of actions in this way may also save resources in the network card (assuming the matches and action can be offloaded). -3. The action `bindcnt`. This is the number of active filters which reference this action. -4. The action `refcnt`. This is the reference count of the action. This number is either equal to the `bindcnt` or is equal to the `bindcnt` plus one. - -If you create an action as part of a filter creation command, then you the `refcnt` and the `bindcnt` numbers will be equal. +1. The action `kind`. + `kind` is a static string which identifies the family of the action. + Examples include `mirred`, which covers packet redirect and mirroring; `gact`, which includes actions like `drop` or `jump`; + and `tunnel_key`, which includes encapsulation and decapsulation actions. + Many other families of actions exist in the linux `tc` subsystem. +2. The action `index`. + `index`, is a unique (_per `kind`_) identifier to track the specific incidence of a given action. + For example, if you might create a `gact` drop action with index 17. + You may then attach that drop command to several different filters by referencing that index on filter creation. + If you recycle an action in this way then all the filters associated with that action will update the hit counters for that action. + Reuse of actions in this way may also save resources in the network card (assuming the matches and action can be offloaded). +3. The action `bindcnt`. + This is the number of active filters which reference this action. +4. The action `refcnt`. + This is the reference count of the action. + This number is either equal to the `bindcnt` or is equal to the `bindcnt` plus one. + +If you create an action as part of a filter creation command, then the `refcnt` and the `bindcnt` numbers will be equal. If you create an action without attaching it to a filter, you need to set the `refcnt` to one (in which case the `bindcnt` will be zero automatically). Linux will automatically remove any action if and only if the `bindcnt` _and_ `refcnt` are equal to zero. @@ -93,9 +107,13 @@ If you configure `tc` [filter]s in the most basic way, you may need to install t This is bad for a few major reasons: -1. It is pointlessly complex. You would need to keep track of all 10 rules. You need to create it 10 times, delete it 10 times, update it 10 times, track the counters 10 times, and so on. -2. It is wasteful of resources in the ASIC. Installing the rule into the hardware once saves TCAM or SRAM in the ASIC (both of which are in limited supply). -3. It is more prone to race conditions. Updating the rule in one place is usually much easier to sequence when transitioning between rule sets. +1. It is pointlessly complex. + You would need to keep track of all 10 rules. + You need to create it 10 times, delete it 10 times, update it 10 times, track the counters 10 times, and so on. +2. It is wasteful of resources in the ASIC. + Installing the rule into the hardware once saves TCAM or SRAM in the ASIC (both of which are in limited supply). +3. It is more prone to race conditions. + Updating the rule in one place is usually much easier to sequence when transitioning between rule sets. [action]: #action [block]: #block diff --git a/justfile b/justfile index ec713f98e3..d89fb58b65 100644 --- a/justfile +++ b/justfile @@ -415,6 +415,12 @@ actionlint: {{ _just_debuggable_ }} actionlint +# Limit linting to tracked Markdown so generated files cannot affect CI. +[script] +markdownlint *args: + {{ _just_debuggable_ }} + git ls-files -z '*.md' | xargs -0 markdownlint-cli2 {{ args }} + [script] license-headers: {{ _just_debuggable_ }} @@ -461,6 +467,7 @@ lint: \ (zizmor) \ (pinact "--fix=false" "--no-api") \ (actionlint) \ + (markdownlint) \ (license-headers) {{ _just_debuggable_ }} diff --git a/tracectl/README.md b/tracectl/README.md index 171b5683c1..081fb74f6e 100644 --- a/tracectl/README.md +++ b/tracectl/README.md @@ -1,48 +1,98 @@ # Dynamic run-time tracing control ## Background, why this crate exists and what it does -* The dataplane implementation uses the *tracing* crate to emit logs, in all of the crates that make it. -* The tasks carried out by those crates are diverse, happen concurrently and have different time requirements. -* Troubleshooting and testing is becoming hard for the usual reason: high log-levels can cause an overwhelming amount of (mostly useless) data; too low of a log-level may not capture enough data to understand a failure. In production, the performance penalty of verbose logging or their demanding storage requirements make it not feasible to set high log-levels other than for a subset of the logs and in small periods of time for troubleshooting. -* The tracing crate allows to dynamically change the log-level of logs/traces via the so-called *EnvFilter*. -* However, we currently (mis)use a single hard-coded EnvFilter, without exploiting its flexibility: pretty much all logs are either disabled or governed by a single log-level set at compile time. -Ideally, we would be able to change traces' log levels (or even enable/disable them) selectively for the distinct subsystems, at runtime. -Run-time adjustment of the log-level can be achieved by the EnvFilter object and its reloading capabilities. The EnvFilter allows adjusting log-levels for each *target*. Targets exist for *events* and *spans*. Think of targets as scopes where traces belong or are sent to: when some trace like `info!("Hey")` is added to the code, a target is implicitly created and automatically named, by default, based on the path of the module/submodule where the log resides in the source code (this is done using the `module_path!()` built-in). Knowing target identifiers is needed to adjust their log-level. This can be problematic in the dataplane implementation since the code is spread across distinct crates: we'd like to control the verbosity of targets across all crates in a **centralized** fashion; however, keeping some "target database" may not be easy because, if we use the implicit path-based identifiers, the targets in such a database may accidentally get out-of-sync if crates get internally re-organized. For instance, a trace emitted within some `masquerade` submodule within the `nat` crate may use an implicit target of `nat::masquerade`. If the nat crate happened to be re-organized or some of the modules renamed, the target ids may become, say, `cgnat::modes::masquerade`. This would require the database of targets to be updated and replace `nat::masquerade` by `cgnat::modes::masquerade`. Some way to get stable identifiers for implicitly-named targets is needed. +- The dataplane implementation uses the _tracing_ crate to emit logs, in all of the crates that make it. +- The tasks carried out by those crates are diverse, happen concurrently and have different time requirements. +- Troubleshooting and testing is becoming hard for the usual reason: high log-levels can cause an overwhelming amount of (mostly useless) data; + too low of a log-level may not capture enough data to understand a failure. + In production, the performance penalty of verbose logging or their demanding storage requirements make it not feasible + to set high log-levels other than for a subset of the logs and in small periods of time for troubleshooting. +- The tracing crate allows to dynamically change the log-level of logs/traces via the so-called _EnvFilter_. +- However, we currently (mis)use a single hard-coded EnvFilter, without exploiting its flexibility: + pretty much all logs are either disabled or governed by a single log-level set at compile time. -One option would be to explicitly set the target names in all the member crates. That would solve the issue of the database becoming out-of-sync on crate reorganization. However, that would not solve the problem of populating the target database in the first place: any time a crate defined a new target, we'd still need to update the database to add it. This may be solved by letting the participating crates register their targets in the database. However, such as solution is inconvenient if done at run-time: how would crates declare their targets? Would there need to be an initialization routine per crate? Who would call that? +Ideally, we would be able to change traces' log levels (or even enable/disable them) selectively for the distinct subsystems, at runtime. +Run-time adjustment of the log-level can be achieved by the EnvFilter object and its reloading capabilities. +The EnvFilter allows adjusting log-levels for each _target_. +Targets exist for _events_ and _spans_. +Think of targets as scopes where traces belong or are sent to: when some trace like `info!("Hey")` is added to the code, +a target is implicitly created and automatically named, by default, +based on the path of the module/submodule where the log resides in the source code (this is done using the `module_path!()` built-in). +Knowing target identifiers is needed to adjust their log-level. +This can be problematic in the dataplane implementation since the code is spread across distinct crates: +we'd like to control the verbosity of targets across all crates in a **centralized** fashion; +however, keeping some "target database" may not be easy because, if we use the implicit path-based identifiers, +the targets in such a database may accidentally get out-of-sync if crates get internally re-organized. +For instance, a trace emitted within some `masquerade` submodule within the `nat` crate may use an implicit target of `nat::masquerade`. +If the nat crate happened to be re-organized or some of the modules renamed, the target ids may become, say, `cgnat::modes::masquerade`. +This would require the database of targets to be updated and replace `nat::masquerade` by `cgnat::modes::masquerade`. +Some way to get stable identifiers for implicitly-named targets is needed. + +One option would be to explicitly set the target names in all the member crates. +That would solve the issue of the database becoming out-of-sync on crate reorganization. +However, that would not solve the problem of populating the target database in the first place: +any time a crate defined a new target, we'd still need to update the database to add it. +This may be solved by letting the participating crates register their targets in the database. +However, such a solution is inconvenient if done at run-time: how would crates declare their targets? +Would there need to be an initialization routine per crate? +Who would call that? This crate exists to solve the above issues allowing: - * the automatic registration of targets across multiple crates in a centralized database, *at build time*, ensuring that: - * target names are *always* up-to-date in the database - * no target is missed - * programmatically changing of each of the target log levels at run-time +- the automatic registration of targets across multiple crates in a centralized database, _at build time_, ensuring that: + - target names are _always_ up-to-date in the database + - no target is missed +- programmatically changing of each of the target log levels at run-time ## Usage model and requirements -* This crate cannot (nor should) understand trace semantics. Trace/log semantics and their relations must be provided by the crates defining them. -* Crates *register* their targets of interest (the targets whose log-level is to be dynamically adjusted) by *declaring* them, along with their initial, default log level, a stable *name* and optional **tags**. Tags serve two purposes. First, they act as stable identifiers to refer to targets independently of their path; e.g. in an API. Second, a target may be associated with multiple tags. This allows controlling multiple targets simultaneously. - -For instance, in a packet pipeline, each network function (NF) may emit logs to a distinct target; e.g. a NAT NF may have a target labeled as *nat*. This may allow enabling / disabling NAT-related debug logs at runtime, while only emitting warnings or errors in production. If, in addition, the NAT (and rest of NFs) are associated with some tag *pipeline*, one may be able to enable / disable the logs (or restrict them to, say, up to INFO) in all of the NFs composing the pipeline. -So, the takeaway is that tags represent **sets of targets** and a target can be member of an arbitrary number of sets. -* Tags are implemented as strings since each crate should be able to define them and having a custom type (e.g. some *enum* in a centralized crate, like this one) would entail needing to update that crate every time some other crate required a new tag. -* However, if a tag is to be shared by distinct crates, the consistency of that needs to be enforced outside of this crate. - -**Note:** The *name* of a target is its main identifier and is automatically treated as a *tag*. Therefore, registered targets always have at least one tag to control them. +- This crate cannot (nor should) understand trace semantics. + Trace/log semantics and their relations must be provided by the crates defining them. +- Crates _register_ their targets of interest (the targets whose log-level is to be dynamically adjusted) by _declaring_ them, + along with their initial, default log level, a stable _name_ and optional **tags**. + Tags serve two purposes. + First, they act as stable identifiers to refer to targets independently of their path; e.g. in an API. + Second, a target may be associated with multiple tags. + This allows controlling multiple targets simultaneously. + + For instance, in a packet pipeline, each network function (NF) may emit logs to a distinct target; + e.g. a NAT NF may have a target labeled as _nat_. + This may allow enabling / disabling NAT-related debug logs at runtime, while only emitting warnings or errors in production. + If, in addition, the NAT (and rest of NFs) are associated with some tag _pipeline_, + one may be able to enable / disable the logs (or restrict them to, say, up to INFO) in all of the NFs composing the pipeline. + So, the takeaway is that tags represent **sets of targets** and a target can be member of an arbitrary number of sets. +- Tags are implemented as strings since each crate should be able to define them + and having a custom type (e.g. some _enum_ in a centralized crate, like this one) would entail needing to update that crate + every time some other crate required a new tag. +- However, if a tag is to be shared by distinct crates, the consistency of that needs to be enforced outside of this crate. + +**Note:** The _name_ of a target is its main identifier and is automatically treated as a _tag_. +Therefore, registered targets always have at least one tag to control them. ## Implementation + This implementation has about 3 pieces: - * a *tracing controller*: a thread-safe database of targets, with some static initialization, that allows changing the log-level of targets and the default one and that allows reporting which targets are available, with which tags and log-level. This is important to be able to expose the targets in some form of API. We may expose the *tags* as stable identifiers. - * the automated discovery of targets at link time so that no APIs are needed to declare targets and tags. - * macros to ease target declaration and simplify the addition of logs -# Usage +- a _tracing controller_: a thread-safe database of targets, with some static initialization, + that allows changing the log-level of targets and the default one + and that allows reporting which targets are available, with which tags and log-level. + This is important to be able to expose the targets in some form of API. + We may expose the _tags_ as stable identifiers. +- the automated discovery of targets at link time so that no APIs are needed to declare targets and tags. +- macros to ease target declaration and simplify the addition of logs + +## Usage -## Target configuration and registration (how to declare targets in crates) +### Target configuration and registration (how to declare targets in crates) -### Implicit targets -Targets are implicitly created by macros like `info!()`, with a path that defaults to `module_path!()`. In order to be able to control the verbosity of those targets, these need to be *registered*. Registering such implicit targets is straightforward. We just need to declare the target in the module with macro `trace_target!` to associate it with a *name*, the initial log-level and additional, optional tags. +#### Implicit targets + +Targets are implicitly created by macros like `info!()`, with a path that defaults to `module_path!()`. +In order to be able to control the verbosity of those targets, these need to be _registered_. +Registering such implicit targets is straightforward. +We just need to declare the target in the module with macro `trace_target!` +to associate it with a _name_, the initial log-level and additional, optional tags. ```rust // import trace_target! macro to register targets @@ -52,24 +102,30 @@ use tracectl::trace_target; trace_target!("mytarget1", LevelFilter::ERROR, &["some-other-tag"]); ``` -**Note**: the *name* of a target (`"mytarget1"` above) needs not be specified in the array of tags and will be automatically added. +**Note**: the _name_ of a target (`"mytarget1"` above) needs not be specified in the array of tags and will be automatically added. -### Custom targets +#### Custom targets Placing a `trace_target!` stanza in each module/submodule should make all traces in a crate controllable via tag(s). We may, however, need more control within a module/submodule and be able to govern log levels at a higher granularity. -This can be achieved by declaring *custom* targets. Registering a custom target --a target with an explicitly-set identifier-- can be done similarly as +This can be achieved by declaring _custom_ targets. +Registering a custom target --a target with an explicitly-set identifier-- can be done similarly as ```rust custom_target!("my-custom-target", LevelFilter::ERROR, &["my-feature"]); ``` -The above simply registers a configuration for a target called `"my-custom-target"`. However that will do nothing if the target does not exist. For such a target to exist, some logs (events/spans) should refer to it. In order to emit logs within some custom target, the existing macros can be used, specifying the target as a key-value: +The above simply registers a configuration for a target called `"my-custom-target"`. +However that will do nothing if the target does not exist. +For such a target to exist, some logs (events/spans) should refer to it. +In order to emit logs within some custom target, the existing macros can be used, specifying the target as a key-value: + ```rust info!(target:"my-custom-target", "This is a log"); ``` In order to make the above less verbose, this crate defines new macros (`terror, twarn, tinfo, tdebug` and `ttrace`) which allow you to write, instead: + ```rust tinfo!("my-custom-target", "This is a log"); ``` @@ -103,78 +159,113 @@ impl BAR { ``` -With the above, each target gets a unique *name* (equal to the target name) and a common tag. +With the above, each target gets a unique _name_ (equal to the target name) and a common tag. So, every target is controlled by 2 tags: a dedicated one and `"my-feature"`. With the initial target log-levels in the example, only the first two logs would be emitted. -### Third-party crate targets -The dataplane uses third-party crates that emit logs themselves (e.g. *tonic* or *h2*), whose source code we don't want to modify. -The solution to control their log-levels is to declare a *custom* target configuration **elsewhere** as +#### Third-party crate targets + +The dataplane uses third-party crates that emit logs themselves (e.g. _tonic_ or _h2_), whose source code we don't want to modify. +The solution to control their log-levels is to declare a _custom_ target configuration **elsewhere** as ```rust custom_target!("tonic", LevelFilter::ERROR, &["third-party"]) ``` -... where `"tonic"` may be used to control all logs in the crate and `"third-party"` be a shared tag to control all third-party crates. -Additional custom targets could be created for the crate's modules, but that would require knowing the internal organization of the crate and such configs could get out of sync. Using a single target config with the name of the crate should guarantee that the configuration is in sync, which suffices in our case since we may use those target configs to mute all logs or limit them to just errors. - -## Notes -* Using custom targets has implications on log formatting, depending on how the formatting layer is configured, since target names may be displayed. -* Targets may be declared *without* tags: they will always get one equal to their *name*. -* The way the target registration works, targets may be declared in any place in the code; even within functions. The recommendation is, however, to place them at the beginning of each source code file. -* The way the Envfilters are built by this crate, if an (implicit) target is not registered (i.e. no `trace_target!()` is explicitly set) in some module/submodule, its log-level will be that of the nearest ancestor in the hierarchy. If no ancestor target is explicitly registered, the log-level will be governed by the *default*. -This means that `trace_target!()` *needs not be added in every source code file*. The rule of thumb should be: if you believe that some set of debug logs are worth being governed separately (e.g. because they are generally verbose and usually not needed, but may be worth enabling at run-time), then declare their target. Else, don't. -* Multiple registrations are possible but discouraged: one may overwrite the other. This crate issues a warning if a target is registered more than once. -## Run-time use (binary) +... where `"tonic"` may be used to control all logs in the crate and `"third-party"` be a shared tag to control all third-party crates. +Additional custom targets could be created for the crate's modules, +but that would require knowing the internal organization of the crate and such configs could get out of sync. +Using a single target config with the name of the crate should guarantee that the configuration is in sync, +which suffices in our case since we may use those target configs to mute all logs or limit them to just errors. + +### Notes + +- Using custom targets has implications on log formatting, depending on how the formatting layer is configured, + since target names may be displayed. +- Targets may be declared _without_ tags: they will always get one equal to their _name_. +- The way the target registration works, targets may be declared in any place in the code; even within functions. + The recommendation is, however, to place them at the beginning of each source code file. +- The way the EnvFilters are built by this crate, if an (implicit) target is not registered + (i.e. no `trace_target!()` is explicitly set) in some module/submodule, + its log-level will be that of the nearest ancestor in the hierarchy. + If no ancestor target is explicitly registered, the log-level will be governed by the _default_. + This means that `trace_target!()` _needs not be added in every source code file_. + The rule of thumb should be: if you believe that some set of debug logs are worth being governed separately + (e.g. because they are generally verbose and usually not needed, but may be worth enabling at run-time), then declare their target. + Else, don't. +- Multiple registrations are possible but discouraged: one may overwrite the other. + This crate issues a warning if a target is registered more than once. + +### Run-time use (binary) + +#### Initialization + +At run-time, target log-levels are governed by a target "controller", which contains the target database. +Such a controller is initialized by -### Initialization -At run-time, target log-levels are governed by a target "controller", which contains the target database. Such a controller is initialized by ```rust TracingControl::init(); ``` -In reality, the above is sugar-syntax to explicitly signal the initialization of a static controller object that gets initialized as soon as function `get_trace_ctl()` is called. That function is the one used to access the controller and call its methods. + +In reality, the above is sugar-syntax to explicitly signal the initialization of a static controller object +that gets initialized as soon as function `get_trace_ctl()` is called. +That function is the one used to access the controller and call its methods. + ```rust let tctl = get_trace_ctl(); ``` + The type returned by the above function is `&'static TracingControl`, but this is of no importance in practice. -### Controlling target log-levels -Target log levels may be adjusted with method `set_tag_level()` indicating a tag and the desired log-level. For instance, to set the log-level for tag "NAT" to Warning (so that only errors and warnings are emitted), one would write: +#### Controlling target log-levels + +Target log levels may be adjusted with method `set_tag_level()` indicating a tag and the desired log-level. +For instance, to set the log-level for tag "NAT" to Warning (so that only errors and warnings are emitted), one would write: + ```rust get_trace_ctl().set_tag_level("NAT", LevelFilter::WARN); ``` -Notice that this method expects a `LevelFilter` struct instead of a variant of the `Level` enum. `LevelFilter` is a transparent wrapper to `Option` defined in the *tracing-core* crate that maps `LevelFilter::OFF` to `None`. So, the method uses `LevelFilter` instead of `Level` so that logs can be disabled with `LevelFilter::OFF` (instead of `None`), avoiding the need to write `Some(Level::XX)` for any other case. +Notice that this method expects a `LevelFilter` struct instead of a variant of the `Level` enum. +`LevelFilter` is a transparent wrapper to `Option` defined in the _tracing-core_ crate that maps `LevelFilter::OFF` to `None`. +So, the method uses `LevelFilter` instead of `Level` so that logs can be disabled with `LevelFilter::OFF` (instead of `None`), +avoiding the need to write `Some(Level::XX)` for any other case. +#### Controlling the default log-level -### Controlling the default log-level -Logs emitted to targets that have not been registered will be governed by the *default* log-level. +Logs emitted to targets that have not been registered will be governed by the _default_ log-level. The tracing controller also allows setting the desired default log-level as: + ```rust get_trace_ctl().set_default_level(LevelFilter::ERROR); ``` +#### Checking targets, tags and levels - -### Checking targets, tags and levels The tracing controller has several other methods to: - * retrieve the log-level for a given target, given its name - * retrieve the config for a given target, given its name - * retrieve the list of tags - * retrieve the current default log-level - * retrieve the target configs for a given tag. + +- retrieve the log-level for a given target, given its name +- retrieve the config for a given target, given its name +- retrieve the list of tags +- retrieve the current default log-level +- retrieve the target configs for a given tag. The first two methods may not seem too useful (other than for custom targets) since implicit target names may be unstable. -However, they are added to be able to look up the config for third-party targets. Also, one may always be able to discover the real (unstable) target names -by retrieving the set of targets having a given tag, provided that targets are given at least one tag. Therefore, the recommendation is to *define at least one dedicated tag for each target, be it implicit or custom*. +However, they are added to be able to look up the config for third-party targets. +Also, one may always be able to discover the real (unstable) target names +by retrieving the set of targets having a given tag, provided that targets are given at least one tag. +Therefore, the recommendation is to _define at least one dedicated tag for each target, be it implicit or custom_. -### Usage in tests -Right now, the tracectl creates a default subscriber. This can interfere with tests annotated as `#[traced_test]`. +#### Usage in tests + +Right now, the tracectl creates a default subscriber. +This can interfere with tests annotated as `#[traced_test]`. If you have a test and want to see logs and control the log levels of targets defined in other crates, the simplest is to: -* remove the `#[traced_test]` -* call ```get_trace_ctl().setup_from_string(TRACING_CONFIG).unwrap()``` at the beginning of the test, where `TRACING_CONFIG` is a string -containing the desired configuration, such as `mgmt=info,routing=debug,pipeline=off`. -This is conveninent since you only need to import `get_trace_ctl()`. +- remove the `#[traced_test]` +- call ```get_trace_ctl().setup_from_string(TRACING_CONFIG).unwrap()``` at the beginning of the test, + where `TRACING_CONFIG` is a string containing the desired configuration, such as `mgmt=info,routing=debug,pipeline=off`. + +This is convenient since you only need to import `get_trace_ctl()`. Make sure to call `unwrap()` so that tests break if a target gets renamed.