Skip to content
Open
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
14 changes: 6 additions & 8 deletions cmd/jaeger/internal/integration/clickhouse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,15 @@ func TestClickHouseStorage(t *testing.T) {
StorageIntegration: integration.StorageIntegration{
CleanUp: purge,
SkipList: []string{
// Tag-related tests are temporarily skipped pending the redesign of
// attribute handling in ClickHouse storage to support typed attributes.
"Tags_in_one_spot_-_Tags",
// The following tests are skipped because ClickHouse does not support
// querying event attributes yet.
"Tags_in_one_spot_-_Logs",
"Tags_in_one_spot_-_Process",
"Tags_in_different_spots",
"Tags_+_Operation_name",
"Tags_+_Operation_name_+_max_Duration",
"Tags_+_Operation_name_+_Duration_range",
"Tags_+_Duration_range",
"Tags_+_max_Duration",
"Multi-spot_Tags_+_Operation_name",
"Multi-spot_Tags_+_Operation_name_+_max_Duration",
"Multi-spot_Tags_+_Duration_range",
"Multi-spot_Tags_+_max_Duration",
},
},
}
Expand Down
8 changes: 8 additions & 0 deletions internal/storage/v2/clickhouse/sql/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ WHERE
GROUP BY name, span_kind
`

const SelectAttributeMetadata = `
SELECT
attribute_key,
type,
level
FROM
attribute_metadata`

const TruncateSpans = `TRUNCATE TABLE spans`

const TruncateServices = `TRUNCATE TABLE services`
Expand Down
84 changes: 84 additions & 0 deletions internal/storage/v2/clickhouse/tracestore/attribute_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) 2026 The Jaeger Authors.
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The copyright year is set to 2026, but the current year is 2025. This should be corrected to 2025.

Suggested change
// Copyright (c) 2026 The Jaeger Authors.
// Copyright (c) 2025 The Jaeger Authors.

Copilot uses AI. Check for mistakes.
// SPDX-License-Identifier: Apache-2.0

package tracestore

import (
"context"
"fmt"
"strings"

"go.opentelemetry.io/collector/pdata/pcommon"

"github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql"
"github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel"
)

// attributeMetadata maps attribute keys to levels, and each level to a list of types.
// Structure: attributeMetadata[key][level] = []type
// Example: attributeMetadata["http.status"]["span"] = ["int", "str"]
type attributeMetadata map[string]map[string][]string

// getAttributeMetadata retrieves the types stored in ClickHouse for string attributes.
//
// The query service forwards all attribute filters as strings (via AsString()), regardless
// of their actual type. For example:
// - A bool attribute stored as true becomes the string "true"
// - An int attribute stored as 123 becomes the string "123"
//
// To query ClickHouse correctly, we need to:
// 1. Look up the actual type(s) from the attribute_metadata table
// 2. Convert the string back to the original type
// 3. Query the appropriate typed column (bool_attributes, int_attributes, etc.)
//
// Since attributes can be stored with different types across different spans
// (e.g. "http.status" could be an int in one span and a string in another),
// the metadata can return multiple types for a single key. We build OR conditions
// to match any of the possible types.
//
// Only string-typed attributes from the query are looked up, since other types
// (bool, int, double, etc.) are already correctly typed in the query parameters.
func (r *Reader) getAttributeMetadata(ctx context.Context, attributes pcommon.Map) (attributeMetadata, error) {
query, args := buildSelectAttributeMetadataQuery(attributes)
rows, err := r.conn.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query attribute metadata: %w", err)
}
defer rows.Close()

metadata := make(attributeMetadata)
for rows.Next() {
var attrMeta dbmodel.AttributeMetadata
if err := rows.ScanStruct(&attrMeta); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}

if metadata[attrMeta.AttributeKey] == nil {
metadata[attrMeta.AttributeKey] = make(map[string][]string)
}
metadata[attrMeta.AttributeKey][attrMeta.Level] = append(metadata[attrMeta.AttributeKey][attrMeta.Level], attrMeta.Type)
}
return metadata, nil
}

func buildSelectAttributeMetadataQuery(attributes pcommon.Map) (string, []any) {
var q strings.Builder
q.WriteString(sql.SelectAttributeMetadata)
args := []any{}

var placeholders []string
for key, attr := range attributes.All() {
if attr.Type() == pcommon.ValueTypeStr {
placeholders = append(placeholders, "?")
args = append(args, key)
}
}

if len(placeholders) > 0 {
q.WriteString(" WHERE attribute_key IN (")
q.WriteString(strings.Join(placeholders, ", "))
q.WriteString(")")
}
q.WriteString(" GROUP BY attribute_key, type, level")
return q.String(), args
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2026 The Jaeger Authors.
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The copyright year is set to 2026, but the current year is 2025. This should be corrected to 2025.

Suggested change
// Copyright (c) 2026 The Jaeger Authors.
// Copyright (c) 2025 The Jaeger Authors.

Copilot uses AI. Check for mistakes.
// SPDX-License-Identifier: Apache-2.0

package tracestore

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/pdata/pcommon"

"github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql"
"github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel"
)

func TestGetAttributeMetadata_ErrorCases(t *testing.T) {
attrs := pcommon.NewMap()
attrs.PutStr("http.method", "GET")

tests := []struct {
name string
driver *testDriver
expectedErr string
}{
{
name: "QueryError",
driver: &testDriver{
t: t,
queryResponses: map[string]*testQueryResponse{
sql.SelectAttributeMetadata: {
rows: nil,
err: assert.AnError,
},
},
},
expectedErr: "failed to query attribute metadata",
},
{
name: "ScanStructError",
driver: &testDriver{
t: t,
queryResponses: map[string]*testQueryResponse{
sql.SelectAttributeMetadata: {
rows: &testRows[dbmodel.AttributeMetadata]{
data: []dbmodel.AttributeMetadata{{
AttributeKey: "http.method",
Type: "str",
Level: "span",
}},
scanErr: assert.AnError,
},
err: nil,
},
},
},
expectedErr: "failed to scan row",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := NewReader(tt.driver, ReaderConfig{})
_, err := reader.getAttributeMetadata(t.Context(), attrs)
require.Error(t, err)
assert.ErrorContains(t, err, tt.expectedErr)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 The Jaeger Authors.
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The copyright year is set to 2026, but the current year is 2025. This should be corrected to 2025.

Suggested change
// Copyright (c) 2026 The Jaeger Authors.
// Copyright (c) 2025 The Jaeger Authors.

Copilot uses AI. Check for mistakes.
// SPDX-License-Identifier: Apache-2.0

package dbmodel

// AttributeMetadata represents metadata about an attribute stored in ClickHouse.
// This is populated by the attribute_metadata materialized view which tracks
// all unique (attribute_key, type, level) tuples observed in the spans table.
//
// The same attribute key can have multiple entries with different types or levels.
// For example, "http.status" might appear as both type="int" and type="str" if
// different spans store it with different types.
type AttributeMetadata struct {
// AttributeKey is the name of the attribute (e.g., "http.status", "service.name")
AttributeKey string `ch:"attribute_key"`
// Type is the data type of the attribute value.
// One of: "bool", "double", "int", "str", "bytes", "map", "slice"
Type string `ch:"type"`
// Level is the scope level where this attribute appears.
// One of: "span", "resource", "scope"
Level string `ch:"level"`
}
Loading
Loading