Skip to content
Draft
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
42 changes: 28 additions & 14 deletions csharp/doc/telemetry-sprint-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,25 +812,34 @@ Implement the core telemetry infrastructure including feature flag management, p

---

### Phase 7: End-to-End Testing
### Phase 7: End-to-End Testing ✅ COMPLETED

#### WI-7.1: E2E Telemetry Tests
**Description**: Comprehensive end-to-end tests for telemetry flow.
**Status**: COMPLETED - All 6 E2E pipeline tests + 9 client telemetry tests pass against live Databricks environment.

**Location**: `csharp/test/E2E/TelemetryTests.cs`
**Location**: `csharp/test/E2E/Telemetry/` (3 files)
- `TelemetryE2ETests.cs` - Full pipeline E2E tests using CapturingTelemetryExporter
- `CapturingTelemetryExporter.cs` - Test exporter capturing TelemetryFrontendLog instances
- `TelemetryTestHelpers.cs` - Helper methods for creating connections with injected telemetry
- `ClientTelemetryE2ETests.cs` - Direct HTTP endpoint tests for DatabricksTelemetryExporter

**Test Expectations**:
**Test Results** (all passing):

| Test Type | Test Name | Input | Expected Output |
|-----------|-----------|-------|-----------------|
| E2E | `Telemetry_Connection_ExportsConnectionEvent` | Open connection to Databricks | Connection event exported to telemetry service |
| E2E | `Telemetry_Statement_ExportsStatementEvent` | Execute SELECT 1 | Statement event exported with execution latency |
| E2E | `Telemetry_CloudFetch_ExportsChunkMetrics` | Execute large query | Statement event includes chunk_count, bytes_downloaded |
| E2E | `Telemetry_Error_ExportsErrorEvent` | Execute invalid SQL | Error event exported with error.type |
| E2E | `Telemetry_FeatureFlagDisabled_NoExport` | Server feature flag off | No telemetry events exported |
| E2E | `Telemetry_MultipleConnections_SameHost_SharesClient` | Open 3 connections to same host | Single telemetry client used |
| E2E | `Telemetry_CircuitBreaker_StopsExportingOnFailure` | Telemetry endpoint unavailable | After threshold failures, events dropped |
| E2E | `Telemetry_GracefulShutdown_FlushesBeforeClose` | Close connection with pending events | All events flushed before connection closes |
| Test Type | Test Name | Input | Expected Output | Status |
|-----------|-----------|-------|-----------------|--------|
| E2E | `Telemetry_Connection_ExportsConnectionEvent` | Open connection + execute query | At least 1 TelemetryFrontendLog captured | ✅ PASS |
| E2E | `Telemetry_Statement_ExportsStatementEvent` | Execute SELECT 1 | Log with sql_statement_id and operation_latency_ms >= 0 | ✅ PASS |
| E2E | `Telemetry_Error_ExportsErrorEvent` | Execute invalid SQL | Log with ErrorInfo != null (error.type captured when available) | ✅ PASS |
| E2E | `Telemetry_FeatureFlagDisabled_NoExport` | telemetry.enabled=false | 0 logs captured, 0 export calls | ✅ PASS |
| E2E | `Telemetry_MultipleConnections_SharesClient` | Open 3 connections to same host | Single exporter factory call, 3+ logs, 3 distinct session IDs | ✅ PASS |
| E2E | `Telemetry_GracefulShutdown_FlushesEvents` | Execute 3 queries then close | All 3 events flushed during dispose | ✅ PASS |

**Implementation Notes**:
- CloudFetch chunk metrics test (`Telemetry_CloudFetch_ExportsChunkMetrics`) deferred - requires large query execution and CloudFetch-enabled warehouse configuration
- Circuit breaker test (`Telemetry_CircuitBreaker_StopsExportingOnFailure`) covered comprehensively in unit tests (22 tests in `CircuitBreakerTelemetryExporterTests.cs`); E2E would require real endpoint failure simulation
- Test injection uses `DatabricksConnection.TestExporterFactory` + `TelemetryClientManager.UseTestInstance()` for complete isolation
- Each test uses `TelemetryTestHelpers.UseFreshTelemetryClientManager()` to ensure exporter factory is always called

---

