-
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 Kronikol.Extensions.Spanner NuGet package. This extension supports gRPC interception (recommended), ADO.NET wrapping, and manual tracking.
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) instead — it tracks all operations regardless of whichSpannerConnectionmethod was used.
dotnet add package Kronikol.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 two 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.
SpannerTracker provides direct LogRequest/LogResponse pairing for manual logging when needed.
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) - Request content: Omitted
-
Response content: Row counts, column names, row previews (when
LogResponseContent = true) -
URI:
spanner:///TableNameorspanner:///unknown
-
Label: Operation with table name (e.g.
SELECT FROM Users,INSERT INTO Orders) -
Request content: SQL text (if
LogSqlText = true) -
Response content: Row counts, column names, row previews (when
LogResponseContent = true) -
URI:
spanner:///TableName
- Label: Full SQL text
-
Request content: SQL text + parameters (if
LogParameters = true) -
Response content: Row counts, column names, row previews (when
LogResponseContent = true) -
URI:
spanner:///databaseId/TableName
Response content is shown at all verbosity levels when LogResponseContent = true (the default). Set LogResponseContent = false to restore previous empty-arrow behaviour.
| Option | Approach | Tracks Spanner-specific methods | Works with real Spanner | WebApplicationFactory | Requires prod code changes | Best for |
|---|---|---|---|---|---|---|
| A | ADO.NET wrapping | No — only CreateCommand() with raw SQL |
Yes | Yes | Yes (return DbConnection) |
Code using raw SQL via DbCommand
|
| B | Manual tracker | Yes (you log manually) | Yes | Yes | No | Full control, custom logging |
| C | DI registration | Registers tracker only | Yes | Yes | No | Used with B or D |
| D ⭐ | gRPC interception | Yes — all operations | Yes |
Yes (with IHttpContextAccessor) |
No | Recommended for most users |
⚠️ 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 Kronikol.Extensions.Spanner;
var connection = new SpannerConnection(connectionString);
var trackingConnection = connection.WithTestTracking(new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallerName = "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 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 Kronikol.Extensions.Spanner;
var tracker = new SpannerTracker(new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallerName = "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) to enable automatic operation capture.
services.AddSpannerTestTracking(options =>
{
options.ServiceName = "Spanner";
options.CallerName = "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 Kronikol.Extensions.Spanner;
var builder = new SpannerConnectionStringBuilder
{
DataSource = "projects/my-project/instances/my-instance/databases/my-db"
};
builder.WithTestTracking(new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallerName = "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",
CallerName = "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
⚠️ When usingEmulatorDetection.EmulatorOnly, you must set theSPANNER_EMULATOR_HOSTenvironment variable and must not setHost/Porton the builder — the SDK forbids an explicit endpoint when the emulator env var is present.
var server = new FakeSpannerServer();
server.Start();
// Set emulator env var — required by the SDK's SpannerClientBuilder
Environment.SetEnvironmentVariable("SPANNER_EMULATOR_HOST", $"localhost:{server.Port}");
var trackingOptions = new SpannerTrackingOptions
{
ServiceName = "Spanner",
CallerName = "OrdersApi",
CurrentTestInfoFetcher = CurrentTestInfo.Fetcher,
ExcludedOperations =
{
SpannerOperation.CreateSession,
SpannerOperation.DeleteSession,
SpannerOperation.BeginTransaction
}
};
// In a WebApplicationFactory, create inside a DI factory to access IHttpContextAccessor:
services.AddSingleton<ISpannerConnectionFactory>(sp =>
{
var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var builder = new SpannerConnectionStringBuilder
{
DataSource = server.DataSource,
EmulatorDetection = EmulatorDetection.EmulatorOnly
}.WithTestTracking(trackingOptions, httpContextAccessor);
return new MyConnectionFactory(builder);
});For non-WebApplicationFactory scenarios (e.g. direct test usage), the simpler form without IHttpContextAccessor works:
var builder = new SpannerConnectionStringBuilder
{
DataSource = server.DataSource,
EmulatorDetection = EmulatorDetection.EmulatorOnly
}.WithTestTracking(trackingOptions);
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.
| Property | Type | Default | Description |
|---|---|---|---|
ServiceName |
string |
"Spanner" |
Display name in diagrams for the Spanner service |
CallerName |
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 |
|
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 |
LogResponseContent |
bool |
true |
Include response data in diagram response arrows at all verbosity levels (v2.37.3+). Set to false to restore previous empty-arrow behaviour |
MaxResponseRows |
int |
10 |
Maximum rows to capture in response content |
ResponseDetail |
SpannerResponseDetail |
RowCountAndColumns |
Level of detail for response content: RowCountOnly, RowCountAndColumns, or FullRows
|
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 | Spanner |
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.
Response arrows in diagrams show actual data instead of being empty. The ResponseDetail option controls the level of detail:
ResponseDetail |
Example output |
|---|---|
RowCountOnly |
3 rows |
RowCountAndColumns (default)
|
3 rows [Name, Email, CreatedAt] |
FullRows |
JSON row preview (up to MaxResponseRows) |
Streaming responses (ExecuteStreamingSql, StreamingRead) are accumulated and logged after stream completion or disposal. CommitResponse and ExecuteBatchDmlResponse are also formatted.
new SpannerTrackingOptions
{
Verbosity = SpannerTrackingVerbosity.Summarised,
LogResponseContent = true, // default — show response data
ResponseDetail = SpannerResponseDetail.RowCountOnly, // just "3 rows" instead of column names
MaxResponseRows = 10 // capture up to 10 rows (default: 10)
}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 testing
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