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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
136 changes: 136 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# 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)

## Quick Start

```bash
# Build the .NET library
dotnet build

# Run tests for a specific framework
dotnet test --framework net8.0

# Pack the NuGet package
dotnet pack src/StatsdClient/StatsdClient.csproj -c Release
# Output: artifacts/package/release/*.nupkg
```

## Building

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
```

## 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 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 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
```

### 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:

```bash
dotnet pack src/StatsdClient/StatsdClient.csproj -c Release

# 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:

```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
```

## Clean Build

Remove build artifacts:

```bash
# Clean .NET build outputs
dotnet clean

# Remove all build and package outputs
rm -rf artifacts/
```
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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), a C# implementation of the DogStatsD protocol for sending metrics, events, and service checks to Datadog.

## Building and Testing

See [BUILD.md](BUILD.md) for build, test, packaging, and benchmark commands, and the list of target frameworks.

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

Metrics flow through: submission API -> router -> client-side aggregation or buffer -> background worker -> transport, with telemetry tracked throughout.

- **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`.

## Design Notes

- 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.
15 changes: 6 additions & 9 deletions src/StatsdClient/IFileSystem.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.IO;
using Mono.Unix.Native;

namespace StatsdClient
{
Expand Down Expand Up @@ -81,15 +79,14 @@ public TextReader OpenText(string path)
/// <returns>True if the file stat was successful, false otherwise</returns>
public bool TryStat(string path, out ulong inode)
{
if (Environment.OSVersion.Platform == PlatformID.Unix &&
Syscall.stat(path, out var stat) > 0)
{
inode = stat.st_ino;
return true;
}

#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
}
}
}
90 changes: 90 additions & 0 deletions src/StatsdClient/NativeMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#if !NETFRAMEWORK

using System;
using System.Runtime.InteropServices;

namespace StatsdClient
{
/// <summary>
/// P/Invoke wrapper for the libc <c>statx</c> system call to retrieve file inodes on Linux.
/// </summary>
/// <remarks>
/// <c>statx</c> is used instead of <c>stat</c> because its result structure (<c>struct statx</c>)
/// 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
/// <c>stat</c> call has a layout that varies by architecture and libc, which is not safe to marshal directly.
/// </remarks>
internal static class NativeMethods
{
// 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;

/// <summary>
/// Gets a value indicating whether the inode lookup is supported on the current platform (Linux only).
/// </summary>
public static bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

/// <summary>
/// Attempts to get the inode number for the specified file path.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="inode">The inode number if successful, 0 otherwise.</param>
/// <returns>true if the inode was successfully retrieved, false otherwise.</returns>
public static bool TryGetInode(string path, out ulong inode)
{
inode = 0;

if (string.IsNullOrEmpty(path))
{
return false;
}

Comment thread
lucaspimentel marked this conversation as resolved.
if (!IsSupported)
{
return false;
}
try
{
// 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)
{
// libc not available
return false;
}
catch (EntryPointNotFoundException)
{
// 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);

/// <summary>
/// Mirror of the kernel's <c>struct statx</c> (256 bytes). Only the inode field is read; the
/// full size is preserved so the kernel does not write past the buffer.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 256)]
private struct StatxBuffer
{
// stx_ino lives at byte offset 32 in struct statx.
[FieldOffset(32)]
public ulong StatxIno;
}
}
}
#endif
1 change: 0 additions & 1 deletion src/StatsdClient/StatsdClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />

<AdditionalFiles Include="../../stylecop.json" />
Expand Down
Loading
Loading