From bfa277fec0c9dfe342374232b1b8619ce66918ce Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 24 Nov 2025 10:04:01 +0100 Subject: [PATCH 1/9] publish: add source code and CI. Signed-off-by: Alexander Mohr /etc/ld.so.conf.d/libdlt.conf && \ + ldconfig + +# install rust toolchain +RUN curl https://sh.rustup.rs -sSf | \ + sh -s -- --default-toolchain 1.88.0 -y + +ENV PATH=/root/.cargo/bin:$PATH + +WORKDIR / +RUN rm -rf /tmp/dlt-build + +# Create workspace directory for devcontainer +RUN mkdir -p /workspace +WORKDIR /workspace + +CMD ["/bin/bash"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..1c19e30 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +{ + "name": "DLT Daemon Development", + "build": { + "dockerfile": "Dockerfile" + }, + "workspaceFolder": "/workspace", + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "BarbossHack.crates-io", + "tamasfe.even-better-toml" + ] + } + }, + "remoteUser": "root" +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d4311af --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# This file specifies who owns what in the repository +# Syntax: + +# Entire repo ownership +* @eclipse-opensovd/automotive-opensovd-committers diff --git a/.github/actions/setup-dlt/action.yml b/.github/actions/setup-dlt/action.yml new file mode 100644 index 0000000..ef2f8a4 --- /dev/null +++ b/.github/actions/setup-dlt/action.yml @@ -0,0 +1,47 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +name: Setup DLT +description: Install system dependencies and build DLT daemon +runs: + using: composite + steps: + - name: Install system dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + cmake \ + build-essential \ + ca-certificates \ + curl \ + zlib1g-dev \ + llvm-dev \ + libclang-dev \ + clang \ + coreutils + - name: Build and install DLT + shell: bash + run: | + git clone https://github.com/COVESA/dlt-daemon.git + cd dlt-daemon + mkdir build && cd build + cmake .. \ + -DDLT_IPC=UNIX_SOCKET \ + -DWITH_DLT_CONSOLE=ON \ + -DWITH_DLT_USE_IPv6=OFF \ + -DDLT_USER_IPC_PATH="/tmp" \ + -DDLT_USER=root \ + -DCMAKE_INSTALL_SYSCONFDIR=/etc + make -j$(nproc) + sudo make install + echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/libdlt.conf + sudo ldconfig diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f6c5881 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,95 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +name: Rust CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + format_and_clippy_nightly_toolchain: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: false + - uses: ./.github/actions/setup-dlt + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + toolchain: nightly-2025-07-14 + components: clippy, rustfmt + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + - name: Formating - check for long lines + run: cargo fmt -- --check --config error_on_unformatted=true,error_on_line_overflow=true,format_strings=true + - name: Formatting - check import order + run: cargo fmt -- --check --config group_imports=StdExternalCrate + - name: Formatting - check imports granularity + run: cargo fmt -- --check --config imports_granularity=Crate + - name: run clippy nightly + run: cargo clippy --all-targets -- -D warnings + - name: Install taplo toml toolkit + run: cargo install --locked --version 0.10.0 taplo-cli + + build_and_test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: false + - uses: ./.github/actions/setup-dlt + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.88.0 + components: clippy, rustfmt + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + - name: Install cargo-deny + run: cargo install --locked --version 0.18.3 cargo-deny + - name: Build + run: cargo build --locked --verbose + - name: Check licenses + run: cargo deny check licenses + - name: Check advisories + run: cargo deny check advisories + - name: Check allowed sources + run: cargo deny check sources + - name: Check banned practises + run: cargo deny check bans + - name: Run tests + env: + RUSTFLAGS: "-L /usr/local/lib" + PKG_CONFIG_PATH: "/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH" + run: cargo test --locked --features integration-tests -- --show-output + + check_copyright_headers: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check License Headers + uses: apache/skywalking-eyes/header@v0.7.0 + with: + config: .licenserc.yaml diff --git a/.github/workflows/generate_documentation.yml b/.github/workflows/generate_documentation.yml new file mode 100644 index 0000000..79941fc --- /dev/null +++ b/.github/workflows/generate_documentation.yml @@ -0,0 +1,55 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +name: Generate documentation + +on: + push: + tags: + - 'v*' + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: write + actions: write + +jobs: + build_documentation: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: false + - uses: ./.github/actions/setup-dlt + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.88.0 + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + - name: Build documentation + run: cargo doc --no-deps --all-features + - name: Add index redirect + run: echo '' > target/doc/index.html + - name: Create docs archive + if: startsWith(github.ref, 'refs/tags/') + run: | + cd target/doc + tar -czf ../../rustdoc.tar.gz . + - name: Upload to release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: rustdoc.tar.gz diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000..2b9cc17 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,46 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +name: pre-commit + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + PYTHON_VERSION: 3.13 + PRE_COMMIT_VERSION: 4.2 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + python-version: ${{ env.PYTHON_VERSION }} + activate-environment: true + cache-dependency-glob: | + .github/workflows/pre-commit.yaml + - name: Run pre-commit + run: uv tool run pre-commit@${{ env.PRE_COMMIT_VERSION }} run --all-files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b76ab1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +build +/target + +*.pcap* +.idea +.vscode/settings.json + +# Docker environment settings +.env + +# Allow generated documentation +!docs/ +!docs/** diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 0000000..2d1f5ac --- /dev/null +++ b/.licenserc.yaml @@ -0,0 +1,50 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +header: + license: + spdx-id: Apache-2.0 + copyright-owner: The Contributors to Eclipse OpenSOVD + software-name: opensovd-cda + content: | + Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + + See the NOTICE file(s) distributed with this work for additional + information regarding copyright ownership. + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + + language: + Rust: + extensions: + - .rs + comment_style_id: SlashAsterisk + + paths-ignore: + - 'docs/**' + - 'target/**' + - '.git/**' + + paths: + - '**/*.toml' + - '**/Dockerfile*' + - '**/*.sh' + - '**/*.yml' + - '**/*.rs' + - '**/*.yaml' + - '**/*.c*' + - '**/*.h' + - '**/*.hpp' + - '**/*.json*' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a1592ee --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + exclude: ^docs/ + - id: check-merge-conflict + - id: end-of-file-fixer + exclude: ^docs/ + - id: trailing-whitespace + exclude: ^docs/ + - id: mixed-line-ending + exclude: ^docs/ + - repo: https://github.com/google/yamlfmt + rev: v0.20.0 + hooks: + - id: yamlfmt + exclude: ^docs/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.5 + hooks: + - id: ruff-check + - id: ruff-format diff --git a/.yamlfmt b/.yamlfmt new file mode 100644 index 0000000..0d7c56d --- /dev/null +++ b/.yamlfmt @@ -0,0 +1,3 @@ +formatter: + type: basic + retain_line_breaks_single: true diff --git a/CODESTYLE.md b/CODESTYLE.md new file mode 100644 index 0000000..c7a3773 --- /dev/null +++ b/CODESTYLE.md @@ -0,0 +1,73 @@ +# Code Style Guide + +## Linting & Clippy + +- **Clippy**: Always run with `clippy::pedantic` enabled for stricter linting. + - Example: `cargo clippy --all-targets --all-features -- -D warnings -W clippy::pedantic` +- **Allow/Forbid**: Use `#[allow(...)]` only when necessary, and always document the reason. + - Example: `#[allow(clippy::ref_option)] // Not compatible with serde derive` +- **Warnings**: Treat all warnings as errors. + +## Formatting + +This repository follows most of the defaults in rustfmt, with some opinionated usage of nightly-only rules such as `group_imports=StdExternalCrate`. + +- **rustfmt**: Use nightly `rustfmt` with the following settings: + - Maximum line width: 100 + - Group imports by `StdExternalCrate`. + - Import granularity at `crate` level. + - Error on line overflow and error if rustfmt is unable to format a section. This prevents rustfmt from silently skipping the rest of the file. + - Enable formatting of strings. + +As we do not want to require nightly rust for the entire repository, these settings are not yet included in `rustfmt.toml` (but will be once stabilized). +Instead, run this command to apply the correct formatting: +```sh +cargo +nightly fmt -- --check --config error_on_unformatted=true,error_on_line_overflow=true,format_strings=true,group_imports=StdExternalCrate,imports_granularity=Crate +``` + +It is recommended to configure your IDE to use nightly rustfmt with these settings as well. +Example for VS Code: +```json +"rust-analyzer.rustfmt.overrideCommand": [ + "rustfmt", + "+nightly", + "--edition", + "2024", + "--config", + "error_on_unformatted=true,error_on_line_overflow=true,format_strings=true,group_imports=StdExternalCrate,imports_granularity=Crate", + "--" +] +``` + +## Imports +As noted in the formatting section, imports must be grouped and separated with a new line as follows: + 1. Standard library + 2. External crates + 3. Internal modules + +Additionally the import granularity is set to `crate` to group all imports from the same crate into a single block. + +## General Style + +- Prefer explicit over implicit: always annotate types when not obvious. +- Use `const` and `static` for constants. +- Use `Arc`, `Mutex`, and `RwLock` for shared state, as seen in the codebase. +- Use tracing macros, like `tracing::info!`, as appropriate to record relevant information. +- Annotate functions with `tracing::instrument` when they are important for creating new tracing spans. +- Error messages should start with a capital letter. +- Literal suffixes (i.e. `u8` vs `_u8`) are preferably written without seperator. + Separting the suffix is allowed, to improve readability, for example for long base 2 literals (i.e. `0b_0000_0111_u8`). + This rule is not enforced by clippy to allow edge cases like the one mentioned above. + +## Licensing & Dependencies + +- Only allow SPDX-approved licenses: + `Apache-2.0`, `BSD-3-Clause`, `ISC`, `MIT`, `Unicode-3.0`, `Zlib` +- Use `cargo-deny` to enforce dependency and license policies. + +## Documentation + +- Document all public items with `///` doc comments. +- Use clear, concise language and provide context for complex logic. + +--- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d12e1cb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ + + +# Contributing + +Welcome to the OpenSOVD community. Start here for info on how to contribute and help improve our project. +Please observe our [Community Code of Conduct](./CODE_OF_CONDUCT.md). + +## How to Contribute + +This project welcomes contributions and suggestions. +For contributions, you'll also need to create an [Eclipse Foundation account](https://accounts.eclipse.org/) and agree to the [Eclipse Contributor Agreement](https://www.eclipse.org/legal/ECA.php). See more info at . + +If you have a bug to report or a feature to suggest, please use the New Issue button on the Issues page to access templates for these items. + +Code contributions are to be submitted via pull requests. +For this fork this repository, apply the suggested changes and create a +pull request to integrate them. +Before creating the request, please ensure the following which we will check +besides a technical review: + +- **No breaks**: All builds and tests pass (GitHub actions). +- Install and run the [pre-commit](https://pre-commit.com/) hooks before opening a pull request. + +## Communication + +Please join our [developer mailing list](https://accounts.eclipse.org/mailing-list/opensovd-dev) for up to date information diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..af3dbd3 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,9 @@ +# This is the list of the classic-diagnostic-adapter's significant contributors. +# +# This does not necessarily list everyone who has contributed code, +# especially since many employees of one corporation may be contributing. +# To see the full list of contributors, see the revision history in +# source control. + +Alexander Mohr +Elena Gantner diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9773902 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,613 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cc" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dlt-sys" +version = "0.1.0" +dependencies = [ + "bindgen", + "cc", + "thiserror", + "tokio", + "tracing", + "tracing-core", +] + +[[package]] +name = "dlt-tracing-appender" +version = "0.1.0" +dependencies = [ + "crc32fast", + "dlt-sys", + "indexmap", + "tokio", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "integration-tests" +version = "0.1.0" +dependencies = [ + "dlt-sys", + "dlt-tracing-appender", + "serial_test", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2c76514 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,62 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[workspace.package] +edition = "2024" +license = "Apache-2.0" +homepage = "https://github.com/eclipse-opensovd/classic-diagnostic-adapter" + +[workspace] +resolver = "3" +members = ["dlt-sys", "dlt-tracing-appender", "integration-tests"] + +[workspace.dependencies] +dlt-tracing-appender = { path = "dlt-tracing-appender" } +dlt-sys = { path = "dlt-sys" } +integration-tests = { path = "integration-tests" } + +# ---- common crates ---- +thiserror = "2.0.17" +tokio = { version = "1.48.0", default-features = false, features = [ + "sync", + "rt", +] } +bindgen = "0.72.1" +cc = "1.2.47" +indexmap = "2.12.1" +serial_test = "3.2.0" + +# ---- tracing & logging crates---- +tracing = "0.1.41" +tracing-core = "0.1.34" +crc32fast = "1.5.0" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.20", default-features = false } + + +[workspace.lints.clippy] +# enable pedantic +pedantic = { level = "warn", priority = -1 } +## exclude some too pedantic lints for now +similar_names = "allow" + +# additional lints +clone_on_ref_ptr = "warn" +indexing_slicing = "deny" +unwrap_used = "deny" +arithmetic_side_effects = "deny" + +[profile.release.package."*"] +opt-level = 3 + +[profile.release-with-debug] +inherits = "release" +debug = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c94d5ba --- /dev/null +++ b/NOTICE @@ -0,0 +1,36 @@ +# Notices for Eclipse OpenSOVD + +This content is produced and maintained by the Eclipse OpenSOVD project. + +- [Project home](https://projects.eclipse.org/projects/automotive.opensovd) + +## Trademarks + +Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. +For more information regarding authorship of content, please consult the +listed source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Apache License Version 2.0 which is available at +. + +SPDX-License-Identifier: Apache-2.0 + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + +## Transitive dependencies + +This software includes transitive dependencies. While we strive to ensure that all included dependencies are compatible with the Apache License, Version 2.0, we cannot guarantee the licensing compliance of every transitive dependency. Users are advised to review the licenses of all dependencies to ensure full compliance with their legal requirements. diff --git a/README.md b/README.md index 338d3d3..edc1720 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,123 @@ -# dlt-tracing-lib -Tracing appender and wrapper for libdlt +# DLT Tracing Library + +A Rust library for integrating the [tracing](https://github.com/tokio-rs/tracing) framework with [COVESA DLT (Diagnostic Log and Trace)](https://github.com/COVESA/dlt-daemon). +This project provides Rust bindings for DLT and a tracing subscriber that allows you to send structured logs and traces to DLT daemon. + +## 📖 Documentation + +[DLT Sys documentation](https://htmlpreview.github.io/?https://github.com/eclipse-opensovd/dlt-tracing-lib/blob/main/docs/dlt_sys/index.html) + +[DLT Trace Appender documentation](https://htmlpreview.github.io/?https://github.com/eclipse-opensovd/dlt-tracing-lib/blob/main/docs/dlt_tracing_appender/index.html) + + +The documentation includes detailed examples, usage patterns, and API reference for all crates. +Get it by running: +```bash + cargo doc --no-deps --all-features +``` + +## Overview + +This workspace contains three crates: +- **`dlt-sys`** - Low-level Rust wrapper around the C libdlt library +- **`dlt-tracing-appender`** - Tracing subscriber/layer that integrates with the tracing framework +- **`integration-tests`** - Common test utilities for integration testing with DLT daemon + +## Features + +- ✅ **Type-safe Rust API** for DLT logging +- ✅ **Tracing integration** - Use standard `tracing` macros with DLT +- ✅ **Structured logging** - Field types preserved when sent to DLT +- ✅ **Span context** - Nested spans appear in log messages +- ✅ **Dynamic log levels** - Responds to DLT daemon log level changes +- ✅ **Thread-safe** - Safe for concurrent use across async tasks +- ✅ **Zero-copy** where possible for performance + +## Quick Start + +### Prerequisites + +- Rust 1.88.0 or later + +### Basic Usage + +#### DLT Sys +```rust +use dlt_sys::{DltApplication, DltId, DltLogLevel}; +fn main() -> Result<(), Box> { + // Register application (one per process) + let app = DltApplication::register(&DltId::new(b"MBTI")?, "Measurement & Bus Trace Interface")?; + let ctx = app.create_context(&DltId::new(b"MEAS")?, "Measurement Context")?; + + // Simple logging + ctx.log(DltLogLevel::Info, "Hello DLT!")?; + + // Structured logging with typed fields + let mut writer = ctx.log_write_start(DltLogLevel::Info)?; + writer.write_string("Temperature:")? + .write_float32(87.5)? + .write_string("°C")?; + writer.finish()?; + Ok(()) +} +``` + +#### Dlt Tracing Appender + +```rust +use dlt_tracing_appender::{DltLayer, DltId}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let dlt_layer = DltLayer::new(&DltId::new(b"MBTI"), "My Beautiful Trace Ingestor")?; + + tracing_subscriber::registry().with(dlt_layer).init(); + + tracing::info!("Application started"); + Ok(()) +} +``` + +For more examples and detailed usage, see the API documentation. +The tracing and dlt-sys crates can be used simultaneously, when the application registration is done through the dlt-tracing-appender crate. + +## Development + +### Building + +```bash +# Build all crates +cargo build + +# Build with DLT load control support +cargo build --features trace_load_ctrl +``` + +### Running Tests + +```bash +# Unit tests only (no DLT daemon required) +cargo test + +# Integration tests (automatically starts DLT daemon) +cargo test -p integration-tests --features integration-tests +``` + +### Development Container + +A devcontainer is provided with DLT daemon pre-installed. Open the project in VS Code with the Dev Containers extension. + +## Contributing +See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +## License +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. + +## References +- [COVESA DLT Daemon](https://github.com/COVESA/dlt-daemon) +- [Tracing Framework](https://github.com/tokio-rs/tracing) + +## Acknowledgments +This project is part of [Eclipse OpenSOVD](https://projects.eclipse.org/projects/automotive.opensovd). +See [CONTRIBUTORS](CONTRIBUTORS) for the list of contributors. diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..d3c7d1e --- /dev/null +++ b/clippy.toml @@ -0,0 +1,20 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +# SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# SPDX-License-Identifier: Apache-2.0 + +## set line threshold a bit higher than the default 100 +too-many-lines-threshold = 130 + +## Unwrap is disallowed but accept it in test code +allow-unwrap-in-tests = true diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..b1f4b38 --- /dev/null +++ b/deny.toml @@ -0,0 +1,75 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +# SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# SPDX-License-Identifier: Apache-2.0 + +[graph] +targets = [ + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", +] + +all-features = false +no-default-features = false + +[output] +feature-depth = 1 + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = ["Apache-2.0", "MIT", "Unicode-3.0"] + +confidence-threshold = 0.8 +exceptions = [] + +[licenses.private] +ignore = true +registries = [] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +multiple-versions = "allow" +wildcards = "allow" +highlight = "all" +workspace-default-features = "allow" +external-default-features = "allow" +allow = [] +deny = [] + +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [] +skip-tree = [] + +[sources] +unknown-registry = "warn" +unknown-git = "warn" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] + +[sources.allow-org] +github = [] +gitlab = [] +bitbucket = [] diff --git a/dlt-sys/Cargo.toml b/dlt-sys/Cargo.toml new file mode 100644 index 0000000..b796dc6 --- /dev/null +++ b/dlt-sys/Cargo.toml @@ -0,0 +1,52 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "dlt-sys" +version = "0.1.0" +edition.workspace = true +publish = false +description = "Wrapper around the C library libdlt to provide DLT logging capabilities for Rust applications" +homepage.workspace = true +license.workspace = true + +[lints] +workspace = true + +[lib] +path = "src/lib.rs" + +[dependencies] + +# external +tokio = { workspace = true } +thiserror = { workspace = true } + +# logging & tracing +tracing = { workspace = true } +tracing-core = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = [ + "test-util", + "macros", + "rt-multi-thread", +] } + +[features] +default = [] +generate-bindings = ["bindgen", "trace_load_ctrl"] +# This is a feature, which might be necessary in some environments to enable +trace_load_ctrl = [] + +[build-dependencies] +cc = { workspace = true } +bindgen = { workspace = true, optional = true } diff --git a/dlt-sys/build.rs b/dlt-sys/build.rs new file mode 100644 index 0000000..44d0d45 --- /dev/null +++ b/dlt-sys/build.rs @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const DLT_WRAPPER: &str = "dlt-wrapper"; +const DLT_HEADER: &str = "dlt-wrapper.h"; +const DLT_SRC: &str = "dlt-wrapper.c"; +#[cfg(feature = "generate-bindings")] +const COPYRIGHT_HEADER: &str = r"/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +"; + +// necessary to ensure that bindings are generated with trace_load_ctrl enabled +// otherwise we cannot enable the feature with the generated bindings as some types will be missing +#[cfg(all(feature = "generate-bindings", not(feature = "trace_load_ctrl")))] +compile_error!("Feature 'generate-bindings' requires 'trace_load_ctrl' to be enabled"); + +fn main() { + let project_dir = std::env::var("CARGO_MANIFEST_DIR") + .expect("CARGO_MANIFEST_DIR environment variable not set"); + + let wrapper_dir = format!("{project_dir}/wrapper"); + + let mut build = cc::Build::new(); + build.cpp(false).file(format!("{wrapper_dir}/{DLT_SRC}")); + + // Add system DLT include paths + if let Ok(include) = std::env::var("DLT_INCLUDE_DIR") { + build.include(&include).include(format!("{include}/dlt")); + } + if let Ok(user_include) = std::env::var("DLT_USER_INCLUDE_DIR") { + build + .include(&user_include) + .include(format!("{user_include}/dlt")); + } + + // Pass trace_load_ctrl feature to C code + // CMake uses -DWITH_DLT_TRACE_LOAD_CTRL=ON which defines DLT_TRACE_LOAD_CTRL_ENABLE + #[cfg(feature = "trace_load_ctrl")] + build.define("DLT_TRACE_LOAD_CTRL_ENABLE", None); + + build.compile(DLT_WRAPPER); + + if let Ok(lib_path) = std::env::var("DLT_LIB_DIR") { + println!("cargo:rustc-link-search=native={lib_path}"); + } + println!("cargo:rustc-link-lib=dylib=dlt"); + + println!("cargo:rerun-if-changed={wrapper_dir}/{DLT_HEADER}"); + println!("cargo:rerun-if-changed={wrapper_dir}/{DLT_SRC}"); + + #[cfg(feature = "generate-bindings")] + generate_bindings(&project_dir, &wrapper_dir); +} + +#[cfg(feature = "generate-bindings")] +fn generate_bindings(project_dir: &str, wrapper_dir: &str) { + let mut builder = bindgen::Builder::default() + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .header(format!("{}/{}", wrapper_dir, DLT_HEADER)); + + // Add clang args for system DLT headers + if let Ok(include) = std::env::var("DLT_INCLUDE_DIR") { + builder = builder.clang_arg(format!("-I{}", include)); + } + if let Ok(user_include) = std::env::var("DLT_USER_INCLUDE_DIR") { + builder = builder.clang_arg(format!("-I{}", user_include)); + } + if cfg!(feature = "trace_load_ctrl") { + builder = builder.clang_arg("-DDLT_TRACE_LOAD_CTRL_ENABLE"); + } + + let target_file = std::path::PathBuf::from(project_dir).join("src/dlt_bindings.rs"); + + builder + // Types + .allowlist_type("DltContext") + .allowlist_type("DltContextData") + .allowlist_type("DltLogLevelType") + .allowlist_type("DltTimestampType") + .allowlist_type("DltReturnValue") + .allowlist_type("DltTraceStatusType") + // Constants + .allowlist_var("DLT_ID_SIZE") + .allowlist_var("DLT_LOG_.*") + .allowlist_var("DLT_RETURN_.*") + // Application management functions + .allowlist_function("registerApplication") + .allowlist_function("unregisterApplicationFlushBufferedLogs") + .allowlist_function("dltFree") + // Context management functions + .allowlist_function("registerContext") + .allowlist_function("unregisterContext") + // Simple logging functions + .allowlist_function("logDlt") + .allowlist_function("logDltString") + .allowlist_function("logDltUint") + .allowlist_function("logDltInt") + // Complex log write API + .allowlist_function("dltUserLogWriteStart") + .allowlist_function("dltUserLogWriteFinish") + .allowlist_function("dltUserLogWriteString") + .allowlist_function("dltUserLogWriteUint") + .allowlist_function("dltUserLogWriteInt") + .allowlist_function("dltUserLogWriteUint64") + .allowlist_function("dltUserLogWriteInt64") + .allowlist_function("dltUserLogWriteFloat32") + .allowlist_function("dltUserLogWriteFloat64") + .allowlist_function("dltUserLogWriteBool") + // Callback registration + .allowlist_function("registerLogLevelChangedCallback") + .generate() + .unwrap_or_else(|err| panic!("Error generating bindings: {}", err)) + .write_to_file(&target_file) + .unwrap_or_else(|err| panic!("Error writing bindings: {}", err)); + + prepend_copyright(target_file.to_str().unwrap()).unwrap(); +} + +#[cfg(feature = "generate-bindings")] +fn prepend_copyright(file_path: &str) -> std::io::Result<()> { + let content = std::fs::read_to_string(file_path)?; + let new_content = format!("{COPYRIGHT_HEADER}{content}"); + std::fs::write(file_path, new_content)?; + Ok(()) +} diff --git a/dlt-sys/src/dlt_bindings.rs b/dlt-sys/src/dlt_bindings.rs new file mode 100644 index 0000000..3b8b715 --- /dev/null +++ b/dlt-sys/src/dlt_bindings.rs @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* automatically generated by rust-bindgen 0.72.1 */ + +pub const DLT_ID_SIZE: u32 = 4; +pub const DltReturnValue_DLT_RETURN_LOAD_EXCEEDED: DltReturnValue = -9; +pub const DltReturnValue_DLT_RETURN_FILESZERR: DltReturnValue = -8; +pub const DltReturnValue_DLT_RETURN_LOGGING_DISABLED: DltReturnValue = -7; +pub const DltReturnValue_DLT_RETURN_USER_BUFFER_FULL: DltReturnValue = -6; +pub const DltReturnValue_DLT_RETURN_WRONG_PARAMETER: DltReturnValue = -5; +pub const DltReturnValue_DLT_RETURN_BUFFER_FULL: DltReturnValue = -4; +pub const DltReturnValue_DLT_RETURN_PIPE_FULL: DltReturnValue = -3; +pub const DltReturnValue_DLT_RETURN_PIPE_ERROR: DltReturnValue = -2; +pub const DltReturnValue_DLT_RETURN_ERROR: DltReturnValue = -1; +pub const DltReturnValue_DLT_RETURN_OK: DltReturnValue = 0; +pub const DltReturnValue_DLT_RETURN_TRUE: DltReturnValue = 1; +pub type DltReturnValue = ::std::os::raw::c_int; +pub const DltLogLevelType_DLT_LOG_DEFAULT: DltLogLevelType = -1; +pub const DltLogLevelType_DLT_LOG_OFF: DltLogLevelType = 0; +pub const DltLogLevelType_DLT_LOG_FATAL: DltLogLevelType = 1; +pub const DltLogLevelType_DLT_LOG_ERROR: DltLogLevelType = 2; +pub const DltLogLevelType_DLT_LOG_WARN: DltLogLevelType = 3; +pub const DltLogLevelType_DLT_LOG_INFO: DltLogLevelType = 4; +pub const DltLogLevelType_DLT_LOG_DEBUG: DltLogLevelType = 5; +pub const DltLogLevelType_DLT_LOG_VERBOSE: DltLogLevelType = 6; +pub const DltLogLevelType_DLT_LOG_MAX: DltLogLevelType = 7; +pub type DltLogLevelType = ::std::os::raw::c_int; +pub const DltTraceStatusType_DLT_TRACE_STATUS_DEFAULT: DltTraceStatusType = -1; +pub const DltTraceStatusType_DLT_TRACE_STATUS_OFF: DltTraceStatusType = 0; +pub const DltTraceStatusType_DLT_TRACE_STATUS_ON: DltTraceStatusType = 1; +pub const DltTraceStatusType_DLT_TRACE_STATUS_MAX: DltTraceStatusType = 2; +pub type DltTraceStatusType = ::std::os::raw::c_int; +pub const DltTimestampType_DLT_AUTO_TIMESTAMP: DltTimestampType = 0; +pub const DltTimestampType_DLT_USER_TIMESTAMP: DltTimestampType = 1; +pub type DltTimestampType = ::std::os::raw::c_uint; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct DltContext { + pub contextID: [::std::os::raw::c_char; 4usize], + pub log_level_pos: i32, + pub log_level_ptr: *mut i8, + pub trace_status_ptr: *mut i8, + pub mcnt: u8, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of DltContext"][::std::mem::size_of::() - 32usize]; + ["Alignment of DltContext"][::std::mem::align_of::() - 8usize]; + ["Offset of field: DltContext::contextID"] + [::std::mem::offset_of!(DltContext, contextID) - 0usize]; + ["Offset of field: DltContext::log_level_pos"] + [::std::mem::offset_of!(DltContext, log_level_pos) - 4usize]; + ["Offset of field: DltContext::log_level_ptr"] + [::std::mem::offset_of!(DltContext, log_level_ptr) - 8usize]; + ["Offset of field: DltContext::trace_status_ptr"] + [::std::mem::offset_of!(DltContext, trace_status_ptr) - 16usize]; + ["Offset of field: DltContext::mcnt"][::std::mem::offset_of!(DltContext, mcnt) - 24usize]; +}; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct DltContextData { + pub handle: *mut DltContext, + pub buffer: *mut ::std::os::raw::c_uchar, + pub size: i32, + pub log_level: i32, + pub trace_status: i32, + pub args_num: i32, + pub context_description: *mut ::std::os::raw::c_char, + pub use_timestamp: DltTimestampType, + pub user_timestamp: u32, + pub verbose_mode: i8, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of DltContextData"][::std::mem::size_of::() - 56usize]; + ["Alignment of DltContextData"][::std::mem::align_of::() - 8usize]; + ["Offset of field: DltContextData::handle"] + [::std::mem::offset_of!(DltContextData, handle) - 0usize]; + ["Offset of field: DltContextData::buffer"] + [::std::mem::offset_of!(DltContextData, buffer) - 8usize]; + ["Offset of field: DltContextData::size"] + [::std::mem::offset_of!(DltContextData, size) - 16usize]; + ["Offset of field: DltContextData::log_level"] + [::std::mem::offset_of!(DltContextData, log_level) - 20usize]; + ["Offset of field: DltContextData::trace_status"] + [::std::mem::offset_of!(DltContextData, trace_status) - 24usize]; + ["Offset of field: DltContextData::args_num"] + [::std::mem::offset_of!(DltContextData, args_num) - 28usize]; + ["Offset of field: DltContextData::context_description"] + [::std::mem::offset_of!(DltContextData, context_description) - 32usize]; + ["Offset of field: DltContextData::use_timestamp"] + [::std::mem::offset_of!(DltContextData, use_timestamp) - 40usize]; + ["Offset of field: DltContextData::user_timestamp"] + [::std::mem::offset_of!(DltContextData, user_timestamp) - 44usize]; + ["Offset of field: DltContextData::verbose_mode"] + [::std::mem::offset_of!(DltContextData, verbose_mode) - 48usize]; +}; +unsafe extern "C" { + pub fn registerApplication( + appId: *const ::std::os::raw::c_char, + appDescription: *const ::std::os::raw::c_char, + ) -> DltReturnValue; +} +unsafe extern "C" { + pub fn unregisterApplicationFlushBufferedLogs() -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltFree() -> DltReturnValue; +} +unsafe extern "C" { + pub fn registerContext( + contextId: *const ::std::os::raw::c_char, + contextDescription: *const ::std::os::raw::c_char, + ) -> *mut DltContext; +} +unsafe extern "C" { + pub fn unregisterContext(context: *mut DltContext) -> DltReturnValue; +} +unsafe extern "C" { + pub fn logDlt( + context: *mut DltContext, + logLevel: DltLogLevelType, + message: *const ::std::os::raw::c_char, + ) -> DltReturnValue; +} +unsafe extern "C" { + pub fn logDltString( + context: *mut DltContext, + logLevel: DltLogLevelType, + message: *const ::std::os::raw::c_char, + ) -> DltReturnValue; +} +unsafe extern "C" { + pub fn logDltUint( + context: *mut DltContext, + logLevel: DltLogLevelType, + value: u32, + ) -> DltReturnValue; +} +unsafe extern "C" { + pub fn logDltInt( + context: *mut DltContext, + logLevel: DltLogLevelType, + value: i32, + ) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteStart( + context: *mut DltContext, + log: *mut DltContextData, + logLevel: DltLogLevelType, + ) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteFinish(log: *mut DltContextData) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteString( + log: *mut DltContextData, + text: *const ::std::os::raw::c_char, + ) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteUint(log: *mut DltContextData, data: u32) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteInt(log: *mut DltContextData, data: i32) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteUint64(log: *mut DltContextData, data: u64) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteInt64(log: *mut DltContextData, data: i64) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteFloat32(log: *mut DltContextData, data: f32) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteFloat64(log: *mut DltContextData, data: f64) -> DltReturnValue; +} +unsafe extern "C" { + pub fn dltUserLogWriteBool(log: *mut DltContextData, data: u8) -> DltReturnValue; +} +unsafe extern "C" { + pub fn registerLogLevelChangedCallback( + handle: *mut DltContext, + callback: ::std::option::Option< + unsafe extern "C" fn( + context_id: *mut ::std::os::raw::c_char, + log_level: u8, + trace_status: u8, + ), + >, + ) -> DltReturnValue; +} diff --git a/dlt-sys/src/lib.rs b/dlt-sys/src/lib.rs new file mode 100644 index 0000000..66b5e29 --- /dev/null +++ b/dlt-sys/src/lib.rs @@ -0,0 +1,976 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Low-level Rust bindings for COVESA DLT (Diagnostic Log and Trace) +//! +//! Safe Rust API for the DLT C library with RAII semantics, enabling applications to send +//! diagnostic logs and traces to the DLT daemon for centralized logging and analysis. +//! +//! # Quick Start +//! +//! ```no_run +//! use dlt_sys::{DltApplication, DltId, DltLogLevel}; +//! +//! # fn main() -> Result<(), Box> { +//! // Register application (one per process) +//! let app = DltApplication::register(&DltId::new(b"MBTI")?, "Measurement & Bus Trace Interface")?; +//! let ctx = app.create_context(&DltId::new(b"MEAS")?, "Measurement Context")?; +//! +//! // Simple logging +//! ctx.log(DltLogLevel::Info, "Hello DLT!")?; +//! +//! // Structured logging with typed fields +//! let mut writer = ctx.log_write_start(DltLogLevel::Info)?; +//! writer.write_string("Temperature:")? +//! .write_float32(87.5)? +//! .write_string("°C")?; +//! writer.finish()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Core Types +//! +//! - [`DltApplication`] - Application registration +//! - [`DltContextHandle`] - Context for logging with specific ID +//! - [`DltLogWriter`] - Builder for structured multi-field messages +//! - [`DltLogLevel`] - Log severity (Fatal, Error, Warn, Info, Debug, Verbose) +//! - [`DltId`] - Type-safe 1-4 byte ASCII identifiers +//! +//! # Features +//! +//! - **RAII cleanup** - Automatic resource management +//! - **Structured logging** - Structured logging messages via [`DltLogWriter`] +//! - **Dynamic control** - Runtime log level changes via +//! [`DltContextHandle::register_log_level_changed_listener()`] +//! - **Thread-safe** - All types are `Send + Sync` +//! +//! # Log Level Control +//! +//! DLT log levels can be changed at runtime by the DLT daemon or other tools. +//! Applications can listen for log level changes. +//! See [`DltLogLevel`] for all available levels and [`LogLevelChangedEvent`] to listen for changes. +//! +//! # See Also +//! +//! - [COVESA DLT](https://github.com/COVESA/dlt-daemon) +use std::{ + collections::HashMap, + ffi::CString, + ptr, + sync::{Arc, OnceLock, RwLock, atomic::AtomicBool}, +}; + +use thiserror::Error; +use tokio::sync::broadcast; + +#[rustfmt::skip] +#[allow(clippy::all, + dead_code, + warnings, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, +)] +mod dlt_bindings; +use dlt_bindings::{DLT_ID_SIZE, DltContext, DltContextData}; + +/// DLT log level +/// +/// Severity level of a log message, ordered from most severe ([`DltLogLevel::Fatal`]) +/// to least severe ([`DltLogLevel::Verbose`]). +/// +/// Use with [`DltContextHandle::log()`] or [`DltContextHandle::log_write_start()`]. +/// The DLT daemon filters messages based on the configured threshold +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum DltLogLevel { + /// Default log level (determined by DLT daemon configuration) + Default, + /// Logging is disabled + Off, + /// Fatal system error - system is unusable + Fatal, + /// Error conditions - operation failed + Error, + /// Warning conditions - something unexpected but recoverable + Warn, + /// Informational messages - normal operation (default level) + Info, + /// Debug-level messages - detailed diagnostic information + Debug, + /// Verbose/trace-level messages - very detailed execution traces + Verbose, +} + +impl From for DltLogLevel { + fn from(value: i32) -> Self { + match value { + dlt_bindings::DltLogLevelType_DLT_LOG_OFF => DltLogLevel::Off, + dlt_bindings::DltLogLevelType_DLT_LOG_FATAL => DltLogLevel::Fatal, + dlt_bindings::DltLogLevelType_DLT_LOG_ERROR => DltLogLevel::Error, + dlt_bindings::DltLogLevelType_DLT_LOG_WARN => DltLogLevel::Warn, + dlt_bindings::DltLogLevelType_DLT_LOG_INFO => DltLogLevel::Info, + dlt_bindings::DltLogLevelType_DLT_LOG_DEBUG => DltLogLevel::Debug, + dlt_bindings::DltLogLevelType_DLT_LOG_VERBOSE => DltLogLevel::Verbose, + _ => DltLogLevel::Default, + } + } +} + +impl From for i32 { + fn from(value: DltLogLevel) -> Self { + match value { + DltLogLevel::Default => dlt_bindings::DltLogLevelType_DLT_LOG_DEFAULT, + DltLogLevel::Off => dlt_bindings::DltLogLevelType_DLT_LOG_OFF, + DltLogLevel::Fatal => dlt_bindings::DltLogLevelType_DLT_LOG_FATAL, + DltLogLevel::Error => dlt_bindings::DltLogLevelType_DLT_LOG_ERROR, + DltLogLevel::Warn => dlt_bindings::DltLogLevelType_DLT_LOG_WARN, + DltLogLevel::Info => dlt_bindings::DltLogLevelType_DLT_LOG_INFO, + DltLogLevel::Debug => dlt_bindings::DltLogLevelType_DLT_LOG_DEBUG, + DltLogLevel::Verbose => dlt_bindings::DltLogLevelType_DLT_LOG_VERBOSE, + } + } +} + +/// Internal error types for Rust-side operations (not from libdlt) +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum DltError { + #[error("Data cannot be converted to a DLT compatible string: {0}")] + InvalidString(String), + #[error("Failed to register DLT context")] + ContextRegistrationFailed, + #[error("Failed to register DLT application")] + ApplicationRegistrationFailed(String), + #[error("A pointer or memory is invalid")] + InvalidMemory, + #[error("Failed to acquire a lock")] + BadLock, + #[error("Input value is invalid")] + InvalidInput, +} + +/// DLT return value error types (from libdlt C library) +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] +pub enum DltSysError { + #[cfg(feature = "trace_load_ctrl")] + #[error("DLT load exceeded")] + /// Only available with the `trace_load_ctrl` feature enabled. + LoadExceeded, + + #[error("DLT file size error")] + FileSizeError, + + #[error("DLT logging disabled")] + LoggingDisabled, + + #[error("DLT user buffer full")] + UserBufferFull, + + #[error("DLT wrong parameter")] + WrongParameter, + + #[error("DLT buffer full")] + BufferFull, + + #[error("DLT pipe full")] + PipeFull, + + #[error("DLT pipe error")] + PipeError, + + #[error("DLT general error")] + Error, + + #[error("DLT unknown error")] + Unknown, +} + +impl DltSysError { + fn from_return_code(code: i32) -> Result<(), Self> { + #[allow(unreachable_patterns)] + match code { + dlt_bindings::DltReturnValue_DLT_RETURN_TRUE + | dlt_bindings::DltReturnValue_DLT_RETURN_OK => Ok(()), + dlt_bindings::DltReturnValue_DLT_RETURN_ERROR => Err(DltSysError::Error), + dlt_bindings::DltReturnValue_DLT_RETURN_PIPE_ERROR => Err(DltSysError::PipeError), + dlt_bindings::DltReturnValue_DLT_RETURN_PIPE_FULL => Err(DltSysError::PipeFull), + dlt_bindings::DltReturnValue_DLT_RETURN_BUFFER_FULL => Err(DltSysError::BufferFull), + dlt_bindings::DltReturnValue_DLT_RETURN_WRONG_PARAMETER => { + Err(DltSysError::WrongParameter) + } + dlt_bindings::DltReturnValue_DLT_RETURN_USER_BUFFER_FULL => { + Err(DltSysError::UserBufferFull) + } + dlt_bindings::DltReturnValue_DLT_RETURN_LOGGING_DISABLED => { + Err(DltSysError::LoggingDisabled) + } + dlt_bindings::DltReturnValue_DLT_RETURN_FILESZERR => Err(DltSysError::FileSizeError), + #[cfg(feature = "trace_load_ctrl")] + dlt_bindings::DltReturnValue_DLT_RETURN_LOAD_EXCEEDED => Err(DltSysError::LoadExceeded), + _ => Err(DltSysError::Unknown), + } + } +} + +/// Size of DLT ID fields (Application ID, Context ID) - re-exported from bindings as usize +pub const DLT_ID_SIZE_USIZE: usize = DLT_ID_SIZE as usize; + +/// A DLT identifier (Application ID or Context ID) +/// +/// DLT IDs are 1-4 ASCII bytes. Create with `DltId::new(b"APP")?`. +/// Shorter IDs are internally padded with nulls +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DltId { + bytes: [u8; DLT_ID_SIZE_USIZE], + len: usize, +} + +impl DltId { + /// Create a new DLT ID from a byte slice of 1 to 4 bytes + /// + /// The ID will be validated as ASCII. + /// IDs shorter than 4 bytes are right-padded with null bytes internally. + /// + /// # Errors + /// Returns [`DltError::InvalidInput`] if the byte slice is empty, longer than 4 bytes, + /// or contains non-ASCII characters. + /// + /// # Examples + /// ```no_run + /// # use dlt_sys::{DltId, DltError}; + /// # fn main() -> Result<(), DltError> { + /// let id = DltId::new(b"APP")?; + /// assert_eq!(id.as_str()?, "APP"); + /// + /// // Too long + /// assert!(DltId::new(b"TOOLONG").is_err()); + /// + /// // Empty + /// assert!(DltId::new(b"").is_err()); + /// # Ok(()) + /// # } + /// ``` + pub fn new(bytes: &[u8; N]) -> Result { + // Validate that N is between 1 and 4 + if N == 0 || N > DLT_ID_SIZE_USIZE { + return Err(DltError::InvalidInput); + } + + // Validate ASCII + if !bytes.is_ascii() { + return Err(DltError::InvalidInput); + } + + let mut padded = [0u8; DLT_ID_SIZE_USIZE]; + // Indexing is safe here: loop condition ensures i < N, and N <= DLT_ID_SIZE by validation + #[allow(clippy::indexing_slicing)] + padded[..N].copy_from_slice(&bytes[..N]); + + Ok(Self { + bytes: padded, + len: N, + }) + } + + /// Get the ID as a string slice + /// + /// # Errors + /// Returns an error if the bytes are not valid UFT-8. + /// This should never happen due to construction constraints. + pub fn as_str(&self) -> Result<&str, DltError> { + let slice = self + .bytes + .get(..self.len) + .ok_or_else(|| DltError::InvalidString("Invalid length".to_string()))?; + let s = std::str::from_utf8(slice).map_err(|e| DltError::InvalidString(e.to_string()))?; + Ok(s) + } +} + +/// DLT trace status +/// +/// Controls whether network trace messages (like packet captures) are enabled. +/// This is separate from log levels. Most applications only use log levels and can +/// ignore trace status. +/// +/// Trace status is included in [`LogLevelChangedEvent`] notifications +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DltTraceStatus { + /// Use default trace status from DLT daemon configuration + Default, + /// Trace messages are disabled + Off, + /// Trace messages are enabled + On, +} + +impl From for DltTraceStatus { + fn from(value: i32) -> Self { + match value { + dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_OFF => DltTraceStatus::Off, + dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_ON => DltTraceStatus::On, + _ => DltTraceStatus::Default, + } + } +} + +impl From for i32 { + fn from(value: DltTraceStatus) -> Self { + match value { + DltTraceStatus::Default => dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_DEFAULT, + DltTraceStatus::Off => dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_OFF, + DltTraceStatus::On => dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_ON, + } + } +} + +/// Event sent when DLT log level or trace status changes +/// +/// Emitted when the DLT daemon changes the log level or trace status for a context. +/// +/// Register a listener with [`DltContextHandle::register_log_level_changed_listener()`] +/// to receive these events +#[derive(Debug, Clone, Copy)] +pub struct LogLevelChangedEvent { + /// The DLT context ID that this change applies to + pub context_id: DltId, + /// The new log level for the context + pub log_level: DltLogLevel, + /// The new trace status for the context + pub trace_status: DltTraceStatus, +} + +struct LogLevelChangedBroadcaster { + sender: broadcast::Sender, + receiver: broadcast::Receiver, +} + +// Global registry for log level change callbacks +static CALLBACK_REGISTRY: OnceLock>> = + OnceLock::new(); + +static APP_REGISTERED: AtomicBool = AtomicBool::new(false); + +/// Internal C callback that forwards to the Rust channel +unsafe extern "C" fn internal_log_level_callback( + context_id: *mut std::os::raw::c_char, + log_level: u8, + trace_status: u8, +) { + if context_id.is_null() { + return; + } + + let Some(registry) = CALLBACK_REGISTRY.get() else { + return; + }; + + let id = unsafe { + let mut ctx_id = [0u8; DLT_ID_SIZE_USIZE]; + ptr::copy( + context_id.cast::(), + ctx_id.as_mut_ptr(), + DLT_ID_SIZE_USIZE, + ); + match DltId::new(&ctx_id) { + Ok(id) => id, + Err(_) => return, // Invalid context ID from DLT daemon + } + }; + + let Ok(lock) = registry.read() else { + return; + }; + + let Some(broadcaster) = lock.get(&id) else { + return; + }; + + let event = LogLevelChangedEvent { + context_id: id, + log_level: DltLogLevel::from(i32::from(log_level)), + trace_status: DltTraceStatus::from(i32::from(trace_status)), + }; + + let _ = broadcaster.sender.send(event); +} + +impl Default for DltContextData { + fn default() -> Self { + DltContextData { + handle: ptr::null_mut(), + buffer: ptr::null_mut(), + size: 0, + log_level: 0, + trace_status: 0, + args_num: 0, + context_description: ptr::null_mut(), + use_timestamp: 0, + user_timestamp: 0, + verbose_mode: 0, + } + } +} + +/// Internal shared state for the DLT application +/// +/// This ensures contexts can keep the application alive through reference counting. +/// When the last reference (either from `DltApplication` or `DltContextHandle`) is +/// dropped, the application is automatically unregistered from DLT. +struct DltApplicationHandle { + _private: (), +} + +impl Drop for DltApplicationHandle { + fn drop(&mut self) { + unsafe { + // unregister from dlt, but ignore errors + dlt_bindings::unregisterApplicationFlushBufferedLogs(); + dlt_bindings::dltFree(); + APP_REGISTERED.store(false, std::sync::atomic::Ordering::SeqCst); + } + } +} + +/// Singleton guard for DLT application registration +/// +/// Only one DLT application can be registered per process. Automatically unregistered +/// when dropped and a new application can be registered. +/// +/// **Lifetime Guarantee**: Contexts maintain an internal reference, keeping the application +/// registered. Safe to drop the application handle before contexts. +/// +/// Cheaply cloneable for sharing across threads +pub struct DltApplication { + inner: Arc, +} + +impl DltApplication { + /// Register a DLT application + /// + /// Only one application can be registered per process. If you need to register + /// a different application, drop this instance first. + /// + /// The returned handle can be cloned to share the application across threads. + /// + /// # Errors + /// Returns `DltError` if the registration fails + pub fn register(app_id: &DltId, app_description: &str) -> Result { + if APP_REGISTERED + .compare_exchange( + false, + true, + std::sync::atomic::Ordering::SeqCst, + std::sync::atomic::Ordering::SeqCst, + ) + .is_err() + { + return Err(DltError::ApplicationRegistrationFailed( + "An application is already registered in this process".to_string(), + )); + } + + let app_id_str = app_id.as_str()?; + let app_id_c = CString::new(app_id_str).map_err(|_| { + DltError::InvalidString("App id could not be converted to string".to_owned()) + })?; + let app_desc_c = CString::new(app_description).map_err(|_| { + DltError::InvalidString("Context id could not be converted to string".to_owned()) + })?; + + unsafe { + let ret = dlt_bindings::registerApplication(app_id_c.as_ptr(), app_desc_c.as_ptr()); + DltSysError::from_return_code(ret).map_err(|_| { + DltError::ApplicationRegistrationFailed(format!( + "Failed to register application: {ret}" + )) + })?; + } + Ok(DltApplication { + inner: Arc::new(DltApplicationHandle { _private: () }), + }) + } + + /// Create a new DLT context within this application + /// + /// The created context maintains an internal reference to the application, + /// ensuring the application remains registered as long as the context exists. + /// + /// # Errors + /// Returns `DltError` if registration fails + pub fn create_context( + &self, + context_id: &DltId, + context_description: &str, + ) -> Result { + DltContextHandle::new(context_id, context_description, Arc::clone(&self.inner)) + } +} + +impl Clone for DltApplication { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +// Safe to send between threads (application registration is process-wide) +unsafe impl Send for DltApplication {} +unsafe impl Sync for DltApplication {} + +/// Safe wrapper around C DLT context with RAII semantics +/// +/// The context holds an internal reference to the application, ensuring the +/// application remains registered as long as any context exists. +pub struct DltContextHandle { + context: *mut DltContext, + _app: Arc, +} + +impl DltContextHandle { + /// Register a new DLT context + /// + /// # Errors + /// Returns `DltError` if registration fails + fn new( + context_id: &DltId, + context_description: &str, + app: Arc, + ) -> Result { + let context_id_str = context_id.as_str()?; + let ctx_id_c = CString::new(context_id_str) + .map_err(|_| DltError::InvalidString("Context ID is not a valid string".to_owned()))?; + let ctx_desc_c = CString::new(context_description).map_err(|_| { + DltError::InvalidString("Context description is not a valid string".to_owned()) + })?; + + unsafe { + let context = dlt_bindings::registerContext(ctx_id_c.as_ptr(), ctx_desc_c.as_ptr()); + if context.is_null() { + Err(DltError::ContextRegistrationFailed) + } else { + Ok(DltContextHandle { context, _app: app }) + } + } + } + + fn raw_context(&self) -> Result { + let context = unsafe { + if self.context.is_null() { + return Err(DltError::ContextRegistrationFailed); + } + *self.context + }; + Ok(context) + } + + /// Get the context ID + /// # Errors + /// Returns `DltError` if the context is invalid or the context is null + pub fn context_id(&self) -> Result { + let ctx_id = unsafe { + // this is a false positive of clippy. + // raw_context.contextID is of type [::std::os::raw::c_char; 4usize], which + // cannot be directly used as &[u8; 4]. + #[allow(clippy::useless_transmute)] + std::mem::transmute::<[std::os::raw::c_char; 4], [u8; 4]>(self.raw_context()?.contextID) + }; + DltId::new(&ctx_id) + } + + /// Get the current trace status of the context + /// # Errors + /// Returns `DltError` if the context is invalid or the context is null + #[must_use] + pub fn trace_status(&self) -> DltTraceStatus { + self.raw_context() + .ok() + .and_then(|rc| { + if rc.log_level_ptr.is_null() { + None + } else { + Some(DltTraceStatus::from(i32::from(unsafe { + *rc.trace_status_ptr + }))) + } + }) + .unwrap_or(DltTraceStatus::Default) + } + + /// Get the current log level of the context + #[must_use] + pub fn log_level(&self) -> DltLogLevel { + self.raw_context() + .ok() + .and_then(|rc| { + if rc.log_level_ptr.is_null() { + None + } else { + Some(DltLogLevel::from(i32::from(unsafe { *rc.log_level_ptr }))) + } + }) + .unwrap_or(DltLogLevel::Default) + } + + /// Log a simple string message + /// + /// # Errors + /// Returns `DltError` if logging fails + pub fn log(&self, log_level: DltLogLevel, message: &str) -> Result<(), DltSysError> { + let msg_c = CString::new(message).map_err(|_| DltSysError::WrongParameter)?; + + unsafe { + let ret = dlt_bindings::logDlt(self.context, log_level.into(), msg_c.as_ptr()); + DltSysError::from_return_code(ret) + } + } + + /// Start a complex log message with a custom timestamp. + /// Can be used to hide original timestamps or to log event recorded earlier. + /// The timestamp is a steady clock, starting from an arbitrary point in time, + /// usually system start. + /// + /// # Errors + /// Returns `DltError` if starting the log message fails + pub fn log_write_start_custom_timestamp( + &self, + log_level: DltLogLevel, + timestamp_microseconds: u64, + ) -> Result { + let mut log_writer = self.log_write_start(log_level)?; + // timestamp resolution in dlt is .1 milliseconds. + let timestamp = + u32::try_from(timestamp_microseconds / 100).map_err(|_| DltSysError::WrongParameter)?; + log_writer.log_data.use_timestamp = dlt_bindings::DltTimestampType_DLT_USER_TIMESTAMP; + log_writer.log_data.user_timestamp = timestamp; + Ok(log_writer) + } + + /// Start a complex log message + /// + /// # Errors + /// Returns `DltError` if starting the log message fails + pub fn log_write_start(&self, log_level: DltLogLevel) -> Result { + let mut log_data = DltContextData::default(); + + unsafe { + let ret = dlt_bindings::dltUserLogWriteStart( + self.context, + &raw mut log_data, + log_level.into(), + ); + + DltSysError::from_return_code(ret)?; + Ok(DltLogWriter { log_data }) + } + } + + /// Register a channel to receive log level change notifications + /// + /// Returns a receiver that will get `LogLevelChangeEvent` + /// when the DLT daemon changes log levels + /// + /// # Errors + /// Returns `InternalError` if callback registration with DLT fails + pub fn register_log_level_changed_listener( + &self, + ) -> Result, DltError> { + let rwlock = CALLBACK_REGISTRY.get_or_init(|| RwLock::new(HashMap::new())); + let mut guard = rwlock.write().map_err(|_| DltError::BadLock)?; + let ctx_id = self.context_id()?; + + if let Some(broadcaster) = guard.get_mut(&ctx_id) { + Ok(broadcaster.receiver.resubscribe()) + } else { + unsafe { + let ret = dlt_bindings::registerLogLevelChangedCallback( + self.context, + Some(internal_log_level_callback), + ); + DltSysError::from_return_code(ret) + .map_err(|_| DltError::ContextRegistrationFailed)?; + } + let (tx, rx) = broadcast::channel(5); + let rx_clone = rx.resubscribe(); + guard.insert( + ctx_id, + LogLevelChangedBroadcaster { + sender: tx, + receiver: rx, + }, + ); + Ok(rx_clone) + } + } +} + +impl Drop for DltContextHandle { + fn drop(&mut self) { + let context_id = self.context_id(); + if let Some(lock) = CALLBACK_REGISTRY.get() + && let Ok(mut guard) = lock.write() + && let Ok(ctx_id) = context_id + { + guard.remove(&ctx_id); + } + + unsafe { + dlt_bindings::unregisterContext(self.context); + } + } +} + +// Safe to send between threads, per DLT documentation +unsafe impl Send for DltContextHandle {} +unsafe impl Sync for DltContextHandle {} + +/// Builder for structured log messages with multiple typed fields +/// +/// Construct log messages with typed data fields sent in binary format for efficiency. +/// Each field retains type information for proper display in DLT viewers. +/// +/// # Usage +/// +/// 1. Start with [`DltContextHandle::log_write_start()`] +/// 2. Chain `write_*` methods to add fields +/// 3. Call [`finish()`](DltLogWriter::finish()) to send +/// +/// Auto-finishes on drop if [`finish()`](DltLogWriter::finish()) not called (errors ignored). +/// +/// # Example +/// +/// ```no_run +/// # use dlt_sys::{DltApplication, DltId, DltLogLevel}; +/// # fn main() -> Result<(), Box> { +/// # let app = DltApplication::register(&DltId::new(b"MBTI")?, +/// "Measurement and Bus Trace Interface")?; +/// # let ctx = app.create_context(&DltId::new(b"MEAS")?, "Context")?; +/// let mut writer = ctx.log_write_start(DltLogLevel::Info)?; +/// writer.write_string("Temperature:")? +/// .write_float32(87.5)? +/// .write_string("°C")?; +/// writer.finish()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// # Available Methods +/// +/// [`write_string()`](DltLogWriter::write_string()) | +/// [`write_i32()`](DltLogWriter::write_i32()) | +/// [`write_u32()`](DltLogWriter::write_u32()) | +/// [`write_int64()`](DltLogWriter::write_int64()) | +/// [`write_uint64()`](DltLogWriter::write_uint64()) | +/// [`write_float32()`](DltLogWriter::write_float32()) | +/// [`write_float64()`](DltLogWriter::write_float64()) | +/// [`write_bool()`](DltLogWriter::write_bool()) +pub struct DltLogWriter { + log_data: DltContextData, +} + +impl DltLogWriter { + /// Write a string to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_string(&mut self, text: &str) -> Result<&mut Self, DltSysError> { + let text_c = CString::new(text).map_err(|_| DltSysError::WrongParameter)?; + + unsafe { + let ret = dlt_bindings::dltUserLogWriteString(&raw mut self.log_data, text_c.as_ptr()); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write an unsigned integer to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_u32(&mut self, value: u32) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_bindings::dltUserLogWriteUint(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a signed integer to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_i32(&mut self, value: i32) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_bindings::dltUserLogWriteInt(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write an unsigned 64-bit integer to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_uint64(&mut self, value: u64) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_bindings::dltUserLogWriteUint64(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a signed 64-bit integer to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_int64(&mut self, value: i64) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_bindings::dltUserLogWriteInt64(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a 32-bit float to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_float32(&mut self, value: f32) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_bindings::dltUserLogWriteFloat32(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a 64-bit float to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_float64(&mut self, value: f64) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_bindings::dltUserLogWriteFloat64(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a boolean to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_bool(&mut self, value: bool) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_bindings::dltUserLogWriteBool(&raw mut self.log_data, u8::from(value)); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Finish and send the log message + /// + /// Explicitly finishes the log message. If not called, the message will be + /// automatically finished when the `DltLogWriter` is dropped, but errors will be ignored. + /// + /// # Errors + /// Returns `DltError` if finishing fails + pub fn finish(mut self) -> Result<(), DltSysError> { + let ret = unsafe { dlt_bindings::dltUserLogWriteFinish(&raw mut self.log_data) }; + // Prevent Drop from running since we've already finished + std::mem::forget(self); + DltSysError::from_return_code(ret) + } +} + +impl Drop for DltLogWriter { + fn drop(&mut self) { + // Auto-finish the log message if finish() wasn't called explicitly + unsafe { + let _ = dlt_bindings::dltUserLogWriteFinish(&raw mut self.log_data); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dlt_error_from_return_code() { + assert!(DltSysError::from_return_code(0).is_ok()); + assert!(DltSysError::from_return_code(1).is_ok()); + assert_eq!(DltSysError::from_return_code(-1), Err(DltSysError::Error)); + assert_eq!( + DltSysError::from_return_code(-5), + Err(DltSysError::WrongParameter) + ); + } + + #[test] + fn test_dlt_id_creation() { + // 1 byte ID + let short_id = DltId::new(b"A").unwrap(); + assert_eq!(short_id.as_str().unwrap(), "A"); + + // 3 byte IDs + let app_id = DltId::new(b"APP").unwrap(); + assert_eq!(app_id.as_str().unwrap(), "APP"); + + let ctx_id = DltId::new(b"CTX").unwrap(); + assert_eq!(ctx_id.as_str().unwrap(), "CTX"); + + // 4 byte ID (maximum) + let full_id = DltId::new(b"ABCD").unwrap(); + assert_eq!(full_id.as_str().unwrap(), "ABCD"); + } + + #[test] + fn test_dlt_id_too_long() { + let result = DltId::new(b"TOOLONG"); + assert_eq!(result.unwrap_err(), DltError::InvalidInput); + } + + #[test] + fn test_dlt_id_empty() { + let result = DltId::new(b""); + assert_eq!(result.unwrap_err(), DltError::InvalidInput); + } + + #[test] + fn test_dlt_id_non_ascii() { + let result = DltId::new(b"\xFF\xFE"); + assert_eq!(result.unwrap_err(), DltError::InvalidInput); + } + + #[test] + fn test_dlt_id_equality() { + let id1 = DltId::new(b"APP").unwrap(); + let id2 = DltId::new(b"APP").unwrap(); + let id3 = DltId::new(b"CTX").unwrap(); + + assert_eq!(id1, id2); + assert_ne!(id1, id3); + + // Different lengths are not equal + let id4 = DltId::new(b"A").unwrap(); + assert_ne!(id1, id4); + } +} diff --git a/dlt-sys/wrapper/dlt-wrapper.c b/dlt-sys/wrapper/dlt-wrapper.c new file mode 100644 index 0000000..0849673 --- /dev/null +++ b/dlt-sys/wrapper/dlt-wrapper.c @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// Requires system DLT headers - set DLT_INCLUDE_DIR environment variable or modify include paths accordingly. +#include "dlt-wrapper.h" +#include +#include +#include + +DltReturnValue registerApplication(const char *appId, const char *appDescription) { + if (appId == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + DLT_REGISTER_APP(appId, appDescription); + return DLT_RETURN_OK; +} + +DltReturnValue unregisterApplicationFlushBufferedLogs(void) { + return dlt_unregister_app_flush_buffered_logs(); +} + +DltReturnValue dltFree(void) { + return dlt_free(); +} + +DltContext *registerContext(const char *contextId, const char *contextDescription) { + if (contextId == NULL) { + return NULL; + } + + DltContext *context = (DltContext *)malloc(sizeof(DltContext)); + if (context == NULL) { + return NULL; + } + + DLT_REGISTER_CONTEXT(*context, contextId, contextDescription); + return context; +} + +DltReturnValue unregisterContext(DltContext *context) { + if (context == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + dlt_unregister_context(context); + free(context); + return DLT_RETURN_OK; +} + +DltReturnValue logDlt(DltContext *context, DltLogLevelType logLevel, const char *message) { + if (context == NULL || message == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + DLT_LOG(*context, logLevel, DLT_CSTRING(message)); + return DLT_RETURN_OK; +} + +DltReturnValue logDltString(DltContext *context, DltLogLevelType logLevel, const char *message) { + return logDlt(context, logLevel, message); +} + +DltReturnValue logDltUint(DltContext *context, DltLogLevelType logLevel, uint32_t value) { + if (context == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + DLT_LOG(*context, logLevel, DLT_UINT32(value)); + return DLT_RETURN_OK; +} + +DltReturnValue logDltInt(DltContext *context, DltLogLevelType logLevel, int32_t value) { + if (context == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + DLT_LOG(*context, logLevel, DLT_INT32(value)); + return DLT_RETURN_OK; +} + +DltReturnValue dltUserLogWriteStart(DltContext *context, DltContextData *log, DltLogLevelType logLevel) { + if (context == NULL || log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_start(context, log, logLevel); +} + +DltReturnValue dltUserLogWriteFinish(DltContextData *log) { + if (log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_finish(log); +} + +DltReturnValue dltUserLogWriteString(DltContextData *log, const char *text) { + if (log == NULL || text == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_string(log, text); +} + +DltReturnValue dltUserLogWriteUint(DltContextData *log, uint32_t data) { + if (log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_uint(log, data); +} + +DltReturnValue dltUserLogWriteInt(DltContextData *log, int32_t data) { + if (log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_int(log, data); +} + +DltReturnValue dltUserLogWriteUint64(DltContextData *log, uint64_t data) { + if (log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_uint64(log, data); +} + +DltReturnValue dltUserLogWriteInt64(DltContextData *log, int64_t data) { + if (log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_int64(log, data); +} + +DltReturnValue dltUserLogWriteFloat32(DltContextData *log, float data) { + if (log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_float32(log, data); +} + +DltReturnValue dltUserLogWriteFloat64(DltContextData *log, double data) { + if (log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_float64(log, data); +} + +DltReturnValue dltUserLogWriteBool(DltContextData *log, uint8_t data) { + if (log == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_user_log_write_bool(log, data); +} + +DltReturnValue registerLogLevelChangedCallback( + DltContext *handle, + void (*callback)(char context_id[DLT_ID_SIZE], uint8_t log_level, uint8_t trace_status) +) { + if (handle == NULL || callback == NULL) { + return DLT_RETURN_WRONG_PARAMETER; + } + + return dlt_register_log_level_changed_callback(handle, callback); +} diff --git a/dlt-sys/wrapper/dlt-wrapper.h b/dlt-sys/wrapper/dlt-wrapper.h new file mode 100644 index 0000000..5f9eded --- /dev/null +++ b/dlt-sys/wrapper/dlt-wrapper.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef DLT_WRAPPER_H +#define DLT_WRAPPER_H + +// Requires system DLT headers - set DLT_INCLUDE_DIR environment variable +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Application management +DltReturnValue registerApplication(const char *appId, const char *appDescription); +DltReturnValue unregisterApplicationFlushBufferedLogs(void); +DltReturnValue dltFree(void); + +// Context management +DltContext *registerContext(const char *contextId, const char *contextDescription); +DltReturnValue unregisterContext(DltContext *context); + +// Logging functions +DltReturnValue logDlt(DltContext *context, DltLogLevelType logLevel, const char *message); +DltReturnValue logDltString(DltContext *context, DltLogLevelType logLevel, const char *message); +DltReturnValue logDltUint(DltContext *context, DltLogLevelType logLevel, uint32_t value); +DltReturnValue logDltInt(DltContext *context, DltLogLevelType logLevel, int32_t value); + +// Log write API (for structured logging) +DltReturnValue dltUserLogWriteStart(DltContext *context, DltContextData *log, DltLogLevelType logLevel); +DltReturnValue dltUserLogWriteFinish(DltContextData *log); +DltReturnValue dltUserLogWriteString(DltContextData *log, const char *text); +DltReturnValue dltUserLogWriteUint(DltContextData *log, uint32_t data); +DltReturnValue dltUserLogWriteInt(DltContextData *log, int32_t data); +DltReturnValue dltUserLogWriteUint64(DltContextData *log, uint64_t data); +DltReturnValue dltUserLogWriteInt64(DltContextData *log, int64_t data); +DltReturnValue dltUserLogWriteFloat32(DltContextData *log, float data); +DltReturnValue dltUserLogWriteFloat64(DltContextData *log, double data); +DltReturnValue dltUserLogWriteBool(DltContextData *log, uint8_t data); + +// Callback for log level changes +DltReturnValue registerLogLevelChangedCallback( + DltContext *handle, + void (*callback)(char context_id[DLT_ID_SIZE], uint8_t log_level, uint8_t trace_status) +); + +#ifdef __cplusplus +} +#endif + +#endif // DLT_WRAPPER_H diff --git a/dlt-tracing-appender/Cargo.toml b/dlt-tracing-appender/Cargo.toml new file mode 100644 index 0000000..81c9544 --- /dev/null +++ b/dlt-tracing-appender/Cargo.toml @@ -0,0 +1,46 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "dlt-tracing-appender" +version = "0.1.0" +edition.workspace = true +publish = false +description = "Appender for tracing crate to write tracing spans and events to DLT using dlt-sys" +homepage.workspace = true +license.workspace = true + +[lints] +workspace = true + +[lib] +path = "src/lib.rs" + +[dependencies] +# internal dependencies +dlt-sys = { workspace = true } + +# external +tokio = { workspace = true } +indexmap = { workspace = true } + +# logging & tracing +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["registry"] } +tracing-core = { workspace = true } +crc32fast = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["test-util", "macros"] } + +[features] +default = ["dlt_layer_internal_logging"] +dlt_layer_internal_logging = [] diff --git a/dlt-tracing-appender/src/lib.rs b/dlt-tracing-appender/src/lib.rs new file mode 100644 index 0000000..dc604d4 --- /dev/null +++ b/dlt-tracing-appender/src/lib.rs @@ -0,0 +1,619 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Tracing integration for COVESA DLT (Diagnostic Log and Trace) +//! +//! [`tracing`](https://docs.rs/tracing) subscriber layer that forwards tracing events to the +//! DLT daemon, enabling standard `tracing` macros (`info!`, `debug!`, `error!`, etc.) to +//! send logs to DLT for centralized diagnostics. +//! +//! # Quick Start +//! +//! ```no_run +//! use dlt_tracing_appender::{DltLayer, DltId}; +//! use tracing::{info, span, Level}; +//! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! // Register DLT layer +//! let app_id = DltId::new(b"MBTI")?; +//! tracing_subscriber::registry() +//! .with(DltLayer::new(&app_id, "My Beautiful Trace Ingestor")?) +//! .init(); +//! +//! // Basic logging (uses default "DFLT" context) +//! info!("Application started"); +//! // DLT Output: MBTI DFLT log info V 1 [lib: Application started] +//! // ^^^^ ^^^^ ^^^ ^^^^ ^ ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//! // | | | | | | Message (crate + msg) +//! // | | | | | Number of arguments +//! // | | | | Verbose flag +//! // | | | Log level +//! // | | Log type +//! // | Context ID (default) +//! // Application ID +//! +//! // Custom DLT context per span +//! let span = span!(Level::INFO, "network", dlt_context = "NET"); +//! let _guard = span.enter(); +//! info!(bytes = 1024, "Data received"); +//! // DLT Output: MBTI NET log info V 2 [network: lib: Data received bytes = 1024] +//! // ^^^ ^^^^^^^^ ^^^^^^^^^^^^ ^^^^ +//! // | Custom context Span name Message Structured field +//! # Ok(()) +//! # } +//! ``` +//! +//! # Core Features +//! +//! - **Per-span contexts** - Use `dlt_context` field to route logs to specific DLT contexts +//! - **Structured logging** - Span fields automatically included in messages with native types +//! - **Dynamic control** - Runtime log level changes via `dlt-control` command +//! - **Layer composition** - Combine with other tracing layers (fmt, file, etc.) +//! - **Thread-safe** - Full `Send + Sync` support +//! +//! # DLT Context Management +//! +//! Events outside spans use the default "DFLT" context. Spans can specify their own +//! context with the `dlt_context` field (auto-creates and caches contexts): +//! +//! ```no_run +//! # use dlt_tracing_appender::{DltLayer, DltId}; +//! # use tracing::{info, span, Level}; +//! # use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let app_id = DltId::new(b"ADAS")?; +//! # tracing_subscriber::registry().with(DltLayer::new(&app_id, "ADAS")?) .init(); +//! // Nested contexts +//! let outer = span!(Level::INFO, "can_bus", dlt_context = "CAN"); +//! let _g1 = outer.enter(); +//! info!(msg_id = 0x123, "CAN frame received"); +//! +//! let inner = span!(Level::DEBUG, "decode", dlt_context = "CTRL"); +//! let _g2 = inner.enter(); +//! info!("Decoded steering command"); +//! # Ok(()) +//! # } +//! ``` +//! +//! # See Also +//! +//! - [`dlt_sys`] - Low-level Rust bindings for DLT +//! - [`tracing`](https://docs.rs/tracing) - Application-level tracing framework +//! - [COVESA DLT](https://github.com/COVESA/dlt-daemon) - The DLT daemon + +use std::{ + collections::HashMap, + fmt, + fmt::Write, + sync::{Arc, RwLock}, +}; + +use dlt_sys::{DltApplication, DltContextHandle, DltLogLevel}; +// Re-export types for users of this library +pub use dlt_sys::{DltError, DltId, DltSysError}; +use indexmap::IndexMap; +use tracing_core::{Event, Subscriber, span}; +use tracing_subscriber::{Layer, filter::LevelFilter, layer::Context, registry::LookupSpan}; + +/// Field name for custom DLT context ID in spans +const DLT_CONTEXT_FIELD: &str = "dlt_context"; + +/// COVESA DLT layer for tracing +/// +/// Integrates `tracing` with DLT (Diagnostic Log and Trace). Spans can specify their +/// own DLT context via the `dlt_context` field; events outside spans use "DFLT" context. +/// +/// See the [crate-level documentation](index.html) for usage examples. +pub struct DltLayer { + pub app: Arc, + /// Default context for events outside of spans + default_context: Arc, + /// Cache of DLT contexts by span name + context_cache: Arc>>>, +} +impl DltLayer { + /// Create a new DLT layer + /// + /// Registers the application with DLT and creates a default "DFLT" context. + /// Span contexts are created on-demand using the `dlt_context` field. + /// + /// # Errors + /// Returns error if DLT registration fails or strings contain null bytes. + /// + /// # Panics + /// If called outside a Tokio runtime context. + pub fn new(app_id: &DltId, app_description: &str) -> Result { + // Register application with DLT + let app = Arc::new(DltApplication::register(app_id, app_description)?); + // Create default context for events outside spans + let default_context = + Arc::new(app.create_context(&DltId::new(b"DFLT")?, "Default context")?); + + Self::register_context_level_changed(&default_context)?; + + Ok(DltLayer { + app, + default_context, + context_cache: Arc::new(RwLock::new(HashMap::new())), + }) + } + + fn register_context_level_changed(context: &DltContextHandle) -> Result<(), DltError> { + let mut receiver = context.register_log_level_changed_listener()?; + // Spawn a background task to handle log level updates for all contexts + tokio::spawn(async move { + while let Ok(_event) = receiver.recv().await { + // Log level is already updated internally in the DltContextHandle + // We just need to rebuild the callsite interest + // cache so that the new level takes effect + tracing_core::callsite::rebuild_interest_cache(); + } + }); + Ok(()) + } + + /// Get or create a DLT context for a span + /// + /// Checks for a `dlt_context` field in the span. If present, uses it as the context ID. + /// Otherwise, returns the default context. + /// Caches contexts to avoid recreating them for the same context ID. + fn get_or_create_context_for_span( + &self, + span: &tracing_subscriber::registry::SpanRef<'_, S>, + ) -> Result, DltError> + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + let span_name = span.name(); + + // Check if span has a custom "dlt_context" field + let dlt_context_id = { + let extensions = span.extensions(); + extensions + .get::>() + .and_then(|fields| { + fields.get(DLT_CONTEXT_FIELD).and_then(|value| match value { + FieldValue::Str(s) => Some(s.clone()), + _ => None, + }) + }) + }; + + // If no custom dlt_context field, use default context + let Some(custom_id) = dlt_context_id else { + return Ok(Arc::clone(&self.default_context)); + }; + + // Check cache for custom context + { + let cache = self.context_cache.read().map_err(|_| DltError::BadLock)?; + if let Some(context) = cache.get(&custom_id) { + return Ok(Arc::clone(context)); + } + } + + // Create new context with custom ID + let ctx_id = Self::span_name_to_dlt_id(&custom_id)?; + let context = Arc::new(self.app.create_context(&ctx_id, span_name)?); + + let mut cache = self.context_cache.write().map_err(|_| DltError::BadLock)?; + cache.insert(custom_id, Arc::clone(&context)); + Self::register_context_level_changed(&context)?; + + Ok(context) + } + + /// Convert a span name to a valid DLT context ID + /// + /// Takes the first 1-4 bytes of the span name, uppercase. + /// If the name is longer, it's truncated. If shorter, it's used as-is. + fn span_name_to_dlt_id(name: &str) -> Result { + if name.is_empty() { + return Err(DltError::InvalidInput); + } + let bytes = name.as_bytes(); + let len = bytes.len().clamp(1, dlt_sys::DLT_ID_SIZE_USIZE); + + let get = |i| bytes.get(i).copied().ok_or(DltError::InvalidInput); + + match len { + 1 => DltId::new(&[get(0)?]), + 2 => DltId::new(&[get(0)?, get(1)?]), + 3 => DltId::new(&[get(0)?, get(1)?, get(2)?]), + _ => DltId::new(&[get(0)?, get(1)?, get(2)?, get(3)?]), + } + } + + #[cfg(feature = "dlt_layer_internal_logging")] + fn log_dlt_error(metadata: &tracing_core::Metadata, level: tracing::Level, e: DltSysError) { + eprintln!("DLT error occurred: {e:?}"); + tracing::warn!( + target: "dlt_layer_internal", + error = ?e, + event_target = metadata.target(), + event_level = ?level, + "DLT error occurred" + ); + } + + #[cfg(not(feature = "dlt_layer_internal_logging"))] + fn log_dlt_error(_metadata: &tracing_core::Metadata, _level: tracing::Level, _e: DltSysError) { + // Silent if internal logging is disabled + } + + fn max_dlt_level(&self) -> DltLogLevel { + if let Ok(cache) = self.context_cache.read() { + cache + .values() + .map(|ctx| ctx.log_level()) + .chain(std::iter::once(self.default_context.log_level())) + .max_by_key(|&level| level as i32) + .unwrap_or(DltLogLevel::Default) + } else { + DltLogLevel::Default + } + } +} + +impl Layer for DltLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: Context<'_, S>) -> bool { + // Prevent our own internal error messages from being logged to DLT + metadata.target() != "dlt_layer_internal" + } + + fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) { + // Store span fields for later context building + if let Some(span) = ctx.span(id) { + let mut visitor = FieldVisitor::new(); + attrs.record(&mut visitor); + + let mut extensions = span.extensions_mut(); + extensions.insert(visitor.fields); + } + } + + fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) { + // Update span fields + if let Some(span) = ctx.span(span) { + let mut visitor = FieldVisitor::new(); + values.record(&mut visitor); + + let mut extensions = span.extensions_mut(); + if let Some(fields) = extensions.get_mut::>() { + fields.extend(visitor.fields); + } else { + extensions.insert(visitor.fields); + } + } + } + + fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { + let metadata = event.metadata(); + let level = metadata.level(); + + // Determine which DLT context to use based on the current span + let dlt_context = ctx + .event_scope(event) + .and_then(|scope| scope.from_root().last()) + .map_or(Arc::clone(&self.default_context), |span| { + self.get_or_create_context_for_span(&span) + .unwrap_or_else(|_| Arc::clone(&self.default_context)) + }); + + let dlt_level = map_level_to_dlt(*level); + let context_log_level = dlt_context.log_level(); + if (dlt_level as i32) > (context_log_level as i32) { + return; // Skip logging if level is above DLT threshold for this context + } + + // Start building the DLT message + let mut log_writer = match dlt_context.log_write_start(dlt_level) { + Ok(log_writer) => log_writer, + Err(e) => { + Self::log_dlt_error(metadata, *level, e); + return; + } + }; + + if let Some(scope) = ctx.event_scope(event) { + let mut span_context = String::new(); + for span in scope.from_root() { + if !span_context.is_empty() { + span_context.push(':'); + } + span_context.push_str(span.name()); + + let extensions = span.extensions(); + if let Some(fields) = extensions.get::>() { + // Filter out dlt_context field from display + let display_fields: Vec<_> = fields + .iter() + .filter(|(name, _)| *name != DLT_CONTEXT_FIELD) + .collect(); + + if !display_fields.is_empty() { + span_context.push('{'); + for (i, (k, v)) in display_fields.iter().enumerate() { + if i > 0 { + span_context.push_str(", "); + } + let _ = write!(span_context, "{k}={v}"); + } + span_context.push('}'); + } + } + } + + if !span_context.is_empty() { + span_context.push(':'); + let _ = log_writer.write_string(&span_context); + } + } + + // Add event fields with native types + let mut visitor = FieldVisitor::new(); + event.record(&mut visitor); + + // Extract the message field if present, so we can + // only take the value without the "message=" prefix + // this reduces the clutter in dlt and gives more space for relevant info + let mut fields = visitor.fields; + let message_field = fields.shift_remove("message"); + let target = metadata.target(); + if let Some(msg) = message_field { + let formatted = if target.is_empty() { + msg.to_string() + } else { + format!("{target}: {msg}") + }; + let _ = log_writer.write_string(&formatted); + } else if !target.is_empty() { + let _ = log_writer.write_string(target); + } + + // Write all other fields normally, the dlt_context field is already filtered out + if let Err(e) = write_fields(&mut log_writer, fields) { + Self::log_dlt_error(metadata, *level, e); + return; + } + + // Finish and send the log message + if let Err(e) = log_writer.finish() { + Self::log_dlt_error(metadata, *level, e); + } + } +} + +// Implement Filter trait to provide dynamic max level hint based on DLT configuration +impl tracing_subscriber::layer::Filter for DltLayer { + /// Determines if a span or event with the given metadata is enabled. + /// + /// This implementation checks if the event/span level is within the current + /// DLT maximum log level across all contexts. It also filters out internal + /// DLT layer errors to prevent recursion. + /// + /// Since we don't know which context will be used at this point, we use the + /// most permissive level across all contexts. + fn enabled(&self, meta: &tracing_core::Metadata<'_>, _cx: &Context<'_, S>) -> bool { + // Prevent our own internal error messages from being logged to DLT + if meta.target() == "dlt_layer_internal" { + return false; + } + + // Check if this log level is enabled by any DLT context + let dlt_level = map_level_to_dlt(*meta.level()); + + // Find the most permissive (highest) log level across all contexts + let max_level = self.max_dlt_level(); + + // Compare log levels - enable if event level is less than or equal to max allowed + (dlt_level as i32) <= (max_level as i32) + } + + /// Returns the current maximum log level from DLT. + /// + /// This hint allows the tracing infrastructure to skip callsites that are + /// more verbose than the current DLT log level, improving performance. + /// + /// When the DLT log level changes (via DLT daemon or configuration), the + /// background task calls `rebuild_interest_cache()` to ensure this new hint + /// takes effect immediately. + /// + /// This returns the most permissive (verbose) level across all contexts, since + /// we can't know which context will be used at callsite registration time. + fn max_level_hint(&self) -> Option { + let max_level = self.max_dlt_level(); + + Some(map_dlt_to_level_filter(max_level)) + } +} + +/// Map tracing log levels to DLT log levels +fn map_level_to_dlt(level: tracing::Level) -> DltLogLevel { + match level { + tracing::Level::ERROR => DltLogLevel::Error, + tracing::Level::WARN => DltLogLevel::Warn, + tracing::Level::INFO => DltLogLevel::Info, + tracing::Level::DEBUG => DltLogLevel::Debug, + tracing::Level::TRACE => DltLogLevel::Verbose, + } +} + +/// Map DLT log level to tracing `LevelFilter` +fn map_dlt_to_level_filter(dlt_level: DltLogLevel) -> LevelFilter { + match dlt_level { + DltLogLevel::Off | DltLogLevel::Default => LevelFilter::OFF, + DltLogLevel::Fatal | DltLogLevel::Error => LevelFilter::ERROR, + DltLogLevel::Warn => LevelFilter::WARN, + DltLogLevel::Info => LevelFilter::INFO, + DltLogLevel::Debug => LevelFilter::DEBUG, + DltLogLevel::Verbose => LevelFilter::TRACE, + } +} + +/// Helper function to write fields to DLT with proper error propagation +fn write_fields( + log_writer: &mut dlt_sys::DltLogWriter, + fields: IndexMap, +) -> Result<(), dlt_sys::DltSysError> { + for (field_name, field_value) in fields { + // Write field name + log_writer.write_string(&field_name)?; + log_writer.write_string("=")?; + + // Write field value with native type + match field_value { + FieldValue::I64(v) => { + log_writer.write_int64(v)?; + } + FieldValue::U64(v) => { + log_writer.write_uint64(v)?; + } + FieldValue::I128(v) => { + // DLT doesn't support i128, convert to string + log_writer.write_string(&v.to_string())?; + } + FieldValue::U128(v) => { + // DLT doesn't support u128, convert to string + log_writer.write_string(&v.to_string())?; + } + FieldValue::F64(v) => { + log_writer.write_float64(v)?; + } + FieldValue::Bool(v) => { + log_writer.write_bool(v)?; + } + FieldValue::Str(s) | FieldValue::Debug(s) => { + log_writer.write_string(&s)?; + } + } + } + Ok(()) +} + +/// Typed field value that preserves the original data type +#[derive(Debug, Clone)] +enum FieldValue { + Str(String), + I64(i64), + U64(u64), + I128(i128), + U128(u128), + F64(f64), + Bool(bool), + Debug(String), +} + +impl fmt::Display for FieldValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FieldValue::I64(v) => write!(f, "{v}"), + FieldValue::U64(v) => write!(f, "{v}"), + FieldValue::I128(v) => write!(f, "{v}"), + FieldValue::U128(v) => write!(f, "{v}"), + FieldValue::F64(v) => write!(f, "{v}"), + FieldValue::Bool(v) => write!(f, "{v}"), + FieldValue::Str(s) => write!(f, "\"{s}\""), + FieldValue::Debug(s) => write!(f, "{s}"), + } + } +} + +/// Helper visitor for extracting span/event fields with type preservation +struct FieldVisitor { + fields: IndexMap, +} + +impl FieldVisitor { + fn new() -> Self { + FieldVisitor { + fields: IndexMap::new(), + } + } +} + +impl tracing::field::Visit for FieldVisitor { + fn record_f64(&mut self, field: &tracing::field::Field, value: f64) { + self.fields + .insert(field.name().to_string(), FieldValue::F64(value)); + } + + fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { + self.fields + .insert(field.name().to_string(), FieldValue::I64(value)); + } + + fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { + self.fields + .insert(field.name().to_string(), FieldValue::U64(value)); + } + + fn record_i128(&mut self, field: &tracing::field::Field, value: i128) { + self.fields + .insert(field.name().to_string(), FieldValue::I128(value)); + } + + fn record_u128(&mut self, field: &tracing::field::Field, value: u128) { + self.fields + .insert(field.name().to_string(), FieldValue::U128(value)); + } + + fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { + self.fields + .insert(field.name().to_string(), FieldValue::Bool(value)); + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + self.fields + .insert(field.name().to_string(), FieldValue::Str(value.to_string())); + } + + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn fmt::Debug) { + self.fields.insert( + field.name().to_string(), + FieldValue::Debug(format!("{value:?}")), + ); + } +} + +#[cfg(test)] +mod tests { + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + + use super::*; + + #[tokio::test] + async fn test_dlt_appender() { + // This works even without a running DLT daemon because + // the DLT C library buffers messages internally. + let app_id = DltId::new(b"APP").unwrap(); + + tracing_subscriber::registry() + .with(DltLayer::new(&app_id, "test").expect("Failed to create DLT layer")) + .init(); + + let outer_span = tracing::info_span!("outer", level = 0); + let _outer_entered = outer_span.enter(); + + let inner_span = tracing::error_span!("inner", level = 1); + let _inner_entered = inner_span.enter(); + + tracing::info!(a_bool = true, answer = 42, message = "first example"); + } +} diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml new file mode 100644 index 0000000..2928761 --- /dev/null +++ b/integration-tests/Cargo.toml @@ -0,0 +1,33 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "integration-tests" +version = "0.1.0" +edition.workspace = true +publish = false +description = "Common test utilities for DLT tracing integration tests" +homepage.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +dlt-sys = { workspace = true } +dlt-tracing-appender = { workspace = true } +tokio = { workspace = true, features = ["test-util", "macros", "rt"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["registry", "fmt"] } +serial_test = { workspace = true } + +[features] +integration-tests = [] diff --git a/integration-tests/tests/dlt_sys/mod.rs b/integration-tests/tests/dlt_sys/mod.rs new file mode 100644 index 0000000..27eccbb --- /dev/null +++ b/integration-tests/tests/dlt_sys/mod.rs @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +use dlt_sys::{DltApplication, DltId, DltLogLevel, LogLevelChangedEvent}; +use serial_test::serial; +use tokio::sync::broadcast; + +use crate::{ + DltReceiver, assert_contains, assert_contains_all, change_dlt_log_level, + ensure_dlt_daemon_running, +}; + +#[tokio::test] +#[serial] +async fn test_register_application_and_context() { + ensure_dlt_daemon_running(); + + let app_id = DltId::new(b"ITST").unwrap(); + let ctx_id = DltId::new(b"CTX1").unwrap(); + + let app = DltApplication::register(&app_id, "Integration test app").unwrap(); + let context = app + .create_context(&ctx_id, "Integration test context") + .unwrap(); + + let receiver = DltReceiver::start(); + + let test_msg = "Integration test message"; + context.log(DltLogLevel::Info, test_msg).unwrap(); + + // Verify message appears in DLT output + let output = receiver.stop_and_get_output(); + assert_contains( + &output, + &format!( + "{} {} log info V 1 [{test_msg}]", + app_id.as_str().unwrap(), + ctx_id.as_str().unwrap() + ), + ); +} + +#[tokio::test] +#[serial] +async fn test_complex_log_message() { + ensure_dlt_daemon_running(); + + let app_id = DltId::new(b"ICMP").unwrap(); + let ctx_id = DltId::new(b"CPLX").unwrap(); + + let app = DltApplication::register(&app_id, "Complex log test").unwrap(); + let context = app.create_context(&ctx_id, "Complex context").unwrap(); + + let receiver = DltReceiver::start(); + + let mut log_writer = context + .log_write_start(DltLogLevel::Error) + .expect("Failed to start log"); + + log_writer.write_string("Test field").unwrap(); + log_writer.write_u32(42).unwrap(); + log_writer.write_i32(-123).unwrap(); + log_writer.write_bool(true).unwrap(); + + log_writer.finish().expect("Failed to finish log"); + + let output = receiver.stop_and_get_output(); + assert_contains( + &output, + &format!( + "{} {} log error V 4 [Test field 42 -123 1]", + app_id.as_str().unwrap(), + ctx_id.as_str().unwrap() + ), + ); +} + +#[tokio::test] +#[serial] +async fn test_different_log_levels() { + ensure_dlt_daemon_running(); + + let app_id = DltId::new(b"ILVL").unwrap(); + let ctx_id = DltId::new(b"LEVL").unwrap(); + + let app = DltApplication::register(&app_id, "Log level test").unwrap(); + let context = app.create_context(&ctx_id, "Level context").unwrap(); + + let receiver = DltReceiver::start(); + + context.log(DltLogLevel::Fatal, "Fatal_unique_msg").unwrap(); + context.log(DltLogLevel::Error, "Error_unique_msg").unwrap(); + context + .log(DltLogLevel::Warn, "Warning_unique_msg") + .unwrap(); + context.log(DltLogLevel::Info, "Info_unique_msg").unwrap(); + context.log(DltLogLevel::Debug, "Debug_unique_msg").unwrap(); + context + .log(DltLogLevel::Verbose, "Verbose_unique_msg") + .unwrap(); + + let output = receiver.stop_and_get_output(); + assert_contains_all( + &output, + &[ + "ILVL LEVL log fatal V 1 [Fatal_unique_msg]", + "ILVL LEVL log error V 1 [Error_unique_msg]", + "ILVL LEVL log warn V 1 [Warning_unique_msg]", + "ILVL LEVL log info V 1 [Info_unique_msg]", + ], + ); +} + +#[tokio::test] +#[serial] +async fn test_registering_application_twice() { + ensure_dlt_daemon_running(); + + let app_id1 = DltId::new(b"APP1").unwrap(); + let app_id2 = DltId::new(b"APP2").unwrap(); + + let app1 = DltApplication::register(&app_id1, "double register test first").unwrap(); + assert!(DltApplication::register(&app_id2, "double register test second").is_err()); + drop(app1); + + // after dropping the first application, registering the second one should work + let _ = DltApplication::register(&app_id2, "double register test second").unwrap(); +} + +#[tokio::test] +#[serial] +async fn test_log_level_changed() { + async fn wait_for_log_level_changed_event( + rx: &mut broadcast::Receiver, + ) -> LogLevelChangedEvent { + tokio::select! { + result = rx.recv() => { + match result { + Ok(event) => { + event + } + Err(e) => { + panic!("Failed to receive log level change: {e:?}"); + } + } + } + () = tokio::time::sleep(tokio::time::Duration::from_secs(3)) => { + panic!("Timeout waiting for log level change"); + } + } + } + + ensure_dlt_daemon_running(); + + let app_id = DltId::new(b"LVCH").unwrap(); + let ctx_id = DltId::new(b"CTX1").unwrap(); + + let app = DltApplication::register(&app_id, "testing log level changes").unwrap(); + let context = app + .create_context(&ctx_id, "context for log level change") + .unwrap(); + let mut rx = context.register_log_level_changed_listener().unwrap(); + let event = wait_for_log_level_changed_event(&mut rx).await; + assert_eq!(event.log_level, DltLogLevel::Info); // default log level, sent on registration + assert_eq!(context.log_level(), DltLogLevel::Info); + + // change given context + let level = DltLogLevel::Debug; + change_dlt_log_level(level, Some(&app_id), Some(&ctx_id)); + let event = wait_for_log_level_changed_event(&mut rx).await; + assert_eq!(event.log_level, level); + assert_eq!(context.log_level(), level); + + // change all contexts + let level = DltLogLevel::Error; + change_dlt_log_level(level, Some(&app_id), None); + let event = wait_for_log_level_changed_event(&mut rx).await; + assert_eq!(event.log_level, level); + assert_eq!(context.log_level(), level); + + // change all applications + let level = DltLogLevel::Fatal; + change_dlt_log_level(level, None, None); + let event = wait_for_log_level_changed_event(&mut rx).await; + assert_eq!(event.log_level, level); + assert_eq!(context.log_level(), level); + + // change unrelated context - should not receive an event + let other_ctx_id = DltId::new(b"CTX2").unwrap(); + change_dlt_log_level(DltLogLevel::Debug, Some(&app_id), Some(&other_ctx_id)); + assert_eq!(context.log_level(), level); +} + +#[tokio::test] +#[serial] +async fn test_context_outlives_application() { + // This test verifies the safety fix: contexts can outlive the application handle + // without causing use-after-free, because contexts keep the application alive + // through internal reference counting. + ensure_dlt_daemon_running(); + + let app_id = DltId::new(b"OLIV").unwrap(); + let ctx_id = DltId::new(b"CTX1").unwrap(); + + let receiver = DltReceiver::start(); + + // Create context, then drop application handle + let context = { + let app = DltApplication::register(&app_id, "Outlive test app").unwrap(); + let ctx = app.create_context(&ctx_id, "Outlive test context").unwrap(); + + // Drop app explicitly - context should keep it alive + drop(app); + + ctx // Return context, which outlives app + }; + + // Context should still be valid and able to log + context + .log(DltLogLevel::Info, "Context_outlived_app") + .unwrap(); + + // Verify message appears in DLT output + let output = receiver.stop_and_get_output(); + assert_contains( + &output, + &format!( + "{} {} log info V 1 [Context_outlived_app]", + app_id.as_str().unwrap(), + ctx_id.as_str().unwrap() + ), + ); +} + +#[tokio::test] +#[serial] +async fn test_multiple_contexts_keep_app_alive() { + // Test that multiple contexts all keep the application alive, + // and the application is only unregistered when all contexts are dropped + ensure_dlt_daemon_running(); + + let app_id = DltId::new(b"MULT").unwrap(); + let ctx1_id = DltId::new(b"CTX1").unwrap(); + let ctx2_id = DltId::new(b"CTX2").unwrap(); + + let receiver = DltReceiver::start(); + + let (ctx1, ctx2) = { + let app = DltApplication::register(&app_id, "Multi context test").unwrap(); + let c1 = app.create_context(&ctx1_id, "Context 1").unwrap(); + let c2 = app.create_context(&ctx2_id, "Context 2").unwrap(); + + // Drop app - both contexts should keep it alive + drop(app); + + (c1, c2) + }; + + // Both contexts should work + ctx1.log(DltLogLevel::Info, "Context1_message").unwrap(); + ctx2.log(DltLogLevel::Info, "Context2_message").unwrap(); + + // Drop first context - second should still work + drop(ctx1); + ctx2.log(DltLogLevel::Info, "Context2_after_ctx1_drop") + .unwrap(); + + let output = receiver.stop_and_get_output(); + assert_contains_all( + &output, + &[ + "MULT CTX1 log info V 1 [Context1_message]", + "MULT CTX2 log info V 1 [Context2_message]", + "MULT CTX2 log info V 1 [Context2_after_ctx1_drop]", + ], + ); +} + +#[tokio::test] +#[serial] +async fn test_clone_application_handle() { + // Test that cloning the application handle works correctly + ensure_dlt_daemon_running(); + + let app_id = DltId::new(b"CLON").unwrap(); + let ctx_id = DltId::new(b"CTX1").unwrap(); + + let receiver = DltReceiver::start(); + + let app = DltApplication::register(&app_id, "Clone test app").unwrap(); + let app_clone = app.clone(); + + // Create context from original + let ctx1 = app.create_context(&ctx_id, "Context 1").unwrap(); + + // Drop original app + drop(app); + + // Create context from clone - should still work + let ctx2_id = DltId::new(b"CTX2").unwrap(); + let ctx2 = app_clone.create_context(&ctx2_id, "Context 2").unwrap(); + + // Both contexts should work + ctx1.log(DltLogLevel::Info, "From_original_app").unwrap(); + ctx2.log(DltLogLevel::Info, "From_cloned_app").unwrap(); + + let output = receiver.stop_and_get_output(); + assert_contains_all( + &output, + &[ + "CLON CTX1 log info V 1 [From_original_app]", + "CLON CTX2 log info V 1 [From_cloned_app]", + ], + ); +} diff --git a/integration-tests/tests/dlt_tracing_appender/mod.rs b/integration-tests/tests/dlt_tracing_appender/mod.rs new file mode 100644 index 0000000..2b56ae6 --- /dev/null +++ b/integration-tests/tests/dlt_tracing_appender/mod.rs @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ +use std::{sync::Arc, time::Duration}; + +use dlt_sys::{DltApplication, DltLogLevel}; +use dlt_tracing_appender::{DltId, DltLayer}; +use serial_test::serial; +use tracing_subscriber::{Registry, layer::SubscriberExt}; + +use crate::{DltReceiver, assert_contains, change_dlt_log_level, ensure_dlt_daemon_running}; + +struct LogFile { + path: std::path::PathBuf, +} + +impl Drop for LogFile { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.path); + } +} + +struct TracingGuard { + _guard: tracing::subscriber::DefaultGuard, + log_file: Option, + dlt_app: Arc, +} + +#[allow(clippy::unwrap_used)] +fn init_tracing(with_file: bool) -> TracingGuard { + ensure_dlt_daemon_running(); + let app_id = DltId::new(b"TEST").unwrap(); + let dlt_layer = DltLayer::new(&app_id, "Default Context Test").unwrap(); + let dlt_app = Arc::clone(&dlt_layer.app); + + let (guard, log_file) = if with_file { + let log_file = + std::env::temp_dir().join(format!("test_console_output_{}.log", std::process::id())); + let file = std::fs::File::create(&log_file).unwrap(); + + let file_layer = tracing_subscriber::fmt::layer() + .with_writer(std::sync::Mutex::new(file)) + .with_ansi(false); + + let subscriber = Registry::default().with(dlt_layer).with(file_layer); + let guard = tracing::subscriber::set_default(subscriber); + (guard, Some(LogFile { path: log_file })) + } else { + let subscriber = Registry::default().with(dlt_layer); + let guard = tracing::subscriber::set_default(subscriber); + (guard, None) + }; + + TracingGuard { + _guard: guard, + log_file, + dlt_app, + } +} + +#[tokio::test] +#[serial] +async fn test_tracing_to_dlt() { + let _guard = init_tracing(false); + let receiver = DltReceiver::start(); + + // Log some messages + tracing::info!("Test info message"); + tracing::warn!("Test warning message"); + tracing::error!("Test error message"); + // Give time for messages to be processed + tokio::time::sleep(Duration::from_millis(200)).await; + let output = receiver.stop_and_get_output(); + for expected_string in [ + "TEST DFLT log info V 1 [lib::dlt_tracing_appender: Test info message]", + "TEST DFLT log warn V 1 [lib::dlt_tracing_appender: Test warning message]", + "TEST DFLT log error V 1 [lib::dlt_tracing_appender: Test error message]", + ] { + assert_contains(&output, expected_string); + } +} + +#[tokio::test] +#[serial] +async fn test_with_spans_and_context_id() { + let _guard = init_tracing(false); + let receiver = DltReceiver::start(); + + { + let outer_span = tracing::span!( + tracing::Level::INFO, + "outer_span", + task = "processing", + dlt_context = "CONTEXT_TOO_LONG" + ); + let _outer_guard = outer_span.enter(); + tracing::info!("Inside outer with context too long"); + { + let inner_span = tracing::span!( + tracing::Level::INFO, + "inner_span", + step = 1, + dlt_context = "CTX1" + ); + let _inner_guard = inner_span.enter(); + tracing::info!("Inside inner span"); + } + tracing::info!("Back in outer span"); + } + tracing::info!("default context"); + + // Verify DLT output, make sure the file appender is also not writing the empty context + let dlt_output = receiver.stop_and_get_output(); + let outer = r#"outer_span{task="processing"}"#; + let inner = "inner_span{step=1}"; + let target = "lib::dlt_tracing_appender"; + for expected_string in [ + format!("TEST CONT log info V 2 [{outer}: {target}: Inside outer with context too long"), + format!("TEST CTX1 log info V 2 [{outer}:{inner}: {target}: Inside inner span]"), + // the dlt_context is still set, because this logs belongs to the outer span + format!("TEST CONT log info V 2 [{outer}: {target}: Back in outer span]"), + format!("TEST DFLT log info V 1 [{target}: default context]"), + ] { + assert_contains(&dlt_output, &expected_string); + } +} + +#[tokio::test] +#[serial] +async fn test_tracing_with_default_context() { + let guard = init_tracing(true); + let receiver = DltReceiver::start(); + + // Create span without dlt_context field - should use default context + // The dlt_context is set to None::<&str>, to test if the empty field is omitted even if + // it's present but empty + let outer_span = tracing::span!( + tracing::Level::INFO, + "outer_span", + task = "processing", + dlt_context = None::<&str> + ); + let _outer_guard = outer_span.enter(); + tracing::info!("Inside outer span"); + { + let inner_span = tracing::span!(tracing::Level::INFO, "inner_span", step = 1); + let _inner_guard = inner_span.enter(); + tracing::info!("inner"); + } + tracing::info!("Back in outer span"); + + // Verify DLT output, make sure the file appender is also not writing the empty context + let dlt_output = receiver.stop_and_get_output(); + let log_file_path = &guard.log_file.as_ref().expect("Log file should exist").path; + let console_output = std::fs::read_to_string(log_file_path).expect("Failed to read log file"); + + for expected_string in [ + r#"outer_span{task="processing"}: lib::dlt_tracing_appender: Inside outer span"#, + r#"outer_span{task="processing"}:inner_span{step=1}: lib::dlt_tracing_appender: inner"#, + r#"outer_span{task="processing"}: lib::dlt_tracing_appender: Back in outer span"#, + ] { + assert_contains(&dlt_output, expected_string); + assert_contains(&console_output, expected_string); + } +} + +#[tokio::test] +#[serial] +async fn test_concurrent_logging() { + let _guard = init_tracing(false); + let receiver = DltReceiver::start(); + // Spawn multiple tasks that log concurrently + let mut handles = vec![]; + for i in 0..5 { + let handle = tokio::spawn(async move { + for j in 0..10 { + tracing::info!(task = i, iteration = j, "Concurrent log message"); + tokio::time::sleep(Duration::from_millis(10)).await; + } + }); + handles.push(handle); + } + // Wait for all tasks to complete + for handle in handles { + handle.await.unwrap(); + } + + let messages = receiver.stop_and_get_output(); + // Verify that all messages are present + for i in 0..5 { + for j in 0..10 { + let expected = format!( + "TEST DFLT log info V 7 [lib::dlt_tracing_appender: Concurrent log message task = \ + {i} iteration = {j}]", + ); + assert_contains(&messages, &expected); + } + } +} + +#[tokio::test] +#[serial] +async fn test_debug_logs() { + let _guard = init_tracing(false); + let receiver = DltReceiver::start(); + + let not_shown_msg = "this debug log should not appear"; + tracing::debug!("{}", not_shown_msg); + + change_dlt_log_level(DltLogLevel::Debug, None, None); + let shown_msg = "now it shows up"; + tracing::debug!(shown_msg); + + let output = receiver.stop_and_get_output(); + assert!( + !output.contains(not_shown_msg), + "Debug log appeared before log level change" + ); + assert_contains(&output, shown_msg); +} + +#[tokio::test] +#[serial] +async fn test_mixed_tracing_and_low_level_dlt() { + // Initialize tracing layer + let guard = init_tracing(false); + let receiver = DltReceiver::start(); + + // Use tracing API (high-level) + tracing::info!("Message from tracing API"); + tracing::warn!(component = "sensor", "Tracing warning with field"); + + // Create a low-level DLT context with a different context ID + let low_level_ctx = guard + .dlt_app + .create_context(&DltId::new(b"LLVL").unwrap(), "Low Level Context") + .unwrap(); + + // Use low-level DLT API + low_level_ctx + .log(DltLogLevel::Info, "Message from low-level DLT") + .unwrap(); + + // Use structured logging with low-level API + let mut writer = low_level_ctx.log_write_start(DltLogLevel::Warn).unwrap(); + writer.write_string("Temperature:").unwrap(); + writer.write_float32(42.42).unwrap(); + writer.write_string("°C").unwrap(); + writer.finish().unwrap(); + + // Mix both APIs - tracing with span and low-level in between + { + let span = tracing::span!(tracing::Level::INFO, "processing", dlt_context = "PROC"); + let _enter = span.enter(); + tracing::info!("Inside tracing span"); + + // Use low-level DLT within the span context + low_level_ctx + .log(DltLogLevel::Error, "Low-level error during processing") + .unwrap(); + + tracing::error!("Tracing error in same span"); + } + + // Back to default tracing context + tracing::info!("Final message from tracing"); + let output = receiver.stop_and_get_output(); + + // Verify tracing messages (using DFLT or PROC context) + assert_contains( + &output, + "TEST DFLT log info V 1 [lib::dlt_tracing_appender: Message from tracing API]", + ); + assert_contains( + &output, + "TEST DFLT log warn V 4 [lib::dlt_tracing_appender: Tracing warning with field component \ + = sensor]", + ); + + // Verify low-level DLT messages (using LLVL context) + assert_contains( + &output, + "TEST LLVL log info V 1 [Message from low-level DLT]", + ); + assert_contains(&output, "TEST LLVL log warn V 3 [Temperature: 42.42 °C]"); + + // Verify messages from within span + assert_contains( + &output, + "TEST PROC log info V 2 [processing: lib::dlt_tracing_appender: Inside tracing span]", + ); + assert_contains( + &output, + "TEST LLVL log error V 1 [Low-level error during processing]", + ); + assert_contains( + &output, + "TEST PROC log error V 2 [processing: lib::dlt_tracing_appender: Tracing error in same \ + span]", + ); + + // Verify final tracing message + assert_contains( + &output, + "TEST DFLT log info V 1 [lib::dlt_tracing_appender: Final message from tracing]", + ); +} diff --git a/integration-tests/tests/lib.rs b/integration-tests/tests/lib.rs new file mode 100644 index 0000000..85694d5 --- /dev/null +++ b/integration-tests/tests/lib.rs @@ -0,0 +1,188 @@ +// Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +//! Common test utilities for DLT tracing integration tests +//! +//! This crate provides helper functions and utilities for testing DLT functionality, +//! including DLT daemon connectivity checks and message verification via dlt-receive. +use std::{ + process::{self, Command, Stdio}, + sync::{Arc, Mutex, OnceLock}, + thread, + time::Duration, +}; + +use ::dlt_sys::{DltId, DltLogLevel}; + +mod dlt_sys; +mod dlt_tracing_appender; + +static DLT_DAEMON: OnceLock>>> = OnceLock::new(); + +pub(crate) fn change_dlt_log_level( + level: DltLogLevel, + app_id: Option<&DltId>, + ctx_id: Option<&DltId>, +) { + let level_num: i32 = level.into(); + + let mut cmd = Command::new("dlt-control"); + cmd.args(["-l", &level_num.to_string()]); + + if let Some(app_id) = app_id { + cmd.args(["-a", app_id.as_str().expect("Invalid application ID")]); + } + if let Some(ctx_id) = ctx_id { + cmd.args(["-c", ctx_id.as_str().expect("Invalid context ID")]); + } + + cmd.args(["127.0.0.1"]); + + let output = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .expect("Failed to execute dlt-control"); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("dlt-control failed: {stderr}"); + } + + // Give time for the log level change to take effect + thread::sleep(Duration::from_millis(100)); +} + +/// Check if the DLT daemon is running by attempting to connect to it +/// +/// Returns true if a connection can be established, false otherwise. +#[must_use] +pub fn is_dlt_daemon_running() -> bool { + // Try to connect using dlt-receive with a timeout and check for successful connection message + let output = Command::new("timeout") + .args(["1", "dlt-receive", "-a", "127.0.0.1"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output(); + + if let Ok(output) = output { + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.contains("New client connection") + } else { + false + } +} + +/// Start the DLT daemon if it's not already running +/// +/// This function ensures the DLT daemon is started only once for all tests. +/// It will check if a daemon is already running, and if not, start a new one. +/// +/// # Panics +/// Panics if the daemon cannot be started +pub fn ensure_dlt_daemon_running() { + let daemon_holder = DLT_DAEMON.get_or_init(|| Arc::new(Mutex::new(None))); + let mut daemon_guard = daemon_holder.lock().expect("Daemon lock failed"); + + // Check if daemon is already running externally + if is_dlt_daemon_running() { + return; + } + + // If we have a daemon process, but it's not responding, clean it up + if let Some(ref mut daemon) = *daemon_guard + && daemon.try_wait().ok().flatten().is_some() + { + *daemon_guard = None; + } + + // Start daemon if we don't have one + if daemon_guard.is_none() { + println!("Starting DLT daemon..."); + let daemon = Command::new("dlt-daemon") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start dlt-daemon. Make sure it's installed."); + + *daemon_guard = Some(daemon); + + // Give daemon time to start up + thread::sleep(Duration::from_millis(250)); + + // Verify it started successfully + assert!( + is_dlt_daemon_running(), + "DLT daemon started but is not responding" + ); + } +} +/// Helper for capturing and verifying DLT messages via dlt-receive +pub struct DltReceiver { + process: process::Child, +} + +impl DltReceiver { + /// Start dlt-receive in background to capture DLT messages + /// + /// # Panics + /// Panics if dlt-receive cannot be started + #[must_use] + pub fn start() -> Self { + // stdbuf is used to disable output buffering for real-time capture + let process = Command::new("stdbuf") + .args(["-o0", "-e0", "dlt-receive", "-a", "127.0.0.1"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to start dlt-receive with stdbuf"); + + // Give dlt-receive time to start and connect + thread::sleep(Duration::from_millis(250)); + DltReceiver { process } + } + /// Stop dlt-receive and get captured output + /// + /// # Panics + /// Panics if output cannot be retrieved + #[must_use] + pub fn stop_and_get_output(mut self) -> String { + // Give time for messages to be processed + thread::sleep(Duration::from_millis(250)); + // Stop dlt-receive + assert!(self.process.kill().is_ok()); + let output = self + .process + .wait_with_output() + .expect("Failed to get output"); + String::from_utf8_lossy(&output.stdout).to_string() + } +} + +/// Assert that output contains expected text +/// +/// # Panics +/// Panics if the expected text is not found in the output +pub fn assert_contains(output: &str, expected: &str) { + assert!( + output.contains(expected), + "Expected text '{expected}' not found in output: '{output}'", + ); +} + +/// Assert that output contains all expected texts +/// +/// # Panics +/// Panics if any of the expected texts is not found in the output +pub fn assert_contains_all(output: &str, expected: &[&str]) { + for text in expected { + assert_contains(output, text); + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..998910d --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,24 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +# SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# SPDX-License-Identifier: Apache-2.0 + +max_width = 100 + +# in case rustfmt stopps working again, enable these options +# to get more information about the error +# requires to run rustfmt with nightly toolchain +# for example: `rustfmt +nightly src/diagtype/mod.rs --edition 2024 --verbose` +# error_on_unformatted = true +# error_on_line_overflow = true +#format_strings = true From aaf88a5eaeb396b97d70b7192945f272f841be4a Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 24 Nov 2025 13:46:05 +0100 Subject: [PATCH 2/9] split and rename crates Signed-off-by: Alexander Mohr dlt_rs}/mod.rs | 2 +- integration-tests/tests/lib.rs | 6 +- .../mod.rs | 33 +- .../Cargo.toml | 7 +- .../src/lib.rs | 15 +- 13 files changed, 1061 insertions(+), 1020 deletions(-) create mode 100644 dlt-rs/Cargo.toml create mode 100644 dlt-rs/src/lib.rs rename integration-tests/tests/{dlt_sys => dlt_rs}/mod.rs (99%) rename integration-tests/tests/{dlt_tracing_appender => tracing_dlt}/mod.rs (88%) rename {dlt-tracing-appender => tracing-dlt}/Cargo.toml (84%) rename {dlt-tracing-appender => tracing-dlt}/src/lib.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 9773902..8207c4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,37 +74,20 @@ dependencies = [ ] [[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "dlt-sys" +name = "dlt-rs" version = "0.1.0" dependencies = [ - "bindgen", - "cc", + "dlt-sys", "thiserror", "tokio", - "tracing", - "tracing-core", ] [[package]] -name = "dlt-tracing-appender" +name = "dlt-sys" version = "0.1.0" dependencies = [ - "crc32fast", - "dlt-sys", - "indexmap", - "tokio", - "tracing", - "tracing-core", - "tracing-subscriber", + "bindgen", + "cc", ] [[package]] @@ -228,11 +211,11 @@ dependencies = [ name = "integration-tests" version = "0.1.0" dependencies = [ - "dlt-sys", - "dlt-tracing-appender", + "dlt-rs", "serial_test", "tokio", "tracing", + "tracing-dlt", "tracing-subscriber", ] @@ -583,6 +566,18 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-dlt" +version = "0.1.0" +dependencies = [ + "dlt-rs", + "indexmap", + "tokio", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" diff --git a/Cargo.toml b/Cargo.toml index 2c76514..954226d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,12 @@ homepage = "https://github.com/eclipse-opensovd/classic-diagnostic-adapter" [workspace] resolver = "3" -members = ["dlt-sys", "dlt-tracing-appender", "integration-tests"] +members = ["dlt-sys", "dlt-rs", "tracing-dlt", "integration-tests"] [workspace.dependencies] -dlt-tracing-appender = { path = "dlt-tracing-appender" } +tracing-dlt = { path = "tracing-dlt" } dlt-sys = { path = "dlt-sys" } +dlt-rs = { path = "dlt-rs" } integration-tests = { path = "integration-tests" } # ---- common crates ---- @@ -37,11 +38,8 @@ serial_test = "3.2.0" # ---- tracing & logging crates---- tracing = "0.1.41" tracing-core = "0.1.34" -crc32fast = "1.5.0" -tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.20", default-features = false } - [workspace.lints.clippy] # enable pedantic pedantic = { level = "warn", priority = -1 } diff --git a/README.md b/README.md index edc1720..7a042d1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ Get it by running: This workspace contains three crates: - **`dlt-sys`** - Low-level Rust wrapper around the C libdlt library -- **`dlt-tracing-appender`** - Tracing subscriber/layer that integrates with the tracing framework +- **`dlt-rs`** - Safe Rust API for DLT logging. This crate depends on `dlt-sys`. +- **`tracing-appender`** - Tracing subscriber/layer that integrates with the tracing framework. This crate depends on `dlt-rs`. - **`integration-tests`** - Common test utilities for integration testing with DLT daemon ## Features diff --git a/dlt-rs/Cargo.toml b/dlt-rs/Cargo.toml new file mode 100644 index 0000000..01d0567 --- /dev/null +++ b/dlt-rs/Cargo.toml @@ -0,0 +1,45 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "dlt-rs" +version = "0.1.0" +edition.workspace = true +publish = false +description = "Safe and idiomatic Rust wrapper for the C library libdlt to provide DLT logging capabilities for Rust applications" +homepage.workspace = true +license.workspace = true + +[lints] +workspace = true + +[lib] +path = "src/lib.rs" + +[dependencies] +dlt-sys = { workspace = true } + +# external +tokio = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = [ + "test-util", + "macros", + "rt-multi-thread", +] } + +[features] +default = [] +trace_load_ctrl = [] + +[build-dependencies] diff --git a/dlt-rs/src/lib.rs b/dlt-rs/src/lib.rs new file mode 100644 index 0000000..511cba6 --- /dev/null +++ b/dlt-rs/src/lib.rs @@ -0,0 +1,958 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Low-level Rust bindings for COVESA DLT (Diagnostic Log and Trace) +//! +//! Safe Rust API for the DLT C library with RAII semantics, enabling applications to send +//! diagnostic logs and traces to the DLT daemon for centralized logging and analysis. +//! +//! # Quick Start +//! +//! ```no_run +//! use dlt_rs::{DltApplication, DltId, DltLogLevel}; +//! +//! # fn main() -> Result<(), Box> { +//! // Register application (one per process) +//! let app = DltApplication::register(&DltId::new(b"MBTI")?, "Measurement & Bus Trace Interface")?; +//! let ctx = app.create_context(&DltId::new(b"MEAS")?, "Measurement Context")?; +//! +//! // Simple logging +//! ctx.log(DltLogLevel::Info, "Hello DLT!")?; +//! +//! // Structured logging with typed fields +//! let mut writer = ctx.log_write_start(DltLogLevel::Info)?; +//! writer.write_string("Temperature:")? +//! .write_float32(87.5)? +//! .write_string("°C")?; +//! writer.finish()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Core Types +//! +//! - [`DltApplication`] - Application registration +//! - [`DltContextHandle`] - Context for logging with specific ID +//! - [`DltLogWriter`] - Builder for structured multi-field messages +//! - [`DltLogLevel`] - Log severity (Fatal, Error, Warn, Info, Debug, Verbose) +//! - [`DltId`] - Type-safe 1-4 byte ASCII identifiers +//! +//! # Features +//! +//! - **RAII cleanup** - Automatic resource management +//! - **Structured logging** - Structured logging messages via [`DltLogWriter`] +//! - **Dynamic control** - Runtime log level changes via +//! [`DltContextHandle::register_log_level_changed_listener()`] +//! - **Thread-safe** - All types are `Send + Sync` +//! +//! # Log Level Control +//! +//! DLT log levels can be changed at runtime by the DLT daemon or other tools. +//! Applications can listen for log level changes. +//! See [`DltLogLevel`] for all available levels and [`LogLevelChangedEvent`] to listen for changes. +//! +//! # See Also +//! +//! - [COVESA DLT](https://github.com/COVESA/dlt-daemon) +use std::{ + collections::HashMap, + ffi::CString, + ptr, + sync::{Arc, OnceLock, RwLock, atomic::AtomicBool}, +}; + +use thiserror::Error; +use tokio::sync::broadcast; + +#[rustfmt::skip] +#[allow(clippy::all, + dead_code, + warnings, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, +)] +pub use dlt_sys::{DLT_ID_SIZE, DltContext, DltContextData}; + +/// DLT log level +/// +/// Severity level of a log message, ordered from most severe ([`DltLogLevel::Fatal`]) +/// to least severe ([`DltLogLevel::Verbose`]). +/// +/// Use with [`DltContextHandle::log()`] or [`DltContextHandle::log_write_start()`]. +/// The DLT daemon filters messages based on the configured threshold +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum DltLogLevel { + /// Default log level (determined by DLT daemon configuration) + Default, + /// Logging is disabled + Off, + /// Fatal system error - system is unusable + Fatal, + /// Error conditions - operation failed + Error, + /// Warning conditions - something unexpected but recoverable + Warn, + /// Informational messages - normal operation (default level) + Info, + /// Debug-level messages - detailed diagnostic information + Debug, + /// Verbose/trace-level messages - very detailed execution traces + Verbose, +} + +impl From for DltLogLevel { + fn from(value: i32) -> Self { + match value { + dlt_sys::DltLogLevelType_DLT_LOG_OFF => DltLogLevel::Off, + dlt_sys::DltLogLevelType_DLT_LOG_FATAL => DltLogLevel::Fatal, + dlt_sys::DltLogLevelType_DLT_LOG_ERROR => DltLogLevel::Error, + dlt_sys::DltLogLevelType_DLT_LOG_WARN => DltLogLevel::Warn, + dlt_sys::DltLogLevelType_DLT_LOG_INFO => DltLogLevel::Info, + dlt_sys::DltLogLevelType_DLT_LOG_DEBUG => DltLogLevel::Debug, + dlt_sys::DltLogLevelType_DLT_LOG_VERBOSE => DltLogLevel::Verbose, + _ => DltLogLevel::Default, + } + } +} + +impl From for i32 { + fn from(value: DltLogLevel) -> Self { + match value { + DltLogLevel::Default => dlt_sys::DltLogLevelType_DLT_LOG_DEFAULT, + DltLogLevel::Off => dlt_sys::DltLogLevelType_DLT_LOG_OFF, + DltLogLevel::Fatal => dlt_sys::DltLogLevelType_DLT_LOG_FATAL, + DltLogLevel::Error => dlt_sys::DltLogLevelType_DLT_LOG_ERROR, + DltLogLevel::Warn => dlt_sys::DltLogLevelType_DLT_LOG_WARN, + DltLogLevel::Info => dlt_sys::DltLogLevelType_DLT_LOG_INFO, + DltLogLevel::Debug => dlt_sys::DltLogLevelType_DLT_LOG_DEBUG, + DltLogLevel::Verbose => dlt_sys::DltLogLevelType_DLT_LOG_VERBOSE, + } + } +} + +/// Internal error types for Rust-side operations (not from libdlt) +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum DltError { + #[error("Data cannot be converted to a DLT compatible string: {0}")] + InvalidString(String), + #[error("Failed to register DLT context")] + ContextRegistrationFailed, + #[error("Failed to register DLT application")] + ApplicationRegistrationFailed(String), + #[error("A pointer or memory is invalid")] + InvalidMemory, + #[error("Failed to acquire a lock")] + BadLock, + #[error("Input value is invalid")] + InvalidInput, +} + +/// DLT return value error types (from libdlt C library) +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] +pub enum DltSysError { + #[cfg(feature = "trace_load_ctrl")] + #[error("DLT load exceeded")] + /// Only available with the `trace_load_ctrl` feature enabled. + LoadExceeded, + + #[error("DLT file size error")] + FileSizeError, + + #[error("DLT logging disabled")] + LoggingDisabled, + + #[error("DLT user buffer full")] + UserBufferFull, + + #[error("DLT wrong parameter")] + WrongParameter, + + #[error("DLT buffer full")] + BufferFull, + + #[error("DLT pipe full")] + PipeFull, + + #[error("DLT pipe error")] + PipeError, + + #[error("DLT general error")] + Error, + + #[error("DLT unknown error")] + Unknown, +} + +impl DltSysError { + fn from_return_code(code: i32) -> Result<(), Self> { + #[allow(unreachable_patterns)] + match code { + dlt_sys::DltReturnValue_DLT_RETURN_TRUE + | dlt_sys::DltReturnValue_DLT_RETURN_OK => Ok(()), + dlt_sys::DltReturnValue_DLT_RETURN_ERROR => Err(DltSysError::Error), + dlt_sys::DltReturnValue_DLT_RETURN_PIPE_ERROR => Err(DltSysError::PipeError), + dlt_sys::DltReturnValue_DLT_RETURN_PIPE_FULL => Err(DltSysError::PipeFull), + dlt_sys::DltReturnValue_DLT_RETURN_BUFFER_FULL => Err(DltSysError::BufferFull), + dlt_sys::DltReturnValue_DLT_RETURN_WRONG_PARAMETER => { + Err(DltSysError::WrongParameter) + } + dlt_sys::DltReturnValue_DLT_RETURN_USER_BUFFER_FULL => { + Err(DltSysError::UserBufferFull) + } + dlt_sys::DltReturnValue_DLT_RETURN_LOGGING_DISABLED => { + Err(DltSysError::LoggingDisabled) + } + dlt_sys::DltReturnValue_DLT_RETURN_FILESZERR => Err(DltSysError::FileSizeError), + #[cfg(feature = "trace_load_ctrl")] + dlt_sys::DltReturnValue_DLT_RETURN_LOAD_EXCEEDED => Err(DltSysError::LoadExceeded), + _ => Err(DltSysError::Unknown), + } + } +} + +/// Size of DLT ID fields (Application ID, Context ID) - re-exported from bindings as usize +pub const DLT_ID_SIZE_USIZE: usize = DLT_ID_SIZE as usize; + +/// A DLT identifier (Application ID or Context ID) +/// +/// DLT IDs are 1-4 ASCII bytes. Create with `DltId::new(b"APP")?`. +/// Shorter IDs are internally padded with nulls +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DltId { + bytes: [u8; DLT_ID_SIZE_USIZE], + len: usize, +} + +impl DltId { + /// Create a new DLT ID from a byte slice of 1 to 4 bytes + /// + /// The ID will be validated as ASCII. + /// IDs shorter than 4 bytes are right-padded with null bytes internally. + /// + /// # Errors + /// Returns [`DltError::InvalidInput`] if the byte slice is empty, longer than 4 bytes, + /// or contains non-ASCII characters. + /// + /// # Examples + /// ```no_run + /// # use dlt_rs::{DltId, DltError}; + /// # fn main() -> Result<(), DltError> { + /// let id = DltId::new(b"APP")?; + /// assert_eq!(id.as_str()?, "APP"); + /// + /// // Too long + /// assert!(DltId::new(b"TOOLONG").is_err()); + /// + /// // Empty + /// assert!(DltId::new(b"").is_err()); + /// # Ok(()) + /// # } + /// ``` + pub fn new(bytes: &[u8; N]) -> Result { + // Validate that N is between 1 and 4 + if N == 0 || N > DLT_ID_SIZE_USIZE { + return Err(DltError::InvalidInput); + } + + // Validate ASCII + if !bytes.is_ascii() { + return Err(DltError::InvalidInput); + } + + let mut padded = [0u8; DLT_ID_SIZE_USIZE]; + // Indexing is safe here: loop condition ensures i < N, and N <= DLT_ID_SIZE by validation + #[allow(clippy::indexing_slicing)] + padded[..N].copy_from_slice(&bytes[..N]); + + Ok(Self { + bytes: padded, + len: N, + }) + } + + /// Get the ID as a string slice + /// + /// # Errors + /// Returns an error if the bytes are not valid UFT-8. + /// This should never happen due to construction constraints. + pub fn as_str(&self) -> Result<&str, DltError> { + let slice = self + .bytes + .get(..self.len) + .ok_or_else(|| DltError::InvalidString("Invalid length".to_string()))?; + let s = std::str::from_utf8(slice).map_err(|e| DltError::InvalidString(e.to_string()))?; + Ok(s) + } +} + +/// DLT trace status +/// +/// Controls whether network trace messages (like packet captures) are enabled. +/// This is separate from log levels. Most applications only use log levels and can +/// ignore trace status. +/// +/// Trace status is included in [`LogLevelChangedEvent`] notifications +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DltTraceStatus { + /// Use default trace status from DLT daemon configuration + Default, + /// Trace messages are disabled + Off, + /// Trace messages are enabled + On, +} + +impl From for DltTraceStatus { + fn from(value: i32) -> Self { + match value { + dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_OFF => DltTraceStatus::Off, + dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_ON => DltTraceStatus::On, + _ => DltTraceStatus::Default, + } + } +} + +impl From for i32 { + fn from(value: DltTraceStatus) -> Self { + match value { + DltTraceStatus::Default => dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_DEFAULT, + DltTraceStatus::Off => dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_OFF, + DltTraceStatus::On => dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_ON, + } + } +} + +/// Event sent when DLT log level or trace status changes +/// +/// Emitted when the DLT daemon changes the log level or trace status for a context. +/// +/// Register a listener with [`DltContextHandle::register_log_level_changed_listener()`] +/// to receive these events +#[derive(Debug, Clone, Copy)] +pub struct LogLevelChangedEvent { + /// The DLT context ID that this change applies to + pub context_id: DltId, + /// The new log level for the context + pub log_level: DltLogLevel, + /// The new trace status for the context + pub trace_status: DltTraceStatus, +} + +struct LogLevelChangedBroadcaster { + sender: broadcast::Sender, + receiver: broadcast::Receiver, +} + +// Global registry for log level change callbacks +static CALLBACK_REGISTRY: OnceLock>> = + OnceLock::new(); + +static APP_REGISTERED: AtomicBool = AtomicBool::new(false); + +/// Internal C callback that forwards to the Rust channel +unsafe extern "C" fn internal_log_level_callback( + context_id: *mut std::os::raw::c_char, + log_level: u8, + trace_status: u8, +) { + if context_id.is_null() { + return; + } + + let Some(registry) = CALLBACK_REGISTRY.get() else { + return; + }; + + let id = unsafe { + let mut ctx_id = [0u8; DLT_ID_SIZE_USIZE]; + ptr::copy( + context_id.cast::(), + ctx_id.as_mut_ptr(), + DLT_ID_SIZE_USIZE, + ); + match DltId::new(&ctx_id) { + Ok(id) => id, + Err(_) => return, // Invalid context ID from DLT daemon + } + }; + + let Ok(lock) = registry.read() else { + return; + }; + + let Some(broadcaster) = lock.get(&id) else { + return; + }; + + let event = LogLevelChangedEvent { + context_id: id, + log_level: DltLogLevel::from(i32::from(log_level)), + trace_status: DltTraceStatus::from(i32::from(trace_status)), + }; + + let _ = broadcaster.sender.send(event); +} + +/// Internal shared state for the DLT application +/// +/// This ensures contexts can keep the application alive through reference counting. +/// When the last reference (either from `DltApplication` or `DltContextHandle`) is +/// dropped, the application is automatically unregistered from DLT. +struct DltApplicationHandle { + _private: (), +} + +impl Drop for DltApplicationHandle { + fn drop(&mut self) { + unsafe { + // unregister from dlt, but ignore errors + dlt_sys::unregisterApplicationFlushBufferedLogs(); + dlt_sys::dltFree(); + APP_REGISTERED.store(false, std::sync::atomic::Ordering::SeqCst); + } + } +} + +/// Singleton guard for DLT application registration +/// +/// Only one DLT application can be registered per process. Automatically unregistered +/// when dropped and a new application can be registered. +/// +/// **Lifetime Guarantee**: Contexts maintain an internal reference, keeping the application +/// registered. Safe to drop the application handle before contexts. +/// +/// Cheaply cloneable for sharing across threads +pub struct DltApplication { + inner: Arc, +} + +impl DltApplication { + /// Register a DLT application + /// + /// Only one application can be registered per process. If you need to register + /// a different application, drop this instance first. + /// + /// The returned handle can be cloned to share the application across threads. + /// + /// # Errors + /// Returns `DltError` if the registration fails + pub fn register(app_id: &DltId, app_description: &str) -> Result { + if APP_REGISTERED + .compare_exchange( + false, + true, + std::sync::atomic::Ordering::SeqCst, + std::sync::atomic::Ordering::SeqCst, + ) + .is_err() + { + return Err(DltError::ApplicationRegistrationFailed( + "An application is already registered in this process".to_string(), + )); + } + + let app_id_str = app_id.as_str()?; + let app_id_c = CString::new(app_id_str).map_err(|_| { + DltError::InvalidString("App id could not be converted to string".to_owned()) + })?; + let app_desc_c = CString::new(app_description).map_err(|_| { + DltError::InvalidString("Context id could not be converted to string".to_owned()) + })?; + + unsafe { + let ret = dlt_sys::registerApplication(app_id_c.as_ptr(), app_desc_c.as_ptr()); + DltSysError::from_return_code(ret).map_err(|_| { + DltError::ApplicationRegistrationFailed(format!( + "Failed to register application: {ret}" + )) + })?; + } + Ok(DltApplication { + inner: Arc::new(DltApplicationHandle { _private: () }), + }) + } + + /// Create a new DLT context within this application + /// + /// The created context maintains an internal reference to the application, + /// ensuring the application remains registered as long as the context exists. + /// + /// # Errors + /// Returns `DltError` if registration fails + pub fn create_context( + &self, + context_id: &DltId, + context_description: &str, + ) -> Result { + DltContextHandle::new(context_id, context_description, Arc::clone(&self.inner)) + } +} + +impl Clone for DltApplication { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +// Safe to send between threads (application registration is process-wide) +unsafe impl Send for DltApplication {} +unsafe impl Sync for DltApplication {} + +/// Safe wrapper around C DLT context with RAII semantics +/// +/// The context holds an internal reference to the application, ensuring the +/// application remains registered as long as any context exists. +pub struct DltContextHandle { + context: *mut DltContext, + _app: Arc, +} + +impl DltContextHandle { + /// Register a new DLT context + /// + /// # Errors + /// Returns `DltError` if registration fails + fn new( + context_id: &DltId, + context_description: &str, + app: Arc, + ) -> Result { + let context_id_str = context_id.as_str()?; + let ctx_id_c = CString::new(context_id_str) + .map_err(|_| DltError::InvalidString("Context ID is not a valid string".to_owned()))?; + let ctx_desc_c = CString::new(context_description).map_err(|_| { + DltError::InvalidString("Context description is not a valid string".to_owned()) + })?; + + unsafe { + let context = dlt_sys::registerContext(ctx_id_c.as_ptr(), ctx_desc_c.as_ptr()); + if context.is_null() { + Err(DltError::ContextRegistrationFailed) + } else { + Ok(DltContextHandle { context, _app: app }) + } + } + } + + fn raw_context(&self) -> Result { + let context = unsafe { + if self.context.is_null() { + return Err(DltError::ContextRegistrationFailed); + } + *self.context + }; + Ok(context) + } + + /// Get the context ID + /// # Errors + /// Returns `DltError` if the context is invalid or the context is null + pub fn context_id(&self) -> Result { + let ctx_id = unsafe { + // this is a false positive of clippy. + // raw_context.contextID is of type [::std::os::raw::c_char; 4usize], which + // cannot be directly used as &[u8; 4]. + #[allow(clippy::useless_transmute)] + std::mem::transmute::<[std::os::raw::c_char; 4], [u8; 4]>(self.raw_context()?.contextID) + }; + DltId::new(&ctx_id) + } + + /// Get the current trace status of the context + /// # Errors + /// Returns `DltError` if the context is invalid or the context is null + #[must_use] + pub fn trace_status(&self) -> DltTraceStatus { + self.raw_context() + .ok() + .and_then(|rc| { + if rc.log_level_ptr.is_null() { + None + } else { + Some(DltTraceStatus::from(i32::from(unsafe { + *rc.trace_status_ptr + }))) + } + }) + .unwrap_or(DltTraceStatus::Default) + } + + /// Get the current log level of the context + #[must_use] + pub fn log_level(&self) -> DltLogLevel { + self.raw_context() + .ok() + .and_then(|rc| { + if rc.log_level_ptr.is_null() { + None + } else { + Some(DltLogLevel::from(i32::from(unsafe { *rc.log_level_ptr }))) + } + }) + .unwrap_or(DltLogLevel::Default) + } + + /// Log a simple string message + /// + /// # Errors + /// Returns `DltError` if logging fails + pub fn log(&self, log_level: DltLogLevel, message: &str) -> Result<(), DltSysError> { + let msg_c = CString::new(message).map_err(|_| DltSysError::WrongParameter)?; + + unsafe { + let ret = dlt_sys::logDlt(self.context, log_level.into(), msg_c.as_ptr()); + DltSysError::from_return_code(ret) + } + } + + /// Start a complex log message with a custom timestamp. + /// Can be used to hide original timestamps or to log event recorded earlier. + /// The timestamp is a steady clock, starting from an arbitrary point in time, + /// usually system start. + /// + /// # Errors + /// Returns `DltError` if starting the log message fails + pub fn log_write_start_custom_timestamp( + &self, + log_level: DltLogLevel, + timestamp_microseconds: u64, + ) -> Result { + let mut log_writer = self.log_write_start(log_level)?; + // timestamp resolution in dlt is .1 milliseconds. + let timestamp = + u32::try_from(timestamp_microseconds / 100).map_err(|_| DltSysError::WrongParameter)?; + log_writer.log_data.use_timestamp = dlt_sys::DltTimestampType_DLT_USER_TIMESTAMP; + log_writer.log_data.user_timestamp = timestamp; + Ok(log_writer) + } + + /// Start a complex log message + /// + /// # Errors + /// Returns `DltError` if starting the log message fails + pub fn log_write_start(&self, log_level: DltLogLevel) -> Result { + let mut log_data = DltContextData::default(); + + unsafe { + let ret = dlt_sys::dltUserLogWriteStart( + self.context, + &raw mut log_data, + log_level.into(), + ); + + DltSysError::from_return_code(ret)?; + Ok(DltLogWriter { log_data }) + } + } + + /// Register a channel to receive log level change notifications + /// + /// Returns a receiver that will get `LogLevelChangeEvent` + /// when the DLT daemon changes log levels + /// + /// # Errors + /// Returns `InternalError` if callback registration with DLT fails + pub fn register_log_level_changed_listener( + &self, + ) -> Result, DltError> { + let rwlock = CALLBACK_REGISTRY.get_or_init(|| RwLock::new(HashMap::new())); + let mut guard = rwlock.write().map_err(|_| DltError::BadLock)?; + let ctx_id = self.context_id()?; + + if let Some(broadcaster) = guard.get_mut(&ctx_id) { + Ok(broadcaster.receiver.resubscribe()) + } else { + unsafe { + let ret = dlt_sys::registerLogLevelChangedCallback( + self.context, + Some(internal_log_level_callback), + ); + DltSysError::from_return_code(ret) + .map_err(|_| DltError::ContextRegistrationFailed)?; + } + let (tx, rx) = broadcast::channel(5); + let rx_clone = rx.resubscribe(); + guard.insert( + ctx_id, + LogLevelChangedBroadcaster { + sender: tx, + receiver: rx, + }, + ); + Ok(rx_clone) + } + } +} + +impl Drop for DltContextHandle { + fn drop(&mut self) { + let context_id = self.context_id(); + if let Some(lock) = CALLBACK_REGISTRY.get() + && let Ok(mut guard) = lock.write() + && let Ok(ctx_id) = context_id + { + guard.remove(&ctx_id); + } + + unsafe { + dlt_sys::unregisterContext(self.context); + } + } +} + +// Safe to send between threads, per DLT documentation +unsafe impl Send for DltContextHandle {} +unsafe impl Sync for DltContextHandle {} + +/// Builder for structured log messages with multiple typed fields +/// +/// Construct log messages with typed data fields sent in binary format for efficiency. +/// Each field retains type information for proper display in DLT viewers. +/// +/// # Usage +/// +/// 1. Start with [`DltContextHandle::log_write_start()`] +/// 2. Chain `write_*` methods to add fields +/// 3. Call [`finish()`](DltLogWriter::finish()) to send +/// +/// Auto-finishes on drop if [`finish()`](DltLogWriter::finish()) not called (errors ignored). +/// +/// # Example +/// +/// ```no_run +/// # use dlt_rs::{DltApplication, DltId, DltLogLevel}; +/// # fn main() -> Result<(), Box> { +/// # let app = DltApplication::register(&DltId::new(b"MBTI")?, +/// "Measurement and Bus Trace Interface")?; +/// # let ctx = app.create_context(&DltId::new(b"MEAS")?, "Context")?; +/// let mut writer = ctx.log_write_start(DltLogLevel::Info)?; +/// writer.write_string("Temperature:")? +/// .write_float32(87.5)? +/// .write_string("°C")?; +/// writer.finish()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// # Available Methods +/// +/// [`write_string()`](DltLogWriter::write_string()) | +/// [`write_i32()`](DltLogWriter::write_i32()) | +/// [`write_u32()`](DltLogWriter::write_u32()) | +/// [`write_int64()`](DltLogWriter::write_int64()) | +/// [`write_uint64()`](DltLogWriter::write_uint64()) | +/// [`write_float32()`](DltLogWriter::write_float32()) | +/// [`write_float64()`](DltLogWriter::write_float64()) | +/// [`write_bool()`](DltLogWriter::write_bool()) +pub struct DltLogWriter { + log_data: DltContextData, +} + +impl DltLogWriter { + /// Write a string to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_string(&mut self, text: &str) -> Result<&mut Self, DltSysError> { + let text_c = CString::new(text).map_err(|_| DltSysError::WrongParameter)?; + + unsafe { + let ret = dlt_sys::dltUserLogWriteString(&raw mut self.log_data, text_c.as_ptr()); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write an unsigned integer to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_u32(&mut self, value: u32) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_sys::dltUserLogWriteUint(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a signed integer to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_i32(&mut self, value: i32) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_sys::dltUserLogWriteInt(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write an unsigned 64-bit integer to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_uint64(&mut self, value: u64) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_sys::dltUserLogWriteUint64(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a signed 64-bit integer to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_int64(&mut self, value: i64) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_sys::dltUserLogWriteInt64(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a 32-bit float to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_float32(&mut self, value: f32) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_sys::dltUserLogWriteFloat32(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a 64-bit float to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_float64(&mut self, value: f64) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_sys::dltUserLogWriteFloat64(&raw mut self.log_data, value); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Write a boolean to the log message + /// + /// # Errors + /// Returns `DltError` if writing fails + pub fn write_bool(&mut self, value: bool) -> Result<&mut Self, DltSysError> { + unsafe { + let ret = dlt_sys::dltUserLogWriteBool(&raw mut self.log_data, u8::from(value)); + DltSysError::from_return_code(ret)?; + } + + Ok(self) + } + + /// Finish and send the log message + /// + /// Explicitly finishes the log message. If not called, the message will be + /// automatically finished when the `DltLogWriter` is dropped, but errors will be ignored. + /// + /// # Errors + /// Returns `DltError` if finishing fails + pub fn finish(mut self) -> Result<(), DltSysError> { + let ret = unsafe { dlt_sys::dltUserLogWriteFinish(&raw mut self.log_data) }; + // Prevent Drop from running since we've already finished + std::mem::forget(self); + DltSysError::from_return_code(ret) + } +} + +impl Drop for DltLogWriter { + fn drop(&mut self) { + // Auto-finish the log message if finish() wasn't called explicitly + unsafe { + let _ = dlt_sys::dltUserLogWriteFinish(&raw mut self.log_data); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dlt_error_from_return_code() { + assert!(DltSysError::from_return_code(0).is_ok()); + assert!(DltSysError::from_return_code(1).is_ok()); + assert_eq!(DltSysError::from_return_code(-1), Err(DltSysError::Error)); + assert_eq!( + DltSysError::from_return_code(-5), + Err(DltSysError::WrongParameter) + ); + } + + #[test] + fn test_dlt_id_creation() { + // 1 byte ID + let short_id = DltId::new(b"A").unwrap(); + assert_eq!(short_id.as_str().unwrap(), "A"); + + // 3 byte IDs + let app_id = DltId::new(b"APP").unwrap(); + assert_eq!(app_id.as_str().unwrap(), "APP"); + + let ctx_id = DltId::new(b"CTX").unwrap(); + assert_eq!(ctx_id.as_str().unwrap(), "CTX"); + + // 4 byte ID (maximum) + let full_id = DltId::new(b"ABCD").unwrap(); + assert_eq!(full_id.as_str().unwrap(), "ABCD"); + } + + #[test] + fn test_dlt_id_too_long() { + let result = DltId::new(b"TOOLONG"); + assert_eq!(result.unwrap_err(), DltError::InvalidInput); + } + + #[test] + fn test_dlt_id_empty() { + let result = DltId::new(b""); + assert_eq!(result.unwrap_err(), DltError::InvalidInput); + } + + #[test] + fn test_dlt_id_non_ascii() { + let result = DltId::new(b"\xFF\xFE"); + assert_eq!(result.unwrap_err(), DltError::InvalidInput); + } + + #[test] + fn test_dlt_id_equality() { + let id1 = DltId::new(b"APP").unwrap(); + let id2 = DltId::new(b"APP").unwrap(); + let id3 = DltId::new(b"CTX").unwrap(); + + assert_eq!(id1, id2); + assert_ne!(id1, id3); + + // Different lengths are not equal + let id4 = DltId::new(b"A").unwrap(); + assert_ne!(id1, id4); + } +} diff --git a/dlt-sys/Cargo.toml b/dlt-sys/Cargo.toml index b796dc6..7988925 100644 --- a/dlt-sys/Cargo.toml +++ b/dlt-sys/Cargo.toml @@ -24,23 +24,6 @@ workspace = true [lib] path = "src/lib.rs" -[dependencies] - -# external -tokio = { workspace = true } -thiserror = { workspace = true } - -# logging & tracing -tracing = { workspace = true } -tracing-core = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = [ - "test-util", - "macros", - "rt-multi-thread", -] } - [features] default = [] generate-bindings = ["bindgen", "trace_load_ctrl"] diff --git a/dlt-sys/src/lib.rs b/dlt-sys/src/lib.rs index 66b5e29..c621e89 100644 --- a/dlt-sys/src/lib.rs +++ b/dlt-sys/src/lib.rs @@ -11,69 +11,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! Low-level Rust bindings for COVESA DLT (Diagnostic Log and Trace) -//! -//! Safe Rust API for the DLT C library with RAII semantics, enabling applications to send -//! diagnostic logs and traces to the DLT daemon for centralized logging and analysis. -//! -//! # Quick Start -//! -//! ```no_run -//! use dlt_sys::{DltApplication, DltId, DltLogLevel}; -//! -//! # fn main() -> Result<(), Box> { -//! // Register application (one per process) -//! let app = DltApplication::register(&DltId::new(b"MBTI")?, "Measurement & Bus Trace Interface")?; -//! let ctx = app.create_context(&DltId::new(b"MEAS")?, "Measurement Context")?; -//! -//! // Simple logging -//! ctx.log(DltLogLevel::Info, "Hello DLT!")?; -//! -//! // Structured logging with typed fields -//! let mut writer = ctx.log_write_start(DltLogLevel::Info)?; -//! writer.write_string("Temperature:")? -//! .write_float32(87.5)? -//! .write_string("°C")?; -//! writer.finish()?; -//! # Ok(()) -//! # } -//! ``` -//! -//! # Core Types -//! -//! - [`DltApplication`] - Application registration -//! - [`DltContextHandle`] - Context for logging with specific ID -//! - [`DltLogWriter`] - Builder for structured multi-field messages -//! - [`DltLogLevel`] - Log severity (Fatal, Error, Warn, Info, Debug, Verbose) -//! - [`DltId`] - Type-safe 1-4 byte ASCII identifiers -//! -//! # Features -//! -//! - **RAII cleanup** - Automatic resource management -//! - **Structured logging** - Structured logging messages via [`DltLogWriter`] -//! - **Dynamic control** - Runtime log level changes via -//! [`DltContextHandle::register_log_level_changed_listener()`] -//! - **Thread-safe** - All types are `Send + Sync` -//! -//! # Log Level Control -//! -//! DLT log levels can be changed at runtime by the DLT daemon or other tools. -//! Applications can listen for log level changes. -//! See [`DltLogLevel`] for all available levels and [`LogLevelChangedEvent`] to listen for changes. -//! -//! # See Also -//! -//! - [COVESA DLT](https://github.com/COVESA/dlt-daemon) -use std::{ - collections::HashMap, - ffi::CString, - ptr, - sync::{Arc, OnceLock, RwLock, atomic::AtomicBool}, -}; - -use thiserror::Error; -use tokio::sync::broadcast; - #[rustfmt::skip] #[allow(clippy::all, dead_code, @@ -82,327 +19,10 @@ use tokio::sync::broadcast; clippy::indexing_slicing, )] mod dlt_bindings; -use dlt_bindings::{DLT_ID_SIZE, DltContext, DltContextData}; - -/// DLT log level -/// -/// Severity level of a log message, ordered from most severe ([`DltLogLevel::Fatal`]) -/// to least severe ([`DltLogLevel::Verbose`]). -/// -/// Use with [`DltContextHandle::log()`] or [`DltContextHandle::log_write_start()`]. -/// The DLT daemon filters messages based on the configured threshold -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum DltLogLevel { - /// Default log level (determined by DLT daemon configuration) - Default, - /// Logging is disabled - Off, - /// Fatal system error - system is unusable - Fatal, - /// Error conditions - operation failed - Error, - /// Warning conditions - something unexpected but recoverable - Warn, - /// Informational messages - normal operation (default level) - Info, - /// Debug-level messages - detailed diagnostic information - Debug, - /// Verbose/trace-level messages - very detailed execution traces - Verbose, -} - -impl From for DltLogLevel { - fn from(value: i32) -> Self { - match value { - dlt_bindings::DltLogLevelType_DLT_LOG_OFF => DltLogLevel::Off, - dlt_bindings::DltLogLevelType_DLT_LOG_FATAL => DltLogLevel::Fatal, - dlt_bindings::DltLogLevelType_DLT_LOG_ERROR => DltLogLevel::Error, - dlt_bindings::DltLogLevelType_DLT_LOG_WARN => DltLogLevel::Warn, - dlt_bindings::DltLogLevelType_DLT_LOG_INFO => DltLogLevel::Info, - dlt_bindings::DltLogLevelType_DLT_LOG_DEBUG => DltLogLevel::Debug, - dlt_bindings::DltLogLevelType_DLT_LOG_VERBOSE => DltLogLevel::Verbose, - _ => DltLogLevel::Default, - } - } -} - -impl From for i32 { - fn from(value: DltLogLevel) -> Self { - match value { - DltLogLevel::Default => dlt_bindings::DltLogLevelType_DLT_LOG_DEFAULT, - DltLogLevel::Off => dlt_bindings::DltLogLevelType_DLT_LOG_OFF, - DltLogLevel::Fatal => dlt_bindings::DltLogLevelType_DLT_LOG_FATAL, - DltLogLevel::Error => dlt_bindings::DltLogLevelType_DLT_LOG_ERROR, - DltLogLevel::Warn => dlt_bindings::DltLogLevelType_DLT_LOG_WARN, - DltLogLevel::Info => dlt_bindings::DltLogLevelType_DLT_LOG_INFO, - DltLogLevel::Debug => dlt_bindings::DltLogLevelType_DLT_LOG_DEBUG, - DltLogLevel::Verbose => dlt_bindings::DltLogLevelType_DLT_LOG_VERBOSE, - } - } -} - -/// Internal error types for Rust-side operations (not from libdlt) -#[derive(Error, Debug, Clone, PartialEq, Eq)] -pub enum DltError { - #[error("Data cannot be converted to a DLT compatible string: {0}")] - InvalidString(String), - #[error("Failed to register DLT context")] - ContextRegistrationFailed, - #[error("Failed to register DLT application")] - ApplicationRegistrationFailed(String), - #[error("A pointer or memory is invalid")] - InvalidMemory, - #[error("Failed to acquire a lock")] - BadLock, - #[error("Input value is invalid")] - InvalidInput, -} - -/// DLT return value error types (from libdlt C library) -#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] -pub enum DltSysError { - #[cfg(feature = "trace_load_ctrl")] - #[error("DLT load exceeded")] - /// Only available with the `trace_load_ctrl` feature enabled. - LoadExceeded, - - #[error("DLT file size error")] - FileSizeError, - - #[error("DLT logging disabled")] - LoggingDisabled, - - #[error("DLT user buffer full")] - UserBufferFull, - - #[error("DLT wrong parameter")] - WrongParameter, - - #[error("DLT buffer full")] - BufferFull, - - #[error("DLT pipe full")] - PipeFull, - - #[error("DLT pipe error")] - PipeError, - - #[error("DLT general error")] - Error, - - #[error("DLT unknown error")] - Unknown, -} - -impl DltSysError { - fn from_return_code(code: i32) -> Result<(), Self> { - #[allow(unreachable_patterns)] - match code { - dlt_bindings::DltReturnValue_DLT_RETURN_TRUE - | dlt_bindings::DltReturnValue_DLT_RETURN_OK => Ok(()), - dlt_bindings::DltReturnValue_DLT_RETURN_ERROR => Err(DltSysError::Error), - dlt_bindings::DltReturnValue_DLT_RETURN_PIPE_ERROR => Err(DltSysError::PipeError), - dlt_bindings::DltReturnValue_DLT_RETURN_PIPE_FULL => Err(DltSysError::PipeFull), - dlt_bindings::DltReturnValue_DLT_RETURN_BUFFER_FULL => Err(DltSysError::BufferFull), - dlt_bindings::DltReturnValue_DLT_RETURN_WRONG_PARAMETER => { - Err(DltSysError::WrongParameter) - } - dlt_bindings::DltReturnValue_DLT_RETURN_USER_BUFFER_FULL => { - Err(DltSysError::UserBufferFull) - } - dlt_bindings::DltReturnValue_DLT_RETURN_LOGGING_DISABLED => { - Err(DltSysError::LoggingDisabled) - } - dlt_bindings::DltReturnValue_DLT_RETURN_FILESZERR => Err(DltSysError::FileSizeError), - #[cfg(feature = "trace_load_ctrl")] - dlt_bindings::DltReturnValue_DLT_RETURN_LOAD_EXCEEDED => Err(DltSysError::LoadExceeded), - _ => Err(DltSysError::Unknown), - } - } -} - -/// Size of DLT ID fields (Application ID, Context ID) - re-exported from bindings as usize -pub const DLT_ID_SIZE_USIZE: usize = DLT_ID_SIZE as usize; - -/// A DLT identifier (Application ID or Context ID) -/// -/// DLT IDs are 1-4 ASCII bytes. Create with `DltId::new(b"APP")?`. -/// Shorter IDs are internally padded with nulls -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct DltId { - bytes: [u8; DLT_ID_SIZE_USIZE], - len: usize, -} - -impl DltId { - /// Create a new DLT ID from a byte slice of 1 to 4 bytes - /// - /// The ID will be validated as ASCII. - /// IDs shorter than 4 bytes are right-padded with null bytes internally. - /// - /// # Errors - /// Returns [`DltError::InvalidInput`] if the byte slice is empty, longer than 4 bytes, - /// or contains non-ASCII characters. - /// - /// # Examples - /// ```no_run - /// # use dlt_sys::{DltId, DltError}; - /// # fn main() -> Result<(), DltError> { - /// let id = DltId::new(b"APP")?; - /// assert_eq!(id.as_str()?, "APP"); - /// - /// // Too long - /// assert!(DltId::new(b"TOOLONG").is_err()); - /// - /// // Empty - /// assert!(DltId::new(b"").is_err()); - /// # Ok(()) - /// # } - /// ``` - pub fn new(bytes: &[u8; N]) -> Result { - // Validate that N is between 1 and 4 - if N == 0 || N > DLT_ID_SIZE_USIZE { - return Err(DltError::InvalidInput); - } - - // Validate ASCII - if !bytes.is_ascii() { - return Err(DltError::InvalidInput); - } - - let mut padded = [0u8; DLT_ID_SIZE_USIZE]; - // Indexing is safe here: loop condition ensures i < N, and N <= DLT_ID_SIZE by validation - #[allow(clippy::indexing_slicing)] - padded[..N].copy_from_slice(&bytes[..N]); - - Ok(Self { - bytes: padded, - len: N, - }) - } - - /// Get the ID as a string slice - /// - /// # Errors - /// Returns an error if the bytes are not valid UFT-8. - /// This should never happen due to construction constraints. - pub fn as_str(&self) -> Result<&str, DltError> { - let slice = self - .bytes - .get(..self.len) - .ok_or_else(|| DltError::InvalidString("Invalid length".to_string()))?; - let s = std::str::from_utf8(slice).map_err(|e| DltError::InvalidString(e.to_string()))?; - Ok(s) - } -} - -/// DLT trace status -/// -/// Controls whether network trace messages (like packet captures) are enabled. -/// This is separate from log levels. Most applications only use log levels and can -/// ignore trace status. -/// -/// Trace status is included in [`LogLevelChangedEvent`] notifications -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DltTraceStatus { - /// Use default trace status from DLT daemon configuration - Default, - /// Trace messages are disabled - Off, - /// Trace messages are enabled - On, -} - -impl From for DltTraceStatus { - fn from(value: i32) -> Self { - match value { - dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_OFF => DltTraceStatus::Off, - dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_ON => DltTraceStatus::On, - _ => DltTraceStatus::Default, - } - } -} - -impl From for i32 { - fn from(value: DltTraceStatus) -> Self { - match value { - DltTraceStatus::Default => dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_DEFAULT, - DltTraceStatus::Off => dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_OFF, - DltTraceStatus::On => dlt_bindings::DltTraceStatusType_DLT_TRACE_STATUS_ON, - } - } -} - -/// Event sent when DLT log level or trace status changes -/// -/// Emitted when the DLT daemon changes the log level or trace status for a context. -/// -/// Register a listener with [`DltContextHandle::register_log_level_changed_listener()`] -/// to receive these events -#[derive(Debug, Clone, Copy)] -pub struct LogLevelChangedEvent { - /// The DLT context ID that this change applies to - pub context_id: DltId, - /// The new log level for the context - pub log_level: DltLogLevel, - /// The new trace status for the context - pub trace_status: DltTraceStatus, -} - -struct LogLevelChangedBroadcaster { - sender: broadcast::Sender, - receiver: broadcast::Receiver, -} - -// Global registry for log level change callbacks -static CALLBACK_REGISTRY: OnceLock>> = - OnceLock::new(); - -static APP_REGISTERED: AtomicBool = AtomicBool::new(false); - -/// Internal C callback that forwards to the Rust channel -unsafe extern "C" fn internal_log_level_callback( - context_id: *mut std::os::raw::c_char, - log_level: u8, - trace_status: u8, -) { - if context_id.is_null() { - return; - } - - let Some(registry) = CALLBACK_REGISTRY.get() else { - return; - }; - - let id = unsafe { - let mut ctx_id = [0u8; DLT_ID_SIZE_USIZE]; - ptr::copy( - context_id.cast::(), - ctx_id.as_mut_ptr(), - DLT_ID_SIZE_USIZE, - ); - match DltId::new(&ctx_id) { - Ok(id) => id, - Err(_) => return, // Invalid context ID from DLT daemon - } - }; - let Ok(lock) = registry.read() else { - return; - }; +use std::ptr; +pub use dlt_bindings::*; - let Some(broadcaster) = lock.get(&id) else { - return; - }; - - let event = LogLevelChangedEvent { - context_id: id, - log_level: DltLogLevel::from(i32::from(log_level)), - trace_status: DltTraceStatus::from(i32::from(trace_status)), - }; - - let _ = broadcaster.sender.send(event); -} impl Default for DltContextData { fn default() -> Self { @@ -419,558 +39,4 @@ impl Default for DltContextData { verbose_mode: 0, } } -} - -/// Internal shared state for the DLT application -/// -/// This ensures contexts can keep the application alive through reference counting. -/// When the last reference (either from `DltApplication` or `DltContextHandle`) is -/// dropped, the application is automatically unregistered from DLT. -struct DltApplicationHandle { - _private: (), -} - -impl Drop for DltApplicationHandle { - fn drop(&mut self) { - unsafe { - // unregister from dlt, but ignore errors - dlt_bindings::unregisterApplicationFlushBufferedLogs(); - dlt_bindings::dltFree(); - APP_REGISTERED.store(false, std::sync::atomic::Ordering::SeqCst); - } - } -} - -/// Singleton guard for DLT application registration -/// -/// Only one DLT application can be registered per process. Automatically unregistered -/// when dropped and a new application can be registered. -/// -/// **Lifetime Guarantee**: Contexts maintain an internal reference, keeping the application -/// registered. Safe to drop the application handle before contexts. -/// -/// Cheaply cloneable for sharing across threads -pub struct DltApplication { - inner: Arc, -} - -impl DltApplication { - /// Register a DLT application - /// - /// Only one application can be registered per process. If you need to register - /// a different application, drop this instance first. - /// - /// The returned handle can be cloned to share the application across threads. - /// - /// # Errors - /// Returns `DltError` if the registration fails - pub fn register(app_id: &DltId, app_description: &str) -> Result { - if APP_REGISTERED - .compare_exchange( - false, - true, - std::sync::atomic::Ordering::SeqCst, - std::sync::atomic::Ordering::SeqCst, - ) - .is_err() - { - return Err(DltError::ApplicationRegistrationFailed( - "An application is already registered in this process".to_string(), - )); - } - - let app_id_str = app_id.as_str()?; - let app_id_c = CString::new(app_id_str).map_err(|_| { - DltError::InvalidString("App id could not be converted to string".to_owned()) - })?; - let app_desc_c = CString::new(app_description).map_err(|_| { - DltError::InvalidString("Context id could not be converted to string".to_owned()) - })?; - - unsafe { - let ret = dlt_bindings::registerApplication(app_id_c.as_ptr(), app_desc_c.as_ptr()); - DltSysError::from_return_code(ret).map_err(|_| { - DltError::ApplicationRegistrationFailed(format!( - "Failed to register application: {ret}" - )) - })?; - } - Ok(DltApplication { - inner: Arc::new(DltApplicationHandle { _private: () }), - }) - } - - /// Create a new DLT context within this application - /// - /// The created context maintains an internal reference to the application, - /// ensuring the application remains registered as long as the context exists. - /// - /// # Errors - /// Returns `DltError` if registration fails - pub fn create_context( - &self, - context_id: &DltId, - context_description: &str, - ) -> Result { - DltContextHandle::new(context_id, context_description, Arc::clone(&self.inner)) - } -} - -impl Clone for DltApplication { - fn clone(&self) -> Self { - Self { - inner: Arc::clone(&self.inner), - } - } -} - -// Safe to send between threads (application registration is process-wide) -unsafe impl Send for DltApplication {} -unsafe impl Sync for DltApplication {} - -/// Safe wrapper around C DLT context with RAII semantics -/// -/// The context holds an internal reference to the application, ensuring the -/// application remains registered as long as any context exists. -pub struct DltContextHandle { - context: *mut DltContext, - _app: Arc, -} - -impl DltContextHandle { - /// Register a new DLT context - /// - /// # Errors - /// Returns `DltError` if registration fails - fn new( - context_id: &DltId, - context_description: &str, - app: Arc, - ) -> Result { - let context_id_str = context_id.as_str()?; - let ctx_id_c = CString::new(context_id_str) - .map_err(|_| DltError::InvalidString("Context ID is not a valid string".to_owned()))?; - let ctx_desc_c = CString::new(context_description).map_err(|_| { - DltError::InvalidString("Context description is not a valid string".to_owned()) - })?; - - unsafe { - let context = dlt_bindings::registerContext(ctx_id_c.as_ptr(), ctx_desc_c.as_ptr()); - if context.is_null() { - Err(DltError::ContextRegistrationFailed) - } else { - Ok(DltContextHandle { context, _app: app }) - } - } - } - - fn raw_context(&self) -> Result { - let context = unsafe { - if self.context.is_null() { - return Err(DltError::ContextRegistrationFailed); - } - *self.context - }; - Ok(context) - } - - /// Get the context ID - /// # Errors - /// Returns `DltError` if the context is invalid or the context is null - pub fn context_id(&self) -> Result { - let ctx_id = unsafe { - // this is a false positive of clippy. - // raw_context.contextID is of type [::std::os::raw::c_char; 4usize], which - // cannot be directly used as &[u8; 4]. - #[allow(clippy::useless_transmute)] - std::mem::transmute::<[std::os::raw::c_char; 4], [u8; 4]>(self.raw_context()?.contextID) - }; - DltId::new(&ctx_id) - } - - /// Get the current trace status of the context - /// # Errors - /// Returns `DltError` if the context is invalid or the context is null - #[must_use] - pub fn trace_status(&self) -> DltTraceStatus { - self.raw_context() - .ok() - .and_then(|rc| { - if rc.log_level_ptr.is_null() { - None - } else { - Some(DltTraceStatus::from(i32::from(unsafe { - *rc.trace_status_ptr - }))) - } - }) - .unwrap_or(DltTraceStatus::Default) - } - - /// Get the current log level of the context - #[must_use] - pub fn log_level(&self) -> DltLogLevel { - self.raw_context() - .ok() - .and_then(|rc| { - if rc.log_level_ptr.is_null() { - None - } else { - Some(DltLogLevel::from(i32::from(unsafe { *rc.log_level_ptr }))) - } - }) - .unwrap_or(DltLogLevel::Default) - } - - /// Log a simple string message - /// - /// # Errors - /// Returns `DltError` if logging fails - pub fn log(&self, log_level: DltLogLevel, message: &str) -> Result<(), DltSysError> { - let msg_c = CString::new(message).map_err(|_| DltSysError::WrongParameter)?; - - unsafe { - let ret = dlt_bindings::logDlt(self.context, log_level.into(), msg_c.as_ptr()); - DltSysError::from_return_code(ret) - } - } - - /// Start a complex log message with a custom timestamp. - /// Can be used to hide original timestamps or to log event recorded earlier. - /// The timestamp is a steady clock, starting from an arbitrary point in time, - /// usually system start. - /// - /// # Errors - /// Returns `DltError` if starting the log message fails - pub fn log_write_start_custom_timestamp( - &self, - log_level: DltLogLevel, - timestamp_microseconds: u64, - ) -> Result { - let mut log_writer = self.log_write_start(log_level)?; - // timestamp resolution in dlt is .1 milliseconds. - let timestamp = - u32::try_from(timestamp_microseconds / 100).map_err(|_| DltSysError::WrongParameter)?; - log_writer.log_data.use_timestamp = dlt_bindings::DltTimestampType_DLT_USER_TIMESTAMP; - log_writer.log_data.user_timestamp = timestamp; - Ok(log_writer) - } - - /// Start a complex log message - /// - /// # Errors - /// Returns `DltError` if starting the log message fails - pub fn log_write_start(&self, log_level: DltLogLevel) -> Result { - let mut log_data = DltContextData::default(); - - unsafe { - let ret = dlt_bindings::dltUserLogWriteStart( - self.context, - &raw mut log_data, - log_level.into(), - ); - - DltSysError::from_return_code(ret)?; - Ok(DltLogWriter { log_data }) - } - } - - /// Register a channel to receive log level change notifications - /// - /// Returns a receiver that will get `LogLevelChangeEvent` - /// when the DLT daemon changes log levels - /// - /// # Errors - /// Returns `InternalError` if callback registration with DLT fails - pub fn register_log_level_changed_listener( - &self, - ) -> Result, DltError> { - let rwlock = CALLBACK_REGISTRY.get_or_init(|| RwLock::new(HashMap::new())); - let mut guard = rwlock.write().map_err(|_| DltError::BadLock)?; - let ctx_id = self.context_id()?; - - if let Some(broadcaster) = guard.get_mut(&ctx_id) { - Ok(broadcaster.receiver.resubscribe()) - } else { - unsafe { - let ret = dlt_bindings::registerLogLevelChangedCallback( - self.context, - Some(internal_log_level_callback), - ); - DltSysError::from_return_code(ret) - .map_err(|_| DltError::ContextRegistrationFailed)?; - } - let (tx, rx) = broadcast::channel(5); - let rx_clone = rx.resubscribe(); - guard.insert( - ctx_id, - LogLevelChangedBroadcaster { - sender: tx, - receiver: rx, - }, - ); - Ok(rx_clone) - } - } -} - -impl Drop for DltContextHandle { - fn drop(&mut self) { - let context_id = self.context_id(); - if let Some(lock) = CALLBACK_REGISTRY.get() - && let Ok(mut guard) = lock.write() - && let Ok(ctx_id) = context_id - { - guard.remove(&ctx_id); - } - - unsafe { - dlt_bindings::unregisterContext(self.context); - } - } -} - -// Safe to send between threads, per DLT documentation -unsafe impl Send for DltContextHandle {} -unsafe impl Sync for DltContextHandle {} - -/// Builder for structured log messages with multiple typed fields -/// -/// Construct log messages with typed data fields sent in binary format for efficiency. -/// Each field retains type information for proper display in DLT viewers. -/// -/// # Usage -/// -/// 1. Start with [`DltContextHandle::log_write_start()`] -/// 2. Chain `write_*` methods to add fields -/// 3. Call [`finish()`](DltLogWriter::finish()) to send -/// -/// Auto-finishes on drop if [`finish()`](DltLogWriter::finish()) not called (errors ignored). -/// -/// # Example -/// -/// ```no_run -/// # use dlt_sys::{DltApplication, DltId, DltLogLevel}; -/// # fn main() -> Result<(), Box> { -/// # let app = DltApplication::register(&DltId::new(b"MBTI")?, -/// "Measurement and Bus Trace Interface")?; -/// # let ctx = app.create_context(&DltId::new(b"MEAS")?, "Context")?; -/// let mut writer = ctx.log_write_start(DltLogLevel::Info)?; -/// writer.write_string("Temperature:")? -/// .write_float32(87.5)? -/// .write_string("°C")?; -/// writer.finish()?; -/// # Ok(()) -/// # } -/// ``` -/// -/// # Available Methods -/// -/// [`write_string()`](DltLogWriter::write_string()) | -/// [`write_i32()`](DltLogWriter::write_i32()) | -/// [`write_u32()`](DltLogWriter::write_u32()) | -/// [`write_int64()`](DltLogWriter::write_int64()) | -/// [`write_uint64()`](DltLogWriter::write_uint64()) | -/// [`write_float32()`](DltLogWriter::write_float32()) | -/// [`write_float64()`](DltLogWriter::write_float64()) | -/// [`write_bool()`](DltLogWriter::write_bool()) -pub struct DltLogWriter { - log_data: DltContextData, -} - -impl DltLogWriter { - /// Write a string to the log message - /// - /// # Errors - /// Returns `DltError` if writing fails - pub fn write_string(&mut self, text: &str) -> Result<&mut Self, DltSysError> { - let text_c = CString::new(text).map_err(|_| DltSysError::WrongParameter)?; - - unsafe { - let ret = dlt_bindings::dltUserLogWriteString(&raw mut self.log_data, text_c.as_ptr()); - DltSysError::from_return_code(ret)?; - } - - Ok(self) - } - - /// Write an unsigned integer to the log message - /// - /// # Errors - /// Returns `DltError` if writing fails - pub fn write_u32(&mut self, value: u32) -> Result<&mut Self, DltSysError> { - unsafe { - let ret = dlt_bindings::dltUserLogWriteUint(&raw mut self.log_data, value); - DltSysError::from_return_code(ret)?; - } - - Ok(self) - } - - /// Write a signed integer to the log message - /// - /// # Errors - /// Returns `DltError` if writing fails - pub fn write_i32(&mut self, value: i32) -> Result<&mut Self, DltSysError> { - unsafe { - let ret = dlt_bindings::dltUserLogWriteInt(&raw mut self.log_data, value); - DltSysError::from_return_code(ret)?; - } - - Ok(self) - } - - /// Write an unsigned 64-bit integer to the log message - /// - /// # Errors - /// Returns `DltError` if writing fails - pub fn write_uint64(&mut self, value: u64) -> Result<&mut Self, DltSysError> { - unsafe { - let ret = dlt_bindings::dltUserLogWriteUint64(&raw mut self.log_data, value); - DltSysError::from_return_code(ret)?; - } - - Ok(self) - } - - /// Write a signed 64-bit integer to the log message - /// - /// # Errors - /// Returns `DltError` if writing fails - pub fn write_int64(&mut self, value: i64) -> Result<&mut Self, DltSysError> { - unsafe { - let ret = dlt_bindings::dltUserLogWriteInt64(&raw mut self.log_data, value); - DltSysError::from_return_code(ret)?; - } - - Ok(self) - } - - /// Write a 32-bit float to the log message - /// - /// # Errors - /// Returns `DltError` if writing fails - pub fn write_float32(&mut self, value: f32) -> Result<&mut Self, DltSysError> { - unsafe { - let ret = dlt_bindings::dltUserLogWriteFloat32(&raw mut self.log_data, value); - DltSysError::from_return_code(ret)?; - } - - Ok(self) - } - - /// Write a 64-bit float to the log message - /// - /// # Errors - /// Returns `DltError` if writing fails - pub fn write_float64(&mut self, value: f64) -> Result<&mut Self, DltSysError> { - unsafe { - let ret = dlt_bindings::dltUserLogWriteFloat64(&raw mut self.log_data, value); - DltSysError::from_return_code(ret)?; - } - - Ok(self) - } - - /// Write a boolean to the log message - /// - /// # Errors - /// Returns `DltError` if writing fails - pub fn write_bool(&mut self, value: bool) -> Result<&mut Self, DltSysError> { - unsafe { - let ret = dlt_bindings::dltUserLogWriteBool(&raw mut self.log_data, u8::from(value)); - DltSysError::from_return_code(ret)?; - } - - Ok(self) - } - - /// Finish and send the log message - /// - /// Explicitly finishes the log message. If not called, the message will be - /// automatically finished when the `DltLogWriter` is dropped, but errors will be ignored. - /// - /// # Errors - /// Returns `DltError` if finishing fails - pub fn finish(mut self) -> Result<(), DltSysError> { - let ret = unsafe { dlt_bindings::dltUserLogWriteFinish(&raw mut self.log_data) }; - // Prevent Drop from running since we've already finished - std::mem::forget(self); - DltSysError::from_return_code(ret) - } -} - -impl Drop for DltLogWriter { - fn drop(&mut self) { - // Auto-finish the log message if finish() wasn't called explicitly - unsafe { - let _ = dlt_bindings::dltUserLogWriteFinish(&raw mut self.log_data); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dlt_error_from_return_code() { - assert!(DltSysError::from_return_code(0).is_ok()); - assert!(DltSysError::from_return_code(1).is_ok()); - assert_eq!(DltSysError::from_return_code(-1), Err(DltSysError::Error)); - assert_eq!( - DltSysError::from_return_code(-5), - Err(DltSysError::WrongParameter) - ); - } - - #[test] - fn test_dlt_id_creation() { - // 1 byte ID - let short_id = DltId::new(b"A").unwrap(); - assert_eq!(short_id.as_str().unwrap(), "A"); - - // 3 byte IDs - let app_id = DltId::new(b"APP").unwrap(); - assert_eq!(app_id.as_str().unwrap(), "APP"); - - let ctx_id = DltId::new(b"CTX").unwrap(); - assert_eq!(ctx_id.as_str().unwrap(), "CTX"); - - // 4 byte ID (maximum) - let full_id = DltId::new(b"ABCD").unwrap(); - assert_eq!(full_id.as_str().unwrap(), "ABCD"); - } - - #[test] - fn test_dlt_id_too_long() { - let result = DltId::new(b"TOOLONG"); - assert_eq!(result.unwrap_err(), DltError::InvalidInput); - } - - #[test] - fn test_dlt_id_empty() { - let result = DltId::new(b""); - assert_eq!(result.unwrap_err(), DltError::InvalidInput); - } - - #[test] - fn test_dlt_id_non_ascii() { - let result = DltId::new(b"\xFF\xFE"); - assert_eq!(result.unwrap_err(), DltError::InvalidInput); - } - - #[test] - fn test_dlt_id_equality() { - let id1 = DltId::new(b"APP").unwrap(); - let id2 = DltId::new(b"APP").unwrap(); - let id3 = DltId::new(b"CTX").unwrap(); - - assert_eq!(id1, id2); - assert_ne!(id1, id3); - - // Different lengths are not equal - let id4 = DltId::new(b"A").unwrap(); - assert_ne!(id1, id4); - } -} +} \ No newline at end of file diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 2928761..8947a64 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -22,8 +22,8 @@ license.workspace = true workspace = true [dependencies] -dlt-sys = { workspace = true } -dlt-tracing-appender = { workspace = true } +dlt-rs = { workspace = true } +tracing-dlt = { workspace = true } tokio = { workspace = true, features = ["test-util", "macros", "rt"] } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["registry", "fmt"] } diff --git a/integration-tests/tests/dlt_sys/mod.rs b/integration-tests/tests/dlt_rs/mod.rs similarity index 99% rename from integration-tests/tests/dlt_sys/mod.rs rename to integration-tests/tests/dlt_rs/mod.rs index 27eccbb..b411422 100644 --- a/integration-tests/tests/dlt_sys/mod.rs +++ b/integration-tests/tests/dlt_rs/mod.rs @@ -11,7 +11,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use dlt_sys::{DltApplication, DltId, DltLogLevel, LogLevelChangedEvent}; +use dlt_rs::{DltApplication, DltId, DltLogLevel, LogLevelChangedEvent}; use serial_test::serial; use tokio::sync::broadcast; diff --git a/integration-tests/tests/lib.rs b/integration-tests/tests/lib.rs index 85694d5..df8b993 100644 --- a/integration-tests/tests/lib.rs +++ b/integration-tests/tests/lib.rs @@ -19,10 +19,10 @@ use std::{ time::Duration, }; -use ::dlt_sys::{DltId, DltLogLevel}; +use ::dlt_rs::{DltId, DltLogLevel}; -mod dlt_sys; -mod dlt_tracing_appender; +mod dlt_rs; +mod tracing_dlt; static DLT_DAEMON: OnceLock>>> = OnceLock::new(); diff --git a/integration-tests/tests/dlt_tracing_appender/mod.rs b/integration-tests/tests/tracing_dlt/mod.rs similarity index 88% rename from integration-tests/tests/dlt_tracing_appender/mod.rs rename to integration-tests/tests/tracing_dlt/mod.rs index 2b56ae6..0ac5cc2 100644 --- a/integration-tests/tests/dlt_tracing_appender/mod.rs +++ b/integration-tests/tests/tracing_dlt/mod.rs @@ -12,8 +12,7 @@ */ use std::{sync::Arc, time::Duration}; -use dlt_sys::{DltApplication, DltLogLevel}; -use dlt_tracing_appender::{DltId, DltLayer}; +use tracing_dlt::{DltId, DltLayer, DltApplication, DltLogLevel}; use serial_test::serial; use tracing_subscriber::{Registry, layer::SubscriberExt}; @@ -81,9 +80,9 @@ async fn test_tracing_to_dlt() { tokio::time::sleep(Duration::from_millis(200)).await; let output = receiver.stop_and_get_output(); for expected_string in [ - "TEST DFLT log info V 1 [lib::dlt_tracing_appender: Test info message]", - "TEST DFLT log warn V 1 [lib::dlt_tracing_appender: Test warning message]", - "TEST DFLT log error V 1 [lib::dlt_tracing_appender: Test error message]", + "TEST DFLT log info V 1 [lib::tracing_dlt: Test info message]", + "TEST DFLT log warn V 1 [lib::tracing_dlt: Test warning message]", + "TEST DFLT log error V 1 [lib::tracing_dlt: Test error message]", ] { assert_contains(&output, expected_string); } @@ -122,7 +121,7 @@ async fn test_with_spans_and_context_id() { let dlt_output = receiver.stop_and_get_output(); let outer = r#"outer_span{task="processing"}"#; let inner = "inner_span{step=1}"; - let target = "lib::dlt_tracing_appender"; + let target = "lib::tracing_dlt"; for expected_string in [ format!("TEST CONT log info V 2 [{outer}: {target}: Inside outer with context too long"), format!("TEST CTX1 log info V 2 [{outer}:{inner}: {target}: Inside inner span]"), @@ -164,9 +163,9 @@ async fn test_tracing_with_default_context() { let console_output = std::fs::read_to_string(log_file_path).expect("Failed to read log file"); for expected_string in [ - r#"outer_span{task="processing"}: lib::dlt_tracing_appender: Inside outer span"#, - r#"outer_span{task="processing"}:inner_span{step=1}: lib::dlt_tracing_appender: inner"#, - r#"outer_span{task="processing"}: lib::dlt_tracing_appender: Back in outer span"#, + r#"outer_span{task="processing"}: lib::tracing_dlt: Inside outer span"#, + r#"outer_span{task="processing"}:inner_span{step=1}: lib::tracing_dlt: inner"#, + r#"outer_span{task="processing"}: lib::tracing_dlt: Back in outer span"#, ] { assert_contains(&dlt_output, expected_string); assert_contains(&console_output, expected_string); @@ -199,8 +198,8 @@ async fn test_concurrent_logging() { for i in 0..5 { for j in 0..10 { let expected = format!( - "TEST DFLT log info V 7 [lib::dlt_tracing_appender: Concurrent log message task = \ - {i} iteration = {j}]", + "TEST DFLT log info V 7 [lib::tracing_dlt: Concurrent log message task = {i} \ + iteration = {j}]", ); assert_contains(&messages, &expected); } @@ -278,12 +277,11 @@ async fn test_mixed_tracing_and_low_level_dlt() { // Verify tracing messages (using DFLT or PROC context) assert_contains( &output, - "TEST DFLT log info V 1 [lib::dlt_tracing_appender: Message from tracing API]", + "TEST DFLT log info V 1 [lib::tracing_dlt: Message from tracing API]", ); assert_contains( &output, - "TEST DFLT log warn V 4 [lib::dlt_tracing_appender: Tracing warning with field component \ - = sensor]", + "TEST DFLT log warn V 4 [lib::tracing_dlt: Tracing warning with field component = sensor]", ); // Verify low-level DLT messages (using LLVL context) @@ -296,7 +294,7 @@ async fn test_mixed_tracing_and_low_level_dlt() { // Verify messages from within span assert_contains( &output, - "TEST PROC log info V 2 [processing: lib::dlt_tracing_appender: Inside tracing span]", + "TEST PROC log info V 2 [processing: lib::tracing_dlt: Inside tracing span]", ); assert_contains( &output, @@ -304,13 +302,12 @@ async fn test_mixed_tracing_and_low_level_dlt() { ); assert_contains( &output, - "TEST PROC log error V 2 [processing: lib::dlt_tracing_appender: Tracing error in same \ - span]", + "TEST PROC log error V 2 [processing: lib::tracing_dlt: Tracing error in same span]", ); // Verify final tracing message assert_contains( &output, - "TEST DFLT log info V 1 [lib::dlt_tracing_appender: Final message from tracing]", + "TEST DFLT log info V 1 [lib::tracing_dlt: Final message from tracing]", ); } diff --git a/dlt-tracing-appender/Cargo.toml b/tracing-dlt/Cargo.toml similarity index 84% rename from dlt-tracing-appender/Cargo.toml rename to tracing-dlt/Cargo.toml index 81c9544..c12bfa1 100644 --- a/dlt-tracing-appender/Cargo.toml +++ b/tracing-dlt/Cargo.toml @@ -10,11 +10,11 @@ # SPDX-License-Identifier: Apache-2.0 [package] -name = "dlt-tracing-appender" +name = "tracing-dlt" version = "0.1.0" edition.workspace = true publish = false -description = "Appender for tracing crate to write tracing spans and events to DLT using dlt-sys" +description = "DLT log sink for " homepage.workspace = true license.workspace = true @@ -26,7 +26,7 @@ path = "src/lib.rs" [dependencies] # internal dependencies -dlt-sys = { workspace = true } +dlt-rs = { workspace = true } # external tokio = { workspace = true } @@ -36,7 +36,6 @@ indexmap = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["registry"] } tracing-core = { workspace = true } -crc32fast = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["test-util", "macros"] } diff --git a/dlt-tracing-appender/src/lib.rs b/tracing-dlt/src/lib.rs similarity index 98% rename from dlt-tracing-appender/src/lib.rs rename to tracing-dlt/src/lib.rs index dc604d4..c8e80c3 100644 --- a/dlt-tracing-appender/src/lib.rs +++ b/tracing-dlt/src/lib.rs @@ -20,7 +20,7 @@ //! # Quick Start //! //! ```no_run -//! use dlt_tracing_appender::{DltLayer, DltId}; +//! use tracing_dlt::{DltLayer, DltId}; //! use tracing::{info, span, Level}; //! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; //! @@ -59,7 +59,6 @@ //! //! - **Per-span contexts** - Use `dlt_context` field to route logs to specific DLT contexts //! - **Structured logging** - Span fields automatically included in messages with native types -//! - **Dynamic control** - Runtime log level changes via `dlt-control` command //! - **Layer composition** - Combine with other tracing layers (fmt, file, etc.) //! - **Thread-safe** - Full `Send + Sync` support //! @@ -69,7 +68,7 @@ //! context with the `dlt_context` field (auto-creates and caches contexts): //! //! ```no_run -//! # use dlt_tracing_appender::{DltLayer, DltId}; +//! # use tracing_dlt::{DltLayer, DltId}; //! # use tracing::{info, span, Level}; //! # use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; //! # #[tokio::main] @@ -101,9 +100,9 @@ use std::{ sync::{Arc, RwLock}, }; -use dlt_sys::{DltApplication, DltContextHandle, DltLogLevel}; +use dlt_rs::{DltContextHandle}; // Re-export types for users of this library -pub use dlt_sys::{DltError, DltId, DltSysError}; +pub use dlt_rs::{DltError, DltId, DltSysError, DltLogLevel, DltApplication}; use indexmap::IndexMap; use tracing_core::{Event, Subscriber, span}; use tracing_subscriber::{Layer, filter::LevelFilter, layer::Context, registry::LookupSpan}; @@ -225,7 +224,7 @@ impl DltLayer { return Err(DltError::InvalidInput); } let bytes = name.as_bytes(); - let len = bytes.len().clamp(1, dlt_sys::DLT_ID_SIZE_USIZE); + let len = bytes.len().clamp(1, dlt_rs::DLT_ID_SIZE_USIZE); let get = |i| bytes.get(i).copied().ok_or(DltError::InvalidInput); @@ -469,9 +468,9 @@ fn map_dlt_to_level_filter(dlt_level: DltLogLevel) -> LevelFilter { /// Helper function to write fields to DLT with proper error propagation fn write_fields( - log_writer: &mut dlt_sys::DltLogWriter, + log_writer: &mut dlt_rs::DltLogWriter, fields: IndexMap, -) -> Result<(), dlt_sys::DltSysError> { +) -> Result<(), DltSysError> { for (field_name, field_value) in fields { // Write field name log_writer.write_string(&field_name)?; From 2112e201678b68caa5787e1ca06b08760c691853 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 24 Nov 2025 14:21:54 +0100 Subject: [PATCH 3/9] add warning that write_float64 is not supported on 32 bit ARM Signed-off-by: Alexander Mohr Result<(), Self> { #[allow(unreachable_patterns)] match code { - dlt_sys::DltReturnValue_DLT_RETURN_TRUE - | dlt_sys::DltReturnValue_DLT_RETURN_OK => Ok(()), + dlt_sys::DltReturnValue_DLT_RETURN_TRUE | dlt_sys::DltReturnValue_DLT_RETURN_OK => { + Ok(()) + } dlt_sys::DltReturnValue_DLT_RETURN_ERROR => Err(DltSysError::Error), dlt_sys::DltReturnValue_DLT_RETURN_PIPE_ERROR => Err(DltSysError::PipeError), dlt_sys::DltReturnValue_DLT_RETURN_PIPE_FULL => Err(DltSysError::PipeFull), dlt_sys::DltReturnValue_DLT_RETURN_BUFFER_FULL => Err(DltSysError::BufferFull), - dlt_sys::DltReturnValue_DLT_RETURN_WRONG_PARAMETER => { - Err(DltSysError::WrongParameter) - } - dlt_sys::DltReturnValue_DLT_RETURN_USER_BUFFER_FULL => { - Err(DltSysError::UserBufferFull) - } + dlt_sys::DltReturnValue_DLT_RETURN_WRONG_PARAMETER => Err(DltSysError::WrongParameter), + dlt_sys::DltReturnValue_DLT_RETURN_USER_BUFFER_FULL => Err(DltSysError::UserBufferFull), dlt_sys::DltReturnValue_DLT_RETURN_LOGGING_DISABLED => { Err(DltSysError::LoggingDisabled) } @@ -270,7 +267,7 @@ impl DltId { } let mut padded = [0u8; DLT_ID_SIZE_USIZE]; - // Indexing is safe here: loop condition ensures i < N, and N <= DLT_ID_SIZE by validation + // Indexing is safe here: function ensures N <= DLT_ID_SIZE by validation #[allow(clippy::indexing_slicing)] padded[..N].copy_from_slice(&bytes[..N]); @@ -646,11 +643,8 @@ impl DltContextHandle { let mut log_data = DltContextData::default(); unsafe { - let ret = dlt_sys::dltUserLogWriteStart( - self.context, - &raw mut log_data, - log_level.into(), - ); + let ret = + dlt_sys::dltUserLogWriteStart(self.context, &raw mut log_data, log_level.into()); DltSysError::from_return_code(ret)?; Ok(DltLogWriter { log_data }) diff --git a/dlt-sys/src/lib.rs b/dlt-sys/src/lib.rs index c621e89..beef2de 100644 --- a/dlt-sys/src/lib.rs +++ b/dlt-sys/src/lib.rs @@ -21,8 +21,8 @@ mod dlt_bindings; use std::ptr; -pub use dlt_bindings::*; +pub use dlt_bindings::*; impl Default for DltContextData { fn default() -> Self { @@ -39,4 +39,4 @@ impl Default for DltContextData { verbose_mode: 0, } } -} \ No newline at end of file +} diff --git a/integration-tests/tests/dlt_rs/mod.rs b/integration-tests/tests/dlt_rs/mod.rs index b411422..8abb4a7 100644 --- a/integration-tests/tests/dlt_rs/mod.rs +++ b/integration-tests/tests/dlt_rs/mod.rs @@ -323,3 +323,25 @@ async fn test_clone_application_handle() { ], ); } + +#[tokio::test] +#[serial] +async fn test_f64_write() { + ensure_dlt_daemon_running(); + + let app_id = DltId::new(b"F64").unwrap(); + let ctx_id = DltId::new(b"CTX1").unwrap(); + + let receiver = DltReceiver::start(); + + let app = DltApplication::register(&app_id, "f64 write test").unwrap(); + let ctx1 = app.create_context(&ctx_id, "Context 1").unwrap(); + let mut log_writer = ctx1 + .log_write_start(DltLogLevel::Info) + .expect("Failed to start log"); + log_writer.write_float64(42.42_f64).unwrap(); + log_writer.finish().unwrap(); + + let output = receiver.stop_and_get_output(); + assert_contains_all(&output, &["F64- CTX1 log info V 1 [42.42]"]); +} diff --git a/integration-tests/tests/tracing_dlt/mod.rs b/integration-tests/tests/tracing_dlt/mod.rs index 0ac5cc2..7c7e109 100644 --- a/integration-tests/tests/tracing_dlt/mod.rs +++ b/integration-tests/tests/tracing_dlt/mod.rs @@ -12,8 +12,8 @@ */ use std::{sync::Arc, time::Duration}; -use tracing_dlt::{DltId, DltLayer, DltApplication, DltLogLevel}; use serial_test::serial; +use tracing_dlt::{DltApplication, DltId, DltLayer, DltLogLevel}; use tracing_subscriber::{Registry, layer::SubscriberExt}; use crate::{DltReceiver, assert_contains, change_dlt_log_level, ensure_dlt_daemon_running}; diff --git a/tracing-dlt/src/lib.rs b/tracing-dlt/src/lib.rs index c8e80c3..192be70 100644 --- a/tracing-dlt/src/lib.rs +++ b/tracing-dlt/src/lib.rs @@ -100,9 +100,9 @@ use std::{ sync::{Arc, RwLock}, }; -use dlt_rs::{DltContextHandle}; +use dlt_rs::DltContextHandle; // Re-export types for users of this library -pub use dlt_rs::{DltError, DltId, DltSysError, DltLogLevel, DltApplication}; +pub use dlt_rs::{DltApplication, DltError, DltId, DltLogLevel, DltSysError}; use indexmap::IndexMap; use tracing_core::{Event, Subscriber, span}; use tracing_subscriber::{Layer, filter::LevelFilter, layer::Context, registry::LookupSpan}; From 6b56f57d4c6156fdb7a45bec152b9b734f96f3a4 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 24 Nov 2025 15:02:14 +0100 Subject: [PATCH 4/9] dlt-sys: use return value from register app and register context Side effect of this is, that the memory associated with the context now has to be managed in rust instead of C. Signed-off-by: Alexander Mohr --- dlt-rs/build.rs | 13 +++++++++++++ dlt-rs/src/lib.rs | 29 ++++++++++++++++++++--------- dlt-sys/src/dlt_bindings.rs | 3 ++- dlt-sys/wrapper/dlt-wrapper.c | 19 +++++-------------- dlt-sys/wrapper/dlt-wrapper.h | 2 +- 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/dlt-rs/build.rs b/dlt-rs/build.rs index 742c224..c05d631 100644 --- a/dlt-rs/build.rs +++ b/dlt-rs/build.rs @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + fn main() { #[cfg(all(target_arch = "arm", target_pointer_width = "32"))] { diff --git a/dlt-rs/src/lib.rs b/dlt-rs/src/lib.rs index 989882b..2e0e81a 100644 --- a/dlt-rs/src/lib.rs +++ b/dlt-rs/src/lib.rs @@ -146,9 +146,11 @@ pub enum DltError { #[error("Data cannot be converted to a DLT compatible string: {0}")] InvalidString(String), #[error("Failed to register DLT context")] - ContextRegistrationFailed, + ContextRegistrationFailed(String), #[error("Failed to register DLT application")] ApplicationRegistrationFailed(String), + #[error("Failed to register a log event change listener")] + LogLevelListenerRegistrationFailed(String), #[error("A pointer or memory is invalid")] InvalidMemory, #[error("Failed to acquire a lock")] @@ -534,19 +536,25 @@ impl DltContextHandle { })?; unsafe { - let context = dlt_sys::registerContext(ctx_id_c.as_ptr(), ctx_desc_c.as_ptr()); - if context.is_null() { - Err(DltError::ContextRegistrationFailed) - } else { - Ok(DltContextHandle { context, _app: app }) - } + let mut context = Box::new(std::mem::zeroed::()); + let rv = + dlt_sys::registerContext(ctx_id_c.as_ptr(), ctx_desc_c.as_ptr(), context.as_mut()); + DltSysError::from_return_code(rv) + .map_err(|e| DltError::ContextRegistrationFailed(format!("{e}")))?; + + Ok(DltContextHandle { + context: Box::into_raw(context), + _app: app, + }) } } fn raw_context(&self) -> Result { let context = unsafe { if self.context.is_null() { - return Err(DltError::ContextRegistrationFailed); + return Err(DltError::ContextRegistrationFailed( + "Context pointer is null".to_string(), + )); } *self.context }; @@ -674,7 +682,7 @@ impl DltContextHandle { Some(internal_log_level_callback), ); DltSysError::from_return_code(ret) - .map_err(|_| DltError::ContextRegistrationFailed)?; + .map_err(|e| DltError::LogLevelListenerRegistrationFailed(format!("{e}")))?; } let (tx, rx) = broadcast::channel(5); let rx_clone = rx.resubscribe(); @@ -702,6 +710,9 @@ impl Drop for DltContextHandle { unsafe { dlt_sys::unregisterContext(self.context); + // free the memory allocated for the context + // not done in the C wrapper, because the wrapper also does not init it + let _ = Box::from_raw(self.context); } } } diff --git a/dlt-sys/src/dlt_bindings.rs b/dlt-sys/src/dlt_bindings.rs index 3b8b715..ec53dc5 100644 --- a/dlt-sys/src/dlt_bindings.rs +++ b/dlt-sys/src/dlt_bindings.rs @@ -122,7 +122,8 @@ unsafe extern "C" { pub fn registerContext( contextId: *const ::std::os::raw::c_char, contextDescription: *const ::std::os::raw::c_char, - ) -> *mut DltContext; + context: *mut DltContext, + ) -> DltReturnValue; } unsafe extern "C" { pub fn unregisterContext(context: *mut DltContext) -> DltReturnValue; diff --git a/dlt-sys/wrapper/dlt-wrapper.c b/dlt-sys/wrapper/dlt-wrapper.c index 0849673..3e98c21 100644 --- a/dlt-sys/wrapper/dlt-wrapper.c +++ b/dlt-sys/wrapper/dlt-wrapper.c @@ -21,8 +21,7 @@ DltReturnValue registerApplication(const char *appId, const char *appDescription if (appId == NULL) { return DLT_RETURN_WRONG_PARAMETER; } - DLT_REGISTER_APP(appId, appDescription); - return DLT_RETURN_OK; + return dlt_register_app(appId, appDescription); } DltReturnValue unregisterApplicationFlushBufferedLogs(void) { @@ -33,18 +32,12 @@ DltReturnValue dltFree(void) { return dlt_free(); } -DltContext *registerContext(const char *contextId, const char *contextDescription) { +DltReturnValue registerContext(const char *contextId, const char *contextDescription, DltContext* context) { if (contextId == NULL) { - return NULL; + return DLT_RETURN_ERROR; } - DltContext *context = (DltContext *)malloc(sizeof(DltContext)); - if (context == NULL) { - return NULL; - } - - DLT_REGISTER_CONTEXT(*context, contextId, contextDescription); - return context; + return dlt_register_context(context, contextId, contextDescription); } DltReturnValue unregisterContext(DltContext *context) { @@ -52,9 +45,7 @@ DltReturnValue unregisterContext(DltContext *context) { return DLT_RETURN_WRONG_PARAMETER; } - dlt_unregister_context(context); - free(context); - return DLT_RETURN_OK; + return dlt_unregister_context(context); } DltReturnValue logDlt(DltContext *context, DltLogLevelType logLevel, const char *message) { diff --git a/dlt-sys/wrapper/dlt-wrapper.h b/dlt-sys/wrapper/dlt-wrapper.h index 5f9eded..8e70e8f 100644 --- a/dlt-sys/wrapper/dlt-wrapper.h +++ b/dlt-sys/wrapper/dlt-wrapper.h @@ -28,7 +28,7 @@ DltReturnValue unregisterApplicationFlushBufferedLogs(void); DltReturnValue dltFree(void); // Context management -DltContext *registerContext(const char *contextId, const char *contextDescription); +DltReturnValue registerContext(const char *contextId, const char *contextDescription, DltContext* context); DltReturnValue unregisterContext(DltContext *context); // Logging functions From 55cd3897a6371f5bad3e7d7d66656057234f7aeb Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 24 Nov 2025 16:12:32 +0100 Subject: [PATCH 5/9] tracing: add feature to propagate trace-load into dlt-rs makes enabling the trace load feature more convienient Signed-off-by: Alexander Mohr --- tracing-dlt/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tracing-dlt/Cargo.toml b/tracing-dlt/Cargo.toml index c12bfa1..72a70ca 100644 --- a/tracing-dlt/Cargo.toml +++ b/tracing-dlt/Cargo.toml @@ -41,5 +41,6 @@ tracing-core = { workspace = true } tokio = { workspace = true, features = ["test-util", "macros"] } [features] -default = ["dlt_layer_internal_logging"] +default = [] dlt_layer_internal_logging = [] +trace_load_ctrl = ["dlt-rs/trace_load_ctrl"] From cce511ab153b5b0a09b7febabfea394a7d48837d Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Tue, 25 Nov 2025 10:12:51 +0100 Subject: [PATCH 6/9] dlt-rs: add TryFrom<&str> for DltId Makes it more convenient to create a DLtId instance from a string slice. Signed-off-by: Alexander Mohr --- dlt-rs/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/dlt-rs/src/lib.rs b/dlt-rs/src/lib.rs index 2e0e81a..88ff402 100644 --- a/dlt-rs/src/lib.rs +++ b/dlt-rs/src/lib.rs @@ -294,6 +294,26 @@ impl DltId { } } +impl TryFrom<&str> for DltId { + type Error = DltError; + + fn try_from(value: &str) -> Result { + let bytes = value.as_bytes(); + if bytes.is_empty() || bytes.len() > DLT_ID_SIZE_USIZE { + return Err(DltError::InvalidInput); + } + let mut padded = [0u8; DLT_ID_SIZE_USIZE]; + padded + .get_mut(..bytes.len()) + .ok_or(DltError::InvalidInput)? + .copy_from_slice(bytes); + Ok(DltId { + bytes: padded, + len: bytes.len(), + }) + } +} + /// DLT trace status /// /// Controls whether network trace messages (like packet captures) are enabled. @@ -960,4 +980,16 @@ mod tests { let id4 = DltId::new(b"A").unwrap(); assert_ne!(id1, id4); } + + #[test] + fn test_dlt_id_try_from_str() { + let id = DltId::try_from("APP").unwrap(); + assert_eq!(id.as_str().unwrap(), "APP"); + + let long_id_result = DltId::try_from("TOOLONG"); + assert_eq!(long_id_result.unwrap_err(), DltError::InvalidInput); + + let empty_id_result = DltId::try_from(""); + assert_eq!(empty_id_result.unwrap_err(), DltError::InvalidInput); + } } From fe421d8b59308ce71c9ba218ec83bc0ca947454a Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Wed, 26 Nov 2025 10:38:08 +0100 Subject: [PATCH 7/9] docs: add github pages job Signed-off-by: Alexander Mohr --- .github/workflows/generate_documentation.yml | 21 +++++++++++++++++++- Cargo.toml | 2 +- dlt-rs/Cargo.toml | 2 +- dlt-sys/Cargo.toml | 4 ++-- tracing-dlt/Cargo.toml | 2 +- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/workflows/generate_documentation.yml b/.github/workflows/generate_documentation.yml index 79941fc..222d357 100644 --- a/.github/workflows/generate_documentation.yml +++ b/.github/workflows/generate_documentation.yml @@ -23,6 +23,8 @@ on: permissions: contents: write actions: write + pages: write + id-token: write jobs: build_documentation: @@ -42,7 +44,12 @@ jobs: - name: Build documentation run: cargo doc --no-deps --all-features - name: Add index redirect - run: echo '' > target/doc/index.html + run: echo '' > target/doc/index.html + - name: Upload artifact for GitHub Pages + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: target/doc - name: Create docs archive if: startsWith(github.ref, 'refs/tags/') run: | @@ -53,3 +60,15 @@ jobs: uses: softprops/action-gh-release@v1 with: files: rustdoc.tar.gz + + deploy_pages: + if: github.ref == 'refs/heads/main' + needs: build_documentation + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Cargo.toml b/Cargo.toml index 954226d..6489410 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ [workspace.package] edition = "2024" license = "Apache-2.0" -homepage = "https://github.com/eclipse-opensovd/classic-diagnostic-adapter" +homepage = "https://github.com/eclipse-opensovd/dlt-tracing-lib" [workspace] resolver = "3" diff --git a/dlt-rs/Cargo.toml b/dlt-rs/Cargo.toml index 01d0567..ab045c7 100644 --- a/dlt-rs/Cargo.toml +++ b/dlt-rs/Cargo.toml @@ -13,7 +13,7 @@ name = "dlt-rs" version = "0.1.0" edition.workspace = true -publish = false +publish = true description = "Safe and idiomatic Rust wrapper for the C library libdlt to provide DLT logging capabilities for Rust applications" homepage.workspace = true license.workspace = true diff --git a/dlt-sys/Cargo.toml b/dlt-sys/Cargo.toml index 7988925..fb8f6b8 100644 --- a/dlt-sys/Cargo.toml +++ b/dlt-sys/Cargo.toml @@ -13,8 +13,8 @@ name = "dlt-sys" version = "0.1.0" edition.workspace = true -publish = false -description = "Wrapper around the C library libdlt to provide DLT logging capabilities for Rust applications" +publish = true +description = "FFI bindings to libdlt" homepage.workspace = true license.workspace = true diff --git a/tracing-dlt/Cargo.toml b/tracing-dlt/Cargo.toml index 72a70ca..a7dc679 100644 --- a/tracing-dlt/Cargo.toml +++ b/tracing-dlt/Cargo.toml @@ -13,7 +13,7 @@ name = "tracing-dlt" version = "0.1.0" edition.workspace = true -publish = false +publish = true description = "DLT log sink for " homepage.workspace = true license.workspace = true From 780a5303086720703212ff48ad381a14b513df5a Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Wed, 26 Nov 2025 17:29:44 +0100 Subject: [PATCH 8/9] docs: add README files for publishable crates Add individual README files for dlt-sys, dlt-rs, and tracing-dlt crates to support publishing to crates.io. Each README provides crate-specific documentation, examples, and usage guidance. Individual READMEs make sure, that the published artifact on crates.io contains the correct documentation. Update root README to serve as workspace overview with table of crates and links to individual documentation. Signed-off-by: Alexander Mohr --- .github/workflows/build.yml | 7 ---- README.md | 72 +++++++++---------------------------- dlt-rs/README.md | 66 ++++++++++++++++++++++++++++++++++ dlt-sys/README.md | 43 ++++++++++++++++++++++ tracing-dlt/README.md | 58 ++++++++++++++++++++++++++++++ 5 files changed, 183 insertions(+), 63 deletions(-) create mode 100644 dlt-rs/README.md create mode 100644 dlt-sys/README.md create mode 100644 tracing-dlt/README.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6c5881..ea9e675 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,13 +58,6 @@ jobs: with: submodules: false - uses: ./.github/actions/setup-dlt - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.88.0 - components: clippy, rustfmt - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 - name: Install cargo-deny diff --git a/README.md b/README.md index 7a042d1..5f2960e 100644 --- a/README.md +++ b/README.md @@ -18,70 +18,30 @@ Get it by running: ## Overview -This workspace contains three crates: -- **`dlt-sys`** - Low-level Rust wrapper around the C libdlt library -- **`dlt-rs`** - Safe Rust API for DLT logging. This crate depends on `dlt-sys`. -- **`tracing-appender`** - Tracing subscriber/layer that integrates with the tracing framework. This crate depends on `dlt-rs`. -- **`integration-tests`** - Common test utilities for integration testing with DLT daemon - -## Features - -- ✅ **Type-safe Rust API** for DLT logging -- ✅ **Tracing integration** - Use standard `tracing` macros with DLT -- ✅ **Structured logging** - Field types preserved when sent to DLT -- ✅ **Span context** - Nested spans appear in log messages -- ✅ **Dynamic log levels** - Responds to DLT daemon log level changes -- ✅ **Thread-safe** - Safe for concurrent use across async tasks -- ✅ **Zero-copy** where possible for performance +This workspace contains three publishable crates: -## Quick Start - -### Prerequisites - -- Rust 1.88.0 or later +| Crate | Description | Documentation | +|-------|-------------|---------------| +| **[`dlt-sys`](dlt-sys/)** | Low-level FFI bindings to libdlt | [README](dlt-sys/README.md) | +| **[`dlt-rs`](dlt-rs/)** | Safe and idiomatic Rust API for DLT logging | [README](dlt-rs/README.md) | +| **[`tracing-dlt`](tracing-dlt/)** | Tracing subscriber/layer for DLT integration | [README](tracing-dlt/README.md) | -### Basic Usage - -#### DLT Sys -```rust -use dlt_sys::{DltApplication, DltId, DltLogLevel}; -fn main() -> Result<(), Box> { - // Register application (one per process) - let app = DltApplication::register(&DltId::new(b"MBTI")?, "Measurement & Bus Trace Interface")?; - let ctx = app.create_context(&DltId::new(b"MEAS")?, "Measurement Context")?; - - // Simple logging - ctx.log(DltLogLevel::Info, "Hello DLT!")?; - - // Structured logging with typed fields - let mut writer = ctx.log_write_start(DltLogLevel::Info)?; - writer.write_string("Temperature:")? - .write_float32(87.5)? - .write_string("°C")?; - writer.finish()?; - Ok(()) -} -``` +**Which crate should you use?** +- Use `tracing-dlt` for integration with the `tracing` ecosystem (recommended) +- Use `dlt-rs` for direct DLT logging with a safe API (non-tracing applications) +- Use `dlt-sys` only if building your own low-level abstraction (not recommended for most users) -#### Dlt Tracing Appender +See each crate's README for detailed examples and API documentation. -```rust -use dlt_tracing_appender::{DltLayer, DltId}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +> **Note:** `tracing-dlt` and `dlt-rs` can be used together when application registration is done through `tracing-dlt`. -#[tokio::main] -async fn main() -> Result<(), Box> { - let dlt_layer = DltLayer::new(&DltId::new(b"MBTI"), "My Beautiful Trace Ingestor")?; +## Quick Start - tracing_subscriber::registry().with(dlt_layer).init(); +### Prerequisites - tracing::info!("Application started"); - Ok(()) -} -``` +- Rust 1.88.0 or later +- **libdlt** must be installed on your system -For more examples and detailed usage, see the API documentation. -The tracing and dlt-sys crates can be used simultaneously, when the application registration is done through the dlt-tracing-appender crate. ## Development diff --git a/dlt-rs/README.md b/dlt-rs/README.md new file mode 100644 index 0000000..59c01be --- /dev/null +++ b/dlt-rs/README.md @@ -0,0 +1,66 @@ +# dlt-rs +[![Crates.io](https://img.shields.io/crates/v/dlt-rs.svg)](https://crates.io/crates/dlt-rs) +[![Documentation](https://docs.rs/dlt-rs/badge.svg)](https://docs.rs/dlt-rs) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](../LICENSE) +Safe and idiomatic Rust wrapper for the COVESA DLT (Diagnostic Log and Trace) library. + +## Overview +`dlt-rs` provides a safe, ergonomic Rust API for logging to the [COVESA DLT daemon](https://github.com/COVESA/dlt-daemon). It wraps the low-level [`dlt-sys`](https://crates.io/crates/dlt-sys) FFI bindings with a type-safe interface. + +## Features +- ✅ **Type-safe API** - No unsafe code in your application +- ✅ **Structured logging** - Log typed fields (integers, floats, strings, etc.) +- ✅ **RAII-based resource management** - Automatic cleanup +- ✅ **Thread-safe** - Safe for concurrent use +- ✅ **Zero-copy** where possible for performance +- ✅ **Dynamic log levels** - Responds to DLT daemon configuration changes + + +## Basic Example +```rust +use dlt_rs::{DltApplication, DltId, DltLogLevel}; +fn main() -> Result<(), Box> { + // Register application (one per process) + let app = DltApplication::register( + &DltId::new(b"MBTI")?, + "Measurement & Bus Trace Interface" + )?; + // Create a logging context + let ctx = app.create_context( + &DltId::new(b"CTX1")?, + "Main Context" + )?; + // Simple text logging + ctx.log(DltLogLevel::Info, "Hello DLT!")?; + Ok(()) +} +``` + +## Structured Logging +```rust +use dlt_rs::{DltLogLevel}; +// Log structured data with typed fields +let mut writer = ctx.log_write_start(DltLogLevel::Info)?; +writer + .write_string("Temperature:")? + .write_float32(87.5)? + .write_string("°C")?; +writer.finish()?; +``` + +## Tracing Integration +For integration with the `tracing` ecosystem, see the [`tracing-dlt`](https://crates.io/crates/tracing-dlt) crate. + +## Features +- `trace_load_ctrl` - Enable DLT load control support (optional) + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](../LICENSE) for details. + +## Contributing +This project is part of [Eclipse OpenSOVD](https://projects.eclipse.org/projects/automotive.opensovd), but can be used independently. +See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. + +## References +- [COVESA DLT Daemon](https://github.com/COVESA/dlt-daemon) +- [DLT Protocol Specification](https://www.autosar.org/fileadmin/standards/foundation/19-11/AUTOSAR_PRS_LogAndTraceProtocol.pdf) diff --git a/dlt-sys/README.md b/dlt-sys/README.md new file mode 100644 index 0000000..d4a6e79 --- /dev/null +++ b/dlt-sys/README.md @@ -0,0 +1,43 @@ +# dlt-sys +[![Crates.io](https://img.shields.io/crates/v/dlt-sys.svg)](https://crates.io/crates/dlt-sys) +[![Documentation](https://docs.rs/dlt-sys/badge.svg)](https://docs.rs/dlt-sys) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](../LICENSE) +Low-level FFI bindings to the COVESA DLT (Diagnostic Log and Trace) C library (`libdlt`). + +## Overview +`dlt-sys` provides unsafe Rust bindings to the [COVESA DLT daemon](https://github.com/COVESA/dlt-daemon) C library. +This crate is intended to be used as a foundation for higher-level safe Rust abstractions (see [`dlt-rs`](https://crates.io/crates/dlt-rs)). +Please note that this is only implements functionality required for dlt-rs and does not cover the entire libdlt API. + +## Features +- Direct FFI bindings to `libdlt` functions +- Custom C wrapper for improved API ergonomics +- Support for all DLT log levels and message types +- Optional `trace_load_ctrl` feature for load control support + +## Prerequisites +- **libdlt** and its development headers must be installed on your system. + +## Usage +This is a low-level crate with unsafe APIs. Most users should use [`dlt-rs`](https://crates.io/crates/dlt-rs) instead for a safe, idiomatic Rust API. + +## Features +- `trace_load_ctrl` - Enable DLT load control support (may be required in some environments, depending on the DLT build time daemon configuration) +- `generate-bindings` - Regenerate bindings from C headers (development only) + +## Safety +All functions in this crate are `unsafe` as they directly call C library functions. Proper usage requires understanding of: +- DLT library initialization and cleanup +- Memory management across FFI boundaries +- Thread safety considerations +For safe abstractions, use the [`dlt-rs`](https://crates.io/crates/dlt-rs) crate. + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](../LICENSE) for details. +## Contributing +This project is part of [Eclipse OpenSOVD](https://projects.eclipse.org/projects/automotive.opensovd), but can be used independently. +See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. + +## References +- [COVESA DLT Daemon](https://github.com/COVESA/dlt-daemon) +- [DLT Protocol Specification](https://www.autosar.org/fileadmin/standards/foundation/19-11/AUTOSAR_PRS_LogAndTraceProtocol.pdf) diff --git a/tracing-dlt/README.md b/tracing-dlt/README.md new file mode 100644 index 0000000..edb569f --- /dev/null +++ b/tracing-dlt/README.md @@ -0,0 +1,58 @@ +# tracing-dlt +[![Crates.io](https://img.shields.io/crates/v/tracing-dlt.svg)](https://crates.io/crates/tracing-dlt) +[![Documentation](https://docs.rs/tracing-dlt/badge.svg)](https://docs.rs/tracing-dlt) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](../LICENSE) +A `tracing` subscriber/layer for sending structured logs and traces to the COVESA DLT daemon. + +## Overview +`tracing-dlt` provides a [tracing](https://github.com/tokio-rs/tracing) layer that forwards logs and spans to the [COVESA DLT daemon](https://github.com/COVESA/dlt-daemon). This allows you to use the standard `tracing` macros in your Rust application while outputting to DLT. + +## Features +- ✅ **Tracing integration** - Use standard `tracing::info!`, `tracing::debug!`, etc. +- ✅ **Structured logging** - Field types are preserved when sent to DLT +- ✅ **Span context** - Nested spans appear in log messages +- ✅ **Dynamic log levels** - Responds to DLT daemon log level changes +- ✅ **Thread-safe** - Safe for concurrent use across async tasks +- ✅ **Multiple contexts** - Support for different logging contexts per span + +> **Note:** The `tracing-dlt` and `dlt-rs` crates can be used simultaneously in the same application, as long as application registration is done through `tracing-dlt`. + +## Basic Example +```rust +use tracing_dlt::{DltLayer, DltId}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +fn main() -> Result<(), Box> { + // Initialize DLT layer + let dlt_layer = DltLayer::new( + &DltId::new(b"MBTI")?, + "My Beautiful Trace Ingestor" + )?; + // Set up tracing subscriber + tracing_subscriber::registry() + .with(dlt_layer) + .init(); + + // Use standard tracing macros + tracing::info!("Application started"); + tracing::warn!(temperature = 95.5, "High temperature detected"); + // Will be logged with context ID "SPCL" + tracing::warn!(dlt_context = "SPCL", "Log message on 'special' context id"); + Ok(()) +} +``` + +## Features +- `trace_load_ctrl` - Enable DLT load control support (optional) +- `dlt_layer_internal_logging` - Enable debug logging for the layer itself + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](../LICENSE) for details. + +## Contributing +This project is part of [Eclipse OpenSOVD](https://projects.eclipse.org/projects/automotive.opensovd), but can be used independently. +See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. + +## References +- [Tracing Framework](https://github.com/tokio-rs/tracing) +- [COVESA DLT Daemon](https://github.com/COVESA/dlt-daemon) +- [DLT Protocol Specification](https://www.autosar.org/fileadmin/standards/foundation/19-11/AUTOSAR_PRS_LogAndTraceProtocol.pdf) From 61c01cce99e9a0866c7bdceb68d0e69c4d1f997c Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 27 Nov 2025 09:44:19 +0100 Subject: [PATCH 9/9] dlt-rs: Improve constructor for DltId and add from_str_clamped Simplifies building the class and adds util function. Signed-off-by: Alexander Mohr --- .gitignore | 8 -------- dlt-rs/src/lib.rs | 35 ++++++++++++++++++++++++++++------- tracing-dlt/src/lib.rs | 23 +---------------------- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 1b76ab1..06515f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,5 @@ build /target -*.pcap* .idea .vscode/settings.json - -# Docker environment settings -.env - -# Allow generated documentation -!docs/ -!docs/** diff --git a/dlt-rs/src/lib.rs b/dlt-rs/src/lib.rs index 88ff402..50029a0 100644 --- a/dlt-rs/src/lib.rs +++ b/dlt-rs/src/lib.rs @@ -257,9 +257,10 @@ impl DltId { /// # Ok(()) /// # } /// ``` - pub fn new(bytes: &[u8; N]) -> Result { + pub fn new(bytes: &[u8]) -> Result { // Validate that N is between 1 and 4 - if N == 0 || N > DLT_ID_SIZE_USIZE { + let len = bytes.len(); + if bytes.is_empty() || len > DLT_ID_SIZE_USIZE { return Err(DltError::InvalidInput); } @@ -271,12 +272,31 @@ impl DltId { let mut padded = [0u8; DLT_ID_SIZE_USIZE]; // Indexing is safe here: function ensures N <= DLT_ID_SIZE by validation #[allow(clippy::indexing_slicing)] - padded[..N].copy_from_slice(&bytes[..N]); + padded[..len].copy_from_slice(&bytes[..len]); - Ok(Self { - bytes: padded, - len: N, - }) + Ok(Self { bytes: padded, len }) + } + + /// Construct a `DltId` from a string slice, clamping to 4 bytes + /// # Errors + /// Returns an error if the string is empty + /// # Example + /// ```no_run + /// # use dlt_rs::{DltId, DltError}; + /// # fn main() -> Result<(), DltError> { + /// let id = DltId::from_str_clamped("APPTOOLONG")?; + /// assert_eq!(id.as_str()?, "APPT"); + /// # Ok(()) + /// # } + /// ``` + pub fn from_str_clamped(id: &str) -> Result { + if id.is_empty() { + return Err(DltError::InvalidInput); + } + let bytes = id.as_bytes(); + let len = bytes.len().clamp(1, DLT_ID_SIZE_USIZE); + + DltId::new(bytes.get(0..len).ok_or(DltError::InvalidInput)?) } /// Get the ID as a string slice @@ -294,6 +314,7 @@ impl DltId { } } +/// Convert a string slice to a DLT ID, will yield an error if the string is too long or empty impl TryFrom<&str> for DltId { type Error = DltError; diff --git a/tracing-dlt/src/lib.rs b/tracing-dlt/src/lib.rs index 192be70..b3efab5 100644 --- a/tracing-dlt/src/lib.rs +++ b/tracing-dlt/src/lib.rs @@ -205,7 +205,7 @@ impl DltLayer { } // Create new context with custom ID - let ctx_id = Self::span_name_to_dlt_id(&custom_id)?; + let ctx_id = DltId::from_str_clamped(&custom_id)?; let context = Arc::new(self.app.create_context(&ctx_id, span_name)?); let mut cache = self.context_cache.write().map_err(|_| DltError::BadLock)?; @@ -215,27 +215,6 @@ impl DltLayer { Ok(context) } - /// Convert a span name to a valid DLT context ID - /// - /// Takes the first 1-4 bytes of the span name, uppercase. - /// If the name is longer, it's truncated. If shorter, it's used as-is. - fn span_name_to_dlt_id(name: &str) -> Result { - if name.is_empty() { - return Err(DltError::InvalidInput); - } - let bytes = name.as_bytes(); - let len = bytes.len().clamp(1, dlt_rs::DLT_ID_SIZE_USIZE); - - let get = |i| bytes.get(i).copied().ok_or(DltError::InvalidInput); - - match len { - 1 => DltId::new(&[get(0)?]), - 2 => DltId::new(&[get(0)?, get(1)?]), - 3 => DltId::new(&[get(0)?, get(1)?, get(2)?]), - _ => DltId::new(&[get(0)?, get(1)?, get(2)?, get(3)?]), - } - } - #[cfg(feature = "dlt_layer_internal_logging")] fn log_dlt_error(metadata: &tracing_core::Metadata, level: tracing::Level, e: DltSysError) { eprintln!("DLT error occurred: {e:?}");