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
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

jobs:
build-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
dotnet-version: ['8.0.x', '9.0.x']

steps:
- uses: actions/checkout@v4

- name: Setup .NET ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ matrix.dotnet-version }}

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Run SDK tests
run: dotnet test tests/LogTide.SDK.Tests.csproj --no-build --configuration Release --verbosity normal

- name: Run Serilog tests
run: dotnet test tests/LogTide.SDK.Serilog.Tests/LogTide.SDK.Serilog.Tests.csproj --no-build --configuration Release --verbosity normal

vulnerability-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Restore dependencies
run: dotnet restore

- name: Check for vulnerable packages
run: dotnet list package --vulnerable --include-transitive 2>&1 | tee /dev/stderr | grep -q "no vulnerable packages"
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Release to NuGet

on:
push:
tags:
- 'v*'

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT

- name: Restore
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Run SDK tests
run: dotnet test tests/LogTide.SDK.Tests.csproj --no-build --configuration Release --verbosity normal

- name: Run Serilog tests
run: dotnet test tests/LogTide.SDK.Serilog.Tests/LogTide.SDK.Serilog.Tests.csproj --no-build --configuration Release --verbosity normal

- name: Pack LogTide.SDK
run: dotnet pack LogTide.SDK.csproj --no-build --configuration Release -p:PackageVersion=${{ steps.version.outputs.VERSION }} --output ./nupkgs

- name: Pack LogTide.SDK.Serilog
run: dotnet pack Serilog/LogTide.SDK.Serilog.csproj --no-build --configuration Release -p:PackageVersion=${{ steps.version.outputs.VERSION }} --output ./nupkgs

