From 33dd60beaf32dd19d4c393d5515de0bdfea9051d Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Tue, 25 Nov 2025 17:00:07 -0500 Subject: [PATCH 01/19] restore UnixEndPoint --- src/StatsdClient/UnixEndPoint.cs | 152 +++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/StatsdClient/UnixEndPoint.cs diff --git a/src/StatsdClient/UnixEndPoint.cs b/src/StatsdClient/UnixEndPoint.cs new file mode 100644 index 00000000..3ee6b52b --- /dev/null +++ b/src/StatsdClient/UnixEndPoint.cs @@ -0,0 +1,152 @@ +// From https://raw.githubusercontent.com/mono/mono/master/mcs/class/Mono.Posix/Mono.Unix/UnixEndPoint.cs + +// +// Mono.Unix.UnixEndPoint: EndPoint derived class for AF_UNIX family sockets. +// +// Authors: +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// +// (C) 2003 Ximian, Inc (http://www.ximian.com) +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Mono.Unix +{ + internal class UnixEndPoint : EndPoint + { + private string filename; + + public UnixEndPoint(string filename) + { + if (filename == null) + { + throw new ArgumentNullException("filename"); + } + + if (filename == string.Empty) + { + throw new ArgumentException("Cannot be empty.", "filename"); + } + + this.filename = filename; + } + + public string Filename + { + get + { + return (filename); + } + + set + { + filename = value; + } + } + + public override AddressFamily AddressFamily + { + get { return AddressFamily.Unix; } + } + + public override EndPoint Create(SocketAddress socketAddress) + { + /* + * Should also check this + * + int addr = (int) AddressFamily.Unix; + if (socketAddress [0] != (addr & 0xFF)) + throw new ArgumentException ("socketAddress is not a unix socket address."); + + if (socketAddress [1] != ((addr & 0xFF00) >> 8)) + throw new ArgumentException ("socketAddress is not a unix socket address."); + */ + + if (socketAddress.Size == 2) + { + // Empty filename. + // Probably from RemoteEndPoint which on linux does not return the file name. + UnixEndPoint uep = new UnixEndPoint("a"); + uep.filename = string.Empty; + return uep; + } + + int size = socketAddress.Size - 2; + byte[] bytes = new byte[size]; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = socketAddress[i + 2]; + // There may be junk after the null terminator, so ignore it all. + if (bytes[i] == 0) + { + size = i; + break; + } + } + + string name = Encoding.UTF8.GetString(bytes, 0, size); + return new UnixEndPoint(name); + } + + public override SocketAddress Serialize() + { + byte[] bytes = Encoding.UTF8.GetBytes(filename); + SocketAddress sa = new SocketAddress(AddressFamily, 2 + bytes.Length + 1); + // sa [0] -> family low byte, sa [1] -> family high byte + for (int i = 0; i < bytes.Length; i++) + { + sa[2 + i] = bytes[i]; + } + + // NULL suffix for non-abstract path + sa[2 + bytes.Length] = 0; + + return sa; + } + + public override string ToString() + { + return (filename); + } + + public override int GetHashCode() + { + return filename.GetHashCode(); + } + + public override bool Equals(object o) + { + UnixEndPoint other = o as UnixEndPoint; + if (other == null) + { + return false; + } + + return (other.filename == filename); + } + } +} From 487f744fb5f3565d2df6a2c09eafff546f209e06 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 17:00:03 -0500 Subject: [PATCH 02/19] update git ignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a3e3744b..2148d1ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .vs/ .vscode/ .idea/ +runtimes/ +artifacts/ +build/ [bB]in/ [oO]bj/ artifacts/ From 8eb8f44e903417ebce9f212b04489b4f0ca147cb Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 17:47:52 -0500 Subject: [PATCH 03/19] add C project (native library) --- src/StatsdClient.Native/CMakeLists.txt | 6 ++ src/StatsdClient.Native/README.md | 116 +++++++++++++++++++++++++ src/StatsdClient.Native/build.sh | 46 ++++++++++ src/StatsdClient.Native/fs.c | 10 +++ src/StatsdClient.Native/fs.h | 6 ++ 5 files changed, 184 insertions(+) create mode 100644 src/StatsdClient.Native/CMakeLists.txt create mode 100644 src/StatsdClient.Native/README.md create mode 100644 src/StatsdClient.Native/build.sh create mode 100644 src/StatsdClient.Native/fs.c create mode 100644 src/StatsdClient.Native/fs.h diff --git a/src/StatsdClient.Native/CMakeLists.txt b/src/StatsdClient.Native/CMakeLists.txt new file mode 100644 index 00000000..7137662c --- /dev/null +++ b/src/StatsdClient.Native/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.15) +project(fs C) + +add_library(fs SHARED fs.c fs.h) + +install(TARGETS fs DESTINATION lib) diff --git a/src/StatsdClient.Native/README.md b/src/StatsdClient.Native/README.md new file mode 100644 index 00000000..93671e31 --- /dev/null +++ b/src/StatsdClient.Native/README.md @@ -0,0 +1,116 @@ +# libfs - Native Library for Linux File System Operations + +This directory contains a small C library (`libfs`) that provides cross-platform access to file inode numbers on Linux. + +## Purpose + +The library wraps the `stat()` system call to retrieve inode numbers, avoiding cross-platform P/Invoke compatibility issues when calling libc's `stat()` directly from C#. + +## Building + +### Using Docker (Recommended) + +Build all 4 Linux variants using Docker from the repository root: + +```bash +# Build all Linux variants +./build-and-test.sh --platform linux + +# Build and test all Linux variants +./build-and-test.sh --test --platform linux +``` + +**Supported RIDs** (per [.NET RID catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids)): +- `linux-x64` - Standard x64 Linux with glibc +- `linux-musl-x64` - Alpine x64 Linux with musl libc +- `linux-arm64` - Standard ARM64 Linux with glibc +- `linux-musl-arm64` - Alpine ARM64 Linux with musl libc + +**Output:** `runtimes/{rid}/native/libfs.so` + +### Local Build (Linux/WSL only) + +Build without Docker: + +```bash +# Build for specific RID (outputs to runtimes/{rid}/native/) +./src/StatsdClient.Native/build.sh linux-x64 +./src/StatsdClient.Native/build.sh linux-musl-x64 +./src/StatsdClient.Native/build.sh linux-arm64 +./src/StatsdClient.Native/build.sh linux-musl-arm64 + +# Build for local development (outputs to src/StatsdClient.Native/build/) +./src/StatsdClient.Native/build.sh +``` + +**Note:** Local builds compile for the current system only. Use Docker to cross-compile for different architectures or libc variants. + +### Testing + +**Using build-and-test.sh:** +```bash +# Test all Linux variants in Docker +./build-and-test.sh --test --platform linux + +# Test specific framework on native platform +./build-and-test.sh --test --platform native --framework net8.0 +``` + +**Using dotnet test directly:** + +After building, run tests using standard .NET tooling. **Always specify `--framework`** to avoid parallel test execution conflicts: + +```bash +# Run native library tests for a specific framework +dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework net8.0 \ + --filter 'FullyQualifiedName~NativeLibraryTests' + +# Run all tests for a specific framework +dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework net8.0 +``` + +The .NET runtime automatically selects the correct native library based on the current platform: +- On glibc systems: uses `runtimes/linux-{arch}/native/libfs.so` +- On musl systems (Alpine): uses `runtimes/linux-musl-{arch}/native/libfs.so` + +## API + +### C API + +```c +int get_inode(const char* path, unsigned long long* ino); +``` + +Returns: +- `0` on success, with `ino` populated with the inode number +- `-1` on failure (e.g., file not found, permission denied) + +### .NET API + +```csharp +using StatsdClient.Native; + +if (NativeInode.TryGetInode("/path/to/file", out ulong inode)) +{ + Console.WriteLine($"Inode: {inode}"); +} +else +{ + Console.WriteLine("Failed to get inode (not on Linux or file not found)"); +} +``` + +The .NET wrapper automatically detects the platform and returns `false` on Windows/macOS. + +## CI/CD + +The GitHub Actions workflow (`.github/workflows/build-and-test.yml`) automatically builds all variants using Docker containers and includes them in the test runs and NuGet package. + +## Files + +- `fs.h` - Header file +- `fs.c` - Implementation +- `CMakeLists.txt` - CMake build configuration +- `README.md` - This file diff --git a/src/StatsdClient.Native/build.sh b/src/StatsdClient.Native/build.sh new file mode 100644 index 00000000..d260bf7f --- /dev/null +++ b/src/StatsdClient.Native/build.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +# Simple build script for libfs +# Usage: ./build.sh [rid] +# Example: ./build.sh linux-glibc-x64 +# If no RID is provided, builds to src/StatsdClient.Native/build/ (for local development) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$SCRIPT_DIR/../.." +RID="$1" + +echo "Building libfs..." + +if [ -n "$RID" ]; then + # Build to runtimes/{rid}/native/ for consistency with Docker builds + OUTPUT_DIR="$REPO_ROOT/runtimes/$RID/native" + BUILD_DIR="$OUTPUT_DIR/build" + mkdir -p "$BUILD_DIR" + cd "$BUILD_DIR" + + # Configure and build + cmake "$SCRIPT_DIR" + cmake --build . --config Release + + # Move the library to the native directory (one level up from build) + mv libfs.so "$OUTPUT_DIR/libfs.so" + + # Clean up build artifacts + cd "$OUTPUT_DIR" + rm -rf build + + echo "Build complete. Library at: $OUTPUT_DIR/libfs.so" +else + # No RID specified - build to local build directory for development + OUTPUT_DIR="$SCRIPT_DIR" + mkdir -p "$OUTPUT_DIR/build" + cd "$OUTPUT_DIR/build" + + # Configure and build + cmake "$SCRIPT_DIR" + cmake --build . --config Release + + echo "Build complete. Library at: $OUTPUT_DIR/build/libfs.so" + echo "Note: To build for testing, specify a RID: ./build.sh linux-glibc-x64" +fi diff --git a/src/StatsdClient.Native/fs.c b/src/StatsdClient.Native/fs.c new file mode 100644 index 00000000..429babc0 --- /dev/null +++ b/src/StatsdClient.Native/fs.c @@ -0,0 +1,10 @@ +#include "fs.h" +#include + +int get_inode(const char* path, unsigned long long* ino) +{ + struct stat s; + if (stat(path, &s) != 0) return -1; + *ino = (unsigned long long)s.st_ino; + return 0; +} diff --git a/src/StatsdClient.Native/fs.h b/src/StatsdClient.Native/fs.h new file mode 100644 index 00000000..4a9bffaf --- /dev/null +++ b/src/StatsdClient.Native/fs.h @@ -0,0 +1,6 @@ +#ifndef FS_H +#define FS_H + +int get_inode(const char* path, unsigned long long* ino); + +#endif From 73f8bcc629e55757bf372444bc83185731ab4b3c Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 17:49:07 -0500 Subject: [PATCH 04/19] use p/invoke to call into native library to get inode --- src/StatsdClient/IFileSystem.cs | 11 +++--- src/StatsdClient/NativeMethods.cs | 64 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 src/StatsdClient/NativeMethods.cs diff --git a/src/StatsdClient/IFileSystem.cs b/src/StatsdClient/IFileSystem.cs index bde45190..d168819f 100644 --- a/src/StatsdClient/IFileSystem.cs +++ b/src/StatsdClient/IFileSystem.cs @@ -1,6 +1,6 @@ using System; using System.IO; -using Mono.Unix.Native; +using System.Runtime.InteropServices; namespace StatsdClient { @@ -81,13 +81,14 @@ public TextReader OpenText(string path) /// True if the file stat was successful, false otherwise public bool TryStat(string path, out ulong inode) { - if (Environment.OSVersion.Platform == PlatformID.Unix && - Syscall.stat(path, out var stat) > 0) +#if !NETFRAMEWORK // Unix Domain Sockets not supported on .NET Framework (always runs on Windows). + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - inode = stat.st_ino; - return true; + return NativeMethods.TryGetInode(path, out inode); } +#endif + // Unsupported .NET runtime or OS inode = 0; return false; } diff --git a/src/StatsdClient/NativeMethods.cs b/src/StatsdClient/NativeMethods.cs new file mode 100644 index 00000000..b872335a --- /dev/null +++ b/src/StatsdClient/NativeMethods.cs @@ -0,0 +1,64 @@ +#if !NETFRAMEWORK + +using System; +using System.Runtime.InteropServices; + +namespace StatsdClient +{ + /// + /// P/Invoke wrapper for libfs native library to retrieve file inodes on Linux. + /// + internal static class NativeMethods + { + private const string LibraryName = "fs"; + + /// + /// Gets a value indicating whether the native library is supported on the current platform (Linux only). + /// + public static bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + /// + /// Attempts to get the inode number for the specified file path. + /// + /// The file path. + /// The inode number if successful, 0 otherwise. + /// true if the inode was successfully retrieved, false otherwise. + public static bool TryGetInode(string path, out ulong inode) + { + if (!IsSupported) + { + inode = 0; + return false; + } + + try + { + return GetInode(path, out inode) == 0; + } + catch (DllNotFoundException) + { + // Native library not available + inode = 0; + return false; + } + catch (EntryPointNotFoundException) + { + // Function not found in library + inode = 0; + return false; + } + } + + /// + /// Gets the inode number for a file path using the native libfs library. + /// + /// The file path. + /// The inode number if successful. + /// 0 on success, -1 on failure. + [DllImport(LibraryName, EntryPoint = "get_inode", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + private static extern int GetInode( + string path, + out ulong inode); + } +} +#endif From 7edda64e4b2ee327411c0f9fbe396e6c7dc64b81 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 17:50:50 -0500 Subject: [PATCH 05/19] add tests for calling native library from C# --- .../StatsdClient.Tests/NativeLibraryTests.cs | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 tests/StatsdClient.Tests/NativeLibraryTests.cs diff --git a/tests/StatsdClient.Tests/NativeLibraryTests.cs b/tests/StatsdClient.Tests/NativeLibraryTests.cs new file mode 100644 index 00000000..9a504495 --- /dev/null +++ b/tests/StatsdClient.Tests/NativeLibraryTests.cs @@ -0,0 +1,233 @@ +#if !NETFRAMEWORK + +using System; +using System.IO; +using System.Runtime.InteropServices; +using NUnit.Framework; +using StatsdClient; + +namespace Tests +{ + /// + /// Integration tests for the native libfs library on Linux. + /// These tests interact directly with the native library rather than mocking. + /// + [TestFixture] + public class NativeLibraryTests + { + private FileSystem _fileSystem; + + [SetUp] + public void SetUp() + { + _fileSystem = new FileSystem(); + } + + [Test] + public void TryStat_WithValidFile_ReturnsTrue() + { + // Only run on Linux where the native library is available + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + // Use a file that should always exist on Linux + var testPath = "/proc/self/exe"; + + var result = _fileSystem.TryStat(testPath, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for valid file"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + } + + [Test] + public void TryStat_WithNonExistentFile_ReturnsFalse() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var testPath = "/this/path/does/not/exist/file.txt"; + + var result = _fileSystem.TryStat(testPath, out ulong inode); + + Assert.IsFalse(result, "TryStat should fail for non-existent file"); + Assert.AreEqual(0UL, inode, "Inode should be 0 on failure"); + } + + [Test] + public void TryStat_WithSameFileTwice_ReturnsSameInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var testPath = "/proc/self/exe"; + + var result1 = _fileSystem.TryStat(testPath, out ulong inode1); + var result2 = _fileSystem.TryStat(testPath, out ulong inode2); + + Assert.IsTrue(result1, "First TryStat should succeed"); + Assert.IsTrue(result2, "Second TryStat should succeed"); + Assert.AreEqual(inode1, inode2, "Same file should have same inode"); + } + + [Test] + public void TryStat_WithDifferentFiles_ReturnsDifferentInodes() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var path1 = "/proc/self/exe"; + var path2 = "/proc/self/cmdline"; + + var result1 = _fileSystem.TryStat(path1, out ulong inode1); + var result2 = _fileSystem.TryStat(path2, out ulong inode2); + + Assert.IsTrue(result1, "First TryStat should succeed"); + Assert.IsTrue(result2, "Second TryStat should succeed"); + Assert.AreNotEqual(inode1, inode2, "Different files should have different inodes"); + } + + [Test] + public void TryStat_WithTemporaryFile_ReturnsValidInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + // Create a temporary file + var tempFile = Path.GetTempFileName(); + + try + { + File.WriteAllText(tempFile, "test content"); + + var result = _fileSystem.TryStat(tempFile, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for temp file"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + [Test] + public void TryStat_WithDirectory_ReturnsValidInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var testPath = "/proc/self"; + + var result = _fileSystem.TryStat(testPath, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for directory"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0 for directory"); + } + + [Test] + public void TryStat_OnNonLinux_ReturnsFalse() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on non-Linux platforms"); + } + + var testPath = "/some/path"; + + var result = _fileSystem.TryStat(testPath, out ulong inode); + + Assert.IsFalse(result, "TryStat should return false on non-Linux platforms"); + Assert.AreEqual(0UL, inode, "Inode should be 0 on non-Linux platforms"); + } + + [Test] + public void TryStat_VerifyHostCgroupNamespaceInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var cgroupNsPath = "/proc/self/ns/cgroup"; + + // This test verifies we can read the cgroup namespace inode + // It will be 0xEFFFFFFB (4026531835) if running in the host namespace + var result = _fileSystem.TryStat(cgroupNsPath, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for cgroup namespace"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + + // Note: We don't assert the specific value because it depends on whether + // we're running in a container or on the host + Console.WriteLine($"Cgroup namespace inode: {inode} (0x{inode:X})"); + } + + [Test] + public void TryStat_WithSymlink_ReturnsTargetInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + // /proc/self/exe is typically a symlink to the actual executable + var symlinkPath = "/proc/self/exe"; + + var result = _fileSystem.TryStat(symlinkPath, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for symlink"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + + // stat() follows symlinks by default, so we should get the target's inode + // We can't easily verify this without lstat support, but at least verify it works + } + + [Test] + public void NativeMethods_DirectCall_WithValidFile() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var testPath = "/proc/self/exe"; + + // Test the NativeMethods wrapper directly + var result = NativeMethods.TryGetInode(testPath, out ulong inode); + + Assert.IsTrue(result, "NativeMethods.TryGetInode should succeed"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + } + + [Test] + public void NativeMethods_IsSupported_ReflectsPlatform() + { + var isSupported = NativeMethods.IsSupported; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.IsTrue(isSupported, "IsSupported should be true on Linux"); + } + else + { + Assert.IsFalse(isSupported, "IsSupported should be false on non-Linux"); + } + } + } +} +#endif From f49db2225f3fecae899c82607d383356e3bfc590 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 17:49:56 -0500 Subject: [PATCH 06/19] add scripts to build and test native library in docker --- .dockerignore | 38 +++++ Dockerfile.linux | 74 ++++++++++ build-and-test.sh | 231 +++++++++++++++++++++++++++++++ src/StatsdClient.Native/build.sh | 0 4 files changed, 343 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile.linux create mode 100755 build-and-test.sh mode change 100644 => 100755 src/StatsdClient.Native/build.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..e8d773b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Build outputs +**/bin/ +**/obj/ +**/out/ + +# Native build outputs +**/build/ +runtimes/ + +# IDE / Editor +.vs/ +.vscode/ +*.suo +*.user +*.userosscache +*.sln.docstates + +# Test results +TestResults/ +*.trx +*.coverage + +# NuGet +*.nupkg +*.snupkg +packages/ + +# Misc +.git/ +.gitignore +.github/ +*.md +*.log +*.zip + +# Keep these files +!README.md +!CLAUDE.md diff --git a/Dockerfile.linux b/Dockerfile.linux new file mode 100644 index 00000000..8c7c11d1 --- /dev/null +++ b/Dockerfile.linux @@ -0,0 +1,74 @@ +# Multi-stage Dockerfile for building and testing libfs for all Linux variants +# Usage: See build-and-test.sh + +ARG BASE_IMAGE +ARG DOTNET_TEST_ARGS="" +FROM ${BASE_IMAGE} AS builder + +# Install build tools based on the base image +RUN if command -v apt-get > /dev/null 2>&1; then \ + apt-get update && apt-get install -y cmake build-essential && rm -rf /var/lib/apt/lists/*; \ + elif command -v apk > /dev/null 2>&1; then \ + apk add --no-cache cmake make gcc g++ musl-dev bash; \ + fi + +WORKDIR /build + +# Copy only the native library source +COPY src/StatsdClient.Native/ ./src/StatsdClient.Native/ + +# Build the library using the build script +ARG RID +RUN chmod +x src/StatsdClient.Native/build.sh && \ + bash src/StatsdClient.Native/build.sh "$RID" + +# The output libfs.so is in /build/runtimes/${RID}/native/libfs.so + +# ============================================================ +# Test stage (optional - only built with --target tester) +# ============================================================ +FROM ${BASE_IMAGE} AS tester + +WORKDIR /build + +# Copy only project files for restore (better caching) +COPY src/StatsdClient/StatsdClient.csproj ./src/StatsdClient/ +COPY tests/StatsdClient.Tests/StatsdClient.Tests.csproj ./tests/StatsdClient.Tests/ + +# Restore dependencies (cached separately from source changes) +RUN dotnet restore src/StatsdClient/StatsdClient.csproj +RUN dotnet restore tests/StatsdClient.Tests/StatsdClient.Tests.csproj + +# Copy remaining source files +COPY src/ ./src/ +COPY tests/ ./tests/ +COPY stylecop.json stylecop.ruleset ./ + +# Copy the pre-built native library from builder stage +ARG RID +COPY --from=builder /build/runtimes/${RID}/native/libfs.so /build/runtimes/${RID}/native/libfs.so + +# Build the .NET project (without restore since already done) +RUN dotnet build src/StatsdClient/StatsdClient.csproj -c Release --no-restore + +# Build the test project (without restore since already done) +RUN dotnet build tests/StatsdClient.Tests/StatsdClient.Tests.csproj -c Release --no-restore + +# Copy library to the location where .NET runtime will find it during tests +# The library is already in the correct RID directory (linux-x64, linux-musl-x64, etc.) +# .NET runtime will automatically select the correct variant based on the environment +# As a workaround for testing, also copy to the root of the test output directory +ARG RID +RUN cp /build/tests/StatsdClient.Tests/bin/Release/net8.0/runtimes/${RID}/native/libfs.so \ + /build/tests/StatsdClient.Tests/bin/Release/net8.0/libfs.so || true + +# Run the native library tests +# DOTNET_TEST_ARGS can be used to pass additional arguments (e.g., --filter) +ARG DOTNET_TEST_ARGS="--filter FullyQualifiedName~NativeLibraryTests" +RUN dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework net8.0 \ + -c Release \ + -v normal \ + --no-build \ + --logger 'console;verbosity=normal' \ + ${DOTNET_TEST_ARGS} diff --git a/build-and-test.sh b/build-and-test.sh new file mode 100755 index 00000000..ad0a2979 --- /dev/null +++ b/build-and-test.sh @@ -0,0 +1,231 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Color output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Default values +RUN_TESTS=false +PLATFORM="" +FRAMEWORK="" +EXTRA_TEST_ARGS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --test) + RUN_TESTS=true + shift + ;; + --platform) + PLATFORM="$2" + shift 2 + ;; + --framework) + FRAMEWORK="$2" + shift 2 + ;; + --) + # Everything after -- is passed to dotnet test + shift + EXTRA_TEST_ARGS=("$@") + break + ;; + --help) + echo "Usage: $0 [--test] [--platform ] [--framework ] [-- ]" + echo "" + echo "Options:" + echo " --test Run tests after building (default: build only)" + echo " --platform Target platform:" + echo " native - Use native dotnet build/test (Windows/macOS)" + echo " linux - Use Docker to build all 4 Linux variants" + echo " (default: auto-detect based on OS)" + echo " --framework Target framework moniker (e.g., net8.0, net48)" + echo " If not specified, tests all frameworks sequentially" + echo " Prevents parallel test execution conflicts" + echo " -- Everything after -- is passed directly to dotnet test" + echo "" + echo "Examples:" + echo " $0 --test --platform native --framework net8.0 # Build and test .NET for net8.0 only" + echo " $0 --test --platform native # Build and test all frameworks sequentially" + echo " $0 --platform linux # Build Linux native libs in Docker" + echo " $0 --test --platform linux # Build and test all Linux variants in Docker" + echo " $0 --test --platform linux -- --filter FullyQualifiedName~NativeLibraryTests" + echo " # Test only NativeLibraryTests in Docker" + echo " $0 # Auto-detect platform, build only" + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + echo "Run '$0 --help' for usage information" + exit 1 + ;; + esac +done + +# Auto-detect platform if not specified +if [ -z "$PLATFORM" ]; then + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + PLATFORM="linux" + echo -e "${YELLOW}Auto-detected platform: linux${NC}" + elif [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + PLATFORM="native" + echo -e "${YELLOW}Auto-detected platform: native${NC}" + else + echo -e "${RED}Could not auto-detect platform. Please specify --platform ${NC}" + exit 1 + fi +fi + +# Validate platform +if [[ "$PLATFORM" != "native" && "$PLATFORM" != "linux" ]]; then + echo -e "${RED}Invalid platform: $PLATFORM${NC}" + echo "Must be 'native' or 'linux'" + exit 1 +fi + +echo -e "${BLUE}Starting build process...${NC}" +echo "" + +# Native platform (Windows/macOS - .NET only, no native libs) +if [ "$PLATFORM" = "native" ]; then + echo -e "${BLUE}Building .NET project (native platform)...${NC}" + dotnet build src/StatsdClient/StatsdClient.csproj -c Release + + if [ "$RUN_TESTS" = true ]; then + if [ -n "$FRAMEWORK" ]; then + # Test a single framework + echo "" + echo -e "${BLUE}Running tests for framework: $FRAMEWORK${NC}" + dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework "$FRAMEWORK" \ + -c Release \ + --no-build \ + "${EXTRA_TEST_ARGS[@]}" + else + # Test all frameworks sequentially + echo "" + echo -e "${BLUE}Running tests for all frameworks sequentially (to avoid named pipe conflicts)...${NC}" + + # Determine which frameworks to test based on OS + FRAMEWORKS="netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0" + + # Add .NET Framework on Windows + if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + FRAMEWORKS="net48 $FRAMEWORKS" + fi + + # Run tests for each framework sequentially + for tfm in $FRAMEWORKS; do + echo "" + echo -e "${BLUE}Testing framework: $tfm${NC}" + dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework $tfm \ + -c Release \ + --no-build \ + "${EXTRA_TEST_ARGS[@]}" + done + fi + fi + + echo "" + echo -e "${GREEN}✓ Native build complete!${NC}" + exit 0 +fi + +# Linux platform (Docker - build native libs for all variants) +if [ "$PLATFORM" = "linux" ]; then + # Check if Docker is available + if ! command -v docker &> /dev/null; then + echo -e "${RED}Error: Docker is not installed or not in PATH${NC}" + exit 1 + fi + + export DOCKER_BUILDKIT=1 + echo "Docker version:" + docker --version + echo "" + + build_and_test_variant() { + local arch=$1 + local libc=$2 + local base_image=$3 + + # Construct proper RID based on official RID catalog + # https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids + local rid="linux-${arch}" + if [ "$libc" = "musl" ]; then + rid="linux-musl-${arch}" + fi + + echo -e "${BLUE}Building $rid...${NC}" + + # Determine Docker platform + local platform="" + if [ "$arch" = "x64" ]; then + platform="linux/amd64" + elif [ "$arch" = "arm64" ]; then + platform="linux/arm64" + fi + + # Determine build target + # For ARM64: only build native lib (skip tests due to QEMU emulation limitations) + local target="builder" + if [ "$RUN_TESTS" = true ] && [ "$arch" = "x64" ]; then + target="tester" + fi + + if [ "$RUN_TESTS" = true ] && [ "$arch" = "arm64" ]; then + echo -e "${YELLOW}Note: Skipping .NET tests for ARM64 due to QEMU emulation limitations${NC}" + fi + + # Build the Docker image + # Convert array to string for passing to Docker + local test_args_string="${EXTRA_TEST_ARGS[*]}" + + docker build \ + --platform "$platform" \ + --build-arg BASE_IMAGE="$base_image" \ + --build-arg RID="$rid" \ + --build-arg DOTNET_TEST_ARGS="$test_args_string" \ + --target "$target" \ + -t "libfs-${target}:${rid}" \ + -f Dockerfile.linux \ + . + + # Create a temporary container to extract the built library + local container_id=$(docker create --platform "$platform" "libfs-${target}:${rid}") + + # Extract the built library + mkdir -p "$SCRIPT_DIR/runtimes/$rid/native" + docker cp "$container_id:/build/runtimes/$rid/native/libfs.so" "$SCRIPT_DIR/runtimes/$rid/native/libfs.so" + + # Clean up + docker rm "$container_id" > /dev/null + + if [ "$RUN_TESTS" = true ] && [ "$arch" = "x64" ]; then + echo -e "${GREEN}✓ Built and tested $rid${NC}" + else + echo -e "${GREEN}✓ Built $rid${NC}" + fi + echo + } + + # Build all combinations + build_and_test_variant "x64" "glibc" "mcr.microsoft.com/dotnet/sdk:10.0" + build_and_test_variant "x64" "musl" "mcr.microsoft.com/dotnet/sdk:10.0-alpine" + build_and_test_variant "arm64" "glibc" "mcr.microsoft.com/dotnet/sdk:10.0" + build_and_test_variant "arm64" "musl" "mcr.microsoft.com/dotnet/sdk:10.0-alpine" + + echo "" + echo -e "${GREEN}All Linux builds complete!${NC}" + echo "Outputs in $SCRIPT_DIR/runtimes/" + echo "" + tree "$SCRIPT_DIR/runtimes/" +fi diff --git a/src/StatsdClient.Native/build.sh b/src/StatsdClient.Native/build.sh old mode 100644 new mode 100755 From 139461c2086b7f8d979fcb4b6265be2c83383620 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 18:01:08 -0500 Subject: [PATCH 07/19] add .gitattributes to fix bash script line endings on Windows --- .gitattributes | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..896a6fbd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +# Set line endings to LF, even on Windows. Otherwise, execution within Docker fails. +# See https://help.github.com/articles/dealing-with-line-endings/ +*.sh text eol=lf +*.bash text eol=lf From d446c7537c28ae864742c876fa6acb9ce3a523c9 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 17:51:08 -0500 Subject: [PATCH 08/19] build native library in github workflow --- .github/workflows/build-and-test.yml | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 00a0dfbd..fd6fa8b2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,9 +8,59 @@ on: workflow_dispatch: jobs: + build-native-linux: + name: Build libfs for ${{ matrix.libc }}-${{ matrix.arch }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + libc: glibc + container: mcr.microsoft.com/dotnet/sdk:8.0 + install_cmd: apt-get update && apt-get install -y cmake build-essential + - arch: x64 + libc: musl + container: mcr.microsoft.com/dotnet/sdk:8.0-alpine + install_cmd: apk add cmake make gcc g++ musl-dev + - arch: arm64 + libc: glibc + container: mcr.microsoft.com/dotnet/sdk:8.0-arm64v8 + install_cmd: apt-get update && apt-get install -y cmake build-essential + - arch: arm64 + libc: musl + container: mcr.microsoft.com/dotnet/sdk:8.0-alpine-arm64v8 + install_cmd: apk add cmake make gcc g++ musl-dev + + container: ${{ matrix.container }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install build tools + run: ${{ matrix.install_cmd }} + + - name: Build native library + run: | + mkdir -p build && cd build + cmake ../src/StatsdClient.Native + cmake --build . --config Release + cd .. + + mkdir -p runtimes/linux-${{ matrix.libc }}-${{ matrix.arch }}/native + cp build/libfs.so runtimes/linux-${{ matrix.libc }}-${{ matrix.arch }}/native/ + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: native-linux-${{ matrix.libc }}-${{ matrix.arch }} + path: runtimes/ + unit-tests: name: ${{ matrix.framework }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} + needs: build-native-linux permissions: actions: read contents: read @@ -67,6 +117,13 @@ jobs: with: fetch-depth: 0 + - name: Download native libraries + uses: actions/download-artifact@v4 + with: + pattern: native-linux-* + path: runtimes/ + merge-multiple: true + - name: Install libssl on Linux # .NET 5.0 and below want libssl v1, which is not available on Ubuntu 24.04 normally if: runner.os == 'Linux' From 145845ef26fcdb06e633d9cdcd54283d8e19d2b5 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 17:50:11 -0500 Subject: [PATCH 09/19] include native library in nuget package --- src/StatsdClient/StatsdClient.csproj | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/StatsdClient/StatsdClient.csproj b/src/StatsdClient/StatsdClient.csproj index c013ff1a..9ffcbf3a 100644 --- a/src/StatsdClient/StatsdClient.csproj +++ b/src/StatsdClient/StatsdClient.csproj @@ -47,4 +47,41 @@ + + + + + + + + + + + + + + + + + From 55dbc7cb5013c7622b41daeb8e6a65315384748f Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Mon, 1 Dec 2025 17:00:51 -0500 Subject: [PATCH 10/19] remove Mono.Unix dependency --- src/StatsdClient/StatsdClient.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/StatsdClient/StatsdClient.csproj b/src/StatsdClient/StatsdClient.csproj index 9ffcbf3a..f4f23a90 100644 --- a/src/StatsdClient/StatsdClient.csproj +++ b/src/StatsdClient/StatsdClient.csproj @@ -38,7 +38,6 @@ - From 73aa5a228f761a4a7eb13c43afea006e2a24833c Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Mon, 1 Dec 2025 18:34:36 -0500 Subject: [PATCH 11/19] add BUILD.md --- BUILD.md | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 BUILD.md diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000..c8972413 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,253 @@ +# Building and Testing + +This document describes how to build and test the DogStatsD C# client library. + +## Prerequisites + +- [.NET SDK 10.0 or above](https://dotnet.microsoft.com/download) +- [Docker](https://www.docker.com/get-started) (required for building Linux native libraries) + +## Quick Start + +```bash +# Build the .NET library +dotnet build + +# Run tests for a specific framework +dotnet test --framework net8.0 + +# Build all Linux native libraries using Docker +./build-and-test.sh --platform linux + +# Pack the NuGet package with all native libraries +dotnet pack src/StatsdClient/StatsdClient.csproj -c Release +``` + +## Building + +### .NET Library + +Build the main .NET library: + +```bash +# Restore dependencies +dotnet restore + +# Build all projects +dotnet build + +# Build specific project +dotnet build src/StatsdClient/StatsdClient.csproj + +# Build for specific configuration +dotnet build -c Release + +# Build for specific target framework +dotnet build src/StatsdClient/StatsdClient.csproj -f netstandard2.0 +``` + +### Native Library (Linux only) + +The repository includes a native C library (`libfs`) for Linux file system operations. This library must be built for multiple Linux variants to support different distributions. + +#### Using Docker (Recommended) + +Build all 4 Linux variants using Docker: + +```bash +# Build all variants (linux-x64, linux-musl-x64, linux-arm64, linux-musl-arm64) +./build-and-test.sh --platform linux + +# Build and test all variants +./build-and-test.sh --test --platform linux +``` + +**Output:** `runtimes/{rid}/native/libfs.so` + +**Supported RIDs:** +- `linux-x64` - Standard x64 Linux (glibc) +- `linux-musl-x64` - Alpine x64 Linux (musl libc) +- `linux-arm64` - Standard ARM64 Linux (glibc) +- `linux-musl-arm64` - Alpine ARM64 Linux (musl libc) + +#### Local Build (Linux/WSL only) + +Build without Docker on Linux or WSL: + +```bash +# Build for specific RID (outputs to runtimes/{rid}/native/) +./src/StatsdClient.Native/build.sh linux-x64 + +# Build for local development only (outputs to src/StatsdClient.Native/build/) +./src/StatsdClient.Native/build.sh +``` + +**Note:** Local builds compile for the current system architecture only. Docker is required to cross-compile for different architectures (x64/ARM64) or libc variants (glibc/musl). + +## Testing + +### Important: Framework Selection + +**Always specify `--framework` when running tests.** Running tests without a framework specification will run all target frameworks in parallel, causing conflicts due to shared named pipes. + +### Using build-and-test.sh + +```bash +# Test specific framework on native platform (Windows/macOS/Linux) +./build-and-test.sh --test --platform native --framework net8.0 + +# Test all frameworks sequentially on native platform +./build-and-test.sh --test --platform native + +# Test all Linux variants in Docker +./build-and-test.sh --test --platform linux +``` + +### Using dotnet test + +```bash +# Run all tests for a specific framework +dotnet test --framework net8.0 + +# Run tests for specific project and framework +dotnet test tests/StatsdClient.Tests/ --framework net8.0 + +# Run only native library tests +dotnet test --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests + +# Run specific test class +dotnet test --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests + +# Run specific test method +dotnet test --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests.Counter +``` + +### Testing All Frameworks Sequentially + +**Linux/macOS:** +```bash +for tfm in netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do + dotnet test --framework $tfm +done +``` + +**Windows (includes .NET Framework):** +```bash +for tfm in net48 netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do + dotnet test --framework $tfm +done +``` + +Or use the build script: +```bash +./build-and-test.sh --test --platform native +``` + +### Supported Target Frameworks + +- .NET Framework 4.8 (Windows only) +- .NET Core 2.1, 3.0, 3.1 +- .NET 5, 6, 7, 8, 9, 10 + +## Packaging + +To create a NuGet package with all native libraries: + +```bash +# Step 1: Build all native library variants +./build-and-test.sh --platform linux + +# Step 2: Pack the NuGet package +dotnet pack src/StatsdClient/StatsdClient.csproj -c Release + +# Output: src/StatsdClient/bin/Release/*.nupkg +``` + +The package will include all 4 Linux native library variants in the correct RID directories. The .NET runtime automatically selects the appropriate variant based on the deployment environment. + +### Verify Package Contents + +**Linux/WSL:** +```bash +unzip -l src/StatsdClient/bin/Release/DogStatsD-CSharp-Client.*.nupkg | grep libfs +``` + +**Windows (PowerShell):** +```powershell +Expand-Archive src/StatsdClient/bin/Release/DogStatsD-CSharp-Client.*.nupkg -DestinationPath temp +Get-ChildItem temp/runtimes/*/native/ +``` + +## Benchmarks + +Run performance benchmarks: + +```bash +dotnet run -c Release --project benchmarks/StatsdClient.Benchmarks/StatsdClient.Benchmarks.csproj +``` + +## Troubleshooting + +### Tests fail with "address already in use" or named pipe conflicts + +Make sure you're specifying `--framework` when running tests. Running multiple frameworks in parallel causes port and named pipe conflicts. + +```bash +# ❌ Wrong - runs all frameworks in parallel +dotnet test + +# ✅ Correct - runs single framework +dotnet test --framework net8.0 +``` + +### Native library not found during tests + +Ensure you've built the native libraries before running native library tests: + +```bash +./build-and-test.sh --platform linux +dotnet test --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests +``` + +### Docker build fails on Windows + +Make sure Docker Desktop is running and configured for Linux containers (not Windows containers). + +### Local native build fails + +Native library builds require: +- CMake 3.10+ +- GCC or Clang +- Standard build tools (make, etc.) + +On Ubuntu/Debian: +```bash +sudo apt-get update +sudo apt-get install -y cmake build-essential +``` + +On Alpine: +```bash +apk add --no-cache cmake make gcc g++ musl-dev +``` + +## Clean Build + +Remove build artifacts: + +```bash +# Clean .NET build outputs +dotnet clean + +# Remove native library outputs +rm -rf runtimes/*/native/libfs.so +rm -rf src/StatsdClient.Native/build/ + +# Remove NuGet packages +rm -rf src/StatsdClient/bin/ src/StatsdClient/obj/ +``` + +## Additional Resources + +- [src/StatsdClient.Native/README.md](src/StatsdClient.Native/README.md) - Native library details +- [.NET RID Catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog) - Runtime identifier documentation From 8073f209a01c9390066632de6008dd206a8ca8dc Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Mon, 9 Feb 2026 14:45:30 -0500 Subject: [PATCH 12/19] add CLAUDE.md --- CLAUDE.md | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..9cafd64c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,180 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +This is the DogStatsD C# client library (https://github.com/DataDog/dogstatsd-csharp-client), which provides a C# implementation of the DogStatsD protocol for sending metrics, events, and service checks to Datadog. + +## Build and Test Commands + +### Building + +#### .NET Library (Cross-platform) +```bash +# Restore dependencies +dotnet restore + +# Build the solution +dotnet build + +# Build specific project +dotnet build src/StatsdClient/StatsdClient.csproj + +# Build for specific target framework +dotnet build src/StatsdClient/StatsdClient.csproj -f netstandard2.0 +``` + +#### Native Library (Linux only) + +The repository includes a small native C library (`libfs`) for Linux inode operations. See `src/StatsdClient.Native/README.md` for details. + +**Using build-and-test.sh (recommended):** +```bash +# Build all 4 Linux variants using Docker (linux-x64, linux-musl-x64, linux-arm64, linux-musl-arm64) +./build-and-test.sh --platform linux + +# Build and test all Linux variants +./build-and-test.sh --test --platform linux + +# Build and test .NET for specific framework (native platform, no Docker) +./build-and-test.sh --test --platform native --framework net8.0 + +# Build and test .NET for all frameworks sequentially (native platform, no Docker) +./build-and-test.sh --test --platform native +``` + +**Local build (Linux/WSL only):** +```bash +# Build for specific RID (outputs to runtimes/{rid}/native/) +./src/StatsdClient.Native/build.sh linux-x64 +./src/StatsdClient.Native/build.sh linux-musl-x64 + +# Build for local development (outputs to src/StatsdClient.Native/build/) +./src/StatsdClient.Native/build.sh +``` + +**Output locations:** +- Docker builds: `runtimes/{rid}/native/libfs.so` +- Local builds with RID: `runtimes/{rid}/native/libfs.so` +- Local builds without RID: `src/StatsdClient.Native/build/libfs.so` + +**Supported RIDs** (per [official .NET RID catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids)): +- `linux-x64` (standard x64 Linux with glibc) +- `linux-musl-x64` (Alpine x64 Linux with musl) +- `linux-arm64` (standard ARM64 Linux with glibc) +- `linux-musl-arm64` (Alpine ARM64 Linux with musl) + +### Testing + +**IMPORTANT**: Always specify `--framework` when running tests. Running tests without a framework will run all target frameworks in parallel, which causes conflicts due to shared named pipes. + +```bash +# Run tests for a specific framework (REQUIRED) +dotnet test tests/StatsdClient.Tests/ --framework net8.0 + +# Run only native library tests +dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests + +# Run a single test class +dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests + +# Run a single test method +dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests.Counter + +# Run all tests sequentially (one framework at a time) +# On Linux/macOS: +for tfm in netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do + dotnet test tests/StatsdClient.Tests/ --framework $tfm +done + +# On Windows (also includes net48): +for tfm in net48 netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do + dotnet test tests/StatsdClient.Tests/ --framework $tfm +done +``` + +### Packaging + +To build the NuGet package with all native libraries: + +```bash +# Step 1: Build all native variants using Docker +./build-and-test.sh --platform linux + +# Step 2: Pack the NuGet package +dotnet pack src/StatsdClient/StatsdClient.csproj -c Release + +# Output: src/StatsdClient/bin/Release/*.nupkg +``` + +The package will include all 4 native library variants in the correct RID directories. + +### Benchmarks +```bash +# Run benchmarks +dotnet run -c Release --project benchmarks/StatsdClient.Benchmarks/StatsdClient.Benchmarks.csproj +``` + +## Architecture + +### Core Components + +**DogStatsdService** (`src/StatsdClient/DogStatsdService.cs`): Thread-safe instance-based API for sending metrics. Requires explicit `Configure()` call before use. Must be disposed to flush metrics. + +**DogStatsd** (static class): Static wrapper around DogStatsdService for applications that prefer a single global instance. Shares the same underlying implementation. + +**StatsRouter** (`src/StatsdClient/StatsRouter.cs`): Routes incoming stats to either client-side aggregators (for Count, Gauge, Set) or directly to BufferBuilder (for Histogram, Distribution, Timing). + +**MetricsSender** (`src/StatsdClient/MetricsSender.cs`): Handles serialization and sending of metrics through the StatsRouter. + +### Client-Side Aggregation + +By default, basic metric types (Count, Gauge, Set) are aggregated client-side before sending to reduce network usage and agent load: +- **CountAggregator** (`src/StatsdClient/Aggregator/CountAggregator.cs`): Aggregates counter values +- **GaugeAggregator** (`src/StatsdClient/Aggregator/GaugeAggregator.cs`): Keeps last gauge value +- **SetAggregator** (`src/StatsdClient/Aggregator/SetAggregator.cs`): Tracks unique set values +- **AggregatorFlusher** (`src/StatsdClient/Aggregator/AggregatorFlusher.cs`): Periodically flushes aggregated metrics + +Aggregation window defaults to 2 seconds (configurable via `ClientSideAggregationConfig.FlushInterval`). Disable by setting `StatsdConfig.ClientSideAggregation` to null. + +### Buffering and Transport + +**BufferBuilder** (`src/StatsdClient/Bufferize/BufferBuilder.cs`): Batches multiple metrics into single datagrams up to max packet size (default 1432 bytes for UDP). + +**AsynchronousWorker** (`src/StatsdClient/Worker/AsynchronousWorker.cs`): Manages background worker threads that process the metrics queue asynchronously. Non-blocking except for `Flush()` and `Dispose()`. + +**Transport Layer** (`src/StatsdClient/Transport/`): +- **UDPTransport**: Standard UDP transport to agent +- **UnixDomainSocketTransport**: Unix domain socket transport (not supported on Windows for Dgram sockets) +- **NamedPipeTransport**: Windows named pipe transport (internal) + +### Configuration + +**StatsdConfig** (`src/StatsdClient/StatsdConfig.cs`): Main configuration class with properties: +- `StatsdServerName`: Agent hostname or unix socket path (e.g., "unix:///tmp/dsd.socket") +- `StatsdPort`: Agent port (defaults to 8125) +- `ClientSideAggregation`: Client-side aggregation settings (null to disable) +- Environment variable support: `DD_AGENT_HOST`, `DD_DOGSTATSD_PORT`, `DD_ENTITY_ID`, `DD_SERVICE`, `DD_ENV`, `DD_VERSION` + +## Target Frameworks + +The library supports: +- .NET Standard 2.0+ +- .NET Core 2.1, 3.0, 3.1 +- .NET 5.0, 6.0, 7.0, 8.0, 9.0 +- .NET Framework 4.8 + +Tests run on all supported frameworks via GitHub Actions (Linux and Windows). + +## Key Design Patterns + +1. **Object Pooling**: Uses custom Pool implementation (`src/StatsdClient/Utils/Pool.cs`) to reduce allocations for frequently created objects like buffers and stats. + +2. **Struct-based Stats**: Internal `Stats` structs (`src/StatsdClient/Statistic/`) minimize heap allocations when passing metrics through the pipeline. + +3. **Thread Safety**: Both DogStatsdService and static DogStatsd are thread-safe. Worker handlers must be thread-safe when `workerThreadCount` > 1. + +4. **Non-blocking Operations**: Metric submission methods are non-blocking (enqueue to worker thread). Only `Flush()` and `Dispose()` block. + +5. **Telemetry**: Built-in telemetry (`src/StatsdClient/Telemetry.cs`) tracks client metrics like bytes sent, packets sent, dropped metrics, etc. From 0391dd5d94cb68188a451fffbb037eacaf9b80e6 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Tue, 2 Jun 2026 17:52:00 -0400 Subject: [PATCH 13/19] Use statx instead of a bundled native library for inode lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Origin detection reads file inode numbers on Linux. This replaces the bundled native libfs.so with a direct statx system call via P/Invoke. struct statx has a fixed, architecture-independent layout, so a single managed definition works across x64/arm64 and glibc/musl without shipping any native binaries. Removes the C library, its Docker cross-build, the CI native-build job, and the per-RID NuGet packaging. The package now has no native assets and no runtime dependencies. 🤖 --- .dockerignore | 38 ---- .github/workflows/build-and-test.yml | 57 ----- .gitignore | 3 - BUILD.md | 123 +---------- CLAUDE.md | 53 ----- Dockerfile.linux | 74 ------- build-and-test.sh | 202 +++--------------- src/StatsdClient.Native/CMakeLists.txt | 6 - src/StatsdClient.Native/README.md | 116 ---------- src/StatsdClient.Native/build.sh | 46 ---- src/StatsdClient.Native/fs.c | 10 - src/StatsdClient.Native/fs.h | 6 - src/StatsdClient/NativeMethods.cs | 56 +++-- src/StatsdClient/StatsdClient.csproj | 37 ---- .../StatsdClient.Tests/NativeLibraryTests.cs | 8 +- 15 files changed, 84 insertions(+), 751 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile.linux delete mode 100644 src/StatsdClient.Native/CMakeLists.txt delete mode 100644 src/StatsdClient.Native/README.md delete mode 100755 src/StatsdClient.Native/build.sh delete mode 100644 src/StatsdClient.Native/fs.c delete mode 100644 src/StatsdClient.Native/fs.h diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index e8d773b5..00000000 --- a/.dockerignore +++ /dev/null @@ -1,38 +0,0 @@ -# Build outputs -**/bin/ -**/obj/ -**/out/ - -# Native build outputs -**/build/ -runtimes/ - -# IDE / Editor -.vs/ -.vscode/ -*.suo -*.user -*.userosscache -*.sln.docstates - -# Test results -TestResults/ -*.trx -*.coverage - -# NuGet -*.nupkg -*.snupkg -packages/ - -# Misc -.git/ -.gitignore -.github/ -*.md -*.log -*.zip - -# Keep these files -!README.md -!CLAUDE.md diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index fd6fa8b2..00a0dfbd 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,59 +8,9 @@ on: workflow_dispatch: jobs: - build-native-linux: - name: Build libfs for ${{ matrix.libc }}-${{ matrix.arch }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - arch: x64 - libc: glibc - container: mcr.microsoft.com/dotnet/sdk:8.0 - install_cmd: apt-get update && apt-get install -y cmake build-essential - - arch: x64 - libc: musl - container: mcr.microsoft.com/dotnet/sdk:8.0-alpine - install_cmd: apk add cmake make gcc g++ musl-dev - - arch: arm64 - libc: glibc - container: mcr.microsoft.com/dotnet/sdk:8.0-arm64v8 - install_cmd: apt-get update && apt-get install -y cmake build-essential - - arch: arm64 - libc: musl - container: mcr.microsoft.com/dotnet/sdk:8.0-alpine-arm64v8 - install_cmd: apk add cmake make gcc g++ musl-dev - - container: ${{ matrix.container }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install build tools - run: ${{ matrix.install_cmd }} - - - name: Build native library - run: | - mkdir -p build && cd build - cmake ../src/StatsdClient.Native - cmake --build . --config Release - cd .. - - mkdir -p runtimes/linux-${{ matrix.libc }}-${{ matrix.arch }}/native - cp build/libfs.so runtimes/linux-${{ matrix.libc }}-${{ matrix.arch }}/native/ - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: native-linux-${{ matrix.libc }}-${{ matrix.arch }} - path: runtimes/ - unit-tests: name: ${{ matrix.framework }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - needs: build-native-linux permissions: actions: read contents: read @@ -117,13 +67,6 @@ jobs: with: fetch-depth: 0 - - name: Download native libraries - uses: actions/download-artifact@v4 - with: - pattern: native-linux-* - path: runtimes/ - merge-multiple: true - - name: Install libssl on Linux # .NET 5.0 and below want libssl v1, which is not available on Ubuntu 24.04 normally if: runner.os == 'Linux' diff --git a/.gitignore b/.gitignore index 2148d1ec..a3e3744b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ .vs/ .vscode/ .idea/ -runtimes/ -artifacts/ -build/ [bB]in/ [oO]bj/ artifacts/ diff --git a/BUILD.md b/BUILD.md index c8972413..4a19ff35 100644 --- a/BUILD.md +++ b/BUILD.md @@ -5,7 +5,6 @@ This document describes how to build and test the DogStatsD C# client library. ## Prerequisites - [.NET SDK 10.0 or above](https://dotnet.microsoft.com/download) -- [Docker](https://www.docker.com/get-started) (required for building Linux native libraries) ## Quick Start @@ -16,17 +15,12 @@ dotnet build # Run tests for a specific framework dotnet test --framework net8.0 -# Build all Linux native libraries using Docker -./build-and-test.sh --platform linux - -# Pack the NuGet package with all native libraries +# Pack the NuGet package dotnet pack src/StatsdClient/StatsdClient.csproj -c Release ``` ## Building -### .NET Library - Build the main .NET library: ```bash @@ -46,44 +40,6 @@ dotnet build -c Release dotnet build src/StatsdClient/StatsdClient.csproj -f netstandard2.0 ``` -### Native Library (Linux only) - -The repository includes a native C library (`libfs`) for Linux file system operations. This library must be built for multiple Linux variants to support different distributions. - -#### Using Docker (Recommended) - -Build all 4 Linux variants using Docker: - -```bash -# Build all variants (linux-x64, linux-musl-x64, linux-arm64, linux-musl-arm64) -./build-and-test.sh --platform linux - -# Build and test all variants -./build-and-test.sh --test --platform linux -``` - -**Output:** `runtimes/{rid}/native/libfs.so` - -**Supported RIDs:** -- `linux-x64` - Standard x64 Linux (glibc) -- `linux-musl-x64` - Alpine x64 Linux (musl libc) -- `linux-arm64` - Standard ARM64 Linux (glibc) -- `linux-musl-arm64` - Alpine ARM64 Linux (musl libc) - -#### Local Build (Linux/WSL only) - -Build without Docker on Linux or WSL: - -```bash -# Build for specific RID (outputs to runtimes/{rid}/native/) -./src/StatsdClient.Native/build.sh linux-x64 - -# Build for local development only (outputs to src/StatsdClient.Native/build/) -./src/StatsdClient.Native/build.sh -``` - -**Note:** Local builds compile for the current system architecture only. Docker is required to cross-compile for different architectures (x64/ARM64) or libc variants (glibc/musl). - ## Testing ### Important: Framework Selection @@ -93,14 +49,11 @@ Build without Docker on Linux or WSL: ### Using build-and-test.sh ```bash -# Test specific framework on native platform (Windows/macOS/Linux) -./build-and-test.sh --test --platform native --framework net8.0 - -# Test all frameworks sequentially on native platform -./build-and-test.sh --test --platform native +# Test a specific framework +./build-and-test.sh --test --framework net8.0 -# Test all Linux variants in Docker -./build-and-test.sh --test --platform linux +# Test all frameworks sequentially +./build-and-test.sh --test ``` ### Using dotnet test @@ -112,9 +65,6 @@ dotnet test --framework net8.0 # Run tests for specific project and framework dotnet test tests/StatsdClient.Tests/ --framework net8.0 -# Run only native library tests -dotnet test --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests - # Run specific test class dotnet test --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests @@ -140,7 +90,7 @@ done Or use the build script: ```bash -./build-and-test.sh --test --platform native +./build-and-test.sh --test ``` ### Supported Target Frameworks @@ -151,33 +101,14 @@ Or use the build script: ## Packaging -To create a NuGet package with all native libraries: +To create a NuGet package: ```bash -# Step 1: Build all native library variants -./build-and-test.sh --platform linux - -# Step 2: Pack the NuGet package dotnet pack src/StatsdClient/StatsdClient.csproj -c Release # Output: src/StatsdClient/bin/Release/*.nupkg ``` -The package will include all 4 Linux native library variants in the correct RID directories. The .NET runtime automatically selects the appropriate variant based on the deployment environment. - -### Verify Package Contents - -**Linux/WSL:** -```bash -unzip -l src/StatsdClient/bin/Release/DogStatsD-CSharp-Client.*.nupkg | grep libfs -``` - -**Windows (PowerShell):** -```powershell -Expand-Archive src/StatsdClient/bin/Release/DogStatsD-CSharp-Client.*.nupkg -DestinationPath temp -Get-ChildItem temp/runtimes/*/native/ -``` - ## Benchmarks Run performance benchmarks: @@ -200,37 +131,6 @@ dotnet test dotnet test --framework net8.0 ``` -### Native library not found during tests - -Ensure you've built the native libraries before running native library tests: - -```bash -./build-and-test.sh --platform linux -dotnet test --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests -``` - -### Docker build fails on Windows - -Make sure Docker Desktop is running and configured for Linux containers (not Windows containers). - -### Local native build fails - -Native library builds require: -- CMake 3.10+ -- GCC or Clang -- Standard build tools (make, etc.) - -On Ubuntu/Debian: -```bash -sudo apt-get update -sudo apt-get install -y cmake build-essential -``` - -On Alpine: -```bash -apk add --no-cache cmake make gcc g++ musl-dev -``` - ## Clean Build Remove build artifacts: @@ -239,15 +139,6 @@ Remove build artifacts: # Clean .NET build outputs dotnet clean -# Remove native library outputs -rm -rf runtimes/*/native/libfs.so -rm -rf src/StatsdClient.Native/build/ - # Remove NuGet packages rm -rf src/StatsdClient/bin/ src/StatsdClient/obj/ ``` - -## Additional Resources - -- [src/StatsdClient.Native/README.md](src/StatsdClient.Native/README.md) - Native library details -- [.NET RID Catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog) - Runtime identifier documentation diff --git a/CLAUDE.md b/CLAUDE.md index 9cafd64c..491ed471 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,8 +9,6 @@ This is the DogStatsD C# client library (https://github.com/DataDog/dogstatsd-cs ## Build and Test Commands ### Building - -#### .NET Library (Cross-platform) ```bash # Restore dependencies dotnet restore @@ -25,46 +23,6 @@ dotnet build src/StatsdClient/StatsdClient.csproj dotnet build src/StatsdClient/StatsdClient.csproj -f netstandard2.0 ``` -#### Native Library (Linux only) - -The repository includes a small native C library (`libfs`) for Linux inode operations. See `src/StatsdClient.Native/README.md` for details. - -**Using build-and-test.sh (recommended):** -```bash -# Build all 4 Linux variants using Docker (linux-x64, linux-musl-x64, linux-arm64, linux-musl-arm64) -./build-and-test.sh --platform linux - -# Build and test all Linux variants -./build-and-test.sh --test --platform linux - -# Build and test .NET for specific framework (native platform, no Docker) -./build-and-test.sh --test --platform native --framework net8.0 - -# Build and test .NET for all frameworks sequentially (native platform, no Docker) -./build-and-test.sh --test --platform native -``` - -**Local build (Linux/WSL only):** -```bash -# Build for specific RID (outputs to runtimes/{rid}/native/) -./src/StatsdClient.Native/build.sh linux-x64 -./src/StatsdClient.Native/build.sh linux-musl-x64 - -# Build for local development (outputs to src/StatsdClient.Native/build/) -./src/StatsdClient.Native/build.sh -``` - -**Output locations:** -- Docker builds: `runtimes/{rid}/native/libfs.so` -- Local builds with RID: `runtimes/{rid}/native/libfs.so` -- Local builds without RID: `src/StatsdClient.Native/build/libfs.so` - -**Supported RIDs** (per [official .NET RID catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids)): -- `linux-x64` (standard x64 Linux with glibc) -- `linux-musl-x64` (Alpine x64 Linux with musl) -- `linux-arm64` (standard ARM64 Linux with glibc) -- `linux-musl-arm64` (Alpine ARM64 Linux with musl) - ### Testing **IMPORTANT**: Always specify `--framework` when running tests. Running tests without a framework will run all target frameworks in parallel, which causes conflicts due to shared named pipes. @@ -73,9 +31,6 @@ The repository includes a small native C library (`libfs`) for Linux inode opera # Run tests for a specific framework (REQUIRED) dotnet test tests/StatsdClient.Tests/ --framework net8.0 -# Run only native library tests -dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests - # Run a single test class dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests @@ -96,20 +51,12 @@ done ### Packaging -To build the NuGet package with all native libraries: - ```bash -# Step 1: Build all native variants using Docker -./build-and-test.sh --platform linux - -# Step 2: Pack the NuGet package dotnet pack src/StatsdClient/StatsdClient.csproj -c Release # Output: src/StatsdClient/bin/Release/*.nupkg ``` -The package will include all 4 native library variants in the correct RID directories. - ### Benchmarks ```bash # Run benchmarks diff --git a/Dockerfile.linux b/Dockerfile.linux deleted file mode 100644 index 8c7c11d1..00000000 --- a/Dockerfile.linux +++ /dev/null @@ -1,74 +0,0 @@ -# Multi-stage Dockerfile for building and testing libfs for all Linux variants -# Usage: See build-and-test.sh - -ARG BASE_IMAGE -ARG DOTNET_TEST_ARGS="" -FROM ${BASE_IMAGE} AS builder - -# Install build tools based on the base image -RUN if command -v apt-get > /dev/null 2>&1; then \ - apt-get update && apt-get install -y cmake build-essential && rm -rf /var/lib/apt/lists/*; \ - elif command -v apk > /dev/null 2>&1; then \ - apk add --no-cache cmake make gcc g++ musl-dev bash; \ - fi - -WORKDIR /build - -# Copy only the native library source -COPY src/StatsdClient.Native/ ./src/StatsdClient.Native/ - -# Build the library using the build script -ARG RID -RUN chmod +x src/StatsdClient.Native/build.sh && \ - bash src/StatsdClient.Native/build.sh "$RID" - -# The output libfs.so is in /build/runtimes/${RID}/native/libfs.so - -# ============================================================ -# Test stage (optional - only built with --target tester) -# ============================================================ -FROM ${BASE_IMAGE} AS tester - -WORKDIR /build - -# Copy only project files for restore (better caching) -COPY src/StatsdClient/StatsdClient.csproj ./src/StatsdClient/ -COPY tests/StatsdClient.Tests/StatsdClient.Tests.csproj ./tests/StatsdClient.Tests/ - -# Restore dependencies (cached separately from source changes) -RUN dotnet restore src/StatsdClient/StatsdClient.csproj -RUN dotnet restore tests/StatsdClient.Tests/StatsdClient.Tests.csproj - -# Copy remaining source files -COPY src/ ./src/ -COPY tests/ ./tests/ -COPY stylecop.json stylecop.ruleset ./ - -# Copy the pre-built native library from builder stage -ARG RID -COPY --from=builder /build/runtimes/${RID}/native/libfs.so /build/runtimes/${RID}/native/libfs.so - -# Build the .NET project (without restore since already done) -RUN dotnet build src/StatsdClient/StatsdClient.csproj -c Release --no-restore - -# Build the test project (without restore since already done) -RUN dotnet build tests/StatsdClient.Tests/StatsdClient.Tests.csproj -c Release --no-restore - -# Copy library to the location where .NET runtime will find it during tests -# The library is already in the correct RID directory (linux-x64, linux-musl-x64, etc.) -# .NET runtime will automatically select the correct variant based on the environment -# As a workaround for testing, also copy to the root of the test output directory -ARG RID -RUN cp /build/tests/StatsdClient.Tests/bin/Release/net8.0/runtimes/${RID}/native/libfs.so \ - /build/tests/StatsdClient.Tests/bin/Release/net8.0/libfs.so || true - -# Run the native library tests -# DOTNET_TEST_ARGS can be used to pass additional arguments (e.g., --filter) -ARG DOTNET_TEST_ARGS="--filter FullyQualifiedName~NativeLibraryTests" -RUN dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ - --framework net8.0 \ - -c Release \ - -v normal \ - --no-build \ - --logger 'console;verbosity=normal' \ - ${DOTNET_TEST_ARGS} diff --git a/build-and-test.sh b/build-and-test.sh index ad0a2979..993affa7 100755 --- a/build-and-test.sh +++ b/build-and-test.sh @@ -1,18 +1,14 @@ #!/bin/bash set -e -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - # Color output GREEN='\033[0;32m' BLUE='\033[0;34m' -YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' # No Color # Default values RUN_TESTS=false -PLATFORM="" FRAMEWORK="" EXTRA_TEST_ARGS=() @@ -23,10 +19,6 @@ while [[ $# -gt 0 ]]; do RUN_TESTS=true shift ;; - --platform) - PLATFORM="$2" - shift 2 - ;; --framework) FRAMEWORK="$2" shift 2 @@ -38,27 +30,21 @@ while [[ $# -gt 0 ]]; do break ;; --help) - echo "Usage: $0 [--test] [--platform ] [--framework ] [-- ]" + echo "Usage: $0 [--test] [--framework ] [-- ]" echo "" echo "Options:" echo " --test Run tests after building (default: build only)" - echo " --platform Target platform:" - echo " native - Use native dotnet build/test (Windows/macOS)" - echo " linux - Use Docker to build all 4 Linux variants" - echo " (default: auto-detect based on OS)" echo " --framework Target framework moniker (e.g., net8.0, net48)" echo " If not specified, tests all frameworks sequentially" echo " Prevents parallel test execution conflicts" echo " -- Everything after -- is passed directly to dotnet test" echo "" echo "Examples:" - echo " $0 --test --platform native --framework net8.0 # Build and test .NET for net8.0 only" - echo " $0 --test --platform native # Build and test all frameworks sequentially" - echo " $0 --platform linux # Build Linux native libs in Docker" - echo " $0 --test --platform linux # Build and test all Linux variants in Docker" - echo " $0 --test --platform linux -- --filter FullyQualifiedName~NativeLibraryTests" - echo " # Test only NativeLibraryTests in Docker" - echo " $0 # Auto-detect platform, build only" + echo " $0 --test --framework net8.0 # Build and test net8.0 only" + echo " $0 --test # Build and test all frameworks sequentially" + echo " $0 # Build only" + echo " $0 --test --framework net8.0 -- --filter FullyQualifiedName~OriginDetectionTests" + echo " # Test only OriginDetectionTests on net8.0" exit 0 ;; *) @@ -69,163 +55,43 @@ while [[ $# -gt 0 ]]; do esac done -# Auto-detect platform if not specified -if [ -z "$PLATFORM" ]; then - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - PLATFORM="linux" - echo -e "${YELLOW}Auto-detected platform: linux${NC}" - elif [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then - PLATFORM="native" - echo -e "${YELLOW}Auto-detected platform: native${NC}" +echo -e "${BLUE}Building .NET project...${NC}" +dotnet build src/StatsdClient/StatsdClient.csproj -c Release + +if [ "$RUN_TESTS" = true ]; then + if [ -n "$FRAMEWORK" ]; then + # Test a single framework + echo "" + echo -e "${BLUE}Running tests for framework: $FRAMEWORK${NC}" + dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework "$FRAMEWORK" \ + -c Release \ + --no-build \ + "${EXTRA_TEST_ARGS[@]}" else - echo -e "${RED}Could not auto-detect platform. Please specify --platform ${NC}" - exit 1 - fi -fi + # Test all frameworks sequentially + echo "" + echo -e "${BLUE}Running tests for all frameworks sequentially (to avoid named pipe conflicts)...${NC}" -# Validate platform -if [[ "$PLATFORM" != "native" && "$PLATFORM" != "linux" ]]; then - echo -e "${RED}Invalid platform: $PLATFORM${NC}" - echo "Must be 'native' or 'linux'" - exit 1 -fi - -echo -e "${BLUE}Starting build process...${NC}" -echo "" + FRAMEWORKS="netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0" -# Native platform (Windows/macOS - .NET only, no native libs) -if [ "$PLATFORM" = "native" ]; then - echo -e "${BLUE}Building .NET project (native platform)...${NC}" - dotnet build src/StatsdClient/StatsdClient.csproj -c Release + # Add .NET Framework on Windows + if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + FRAMEWORKS="net48 $FRAMEWORKS" + fi - if [ "$RUN_TESTS" = true ]; then - if [ -n "$FRAMEWORK" ]; then - # Test a single framework + # Run tests for each framework sequentially + for tfm in $FRAMEWORKS; do echo "" - echo -e "${BLUE}Running tests for framework: $FRAMEWORK${NC}" + echo -e "${BLUE}Testing framework: $tfm${NC}" dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ - --framework "$FRAMEWORK" \ + --framework "$tfm" \ -c Release \ --no-build \ "${EXTRA_TEST_ARGS[@]}" - else - # Test all frameworks sequentially - echo "" - echo -e "${BLUE}Running tests for all frameworks sequentially (to avoid named pipe conflicts)...${NC}" - - # Determine which frameworks to test based on OS - FRAMEWORKS="netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0" - - # Add .NET Framework on Windows - if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then - FRAMEWORKS="net48 $FRAMEWORKS" - fi - - # Run tests for each framework sequentially - for tfm in $FRAMEWORKS; do - echo "" - echo -e "${BLUE}Testing framework: $tfm${NC}" - dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ - --framework $tfm \ - -c Release \ - --no-build \ - "${EXTRA_TEST_ARGS[@]}" - done - fi + done fi - - echo "" - echo -e "${GREEN}✓ Native build complete!${NC}" - exit 0 fi -# Linux platform (Docker - build native libs for all variants) -if [ "$PLATFORM" = "linux" ]; then - # Check if Docker is available - if ! command -v docker &> /dev/null; then - echo -e "${RED}Error: Docker is not installed or not in PATH${NC}" - exit 1 - fi - - export DOCKER_BUILDKIT=1 - echo "Docker version:" - docker --version - echo "" - - build_and_test_variant() { - local arch=$1 - local libc=$2 - local base_image=$3 - - # Construct proper RID based on official RID catalog - # https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids - local rid="linux-${arch}" - if [ "$libc" = "musl" ]; then - rid="linux-musl-${arch}" - fi - - echo -e "${BLUE}Building $rid...${NC}" - - # Determine Docker platform - local platform="" - if [ "$arch" = "x64" ]; then - platform="linux/amd64" - elif [ "$arch" = "arm64" ]; then - platform="linux/arm64" - fi - - # Determine build target - # For ARM64: only build native lib (skip tests due to QEMU emulation limitations) - local target="builder" - if [ "$RUN_TESTS" = true ] && [ "$arch" = "x64" ]; then - target="tester" - fi - - if [ "$RUN_TESTS" = true ] && [ "$arch" = "arm64" ]; then - echo -e "${YELLOW}Note: Skipping .NET tests for ARM64 due to QEMU emulation limitations${NC}" - fi - - # Build the Docker image - # Convert array to string for passing to Docker - local test_args_string="${EXTRA_TEST_ARGS[*]}" - - docker build \ - --platform "$platform" \ - --build-arg BASE_IMAGE="$base_image" \ - --build-arg RID="$rid" \ - --build-arg DOTNET_TEST_ARGS="$test_args_string" \ - --target "$target" \ - -t "libfs-${target}:${rid}" \ - -f Dockerfile.linux \ - . - - # Create a temporary container to extract the built library - local container_id=$(docker create --platform "$platform" "libfs-${target}:${rid}") - - # Extract the built library - mkdir -p "$SCRIPT_DIR/runtimes/$rid/native" - docker cp "$container_id:/build/runtimes/$rid/native/libfs.so" "$SCRIPT_DIR/runtimes/$rid/native/libfs.so" - - # Clean up - docker rm "$container_id" > /dev/null - - if [ "$RUN_TESTS" = true ] && [ "$arch" = "x64" ]; then - echo -e "${GREEN}✓ Built and tested $rid${NC}" - else - echo -e "${GREEN}✓ Built $rid${NC}" - fi - echo - } - - # Build all combinations - build_and_test_variant "x64" "glibc" "mcr.microsoft.com/dotnet/sdk:10.0" - build_and_test_variant "x64" "musl" "mcr.microsoft.com/dotnet/sdk:10.0-alpine" - build_and_test_variant "arm64" "glibc" "mcr.microsoft.com/dotnet/sdk:10.0" - build_and_test_variant "arm64" "musl" "mcr.microsoft.com/dotnet/sdk:10.0-alpine" - - echo "" - echo -e "${GREEN}All Linux builds complete!${NC}" - echo "Outputs in $SCRIPT_DIR/runtimes/" - echo "" - tree "$SCRIPT_DIR/runtimes/" -fi +echo "" +echo -e "${GREEN}✓ Build complete!${NC}" diff --git a/src/StatsdClient.Native/CMakeLists.txt b/src/StatsdClient.Native/CMakeLists.txt deleted file mode 100644 index 7137662c..00000000 --- a/src/StatsdClient.Native/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -cmake_minimum_required(VERSION 3.15) -project(fs C) - -add_library(fs SHARED fs.c fs.h) - -install(TARGETS fs DESTINATION lib) diff --git a/src/StatsdClient.Native/README.md b/src/StatsdClient.Native/README.md deleted file mode 100644 index 93671e31..00000000 --- a/src/StatsdClient.Native/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# libfs - Native Library for Linux File System Operations - -This directory contains a small C library (`libfs`) that provides cross-platform access to file inode numbers on Linux. - -## Purpose - -The library wraps the `stat()` system call to retrieve inode numbers, avoiding cross-platform P/Invoke compatibility issues when calling libc's `stat()` directly from C#. - -## Building - -### Using Docker (Recommended) - -Build all 4 Linux variants using Docker from the repository root: - -```bash -# Build all Linux variants -./build-and-test.sh --platform linux - -# Build and test all Linux variants -./build-and-test.sh --test --platform linux -``` - -**Supported RIDs** (per [.NET RID catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids)): -- `linux-x64` - Standard x64 Linux with glibc -- `linux-musl-x64` - Alpine x64 Linux with musl libc -- `linux-arm64` - Standard ARM64 Linux with glibc -- `linux-musl-arm64` - Alpine ARM64 Linux with musl libc - -**Output:** `runtimes/{rid}/native/libfs.so` - -### Local Build (Linux/WSL only) - -Build without Docker: - -```bash -# Build for specific RID (outputs to runtimes/{rid}/native/) -./src/StatsdClient.Native/build.sh linux-x64 -./src/StatsdClient.Native/build.sh linux-musl-x64 -./src/StatsdClient.Native/build.sh linux-arm64 -./src/StatsdClient.Native/build.sh linux-musl-arm64 - -# Build for local development (outputs to src/StatsdClient.Native/build/) -./src/StatsdClient.Native/build.sh -``` - -**Note:** Local builds compile for the current system only. Use Docker to cross-compile for different architectures or libc variants. - -### Testing - -**Using build-and-test.sh:** -```bash -# Test all Linux variants in Docker -./build-and-test.sh --test --platform linux - -# Test specific framework on native platform -./build-and-test.sh --test --platform native --framework net8.0 -``` - -**Using dotnet test directly:** - -After building, run tests using standard .NET tooling. **Always specify `--framework`** to avoid parallel test execution conflicts: - -```bash -# Run native library tests for a specific framework -dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ - --framework net8.0 \ - --filter 'FullyQualifiedName~NativeLibraryTests' - -# Run all tests for a specific framework -dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ - --framework net8.0 -``` - -The .NET runtime automatically selects the correct native library based on the current platform: -- On glibc systems: uses `runtimes/linux-{arch}/native/libfs.so` -- On musl systems (Alpine): uses `runtimes/linux-musl-{arch}/native/libfs.so` - -## API - -### C API - -```c -int get_inode(const char* path, unsigned long long* ino); -``` - -Returns: -- `0` on success, with `ino` populated with the inode number -- `-1` on failure (e.g., file not found, permission denied) - -### .NET API - -```csharp -using StatsdClient.Native; - -if (NativeInode.TryGetInode("/path/to/file", out ulong inode)) -{ - Console.WriteLine($"Inode: {inode}"); -} -else -{ - Console.WriteLine("Failed to get inode (not on Linux or file not found)"); -} -``` - -The .NET wrapper automatically detects the platform and returns `false` on Windows/macOS. - -## CI/CD - -The GitHub Actions workflow (`.github/workflows/build-and-test.yml`) automatically builds all variants using Docker containers and includes them in the test runs and NuGet package. - -## Files - -- `fs.h` - Header file -- `fs.c` - Implementation -- `CMakeLists.txt` - CMake build configuration -- `README.md` - This file diff --git a/src/StatsdClient.Native/build.sh b/src/StatsdClient.Native/build.sh deleted file mode 100755 index d260bf7f..00000000 --- a/src/StatsdClient.Native/build.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -e - -# Simple build script for libfs -# Usage: ./build.sh [rid] -# Example: ./build.sh linux-glibc-x64 -# If no RID is provided, builds to src/StatsdClient.Native/build/ (for local development) - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$SCRIPT_DIR/../.." -RID="$1" - -echo "Building libfs..." - -if [ -n "$RID" ]; then - # Build to runtimes/{rid}/native/ for consistency with Docker builds - OUTPUT_DIR="$REPO_ROOT/runtimes/$RID/native" - BUILD_DIR="$OUTPUT_DIR/build" - mkdir -p "$BUILD_DIR" - cd "$BUILD_DIR" - - # Configure and build - cmake "$SCRIPT_DIR" - cmake --build . --config Release - - # Move the library to the native directory (one level up from build) - mv libfs.so "$OUTPUT_DIR/libfs.so" - - # Clean up build artifacts - cd "$OUTPUT_DIR" - rm -rf build - - echo "Build complete. Library at: $OUTPUT_DIR/libfs.so" -else - # No RID specified - build to local build directory for development - OUTPUT_DIR="$SCRIPT_DIR" - mkdir -p "$OUTPUT_DIR/build" - cd "$OUTPUT_DIR/build" - - # Configure and build - cmake "$SCRIPT_DIR" - cmake --build . --config Release - - echo "Build complete. Library at: $OUTPUT_DIR/build/libfs.so" - echo "Note: To build for testing, specify a RID: ./build.sh linux-glibc-x64" -fi diff --git a/src/StatsdClient.Native/fs.c b/src/StatsdClient.Native/fs.c deleted file mode 100644 index 429babc0..00000000 --- a/src/StatsdClient.Native/fs.c +++ /dev/null @@ -1,10 +0,0 @@ -#include "fs.h" -#include - -int get_inode(const char* path, unsigned long long* ino) -{ - struct stat s; - if (stat(path, &s) != 0) return -1; - *ino = (unsigned long long)s.st_ino; - return 0; -} diff --git a/src/StatsdClient.Native/fs.h b/src/StatsdClient.Native/fs.h deleted file mode 100644 index 4a9bffaf..00000000 --- a/src/StatsdClient.Native/fs.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef FS_H -#define FS_H - -int get_inode(const char* path, unsigned long long* ino); - -#endif diff --git a/src/StatsdClient/NativeMethods.cs b/src/StatsdClient/NativeMethods.cs index b872335a..84b05132 100644 --- a/src/StatsdClient/NativeMethods.cs +++ b/src/StatsdClient/NativeMethods.cs @@ -6,14 +6,24 @@ namespace StatsdClient { /// - /// P/Invoke wrapper for libfs native library to retrieve file inodes on Linux. + /// P/Invoke wrapper for the libc statx system call to retrieve file inodes on Linux. /// + /// + /// statx is used instead of stat because its result structure (struct statx) + /// has a fixed, architecture-independent layout defined by the kernel, so a single managed struct + /// definition works across all architectures and libc implementations (glibc and musl). The older + /// stat call has a layout that varies by architecture and libc, which is not safe to marshal directly. + /// internal static class NativeMethods { - private const string LibraryName = "fs"; + // Resolve the path relative to the current working directory (used when the path is not a dirfd-relative path). + private const int AT_FDCWD = -100; + + // Request only the inode number (stx_ino) from statx. + private const uint STATX_INO = 0x00000100; /// - /// Gets a value indicating whether the native library is supported on the current platform (Linux only). + /// Gets a value indicating whether the inode lookup is supported on the current platform (Linux only). /// public static bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); @@ -25,40 +35,52 @@ internal static class NativeMethods /// true if the inode was successfully retrieved, false otherwise. public static bool TryGetInode(string path, out ulong inode) { + inode = 0; + if (!IsSupported) { - inode = 0; return false; } try { - return GetInode(path, out inode) == 0; + // flags = 0 means symlinks are followed, matching stat() behavior (needed for the + // /proc/self/ns/cgroup magic link). statx returns -1 on failure, including ENOSYS on + // kernels older than 4.11, in which case we fall back to no inode. + if (Statx(AT_FDCWD, path, 0, STATX_INO, out var buffer) != 0) + { + return false; + } + + inode = buffer.StatxIno; + return true; } catch (DllNotFoundException) { - // Native library not available - inode = 0; + // libc not available return false; } catch (EntryPointNotFoundException) { - // Function not found in library - inode = 0; + // statx not available (e.g. glibc older than 2.28) return false; } } + [DllImport("libc", EntryPoint = "statx", SetLastError = true, CharSet = CharSet.Ansi)] + private static extern int Statx(int dirfd, string pathname, int flags, uint mask, out StatxBuffer buffer); + /// - /// Gets the inode number for a file path using the native libfs library. + /// Mirror of the kernel's struct statx (256 bytes). Only the inode field is read; the + /// full size is preserved so the kernel does not write past the buffer. /// - /// The file path. - /// The inode number if successful. - /// 0 on success, -1 on failure. - [DllImport(LibraryName, EntryPoint = "get_inode", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - private static extern int GetInode( - string path, - out ulong inode); + [StructLayout(LayoutKind.Explicit, Size = 256)] + private struct StatxBuffer + { + // stx_ino lives at byte offset 32 in struct statx. + [FieldOffset(32)] + public ulong StatxIno; + } } } #endif diff --git a/src/StatsdClient/StatsdClient.csproj b/src/StatsdClient/StatsdClient.csproj index f4f23a90..48812830 100644 --- a/src/StatsdClient/StatsdClient.csproj +++ b/src/StatsdClient/StatsdClient.csproj @@ -46,41 +46,4 @@ - - - - - - - - - - - - - - - - - diff --git a/tests/StatsdClient.Tests/NativeLibraryTests.cs b/tests/StatsdClient.Tests/NativeLibraryTests.cs index 9a504495..f7a9d13c 100644 --- a/tests/StatsdClient.Tests/NativeLibraryTests.cs +++ b/tests/StatsdClient.Tests/NativeLibraryTests.cs @@ -9,8 +9,8 @@ namespace Tests { /// - /// Integration tests for the native libfs library on Linux. - /// These tests interact directly with the native library rather than mocking. + /// Integration tests for the inode lookup (statx P/Invoke) on Linux. + /// These tests exercise the real system call rather than mocking. /// [TestFixture] public class NativeLibraryTests @@ -26,7 +26,7 @@ public void SetUp() [Test] public void TryStat_WithValidFile_ReturnsTrue() { - // Only run on Linux where the native library is available + // Only run on Linux where statx is available if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { Assert.Ignore("Test only runs on Linux"); @@ -193,7 +193,7 @@ public void TryStat_WithSymlink_ReturnsTargetInode() Assert.IsTrue(result, "TryStat should succeed for symlink"); Assert.Greater(inode, 0UL, "Inode should be greater than 0"); - // stat() follows symlinks by default, so we should get the target's inode + // statx with flags = 0 follows symlinks, so we should get the target's inode // We can't easily verify this without lstat support, but at least verify it works } From 5fde5531b95cb73395340bd047f8162a2b5a944f Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Tue, 2 Jun 2026 18:26:06 -0400 Subject: [PATCH 14/19] Fix doc inaccuracies in BUILD.md and CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct the NuGet pack output path to the artifacts/ layout, fix clean instructions, and clarify that the wider framework list applies to the test project while the library targets net461/netstandard2.0/netcoreapp3.1/net6.0. 🤖 --- BUILD.md | 15 +++++++++++---- CLAUDE.md | 12 +++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/BUILD.md b/BUILD.md index 4a19ff35..88dd6916 100644 --- a/BUILD.md +++ b/BUILD.md @@ -17,6 +17,7 @@ dotnet test --framework net8.0 # Pack the NuGet package dotnet pack src/StatsdClient/StatsdClient.csproj -c Release +# Output: artifacts/package/release/*.nupkg ``` ## Building @@ -93,12 +94,16 @@ Or use the build script: ./build-and-test.sh --test ``` -### Supported Target Frameworks +### Supported Test Frameworks + +The test project runs against: - .NET Framework 4.8 (Windows only) - .NET Core 2.1, 3.0, 3.1 - .NET 5, 6, 7, 8, 9, 10 +The library itself (`src/StatsdClient/StatsdClient.csproj`) targets `net461`, `netstandard2.0`, `netcoreapp3.1`, and `net6.0`. + ## Packaging To create a NuGet package: @@ -106,9 +111,11 @@ To create a NuGet package: ```bash dotnet pack src/StatsdClient/StatsdClient.csproj -c Release -# Output: src/StatsdClient/bin/Release/*.nupkg +# Output: artifacts/package/release/*.nupkg ``` +The build uses .NET's artifacts output layout (`UseArtifactsOutput` in `Directory.Build.props`), so build and package outputs go to the top-level `artifacts/` directory rather than per-project `bin/` and `obj/` folders. + ## Benchmarks Run performance benchmarks: @@ -139,6 +146,6 @@ Remove build artifacts: # Clean .NET build outputs dotnet clean -# Remove NuGet packages -rm -rf src/StatsdClient/bin/ src/StatsdClient/obj/ +# Remove all build and package outputs +rm -rf artifacts/ ``` diff --git a/CLAUDE.md b/CLAUDE.md index 491ed471..7180a2ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,7 @@ done ```bash dotnet pack src/StatsdClient/StatsdClient.csproj -c Release -# Output: src/StatsdClient/bin/Release/*.nupkg +# Output: artifacts/package/release/*.nupkg (UseArtifactsOutput is enabled in Directory.Build.props) ``` ### Benchmarks @@ -106,13 +106,11 @@ Aggregation window defaults to 2 seconds (configurable via `ClientSideAggregatio ## Target Frameworks -The library supports: -- .NET Standard 2.0+ -- .NET Core 2.1, 3.0, 3.1 -- .NET 5.0, 6.0, 7.0, 8.0, 9.0 -- .NET Framework 4.8 +The library (`src/StatsdClient/StatsdClient.csproj`) targets: `net461`, `netstandard2.0`, `netcoreapp3.1`, `net6.0`. -Tests run on all supported frameworks via GitHub Actions (Linux and Windows). +The test project (`tests/StatsdClient.Tests/StatsdClient.Tests.csproj`) targets a wider range to validate the library on all supported runtimes: netcoreapp2.1, 3.0, 3.1; net5.0 through net10.0; plus net48 on Windows. + +Tests run on all test frameworks via GitHub Actions (Linux, Windows, and macOS). See `.github/workflows/build-and-test.yml`. ## Key Design Patterns From ad2dbb902ff38988f29fa0521c019fb8503d4891 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 3 Jun 2026 11:13:09 -0400 Subject: [PATCH 15/19] tweak code comment --- src/StatsdClient/IFileSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StatsdClient/IFileSystem.cs b/src/StatsdClient/IFileSystem.cs index d168819f..8285136a 100644 --- a/src/StatsdClient/IFileSystem.cs +++ b/src/StatsdClient/IFileSystem.cs @@ -81,7 +81,7 @@ public TextReader OpenText(string path) /// True if the file stat was successful, false otherwise public bool TryStat(string path, out ulong inode) { -#if !NETFRAMEWORK // Unix Domain Sockets not supported on .NET Framework (always runs on Windows). +#if !NETFRAMEWORK // Unix Domain Sockets not supported on .NET Framework which always runs on Windows. if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { return NativeMethods.TryGetInode(path, out inode); From 8bfd4d063cd9741c33b90a5422049d46c5e75ba8 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 3 Jun 2026 11:21:47 -0400 Subject: [PATCH 16/19] Remove unused build-and-test.sh dev script --- BUILD.md | 15 -------- build-and-test.sh | 97 ----------------------------------------------- 2 files changed, 112 deletions(-) delete mode 100755 build-and-test.sh diff --git a/BUILD.md b/BUILD.md index 88dd6916..c3e05cfb 100644 --- a/BUILD.md +++ b/BUILD.md @@ -47,16 +47,6 @@ dotnet build src/StatsdClient/StatsdClient.csproj -f netstandard2.0 **Always specify `--framework` when running tests.** Running tests without a framework specification will run all target frameworks in parallel, causing conflicts due to shared named pipes. -### Using build-and-test.sh - -```bash -# Test a specific framework -./build-and-test.sh --test --framework net8.0 - -# Test all frameworks sequentially -./build-and-test.sh --test -``` - ### Using dotnet test ```bash @@ -89,11 +79,6 @@ for tfm in net48 netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 done ``` -Or use the build script: -```bash -./build-and-test.sh --test -``` - ### Supported Test Frameworks The test project runs against: diff --git a/build-and-test.sh b/build-and-test.sh deleted file mode 100755 index 993affa7..00000000 --- a/build-and-test.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash -set -e - -# Color output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Default values -RUN_TESTS=false -FRAMEWORK="" -EXTRA_TEST_ARGS=() - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - --test) - RUN_TESTS=true - shift - ;; - --framework) - FRAMEWORK="$2" - shift 2 - ;; - --) - # Everything after -- is passed to dotnet test - shift - EXTRA_TEST_ARGS=("$@") - break - ;; - --help) - echo "Usage: $0 [--test] [--framework ] [-- ]" - echo "" - echo "Options:" - echo " --test Run tests after building (default: build only)" - echo " --framework Target framework moniker (e.g., net8.0, net48)" - echo " If not specified, tests all frameworks sequentially" - echo " Prevents parallel test execution conflicts" - echo " -- Everything after -- is passed directly to dotnet test" - echo "" - echo "Examples:" - echo " $0 --test --framework net8.0 # Build and test net8.0 only" - echo " $0 --test # Build and test all frameworks sequentially" - echo " $0 # Build only" - echo " $0 --test --framework net8.0 -- --filter FullyQualifiedName~OriginDetectionTests" - echo " # Test only OriginDetectionTests on net8.0" - exit 0 - ;; - *) - echo -e "${RED}Unknown option: $1${NC}" - echo "Run '$0 --help' for usage information" - exit 1 - ;; - esac -done - -echo -e "${BLUE}Building .NET project...${NC}" -dotnet build src/StatsdClient/StatsdClient.csproj -c Release - -if [ "$RUN_TESTS" = true ]; then - if [ -n "$FRAMEWORK" ]; then - # Test a single framework - echo "" - echo -e "${BLUE}Running tests for framework: $FRAMEWORK${NC}" - dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ - --framework "$FRAMEWORK" \ - -c Release \ - --no-build \ - "${EXTRA_TEST_ARGS[@]}" - else - # Test all frameworks sequentially - echo "" - echo -e "${BLUE}Running tests for all frameworks sequentially (to avoid named pipe conflicts)...${NC}" - - FRAMEWORKS="netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0" - - # Add .NET Framework on Windows - if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then - FRAMEWORKS="net48 $FRAMEWORKS" - fi - - # Run tests for each framework sequentially - for tfm in $FRAMEWORKS; do - echo "" - echo -e "${BLUE}Testing framework: $tfm${NC}" - dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ - --framework "$tfm" \ - -c Release \ - --no-build \ - "${EXTRA_TEST_ARGS[@]}" - done - fi -fi - -echo "" -echo -e "${GREEN}✓ Build complete!${NC}" From 9407ecd0625f58dccf80ececad6af3d211338e60 Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 3 Jun 2026 13:41:42 -0400 Subject: [PATCH 17/19] Simplify TryStat platform check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delegate the Linux platform check to NativeMethods.TryGetInode instead of duplicating it in TryStat, and drop the now-unused usings. 🤖 --- src/StatsdClient/IFileSystem.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/StatsdClient/IFileSystem.cs b/src/StatsdClient/IFileSystem.cs index 8285136a..8a1676d9 100644 --- a/src/StatsdClient/IFileSystem.cs +++ b/src/StatsdClient/IFileSystem.cs @@ -1,6 +1,4 @@ -using System; using System.IO; -using System.Runtime.InteropServices; namespace StatsdClient { @@ -81,16 +79,14 @@ public TextReader OpenText(string path) /// True if the file stat was successful, false otherwise public bool TryStat(string path, out ulong inode) { -#if !NETFRAMEWORK // Unix Domain Sockets not supported on .NET Framework which always runs on Windows. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return NativeMethods.TryGetInode(path, out inode); - } -#endif - - // Unsupported .NET runtime or OS +#if !NETFRAMEWORK + // Inode lookup is only supported on Linux; TryGetInode performs the platform check. + return NativeMethods.TryGetInode(path, out inode); +#else + // Inode lookup is unsupported on .NET Framework, which always runs on Windows. inode = 0; return false; +#endif } } } From 277f9521e7e9ddcd9a4d549a15d017628d2546fb Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 3 Jun 2026 13:59:13 -0400 Subject: [PATCH 18/19] check for null input Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/StatsdClient/NativeMethods.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/StatsdClient/NativeMethods.cs b/src/StatsdClient/NativeMethods.cs index 84b05132..cdb7ffa2 100644 --- a/src/StatsdClient/NativeMethods.cs +++ b/src/StatsdClient/NativeMethods.cs @@ -37,11 +37,15 @@ public static bool TryGetInode(string path, out ulong inode) { inode = 0; - if (!IsSupported) + if (string.IsNullOrEmpty(path)) { return false; } + if (!IsSupported) + { + return false; + } try { // flags = 0 means symlinks are followed, matching stat() behavior (needed for the From 8b5cec734e577ca2aa6af69473eeaab2eacfac4c Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 3 Jun 2026 14:17:15 -0400 Subject: [PATCH 19/19] Trim CLAUDE.md to avoid duplicating BUILD.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reference BUILD.md for build/test commands instead of duplicating them, and drop stale file listings the agent can discover by grepping. 🤖 --- CLAUDE.md | 122 ++++++------------------------------------------------ 1 file changed, 12 insertions(+), 110 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7180a2ae..1ab90b42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,122 +4,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository Overview -This is the DogStatsD C# client library (https://github.com/DataDog/dogstatsd-csharp-client), which provides a C# implementation of the DogStatsD protocol for sending metrics, events, and service checks to Datadog. +This is the DogStatsD C# client library (https://github.com/DataDog/dogstatsd-csharp-client), a C# implementation of the DogStatsD protocol for sending metrics, events, and service checks to Datadog. -## Build and Test Commands +## Building and Testing -### Building -```bash -# Restore dependencies -dotnet restore +See [BUILD.md](BUILD.md) for build, test, packaging, and benchmark commands, and the list of target frameworks. -# Build the solution -dotnet build - -# Build specific project -dotnet build src/StatsdClient/StatsdClient.csproj - -# Build for specific target framework -dotnet build src/StatsdClient/StatsdClient.csproj -f netstandard2.0 -``` - -### Testing - -**IMPORTANT**: Always specify `--framework` when running tests. Running tests without a framework will run all target frameworks in parallel, which causes conflicts due to shared named pipes. - -```bash -# Run tests for a specific framework (REQUIRED) -dotnet test tests/StatsdClient.Tests/ --framework net8.0 - -# Run a single test class -dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests - -# Run a single test method -dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests.Counter - -# Run all tests sequentially (one framework at a time) -# On Linux/macOS: -for tfm in netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do - dotnet test tests/StatsdClient.Tests/ --framework $tfm -done - -# On Windows (also includes net48): -for tfm in net48 netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do - dotnet test tests/StatsdClient.Tests/ --framework $tfm -done -``` - -### Packaging - -```bash -dotnet pack src/StatsdClient/StatsdClient.csproj -c Release - -# Output: artifacts/package/release/*.nupkg (UseArtifactsOutput is enabled in Directory.Build.props) -``` - -### Benchmarks -```bash -# Run benchmarks -dotnet run -c Release --project benchmarks/StatsdClient.Benchmarks/StatsdClient.Benchmarks.csproj -``` +Gotcha: always pass `--framework` to `dotnet test` (e.g. `dotnet test --framework net8.0`). Running all target frameworks in parallel causes named-pipe conflicts. ## Architecture -### Core Components - -**DogStatsdService** (`src/StatsdClient/DogStatsdService.cs`): Thread-safe instance-based API for sending metrics. Requires explicit `Configure()` call before use. Must be disposed to flush metrics. - -**DogStatsd** (static class): Static wrapper around DogStatsdService for applications that prefer a single global instance. Shares the same underlying implementation. - -**StatsRouter** (`src/StatsdClient/StatsRouter.cs`): Routes incoming stats to either client-side aggregators (for Count, Gauge, Set) or directly to BufferBuilder (for Histogram, Distribution, Timing). - -**MetricsSender** (`src/StatsdClient/MetricsSender.cs`): Handles serialization and sending of metrics through the StatsRouter. - -### Client-Side Aggregation - -By default, basic metric types (Count, Gauge, Set) are aggregated client-side before sending to reduce network usage and agent load: -- **CountAggregator** (`src/StatsdClient/Aggregator/CountAggregator.cs`): Aggregates counter values -- **GaugeAggregator** (`src/StatsdClient/Aggregator/GaugeAggregator.cs`): Keeps last gauge value -- **SetAggregator** (`src/StatsdClient/Aggregator/SetAggregator.cs`): Tracks unique set values -- **AggregatorFlusher** (`src/StatsdClient/Aggregator/AggregatorFlusher.cs`): Periodically flushes aggregated metrics - -Aggregation window defaults to 2 seconds (configurable via `ClientSideAggregationConfig.FlushInterval`). Disable by setting `StatsdConfig.ClientSideAggregation` to null. - -### Buffering and Transport - -**BufferBuilder** (`src/StatsdClient/Bufferize/BufferBuilder.cs`): Batches multiple metrics into single datagrams up to max packet size (default 1432 bytes for UDP). - -**AsynchronousWorker** (`src/StatsdClient/Worker/AsynchronousWorker.cs`): Manages background worker threads that process the metrics queue asynchronously. Non-blocking except for `Flush()` and `Dispose()`. - -**Transport Layer** (`src/StatsdClient/Transport/`): -- **UDPTransport**: Standard UDP transport to agent -- **UnixDomainSocketTransport**: Unix domain socket transport (not supported on Windows for Dgram sockets) -- **NamedPipeTransport**: Windows named pipe transport (internal) - -### Configuration - -**StatsdConfig** (`src/StatsdClient/StatsdConfig.cs`): Main configuration class with properties: -- `StatsdServerName`: Agent hostname or unix socket path (e.g., "unix:///tmp/dsd.socket") -- `StatsdPort`: Agent port (defaults to 8125) -- `ClientSideAggregation`: Client-side aggregation settings (null to disable) -- Environment variable support: `DD_AGENT_HOST`, `DD_DOGSTATSD_PORT`, `DD_ENTITY_ID`, `DD_SERVICE`, `DD_ENV`, `DD_VERSION` - -## Target Frameworks - -The library (`src/StatsdClient/StatsdClient.csproj`) targets: `net461`, `netstandard2.0`, `netcoreapp3.1`, `net6.0`. - -The test project (`tests/StatsdClient.Tests/StatsdClient.Tests.csproj`) targets a wider range to validate the library on all supported runtimes: netcoreapp2.1, 3.0, 3.1; net5.0 through net10.0; plus net48 on Windows. - -Tests run on all test frameworks via GitHub Actions (Linux, Windows, and macOS). See `.github/workflows/build-and-test.yml`. - -## Key Design Patterns - -1. **Object Pooling**: Uses custom Pool implementation (`src/StatsdClient/Utils/Pool.cs`) to reduce allocations for frequently created objects like buffers and stats. - -2. **Struct-based Stats**: Internal `Stats` structs (`src/StatsdClient/Statistic/`) minimize heap allocations when passing metrics through the pipeline. +Metrics flow through: submission API -> router -> client-side aggregation or buffer -> background worker -> transport, with telemetry tracked throughout. -3. **Thread Safety**: Both DogStatsdService and static DogStatsd are thread-safe. Worker handlers must be thread-safe when `workerThreadCount` > 1. +- **Public API**: `DogStatsdService` is the thread-safe, instance-based entry point; it must be `Configure()`d before use and disposed to flush. The static `DogStatsd` class wraps a single global instance over the same implementation. +- **Routing and aggregation**: stats are routed to client-side aggregators (Count, Gauge, Set) or sent directly to the buffer (Histogram, Distribution, Timing). Aggregation flushes on an interval (default 2s, configurable; set `StatsdConfig.ClientSideAggregation` to null to disable). +- **Buffering and transport**: metrics are batched into datagrams up to a max packet size, then sent over UDP, a Unix domain socket, or a Windows named pipe. Submission is non-blocking; only `Flush()` and `Dispose()` block. +- **Configuration**: `StatsdConfig` holds the server name/port and aggregation settings and reads environment variables such as `DD_AGENT_HOST`, `DD_DOGSTATSD_PORT`, `DD_ENTITY_ID`, `DD_SERVICE`, `DD_ENV`, and `DD_VERSION`. -4. **Non-blocking Operations**: Metric submission methods are non-blocking (enqueue to worker thread). Only `Flush()` and `Dispose()` block. +## Design Notes -5. **Telemetry**: Built-in telemetry (`src/StatsdClient/Telemetry.cs`) tracks client metrics like bytes sent, packets sent, dropped metrics, etc. +- Both `DogStatsdService` and the static `DogStatsd` are thread-safe. Custom worker handlers must be thread-safe when `workerThreadCount > 1`. +- Hot paths use object pooling and struct-based stats to minimize allocations; keep this in mind before adding heap allocations to the submission path.