-
Notifications
You must be signed in to change notification settings - Fork 1
Integration Spanner Extension
Track Google Cloud Spanner operations in your test diagrams using the TestTrackingDiagrams.Extensions.Spanner NuGet package. This extension supports three interception strategies: gRPC interception (recommended), ADO.NET wrapping, and server-side observation (for Spanner.InMemoryEmulator users).
Using a shared library or abstraction layer? If your code doesn't use the Spanner SDK directly — e.g. it goes through a shared data-access library, wrapper, or custom abstraction — this extension won't be able to intercept the underlying calls. See Tracking Custom Dependencies for alternative approaches including
RequestResponseLogger.LogPair(),TrackingProxy<T>, andMessageTracker.
Using Spanner-specific methods like
CreateInsertCommand,CreateSelectCommand,CreateInsertOrUpdateCommand? Option A (ADO.NET Wrapping) only intercepts commands created viaDbConnection.CreateCommand()with raw SQL. Spanner-specific factory methods bypassCreateDbCommand()and will not be tracked. Use Option D (gRPC Interception) or Option E (Server-Side Observation) instead — these track all operations regardless of whichSpannerConnectionmethod was used.
dotnet add package TestTrackingDiagrams.Extensions.Spanner
Google Cloud Spanner has two .NET client libraries:
-
ADO.NET (
Google.Cloud.Spanner.Data) —SpannerConnection,SpannerCommand, standardDbCommand.ExecuteReader()etc. -
Low-level gRPC (
Google.Cloud.Spanner.V1) —SpannerClient, direct RPC calls likeExecuteSql,Read,Commit.
This extension provides three interception paths:
-
SpannerTrackingInterceptor(v2.27.0+) intercepts at the gRPC transport layer viaSpannerSettings.Interceptor. Captures all operations including Spanner-specific methods. Recommended. -
TrackingSpannerConnectionwrapsDbConnectionand intercepts SQL commands created viaCreateCommand()only. -
SpannerTrackerprovides directLogRequest/LogResponsepairing for manual logging, or server-side observation viaCreateServerObservers().
All paths use SpannerOperationClassifier to classify operations and SpannerTracker to log to RequestResponseLogger with RequestResponseMetaType.Event and DependencyCategory: "Spanner".
| Operation | SQL Pattern | gRPC Method |
|---|---|---|
Query |
SELECT ... |
ExecuteSql, ExecuteStreamingSql
|
Read |
— | Read |
StreamingRead |
— | StreamingRead |
Insert |
INSERT INTO ... |
— |
Update |
UPDATE ... |
— |
Delete |
DELETE FROM ... |
— |
InsertOrUpdate |
— | Mutation-based |
Replace |
— | Mutation-based |
Commit |
— | Commit |
Rollback |
— | Rollback |
BeginTransaction |
— | BeginTransaction |
BatchDml |
— | ExecuteBatchDml |
PartitionQuery |
— | PartitionQuery |
PartitionRead |
— | PartitionRead |
Ddl |
CREATE TABLE, ALTER TABLE, DROP TABLE
|
— |
CreateSession |
— |
CreateSession, BatchCreateSessions
|
DeleteSession |
— | DeleteSession |
-
Label: Short keyword (e.g.
SELECT,INSERT,Commit) - Content: Omitted
-
URI:
spanner:///TableNameorspanner:///unknown
-
Label: Operation with table name (e.g.
SELECT FROM Users,INSERT INTO Orders) -
Content: SQL text (if
LogSqlText = true) -
URI:
spanner:///TableName
- Label: Full SQL text
-
Content: SQL text + parameters (if
LogParameters = true) -
URI:
spanner:///databaseId/TableName
| Option | Approach | Tracks Spanner-specific methods | Works with real Spanner | Requires prod code changes | Best for |
|---|---|---|---|---|---|
| A | ADO.NET wrapping | No — only CreateCommand() with raw SQL |
Yes | Yes (return DbConnection) |
Code using raw SQL via DbCommand
|
| B | Manual tracker | Yes (you log manually) | Yes | No | Full control, custom logging |
| C | DI registration | Registers tracker only | Yes | No | Used with B, D, or E |
| D ⭐ | gRPC interception | Yes — all operations | Yes | No | Recommended for most users |
| E | Server-side observation | Yes — all operations | No (test only) | No | Using Spanner.InMemoryEmulator
|
⚠️ This only intercepts commands created viaDbConnection.CreateCommand()with raw SQL. Spanner-specific methods likeCreateInsertCommand,CreateSelectCommand,CreateInsertOrUpdateCommandare not tracked. Use Option D for full coverage.
Wrap your SpannerConnection (or any DbConnection) with WithTestTracking():
using TestTrackingDiagrams.Extensions.Spanner;
var connection = new SpannerConnection(connectionString);
var trackingConnection = connection.WithTestTracking(new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallingServiceName = "OrdersApi",
CurrentTestInfoFetcher = CurrentTestInfo.Fetcher
});
// Use trackingConnection in place of connection
using var cmd = trackingConnection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Id = @id";
// ...The tracking connection creates TrackingSpannerCommand wrappers that intercept ExecuteReader, ExecuteNonQuery, and ExecuteScalar (both sync and async).
Prefer Option D or E for automatic tracking. Use Option B when you need full control over what is logged, or when integrating with a custom gRPC client.
For low-level SpannerClient usage or manual logging:
using TestTrackingDiagrams.Extensions.Spanner;
var tracker = new SpannerTracker(new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallingServiceName = "OrdersApi",
CurrentTestInfoFetcher = () => (testName, testId)
});
// Classify and log
var op = SpannerOperationClassifier.ClassifyGrpc("ExecuteSql", "Users", databaseId);
var (reqId, traceId) = tracker.LogRequest(op, "SELECT * FROM Users");
// ... execute operation ...
tracker.LogResponse(op, reqId, traceId, "5 rows returned");This registers
SpannerTrackerin DI. Combine with Option D (gRPC interception) or Option E (server-side observation) to enable automatic operation capture.
services.AddSpannerTestTracking(options =>
{
options.ServiceName = "Spanner";
options.CallingServiceName = "OrdersApi";
options.CurrentTestInfoFetcher = CurrentTestInfo.Fetcher;
});Then inject SpannerTracker where needed.
Intercepts at the gRPC transport layer underneath SpannerConnection via SpannerSettings.Interceptor. Captures all operations — CreateInsertCommand, CreateSelectCommand, mutations, DML, DDL, transactions — regardless of which SpannerConnection method was used. Zero production code changes required.
SpannerConnection (sealed — unchanged)
└── CreateInsertCommand / CreateSelectCommand / CreateCommand / etc.
└── Internal SpannerClient
└── gRPC CallInvoker
└── SpannerTrackingInterceptor ← intercepts here
├── Classifies: method name + SQL + table from protobuf
├── Logs to SpannerTracker
└── Forwards to real gRPC channel
using TestTrackingDiagrams.Extensions.Spanner;
var builder = new SpannerConnectionStringBuilder
{
DataSource = "projects/my-project/instances/my-instance/databases/my-db"
};
builder.WithTestTracking(new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallingServiceName = "OrdersApi",
CurrentTestInfoFetcher = CurrentTestInfo.Fetcher
});
var connection = new SpannerConnection(builder);
// All operations — including CreateInsertCommand, CreateSelectCommand — are now tracked
⚠️ Important: InWebApplicationFactory/TestServerscenarios,AsyncLocal-based test identity (e.g.CurrentTestInfo.Fetcher) does not propagate through the TestServer's HTTP request pipeline to the gRPC interceptor. You must passIHttpContextAccessorso the interceptor reads test identity from HTTP headers (propagated byTestTrackingMessageHandler). Without this, the interceptor fires but silently skips all operations because it cannot determine which test is running.
var trackingOptions = new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallingServiceName = "OrdersApi",
CurrentTestInfoFetcher = CurrentTestInfo.Fetcher
};
// Create the builder inside the DI factory so IHttpContextAccessor is available
services.AddSingleton<ISpannerConnectionFactory>(sp =>
{
var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var builder = new SpannerConnectionStringBuilder { DataSource = "..." }
.WithTestTracking(trackingOptions, httpContextAccessor);
return new TrackedFactory(() => new SpannerConnection(builder));
});The IHttpContextAccessor overload (v2.27.3+) resolves test identity in this order:
-
IHttpContextAccessor→ readsX-Test-Name/X-Test-Idheaders set byTestTrackingMessageHandler - Falls back to
CurrentTestInfoFetcher(AsyncLocal) — works when called from the test thread directly
var server = new FakeSpannerServer();
server.Start();
var builder = new SpannerConnectionStringBuilder
{
DataSource = server.DataSource,
EmulatorDetection = EmulatorDetection.EmulatorOnly
};
builder.WithTestTracking(new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallingServiceName = "OrdersApi",
CurrentTestInfoFetcher = CurrentTestInfo.Fetcher
});
var connection = new SpannerConnection(builder);| SpannerConnection Method | gRPC Method Intercepted | Extracted Data |
|---|---|---|
CreateInsertCommand("Users") |
Commit (mutation) |
Table: Users, Op: Insert |
CreateSelectCommand(sql) |
ExecuteSql |
SQL text, table from SQL |
CreateInsertOrUpdateCommand("Users") |
Commit (mutation) |
Table: Users, Op: InsertOrUpdate |
CreateUpdateCommand("Users") |
Commit (mutation) |
Table: Users, Op: Update |
CreateDeleteCommand("Users", keys) |
Commit (mutation) |
Table: Users, Op: Delete |
CreateDmlCommand(sql) |
ExecuteSql |
SQL text, table from SQL |
CreateReadCommand(table, ...) |
Read |
Table name |
CreateBatchDmlCommand() |
ExecuteBatchDml |
SQL text per statement |
connection.CreateCommand() + raw SQL |
ExecuteSql |
SQL text, table from SQL |
Session and transaction management generates many gRPC calls. Exclude them for cleaner diagrams:
options.ExcludedOperations = new HashSet<SpannerOperation>
{
SpannerOperation.CreateSession,
SpannerOperation.DeleteSession,
SpannerOperation.BeginTransaction
};WithTestTracking creates a custom SessionPoolManager via SessionPoolManager.CreateWithSettings(new SessionPoolOptions(), settings) where settings.Interceptor is set to a SpannerTrackingInterceptor. This is the documented public API of the Spanner SDK — not a hack or reflection. The SessionPoolManager is assigned to builder.SessionPoolManager, which SpannerConnection uses to create its internal SpannerClient.
If you're already using Spanner.InMemoryEmulator's FakeSpannerServer, you can observe all operations at the server level. The server sees every gRPC call regardless of which client API was used. Simpler setup than Option D, but only works with the in-memory emulator — not with real Spanner or the Docker emulator.
Requires
Spanner.InMemoryEmulatorv1.1.0+ which addsOnRequestReceived/OnResponseSentcallbacks.
using TestTrackingDiagrams.Extensions.Spanner;
var server = new FakeSpannerServer();
server.Start();
var tracker = new SpannerTracker(new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallingServiceName = "OrdersApi",
CurrentTestInfoFetcher = CurrentTestInfo.Fetcher
});
// Wire server callbacks to tracker using TTD's helper factory
var (onRequest, onResponse) = SpannerTracker.CreateServerObservers(tracker);
server.OnRequestReceived = onRequest;
server.OnResponseSent = onResponse;
// Use the server's connection as normal — all operations are tracked
var connection = server.CreateConnection();SpannerConnection (unchanged)
└── gRPC calls
└── FakeSpannerServer (in-process gRPC server)
├── OnRequestReceived → SpannerTracker.LogRequest()
├── Executes operation against InMemorySpannerDatabase
└── OnResponseSent → SpannerTracker.LogResponse()
// In ConfigureTestServices:
var tracker = new SpannerTracker(trackingOptions);
var (onRequest, onResponse) = SpannerTracker.CreateServerObservers(tracker);
server.OnRequestReceived = onRequest;
server.OnResponseSent = onResponse;
services.RemoveAll<ISpannerConnectionFactory>();
services.AddSingleton<ISpannerConnectionFactory>(
_ => new SpannerConnectionFactory(server.ConnectionString));With server-side observation, you don't need to modify the connection builder — the server captures everything.
| Option D (gRPC Interceptor) | Option E (Server-Side) | |
|---|---|---|
| Works with real Spanner | Yes | No |
| Works with Docker emulator | Yes | No |
| Works with FakeSpannerServer | Yes | Yes |
| Requires connection builder changes | Yes (WithTestTracking) |
No |
| Sees internal session/transaction gRPC | Yes | Yes |
| Setup complexity | Medium | Low |
| Property | Type | Default | Description |
|---|---|---|---|
ServiceName |
string |
"Spanner" |
Display name in diagrams for the Spanner service |
CallingServiceName |
string |
"Caller" |
Calling service name in diagrams |
Verbosity |
SpannerTrackingVerbosity |
Detailed |
Verbosity level (Raw, Detailed, Summarised) |
CurrentTestInfoFetcher |
Func<(string Name, string Id)>? |
null |
Required: provides test context for log correlation |
CurrentStepTypeFetcher |
Func<string?>? |
null |
Optional — returns the current BDD step type |
LogSqlText |
bool |
true |
Include SQL text in logged content (Detailed mode) |
LogParameters |
bool |
false |
Include command parameters in logged content (Raw mode) |
ExcludedOperations |
HashSet<SpannerOperation> |
[] |
Operations to skip (e.g. CreateSession, DeleteSession) |
SetupVerbosity |
SpannerTrackingVerbosity? |
null |
Verbosity override for the Setup phase. See Phase-Aware Tracking |
ActionVerbosity |
SpannerTrackingVerbosity? |
null |
Verbosity override for the Action phase. See Phase-Aware Tracking |
TrackDuringSetup |
bool |
true |
When false, tracking is suppressed during Setup |
TrackDuringAction |
bool |
true |
When false, tracking is suppressed during Action |
If you already use the Integration Dapper Extension, it can also intercept SpannerConnection queries (since SpannerConnection is a DbConnection). The key difference:
| Spanner Extension | Dapper Extension | |
|---|---|---|
| Classification | Spanner-specific (Read, StreamingRead, Mutations, DDL, Partitioned ops) | Generic SQL (SELECT, INSERT, UPDATE, DELETE) |
| gRPC support | Yes (SpannerClient wrapping + classifier) | No |
| Dependency category | Database |
SQL |
| URI scheme | spanner:/// |
sql:/// |
Use the Spanner extension when you want Spanner-specific operation visibility. Use the Dapper extension if you only need basic SQL classification.
SpannerTracker and TrackingSpannerConnection both implement ITrackingComponent and auto-register:
-
ComponentName:"SpannerTracker ({ServiceName})" -
WasInvoked:trueafter first operation -
InvocationCount: Total operations logged
- Integration Grpc Extension — gRPC interception patterns (generic)
- Integration Dapper Extension
- Integration BigQuery Extension
- Integration Bigtable Extension
- Integration CosmosDB Extension
- Phase-Aware Tracking — per-phase verbosity overrides
- Spanner.InMemoryEmulator — in-memory fake for Option E
Getting Started
Common Tasks
Integration Guides
- Integration xUnit3
- Integration xUnit2
- Integration NUnit
- Integration MSTest
- Integration TUnit
- Integration BDDfy xUnit3
- Integration LightBDD xUnit2
- Integration LightBDD xUnit3
- Integration LightBDD TUnit
- Integration ReqNRoll xUnit2
- Integration ReqNRoll xUnit3
- Integration ReqNRoll TUnit
Extensions
- Integration AtlasDataApi Extension
- Integration BigQuery Extension
- Integration Bigtable Extension
- Integration BlobStorage Extension
- Integration ClickHouse Extension
- Integration CloudStorage Extension
- Integration CosmosDB Extension
- Integration Dapper Extension
- Integration DynamoDB Extension
- Integration EF Core Relational Extension
- Integration Elasticsearch Extension
- Integration EventBridge Extension
- Integration EventHubs Extension
- Integration Grpc Extension
- Integration Kafka Extension
- Integration MassTransit Extension
- Integration MongoDB Extension
- Integration MySqlConnector Extension
- Integration Npgsql Extension
- Integration Oracle Extension
- Integration PubSub Extension
- Integration Redis Extension
- Integration S3 Extension
- Integration ServiceBus Extension
- Integration SNS Extension
- Integration Spanner Extension
- Integration SqlClient Extension
- Integration Sqlite Extension
- Integration SQS Extension
- Integration StorageQueues Extension
- Integration OpenTelemetry Extension
- Integration DispatchProxy Extension
- Integration MediatR Extension
- Integration PlantUML IKVM
Configuration
- Tracking Dependencies
- Tracking Custom Dependencies
- HTTP Tracking Setup
- Report Configuration
- Diagram Customisation
- Phase-Aware Tracking
- Content Formatting
- PlantUML Server Configuration
Features
- Generated Reports
- Search Syntax
- Component Diagrams
- PlantUML Browser Rendering
- Inline SVG Rendering
- Internal Flow Tracking
- Tags and Attributes
- Excluding Requests
- Excluded Headers
- Multi-Host Test Architectures
- Event-Driven Architecture Testing
- Service Bus Tracking Patterns
- Background Thread Correlation
- Parallel-Safe Background Correlation
- Event & Message Tracking
- Assertion Tracking
- Step Tracking
- Tabular Attributes
- Large Response and Diagram Handling
- Diagnostics and Debugging
- CI Summary Integration
- CI Artifact Upload
- Merging Parallel Reports
Reference