From 8210796ff1fc819056ea99af874f82f7e7211d4f Mon Sep 17 00:00:00 2001
From: angelapwen
Date: Fri, 19 Feb 2021 10:47:38 +0100
Subject: [PATCH] builtins: add generator builtin for span payloads
Previously we introduced a `crdb_internal.payloads_for_span`
builtin that returned a JSONB array representing all payloads for
a particular span. To improve the user experience of the builtin
as well as prevent OOMs resulting from builtin usage, we replace it
with a generator builtin that returns a table representing all payloads
for a span, where each row represents a single payload.
The new builtin, also called `crdb_internal.payloads_for_span`, has
columns for the `payload_type` so that the user has the ability to
easily filter on the type, and `payload_jsonb` so the user can use
jsonb builtins to filter further.
Release note (sql change): Update `crdb_internal.payloads_for_span`
builtin to return a table instead of a JSONB array. Each row of the
table represents one payload for the given span. It has columns for
`payload_type` and `payload_jsonb`.
---
docs/generated/sql/functions.md | 2 +-
.../testdata/logic_test/builtin_function | 31 ++++
.../testdata/logic_test/contention_event | 20 +--
pkg/sql/sem/builtins/builtins.go | 49 +-----
pkg/sql/sem/builtins/generator_builtins.go | 150 ++++++++++++++++++
pkg/sql/sem/tree/eval.go | 2 +-
6 files changed, 196 insertions(+), 58 deletions(-)
diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md
index 529a7dcc8745..a08e6c9d38de 100644
--- a/docs/generated/sql/functions.md
+++ b/docs/generated/sql/functions.md
@@ -2638,7 +2638,7 @@ SELECT * FROM crdb_internal.check_consistency(true, ‘\x02’, ‘\x04’)
crdb_internal.payloads_for_span(span ID: int) → jsonb | Returns the payload(s) of the span whose ID is passed in the argument.
+crdb_internal.payloads_for_span(span_id: int) → tuple{string AS payload_type, jsonb AS payload_jsonb} | Returns the payload(s) of the requested span.
|
crdb_internal.pretty_key(raw_key: bytes, skip_fields: int) → string | This function is used only by CockroachDB’s developers for testing purposes.
|
diff --git a/pkg/sql/logictest/testdata/logic_test/builtin_function b/pkg/sql/logictest/testdata/logic_test/builtin_function
index b708e533f7fd..96ae4fbc2624 100644
--- a/pkg/sql/logictest/testdata/logic_test/builtin_function
+++ b/pkg/sql/logictest/testdata/logic_test/builtin_function
@@ -2913,3 +2913,34 @@ query T
SELECT regexp_split_to_array('3aaa0AAa1', 'a+', 'i')
----
{3,0,1}
+
+subtest crdb_internal.trace_id
+
+# switch users -- this one has no permissions so expect errors
+user testuser
+
+query error insufficient privilege
+SELECT * FROM crdb_internal.trace_id()
+
+user root
+
+query B
+SELECT count(*) = 1 FROM crdb_internal.trace_id()
+----
+true
+
+subtest crdb_internal.payloads_for_span
+
+# switch users -- this one has no permissions so expect errors
+user testuser
+
+query error pq: only users with the admin role are allowed to use crdb_internal.payloads_for_span
+SELECT * FROM crdb_internal.payloads_for_span(0)
+
+user root
+
+query TT colnames
+SELECT * FROM crdb_internal.payloads_for_span(0)
+WHERE false
+----
+payload_type payload_jsonb
diff --git a/pkg/sql/logictest/testdata/logic_test/contention_event b/pkg/sql/logictest/testdata/logic_test/contention_event
index 42f6c904223e..cc51e18fbfd4 100644
--- a/pkg/sql/logictest/testdata/logic_test/contention_event
+++ b/pkg/sql/logictest/testdata/logic_test/contention_event
@@ -9,7 +9,7 @@ GRANT ADMIN TO testuser
statement ok
CREATE TABLE kv (k VARCHAR PRIMARY KEY, v VARCHAR);
-ALTER TABLE kv SPLIT AT VALUES ('b'), ('d'), ('q'), ('z');
+ALTER TABLE kv SPLIT AT VALUES ('b'), ('d'), ('q'), ('z')
query TT
SELECT * FROM kv
@@ -18,7 +18,7 @@ SELECT * FROM kv
user testuser
statement ok
-BEGIN;
+BEGIN
statement ok
INSERT INTO kv VALUES('k', 'v')
@@ -32,7 +32,7 @@ user root
statement ok
BEGIN;
SET TRANSACTION PRIORITY HIGH;
-SELECT * FROM kv;
+SELECT * FROM kv
user testuser
@@ -49,16 +49,16 @@ user root
#
# NB: this needs the 5node-pretend59315 config because otherwise the span is not
# tracked.
-#
query B
WITH spans AS (
- SELECT span_id FROM crdb_internal.node_inflight_trace_spans
+ SELECT span_id
+ FROM crdb_internal.node_inflight_trace_spans
WHERE trace_id = crdb_internal.trace_id()
-), payload_types AS (
- SELECT jsonb_array_elements(crdb_internal.payloads_for_span(span_id))->>'@type' AS payload_type
- FROM spans
+), payloads AS (
+ SELECT *
+ FROM spans, LATERAL crdb_internal.payloads_for_span(spans.span_id)
) SELECT count(*) > 0
- FROM payload_types
- WHERE payload_type = 'type.googleapis.com/cockroach.roachpb.ContentionEvent';
+ FROM payloads
+ WHERE payload_type = 'roachpb.ContentionEvent'
----
true
diff --git a/pkg/sql/sem/builtins/builtins.go b/pkg/sql/sem/builtins/builtins.go
index b25ea2527c24..e0f3684f6b32 100644
--- a/pkg/sql/sem/builtins/builtins.go
+++ b/pkg/sql/sem/builtins/builtins.go
@@ -73,7 +73,6 @@ import (
"github.com/cockroachdb/cockroach/pkg/util/unaccent"
"github.com/cockroachdb/cockroach/pkg/util/uuid"
"github.com/cockroachdb/errors"
- pbtypes "github.com/gogo/protobuf/types"
"github.com/knz/strtime"
)
@@ -3593,7 +3592,9 @@ may increase either contention or retry errors, or both.`,
return nil, err
}
if !isAdmin {
- return nil, pgerror.Newf(pgcode.InsufficientPrivilege, "user needs the admin role to view trace ID")
+ if err := checkPrivilegedUser(ctx); err != nil {
+ return nil, err
+ }
}
sp := tracing.SpanFromContext(ctx.Context)
@@ -3608,50 +3609,6 @@ may increase either contention or retry errors, or both.`,
},
),
- "crdb_internal.payloads_for_span": makeBuiltin(
- tree.FunctionProperties{Category: categorySystemInfo},
- tree.Overload{
- Types: tree.ArgTypes{{"span ID", types.Int}},
- ReturnType: tree.FixedReturnType(types.Jsonb),
- Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) {
- // The user must be an admin to use this builtin.
- isAdmin, err := ctx.SessionAccessor.HasAdminRole(ctx.Context)
- if err != nil {
- return nil, err
- }
- if !isAdmin {
- return nil, pgerror.Newf(pgcode.InsufficientPrivilege, "user needs the admin role to view payloads")
- }
-
- builder := json.NewArrayBuilder(len(args))
-
- spanID := uint64(*(args[0].(*tree.DInt)))
- span, found := ctx.Settings.Tracer.GetActiveSpanFromID(spanID)
- // A span may not be found if its ID was surfaced previously but its
- // corresponding trace has ended by the time this builtin was run.
- if !found {
- // Returns an empty JSON array.
- return tree.NewDJSON(builder.Build()), nil
- }
-
- for _, rec := range span.GetRecording() {
- rec.Structured(func(item *pbtypes.Any) {
- payload, err := protoreflect.MessageToJSON(item, true /* emitDefaults */)
- if err != nil {
- return
- }
- if payload != nil {
- builder.Add(payload)
- }
- })
- }
- return tree.NewDJSON(builder.Build()), nil
- },
- Info: "Returns the payload(s) of the span whose ID is passed in the argument.",
- Volatility: tree.VolatilityVolatile,
- },
- ),
-
"crdb_internal.locality_value": makeBuiltin(
tree.FunctionProperties{Category: categorySystemInfo},
tree.Overload{
diff --git a/pkg/sql/sem/builtins/generator_builtins.go b/pkg/sql/sem/builtins/generator_builtins.go
index 9f121e1403db..f4f7a23465a8 100644
--- a/pkg/sql/sem/builtins/generator_builtins.go
+++ b/pkg/sql/sem/builtins/generator_builtins.go
@@ -13,6 +13,7 @@ package builtins
import (
"bytes"
"context"
+ "strings"
"time"
"github.com/cockroachdb/cockroach/pkg/keys"
@@ -22,13 +23,16 @@ import (
"github.com/cockroachdb/cockroach/pkg/sql/lex"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
+ "github.com/cockroachdb/cockroach/pkg/sql/protoreflect"
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
"github.com/cockroachdb/cockroach/pkg/sql/types"
"github.com/cockroachdb/cockroach/pkg/util/arith"
"github.com/cockroachdb/cockroach/pkg/util/duration"
"github.com/cockroachdb/cockroach/pkg/util/errorutil"
"github.com/cockroachdb/cockroach/pkg/util/json"
+ "github.com/cockroachdb/cockroach/pkg/util/tracing"
"github.com/cockroachdb/errors"
+ pbtypes "github.com/gogo/protobuf/types"
)
// See the comments at the start of generators.go for details about
@@ -326,6 +330,22 @@ var generators = map[string]builtinDefinition{
tree.VolatilityVolatile,
),
),
+
+ "crdb_internal.payloads_for_span": makeBuiltin(
+ tree.FunctionProperties{
+ Class: tree.GeneratorClass,
+ Category: categorySystemInfo,
+ },
+ makeGeneratorOverload(
+ tree.ArgTypes{
+ {Name: "span_id", Typ: types.Int},
+ },
+ payloadsForSpanGeneratorType,
+ makePayloadsForSpanGenerator,
+ "Returns the payload(s) of the requested span.",
+ tree.VolatilityVolatile,
+ ),
+ ),
}
func makeGeneratorOverload(
@@ -1363,3 +1383,133 @@ func (rk *rangeKeyIterator) Values() (tree.Datums, error) {
// Close implements the tree.ValueGenerator interface.
func (rk *rangeKeyIterator) Close() {}
+
+var payloadsForSpanGeneratorLabels = []string{"payload_type", "payload_jsonb"}
+
+var payloadsForSpanGeneratorType = types.MakeLabeledTuple(
+ []*types.T{types.String, types.Jsonb},
+ payloadsForSpanGeneratorLabels,
+)
+
+// payloadsForSpanGenerator is a value generator that iterates over all payloads
+// over all recordings for a given Span.
+type payloadsForSpanGenerator struct {
+ // The span to iterate over.
+ span *tracing.Span
+
+ // recordingIndex maintains the current position of the index of the iterator
+ // in the list of recordings surfaced by a given span. The payloads of the
+ // recording that this iterator points to are buffered in `payloads`
+ recordingIndex int
+
+ // payloads represents all payloads for a given recording currently accessed
+ // by the iterator, and accesses more in a streaming fashion.
+ payloads []json.JSON
+
+ // payloadIndex maintains the current position of the index of the iterator
+ // in the list of `payloads` associated with a given recording.
+ payloadIndex int
+}
+
+func makePayloadsForSpanGenerator(
+ ctx *tree.EvalContext, args tree.Datums,
+) (tree.ValueGenerator, error) {
+ // The user must be an admin to use this builtin.
+ isAdmin, err := ctx.SessionAccessor.HasAdminRole(ctx.Context)
+ if err != nil {
+ return nil, err
+ }
+ if !isAdmin {
+ return nil, pgerror.Newf(
+ pgcode.InsufficientPrivilege,
+ "only users with the admin role are allowed to use crdb_internal.payloads_for_span",
+ )
+ }
+ spanID := uint64(*(args[0].(*tree.DInt)))
+ span, found := ctx.Settings.Tracer.GetActiveSpanFromID(spanID)
+ if !found {
+ return nil, nil
+ }
+
+ return &payloadsForSpanGenerator{span: span}, nil
+}
+
+// ResolvedType implements the tree.ValueGenerator interface.
+func (p *payloadsForSpanGenerator) ResolvedType() *types.T {
+ return payloadsForSpanGeneratorType
+}
+
+// Start implements the tree.ValueGenerator interface.
+func (p *payloadsForSpanGenerator) Start(_ context.Context, _ *kv.Txn) error {
+ // The user of the generator first calls Next(), then Values(), so the index
+ // managing the iterator's position needs to start at -1 instead of 0.
+ p.recordingIndex = -1
+ p.payloadIndex = -1
+
+ return nil
+}
+
+// Next implements the tree.ValueGenerator interface.
+func (p *payloadsForSpanGenerator) Next(_ context.Context) (bool, error) {
+ p.payloadIndex++
+
+ // If payloadIndex is within payloads and there are some payloads, then we
+ // have more buffered payloads to return.
+ if p.payloads != nil && p.payloadIndex < len(p.payloads) {
+ return true, nil
+ }
+
+ // Otherwise either there are no payloads or we have exhausted the payloads in
+ // our current recording, and we need to access another set of payloads from
+ // another recording.
+ p.payloads = nil
+
+ // Keep searching recordings for one with a valid (non-nil) payload.
+ for p.payloads == nil {
+ p.recordingIndex++
+ // If there are no more recordings, then we cannot continue.
+ if !(p.recordingIndex < p.span.GetRecording().Len()) {
+ return false, nil
+ }
+ currRecording := p.span.GetRecording()[p.recordingIndex]
+ currRecording.Structured(func(item *pbtypes.Any) {
+ payload, err := protoreflect.MessageToJSON(item, true /* emitDefaults */)
+ if err != nil {
+ return
+ }
+ if payload != nil {
+ p.payloads = append(p.payloads, payload)
+ }
+ })
+ }
+
+ p.payloadIndex = 0
+ return true, nil
+}
+
+// Values implements the tree.ValueGenerator interface.
+func (p *payloadsForSpanGenerator) Values() (tree.Datums, error) {
+ payload := p.payloads[p.payloadIndex]
+ payloadTypeAsJSON, err := payload.FetchValKey("@type")
+ if err != nil {
+ return nil, err
+ }
+
+ // We trim the proto type prefix as well as the enclosing double quotes
+ // leftover from JSON value conversion.
+ payloadTypeAsString := strings.TrimSuffix(
+ strings.TrimPrefix(
+ payloadTypeAsJSON.String(),
+ "\"type.googleapis.com/cockroach.",
+ ),
+ "\"",
+ )
+
+ return tree.Datums{
+ tree.NewDString(payloadTypeAsString),
+ tree.NewDJSON(payload),
+ }, nil
+}
+
+// Close implements the tree.ValueGenerator interface.
+func (p *payloadsForSpanGenerator) Close() {}
diff --git a/pkg/sql/sem/tree/eval.go b/pkg/sql/sem/tree/eval.go
index 34431f70499a..e40a64c0729a 100644
--- a/pkg/sql/sem/tree/eval.go
+++ b/pkg/sql/sem/tree/eval.go
@@ -3940,7 +3940,7 @@ func EvalComparisonExprWithSubOperator(
return evalDatumsCmp(ctx, expr.Operator, expr.SubOperator, expr.Fn, left, datums)
}
-// EvalArgsAndGetGenerator evaluates the arguments and instanciates a
+// EvalArgsAndGetGenerator evaluates the arguments and instantiates a
// ValueGenerator for use by set projections.
func (expr *FuncExpr) EvalArgsAndGetGenerator(ctx *EvalContext) (ValueGenerator, error) {
if expr.fn == nil || expr.fnProps.Class != GeneratorClass {
|