Expand Down Expand Up @@ -936,7 +945,12 @@ csharp/test/
│ ├── MetricsAggregatorTests.cs
│ └── DatabricksActivityListenerTests.cs
└── E2E/
└── TelemetryTests.cs (enhanced)
├── TelemetryTests.cs (base class wrapper)
└── Telemetry/
├── TelemetryE2ETests.cs (pipeline E2E tests)
├── CapturingTelemetryExporter.cs (test exporter)
├── TelemetryTestHelpers.cs (connection helpers)
└── ClientTelemetryE2ETests.cs (HTTP endpoint tests)
```

## Test Coverage Goals
Expand Down
5 changes: 4 additions & 1 deletion csharp/src/Telemetry/DatabricksActivityListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ internal sealed class DatabricksActivityListener : IDisposable
{
/// <summary>
/// The ActivitySource name used by the Databricks ADBC driver.
/// This must match the assembly name used by <see cref="DatabricksConnection"/>
/// as the ActivitySource name in <c>TracingConnection</c>.
/// </summary>
internal const string DatabricksActivitySourceName = "Databricks.Adbc.Driver";
internal static readonly string DatabricksActivitySourceName =
typeof(DatabricksConnection).Assembly.GetName().Name!;

private static readonly Lazy<DatabricksActivityListener> s_instance =
new Lazy<DatabricksActivityListener>(() => new DatabricksActivityListener());
Expand Down
5 changes: 4 additions & 1 deletion csharp/src/Telemetry/MetricsAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ internal sealed class MetricsAggregator : IDisposable
/// <summary>
/// The ActivitySource name used by the Databricks ADBC driver.
/// Used to determine if an activity is a root activity.
/// This must match the assembly name used by <see cref="DatabricksConnection"/>
/// as the ActivitySource name in <c>TracingConnection</c>.
/// </summary>
internal const string DatabricksActivitySourceName = "Databricks.Adbc.Driver";
internal static readonly string DatabricksActivitySourceName =
DatabricksActivityListener.DatabricksActivitySourceName;

private readonly ITelemetryClient _telemetryClient;
private readonly TelemetryConfiguration _config;
Expand Down
130 changes: 130 additions & 0 deletions csharp/test/E2E/Telemetry/CapturingTelemetryExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (c) 2025 ADBC Drivers Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdbcDrivers.Databricks.Telemetry;
using AdbcDrivers.Databricks.Telemetry.Models;

namespace AdbcDrivers.Databricks.Tests.E2E.Telemetry
{
/// <summary>
/// A test implementation of <see cref="ITelemetryExporter"/> that captures all exported
/// <see cref="TelemetryFrontendLog"/> instances in memory for test assertions.
/// </summary>
/// <remarks>
/// This exporter never makes HTTP calls. It stores all exported logs in a thread-safe
/// collection that tests can inspect after driver operations complete.
/// Supports configurable failure simulation for circuit breaker and error path testing.
/// </remarks>
internal sealed class CapturingTelemetryExporter : ITelemetryExporter
{
private readonly ConcurrentBag<TelemetryFrontendLog> _capturedLogs = new ConcurrentBag<TelemetryFrontendLog>();
private volatile bool _shouldFail;
private volatile int _exportCallCount;

/// <summary>
/// Gets all captured telemetry frontend logs.
/// </summary>
public IReadOnlyList<TelemetryFrontendLog> CapturedLogs => _capturedLogs.ToList().AsReadOnly();

/// <summary>
/// Gets the total number of captured logs.
/// </summary>
public int CapturedLogCount => _capturedLogs.Count;

/// <summary>
/// Gets the total number of times <see cref="ExportAsync"/> was called.
/// </summary>
public int ExportCallCount => _exportCallCount;

/// <summary>
/// Gets or sets whether the exporter should simulate failures.
/// When true, <see cref="ExportAsync"/> returns false to simulate export failure.
/// </summary>
public bool ShouldFail
{
get => _shouldFail;
set => _shouldFail = value;
}

/// <summary>
/// Export telemetry frontend logs by capturing them in memory.
/// </summary>
/// <param name="logs">The list of telemetry frontend logs to capture.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>
/// True if capture succeeded (and <see cref="ShouldFail"/> is false),
/// false if <see cref="ShouldFail"/> is true.
/// Returns true for empty/null logs.
/// </returns>
public Task<bool> ExportAsync(IReadOnlyList<TelemetryFrontendLog> logs, CancellationToken ct = default)
{
Interlocked.Increment(ref _exportCallCount);

if (logs == null || logs.Count == 0)
{
return Task.FromResult(true);
}

if (_shouldFail)
{
return Task.FromResult(false);
}

foreach (TelemetryFrontendLog log in logs)
{
_capturedLogs.Add(log);
}

return Task.FromResult(true);
}

/// <summary>
/// Clears all captured logs and resets the call counter.
/// </summary>
public void Reset()
{
while (_capturedLogs.TryTake(out _))
{
// Drain the bag
}
_exportCallCount = 0;
_shouldFail = false;
}

/// <summary>
/// Waits until at least the specified number of logs have been captured,
/// or the timeout is exceeded.
/// </summary>
/// <param name="expectedCount">The minimum number of logs to wait for.</param>
/// <param name="timeout">Maximum time to wait.</param>
/// <returns>True if the expected count was reached; false if timed out.</returns>
public async Task<bool> WaitForLogsAsync(int expectedCount, TimeSpan timeout)
{
DateTime deadline = DateTime.UtcNow + timeout;
while (_capturedLogs.Count < expectedCount && DateTime.UtcNow < deadline)
{
await Task.Delay(50).ConfigureAwait(false);
}
return _capturedLogs.Count >= expectedCount;
}
}
}
Loading
Loading