- name: Push to NuGet
run: dotnet nuget push ./nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: ./nupkgs/*.nupkg
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ coverage*.xml
coverage/
TestResults/

# Git worktrees
.worktrees/

# OS generated files
.DS_Store
.DS_Store?
Expand Down
10 changes: 10 additions & 0 deletions Breadcrumbs/Breadcrumb.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace LogTide.SDK.Breadcrumbs;

public sealed class Breadcrumb
{
public string Type { get; set; } = "custom";
public string Message { get; set; } = string.Empty;
public string? Level { get; set; }
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
public Dictionary<string, object?> Data { get; set; } = new();
}
24 changes: 24 additions & 0 deletions Breadcrumbs/BreadcrumbBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace LogTide.SDK.Breadcrumbs;

internal sealed class BreadcrumbBuffer
{
private readonly int _maxSize;
private readonly Queue<Breadcrumb> _queue = new();
private readonly object _lock = new();

public BreadcrumbBuffer(int maxSize = 50) => _maxSize = maxSize;

public void Add(Breadcrumb breadcrumb)
{
lock (_lock)
{
if (_queue.Count >= _maxSize) _queue.Dequeue();
_queue.Enqueue(breadcrumb);
}
}

public IReadOnlyList<Breadcrumb> GetAll()
{
lock (_lock) { return _queue.ToArray(); }
}
}
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,63 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.3] - 2026-03-23

### Added

- **W3C traceparent** distributed tracing (replaces `X-Trace-Id`)
- **AsyncLocal `LogTideScope`** for ambient trace context across async flows
- **Span tracking** with `StartSpan`/`FinishSpan` and OpenTelemetry-compatible OTLP export
- **Breadcrumbs** — `Breadcrumb` model and ring-buffer `BreadcrumbBuffer` attached to scopes
- **Composable transport layer** — `ILogTransport`, `ISpanTransport`, `BatchTransport`, `LogTideHttpTransport`, `OtlpHttpTransport`
- **`ILogTideClient` interface** for DI and testability (with `NSubstitute`)
- **Integration system** — `IIntegration` interface, `GlobalErrorIntegration` for unhandled exceptions
- **Serilog sink** — new `LogTide.SDK.Serilog` project with `LogTideSink` and `WriteTo.LogTide()` extension
- **DSN connection string** support via `ClientOptions.FromDsn()` and `LogTideClient.FromDsn()`
- `SpanId`, `SessionId` fields on `LogEntry`
- `ServiceName`, `TracesSampleRate`, `Integrations` on `ClientOptions`
- `LogTideErrorHandlerMiddleware` for catching unhandled exceptions in ASP.NET Core
- Sensitive header filtering in middleware (`Authorization`, `Cookie`, `X-API-Key`, etc.)
- `NuGetAudit` enabled on all projects
- `FinishSpan` added to `ILogTideClient` interface

### Changed

- Target frameworks: `net8.0;net9.0` (dropped net6.0, net7.0)
- `LangVersion` updated to 13
- `LogTideClient` rewritten as thin facade over `BatchTransport` with scope enrichment
- `LogTideClient` is now `sealed` and implements `ILogTideClient`
- Middleware now uses W3C `traceparent` header (fallback to `X-Trace-Id`)
- Middleware resolved from DI (`ILogTideClient`) instead of `LogTideMiddlewareOptions.Client`
- `AddLogTide()` now registers `IHttpClientFactory` named client
- `System.Text.Json` updated to 9.0.0, `Microsoft.Extensions.Http` to 9.0.0

### Fixed

- **Circuit breaker HalfOpen** now allows exactly one probe (was allowing unlimited)
- **`DisposeAsync`** was setting `_disposed=true` before flushing, silently dropping all buffered logs
- **`Dispose()`** was not flushing buffered logs at all
- **Double-counting `LogsDropped`** when circuit breaker rejected a batch
- **`RecordFailure` called per retry attempt** instead of once — circuit breaker tripped prematurely
- **`FromDsn` HttpClient** was never disposed (resource leak)
- **`IHttpClientFactory` constructor** created 3 separate `HttpClient` instances instead of 1
- **`W3CTraceContext.Parse`** now validates hex characters, flags field, and rejects all-zeros trace/span IDs per W3C spec
- **Duplicate `X-API-Key` header** when `FromDsn` passed pre-configured HttpClient to constructor
- **Span leak in middleware** when `LogRequest` threw before `try` block — now wrapped in `try/finally`
- **Dispose race condition** — non-atomic check-then-act on `_disposed` replaced with `Interlocked.CompareExchange`
- **Sync-over-async deadlock** — `Dispose()` now uses `Task.Run` to avoid `SynchronizationContext` capture
- **`GlobalErrorIntegration._client`** visibility across threads (now `volatile`)
- Removed vulnerable `System.Net.Http 4.3.4` and `System.Text.RegularExpressions 4.3.1` from test project
- Removed vulnerable `Microsoft.AspNetCore.Http.Abstractions 2.2.0` explicit pin (uses `FrameworkReference` now)
- Removed unnecessary `System.Text.Encodings.Web` explicit pin

### Removed

- `SetTraceId()`, `GetTraceId()`, `WithTraceId()`, `WithNewTraceId()` — use `LogTideScope.Create(traceId)`
- `LogTideMiddlewareOptions.Client` property — client resolved from DI
- `LogTideMiddlewareOptions.TraceIdHeader` property — W3C `traceparent` is now the standard
- `Moq` dependency — replaced with `NSubstitute`

## [0.1.0] - 2026-01-13

### Added
Expand All @@ -27,4 +84,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Full async/await support
- Support for .NET 6.0, 7.0, and 8.0

[0.8.3]: https://github.com/logtide-dev/logtide-sdk-csharp/compare/v0.1.0...v0.8.3
[0.1.0]: https://github.com/logtide-dev/logtide-sdk-csharp/releases/tag/v0.1.0
25 changes: 25 additions & 0 deletions Core/ILogTideClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using LogTide.SDK.Breadcrumbs;
using LogTide.SDK.Enums;
using LogTide.SDK.Models;
using LogTide.SDK.Tracing;

namespace LogTide.SDK.Core;

public interface ILogTideClient : IDisposable, IAsyncDisposable
{
void Log(LogEntry entry);
void Debug(string service, string message, Dictionary<string, object?>? metadata = null);
void Info(string service, string message, Dictionary<string, object?>? metadata = null);
void Warn(string service, string message, Dictionary<string, object?>? metadata = null);
void Error(string service, string message, Dictionary<string, object?>? metadata = null);
void Error(string service, string message, Exception exception);
void Critical(string service, string message, Dictionary<string, object?>? metadata = null);
void Critical(string service, string message, Exception exception);
Task FlushAsync(CancellationToken cancellationToken = default);
Span StartSpan(string name, string? parentSpanId = null);
void FinishSpan(Span span, SpanStatus status = SpanStatus.Ok);
void AddBreadcrumb(Breadcrumb breadcrumb);
ClientMetrics GetMetrics();
void ResetMetrics();
CircuitState GetCircuitBreakerState();
}
Loading
Loading