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
38 changes: 30 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ Application Options:
--param= [name]=[Cloud Spanner type(PLAN only) or literal]; legacy name:value OK
--param-file= YAML or JSON file of query parameters
--log-grpc Show gRPC logs
--experimental-trace-project=
--experimental-trace-stdout Export traces to stderr (local debugging)
--experimental-trace-project= Export traces to Cloud Trace
--experimental-trace-stdout Export spans to stderr as pretty JSON
--experimental-trace-otlp Export spans via OTLP/gRPC (local collector)
--experimental-trace-otlp-endpoint= OTLP/gRPC endpoint (default: localhost:4317)
--enable-partitioned-dml Execute DML statement using Partitioned DML
--timeout= Maximum time to wait for the SQL query to complete (default: 10m)
--try-partition-query (Experimental) Check whether the query can be executed as partition query or not
Expand Down Expand Up @@ -240,23 +242,43 @@ $ execspansql ${DATABASE_ID} --query-mode=NORMAL \
--param='songinfo=STRUCT<SongName STRING, ArtistNames ARRAY<STRUCT<FirstName STRING, LastName STRING>>>("Imagination", [("Elena", "Campbell"), ("Hannah", "Harris")])'
```

### (Experimental) Cloud Trace integration
### (Experimental) OpenTelemetry tracing

Export PROFILE query plans and Spanner client spans via OpenTelemetry (`spannerotel` + the Spanner client's native OTel instrumentation).
Export Spanner client spans and PROFILE query plans via OpenTelemetry (`spannerotel` + the Spanner client's native OTel instrumentation).

Export to Cloud Trace:
Plan node spans (`spannerotel/plantotrace`) appear only with **`--query-mode=PROFILE`** (or equivalent stats that include a query plan). NORMAL mode still records Spanner client spans, but not per-plan-node children.

Exactly one trace export flag may be set: `--experimental-trace-project`, `--experimental-trace-stdout`, or `--experimental-trace-otlp`.

#### Cloud Trace

```sh
$ execspansql $DATABASE_ID --sql "SELECT * FROM Singers@{FORCE_INDEX=SingersByFirstLastName}" --query-mode=PROFILE --experimental-trace-project=$PROJECT_ID
$ execspansql $DATABASE_ID --sql "SELECT * FROM Singers@{FORCE_INDEX=SingersByFirstLastName}" \
--query-mode=PROFILE --experimental-trace-project=$PROJECT_ID
```

For local debugging without Cloud Trace credentials, write spans to stderr:
#### Local collector (OTLP/gRPC)

Send spans to a local OpenTelemetry Collector, Jaeger, Grafana Tempo, etc.:

```sh
# Example: collector listening on localhost:4317
$ execspansql $DATABASE_ID --query-mode=PROFILE --sql 'SELECT 1' --experimental-trace-otlp

# Custom endpoint
$ execspansql $DATABASE_ID --query-mode=PROFILE --sql 'SELECT 1' \
--experimental-trace-otlp --experimental-trace-otlp-endpoint=127.0.0.1:4317
```

#### stderr JSON (no collector)

Pretty-printed span JSON to stderr:

```sh
$ execspansql $DATABASE_ID --query-mode=PROFILE --sql 'SELECT 1' --experimental-trace-stdout
```

`--experimental-trace-stdout` and `--experimental-trace-project` are mutually exclusive.
Note: `--experimental-trace-stdout` writes to **stderr**, not stdout.

![trace.png](docs/trace.png)

Expand Down
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/apstndb/memebridge v0.6.1
github.com/apstndb/spanemuboost v0.4.6
github.com/apstndb/spaniter v0.3.0
github.com/apstndb/spannerotel v0.1.0
github.com/apstndb/spannerotel v0.2.0
github.com/apstndb/spanvalue v0.8.0
github.com/cloudspannerecosystem/memefish v0.6.2
github.com/goccy/go-yaml v1.19.2
Expand Down Expand Up @@ -43,6 +43,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/apstndb/spantype v0.3.11 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
Expand All @@ -67,6 +68,7 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/itchyny/timefmt-go v0.1.7 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
Expand Down Expand Up @@ -100,10 +102,13 @@ require (
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/net v0.55.0 // indirect
Expand Down
14 changes: 12 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,17 @@ github.com/apstndb/spanemuboost v0.4.6 h1:9f1qQDLNrPQJiswNLtiTaR0U//zToqxjcEGWGk
github.com/apstndb/spanemuboost v0.4.6/go.mod h1:urUe85EvqomWV4vCj2pALluNhIksbu9RAQZufgq1b8E=
github.com/apstndb/spaniter v0.3.0 h1:b0zXMONClGRfvWf7ciwsIYVso9qkhqFdjH0+PqOnQGI=
github.com/apstndb/spaniter v0.3.0/go.mod h1:aBSHcHIqgAZXCxFdi734R/wAQUIuCQ6WZ+CjOCxARIM=
github.com/apstndb/spannerotel v0.1.0 h1:gAEyMMhkKD2+OyUw6NrDCEhYVx7q7GkB75l7vWsamK4=
github.com/apstndb/spannerotel v0.1.0/go.mod h1:WHD4+pRgOBckpBXnL7bd4lYrZebYjYLZ08KGCtQrrXg=
github.com/apstndb/spannerotel v0.2.0 h1:EpGzxB9CfnRedlOlO/x4+c8+vOEbptGcd0PegEMsKYk=
github.com/apstndb/spannerotel v0.2.0/go.mod h1:tD+JGppXRRgxPvlfc4OADUGgpHNSkoZ2qbHwbZ3rtJo=
github.com/apstndb/spantype v0.3.11 h1:wKue4WLYGT82MH3B3TRSFn8tWIbk1Geczs+5BAEtnK4=
github.com/apstndb/spantype v0.3.11/go.mod h1:9eHowE7LcJ155ukCYUyuNzVAw9Ne0GXPXpHmu+iaMyk=
github.com/apstndb/spanvalue v0.8.0 h1:wLHl/0m5C6PvRwJOJoz1nRL4qSpS17eS1tW3Pjzcvo8=
github.com/apstndb/spanvalue v0.8.0/go.mod h1:bqVJYydQf+D0Tux1LtyRlREPVwrVYNG+1usZJ489GTA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
Expand Down Expand Up @@ -167,6 +169,8 @@ github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA=
Expand Down Expand Up @@ -278,6 +282,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:Oyrsyzu
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D/gAmznQTd4dxxMA93aKbcpKqiTCs=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
Expand All @@ -290,6 +298,8 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
Expand Down
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ type opts struct {
ParamFile string `name:"param-file" help:"YAML or JSON file of query parameters (name to type/literal string)"`
LogGrpc bool `name:"log-grpc" help:"Show gRPC logs"`
TraceProject string `name:"experimental-trace-project" xor:"trace" help:"Export traces to Cloud Trace in the given project."`
TraceStdout bool `name:"experimental-trace-stdout" xor:"trace" help:"Export traces to stderr (local debugging; no Cloud Trace credentials required)."`
TraceStdout bool `name:"experimental-trace-stdout" xor:"trace" help:"Export spans to stderr as pretty JSON (local debugging)."`
TraceOTLP bool `name:"experimental-trace-otlp" xor:"trace" help:"Export spans via OTLP/gRPC to a local OpenTelemetry collector."`
TraceOTLPEndpoint string `name:"experimental-trace-otlp-endpoint" default:"localhost:4317" help:"OTLP/gRPC endpoint used with --experimental-trace-otlp."`
EnablePartitionedDML bool `name:"enable-partitioned-dml" help:"Execute DML statement using Partitioned DML"`
Timeout time.Duration `name:"timeout" default:"10m" help:"Maximum time to wait for the SQL query to complete"`
TryPartitionQuery bool `name:"try-partition-query" help:"(Experimental) Check whether the query can be executed as partition query or not"`
Expand Down
28 changes: 25 additions & 3 deletions trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,45 @@ import (
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

const traceServiceName = "execspansql"

func tracingEnabled(o opts) bool {
return o.TraceStdout || o.TraceProject != ""
return o.TraceStdout || o.TraceProject != "" || o.TraceOTLP
}

func traceConfig(o opts) (tracing.Config, error) {
n := 0
if o.TraceOTLP {
n++
}
if o.TraceStdout {
n++
}
if o.TraceProject != "" {
n++
}
if n != 1 {
return tracing.Config{}, fmt.Errorf("exactly one of --experimental-trace-otlp, --experimental-trace-stdout, or --experimental-trace-project must be set")
}
Comment on lines +30 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

By checking n != 1, any case where no tracing flags are set (n == 0) will trigger the error exactly one of ... must be set. This makes the default branch in the switch statement (which returns tracing is not configured) completely unreachable.

Additionally, if traceConfig is called when tracing is disabled, the error message claiming that one of the flags must be set is misleading. Changing the check to n > 1 allows the n == 0 case to correctly fall through to the default branch.

Suggested change
if n != 1 {
return tracing.Config{}, fmt.Errorf("exactly one of --experimental-trace-otlp, --experimental-trace-stdout, or --experimental-trace-project must be set")
}
if n > 1 {
return tracing.Config{}, fmt.Errorf("at most one of --experimental-trace-otlp, --experimental-trace-stdout, or --experimental-trace-project can be set")
}
References
  1. When configuring mutually exclusive CLI flags, omit the required constraint if the flags are optional as a group, allowing the user to specify none of them.

switch {
case o.TraceStdout && o.TraceProject != "":
return tracing.Config{}, fmt.Errorf("use either --experimental-trace-stdout or --experimental-trace-project, not both")
case o.TraceOTLP:
return tracing.Config{
Exporter: tracing.ExporterOTLP,
ServiceName: traceServiceName,
OTLPEndpoint: o.TraceOTLPEndpoint,
OTLPInsecure: true,
}, nil
case o.TraceStdout:
return tracing.Config{
Exporter: tracing.ExporterStdout,
ServiceName: traceServiceName,
StdoutWriter: os.Stderr,
PrettyStdout: true,
}, nil
case o.TraceProject != "":
return tracing.Config{
Exporter: tracing.ExporterCloudTrace,
ServiceName: traceServiceName,
CloudTraceProject: o.TraceProject,
}, nil
default:
Expand Down
24 changes: 24 additions & 0 deletions trace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ func TestTraceConfigMutuallyExclusive(t *testing.T) {
}
}

func TestTraceConfigOTLP(t *testing.T) {
t.Parallel()

cfg, err := traceConfig(opts{TraceOTLP: true, TraceOTLPEndpoint: "127.0.0.1:4317"})
if err != nil {
t.Fatal(err)
}
if cfg.Exporter != tracing.ExporterOTLP {
t.Fatalf("exporter = %q, want %q", cfg.Exporter, tracing.ExporterOTLP)
}
if cfg.OTLPEndpoint != "127.0.0.1:4317" {
t.Fatalf("endpoint = %q", cfg.OTLPEndpoint)
}
if cfg.ServiceName != "execspansql" {
t.Fatalf("service name = %q", cfg.ServiceName)
}
if !cfg.OTLPInsecure {
t.Fatal("expected insecure otlp for local collector")
}
}

func TestTraceFlagsMutuallyExclusiveViaKong(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -82,6 +103,9 @@ func TestTracingEnabled(t *testing.T) {
if !tracingEnabled(opts{TraceProject: "demo"}) {
t.Fatal("project should enable tracing")
}
if !tracingEnabled(opts{TraceOTLP: true}) {
t.Fatal("otlp should enable tracing")
}
if tracingEnabled(opts{}) {
t.Fatal("expected tracing disabled by default")
}
Expand Down
Loading