Skip to content

Integration Spanner Extension

aryehcitron@gmail.com edited this page May 17, 2026 · 11 revisions

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>, and MessageTracker.

Using Spanner-specific methods like CreateInsertCommand, CreateSelectCommand, CreateInsertOrUpdateCommand? Option A (ADO.NET Wrapping) only intercepts commands created via DbConnection.CreateCommand() with raw SQL. Spanner-specific factory methods bypass CreateDbCommand() and will not be tracked. Use Option D (gRPC Interception) instead — it tracks all operations regardless of which SpannerConnection method was used.

Installation

dotnet add package Kronikol.Extensions.Spanner

How It Works

Google Cloud Spanner has two .NET client libraries:

  1. ADO.NET (Google.Cloud.Spanner.Data) — SpannerConnection, SpannerCommand, standard DbCommand.ExecuteReader() etc.
  2. Low-level gRPC (Google.Cloud.Spanner.V1) — SpannerClient, direct RPC calls like ExecuteSql, Read, Commit.

This extension provides two interception paths:

  • SpannerTrackingInterceptor (v2.27.0+) intercepts at the gRPC transport layer via SpannerSettings.Interceptor. Captures all operations including Spanner-specific methods. Recommended.
  • TrackingSpannerConnection wraps DbConnection and intercepts SQL commands created via CreateCommand() 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".


Supported Operations

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

Verbosity Levels

SpannerTrackingVerbosity.Summarised

  • Label: Short keyword (e.g. SELECT, INSERT, Commit)
  • Request content: Omitted
  • Response content: Row counts, column names, row previews (when LogResponseContent = true)
  • URI: spanner:///TableName or spanner:///unknown

SpannerTrackingVerbosity.Detailed

  • 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

SpannerTrackingVerbosity.Raw

  • 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.


Choosing a Setup Option

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

Setup

Option A: ADO.NET Wrapping (SpannerConnection)

⚠️ This only intercepts commands created via DbConnection.CreateCommand() with raw SQL. Spanner-specific methods like CreateInsertCommand, CreateSelectCommand, CreateInsertOrUpdateCommand are 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).

Option B: Direct Tracker Usage (gRPC / manual)

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");

Option C: DI Registration

This registers SpannerTracker in 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.

Option D: gRPC Interception (Recommended) — v2.27.0+

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.

How It Works
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
Basic Setup
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
DI Setup (WebApplicationFactory)

⚠️ Important: In WebApplicationFactory / TestServer scenarios, AsyncLocal-based test identity (e.g. CurrentTestInfo.Fetcher) does not propagate through the TestServer's HTTP request pipeline to the gRPC interceptor. You must pass IHttpContextAccessor so the interceptor reads test identity from HTTP headers (propagated by TestTrackingMessageHandler). 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:

  1. IHttpContextAccessor → reads X-Test-Name / X-Test-Id headers set by TestTrackingMessageHandler
  2. Falls back to CurrentTestInfoFetcher (AsyncLocal) — works when called from the test thread directly
With Spanner.InMemoryEmulator

⚠️ When using EmulatorDetection.EmulatorOnly, you must set the SPANNER_EMULATOR_HOST environment variable and must not set Host/Port on 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);
What Gets Captured
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
Excluding Noisy Operations

Session and transaction management generates many gRPC calls. Exclude them for cleaner diagrams:

options.ExcludedOperations = new HashSet<SpannerOperation>
{
    SpannerOperation.CreateSession,
    SpannerOperation.DeleteSession,
    SpannerOperation.BeginTransaction
};
Technical Detail: SessionPoolManager

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.


Configuration

SpannerTrackingOptions

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 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
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 5 Maximum rows to capture in response content
ResponseDetail SpannerResponseDetail RowCountAndColumns Level of detail for response content: RowCountOnly, RowCountAndColumns, or FullRows

Spanner vs Dapper Extension

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 Payload Capture (v2.36.5+)

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.

Tuning Response Output

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: 5)
}

ITrackingComponent

SpannerTracker and TrackingSpannerConnection both implement ITrackingComponent and auto-register:

  • ComponentName: "SpannerTracker ({ServiceName})"
  • WasInvoked: true after first operation
  • InvocationCount: Total operations logged

See Also

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally