diff --git a/README.md b/README.md index 623dfc5..9a2df9f 100644 --- a/README.md +++ b/README.md @@ -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 @@ -240,23 +242,43 @@ $ execspansql ${DATABASE_ID} --query-mode=NORMAL \ --param='songinfo=STRUCT>>("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) diff --git a/go.mod b/go.mod index 4079871..c4929ff 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 9a9e8e9..1931931 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ 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= @@ -65,6 +65,8 @@ github.com/apstndb/spanvalue v0.8.0/go.mod h1:bqVJYydQf+D0Tux1LtyRlREPVwrVYNG+1u 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= @@ -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= @@ -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= @@ -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= diff --git a/main.go b/main.go index 8af89b1..49317e2 100644 --- a/main.go +++ b/main.go @@ -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"` diff --git a/trace.go b/trace.go index 8a7930d..da991be 100644 --- a/trace.go +++ b/trace.go @@ -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") + } 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: diff --git a/trace_test.go b/trace_test.go index 87486b3..3b3c08d 100644 --- a/trace_test.go +++ b/trace_test.go @@ -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() @@ -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") }