mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
* debrand: signoz/SigNoz/SIGNOZ -> o11y/O11y/O11Y across the tree Rename all SigNoz *branding* to O11y in file contents, file/dir names, package names, env vars, config keys, comments, docs, SDK code, and the ClickHouse schema identifiers the querier reads. Collector dependency: - Repoint github.com/hanzoai/signoz-otel-collector -> github.com/hanzoai/otel-collector and bump v0.144.6 -> v0.144.7 (go.mod + go.sum reconciled via go mod tidy). Internal package paths also debranded upstream: signozschemamigrator -> o11yschemamigrator, signozlogspipelineprocessor -> o11ylogspipelineprocessor. - Fix: v0.144.7 dropped otelconst.DistributedFieldKeysTable from the collector's public constants; pin FieldKeysTable = "distributed_field_keys" locally in pkg/telemetrymetadata/tables.go. Package/dir renames (package decl + all importers): pkg/apiserver/signozapiserver -> o11yapiserver, plus signozalertmanager, signozauthzapi, signozglobal, signozquerier, signozruler, and pkg/signoz -> pkg/o11y. Schema (read plane): signoz_traces/metrics/logs/metadata/analytics/meter and signoz_index_v3/index_v2/error_index_v2/spans (+ distributed_*) -> o11y_*. Data-preserving cutover migration added: deploy/clickhouse/migrations/0001_rename_signoz_to_o11y.sql (metadata-only RENAME DATABASE/TABLE, no drop/recreate). Preserved (attribution / external / wire contracts): - LICENSE + NOTICE verbatim (incl. "software from SigNoz", "Copyright ... SigNoz Inc."). - Upstream github.com/SigNoz/* repo URLs + @SigNoz/* CODEOWNERS teams. - Billing resource-attribute regex "signoz.workspace.*" (external wire contract). go build ./pkg/... ./cmd/... = 0; gofmt clean; renamed+schema pkg tests pass. * o11y: bump collector to v0.144.8 (o11y_* physical schema writer) + lockstep deploy plan Collector v0.144.8's schema-migrator + exporters now CREATE/WRITE the same o11y_* physical databases/tables the querier reads and the RENAME migration (0001_rename_signoz_to_o11y.sql) targets. Cross-checked byte-identical: writer DB set == migration target DB set (6); writer prefixed-table set == migration target table set (8); o11y querier reads and cloud reads are subsets of that set. Zero table-name mismatches between writer, readers, and migration. Collector go.mod unchanged between v0.144.7 and v0.144.8 (identical go.mod hash) — pure source rename, no module-graph change. Adds deploy/clickhouse/DEPLOY_signoz_to_o11y_cutover.md: the lockstep order (collector v0.144.8 -> RENAME migration -> o11y+cloud readers) with a fresh/scratch-ClickHouse pre-flight (create via migrator, emit trace+metric, query back) and rollback. Ships together or telemetry blackholes. go build ./pkg/... ./cmd/community = 0.
228 lines
13 KiB
Go
228 lines
13 KiB
Go
package telemetryaudit
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/o11y/pkg/flagger/flaggertest"
|
|
"github.com/hanzoai/o11y/pkg/instrumentation/instrumentationtest"
|
|
"github.com/hanzoai/o11y/pkg/querybuilder"
|
|
qbtypes "github.com/hanzoai/o11y/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
|
|
"github.com/hanzoai/o11y/pkg/types/telemetrytypes/telemetrytypestest"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func auditFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
|
key := func(name string, ctx telemetrytypes.FieldContext, dt telemetrytypes.FieldDataType, materialized bool) *telemetrytypes.TelemetryFieldKey {
|
|
return &telemetrytypes.TelemetryFieldKey{
|
|
Name: name,
|
|
Signal: telemetrytypes.SignalLogs,
|
|
FieldContext: ctx,
|
|
FieldDataType: dt,
|
|
Materialized: materialized,
|
|
}
|
|
}
|
|
|
|
attr := telemetrytypes.FieldContextAttribute
|
|
res := telemetrytypes.FieldContextResource
|
|
str := telemetrytypes.FieldDataTypeString
|
|
i64 := telemetrytypes.FieldDataTypeInt64
|
|
|
|
return map[string][]*telemetrytypes.TelemetryFieldKey{
|
|
"service.name": {key("service.name", res, str, false)},
|
|
"o11y.audit.action": {key("o11y.audit.action", attr, str, true)},
|
|
"o11y.audit.outcome": {key("o11y.audit.outcome", attr, str, true)},
|
|
"o11y.audit.principal.email": {key("o11y.audit.principal.email", attr, str, true)},
|
|
"o11y.audit.principal.id": {key("o11y.audit.principal.id", attr, str, true)},
|
|
"o11y.audit.principal.type": {key("o11y.audit.principal.type", attr, str, true)},
|
|
"o11y.audit.resource.kind": {key("o11y.audit.resource.kind", res, str, false)},
|
|
"o11y.audit.resource.id": {key("o11y.audit.resource.id", res, str, false)},
|
|
"o11y.audit.action_category": {key("o11y.audit.action_category", attr, str, false)},
|
|
"o11y.audit.error.type": {key("o11y.audit.error.type", attr, str, false)},
|
|
"o11y.audit.error.code": {key("o11y.audit.error.code", attr, str, false)},
|
|
"http.request.method": {key("http.request.method", attr, str, false)},
|
|
"http.response.status_code": {key("http.response.status_code", attr, i64, false)},
|
|
}
|
|
}
|
|
|
|
func newTestAuditStatementBuilder(t *testing.T) *auditQueryStatementBuilder {
|
|
t.Helper()
|
|
fl := flaggertest.New(t)
|
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
|
mockMetadataStore.KeysMap = auditFieldKeyMap()
|
|
|
|
fm := NewFieldMapper()
|
|
cb := NewConditionBuilder(fm)
|
|
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
|
|
|
|
return NewAuditQueryStatementBuilder(
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
mockMetadataStore,
|
|
fm,
|
|
cb,
|
|
aggExprRewriter,
|
|
DefaultFullTextColumn,
|
|
nil,
|
|
fl,
|
|
)
|
|
}
|
|
|
|
func TestStatementBuilder(t *testing.T) {
|
|
statementBuilder := newTestAuditStatementBuilder(t)
|
|
ctx := context.Background()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
requestType qbtypes.RequestType
|
|
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
|
|
expected qbtypes.Statement
|
|
expectedErr error
|
|
}{
|
|
// List: all actions by a specific user (materialized principal.id filter)
|
|
{
|
|
name: "ListByPrincipalID",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
|
Signal: telemetrytypes.SignalLogs,
|
|
Source: telemetrytypes.SourceAudit,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "o11y.audit.principal.id = '019a-1234-abcd-5678'",
|
|
},
|
|
Limit: 100,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM o11y_audit.distributed_logs WHERE (`attribute_string_o11y$$audit$$principal$$id` = ? AND `attribute_string_o11y$$audit$$principal$$id_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
|
Args: []any{"019a-1234-abcd-5678", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
|
},
|
|
},
|
|
// List: all failed actions (materialized outcome filter)
|
|
{
|
|
name: "ListByOutcomeFailure",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
|
Signal: telemetrytypes.SignalLogs,
|
|
Source: telemetrytypes.SourceAudit,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "o11y.audit.outcome = 'failure'",
|
|
},
|
|
Limit: 100,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM o11y_audit.distributed_logs WHERE (`attribute_string_o11y$$audit$$outcome` = ? AND `attribute_string_o11y$$audit$$outcome_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
|
Args: []any{"failure", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
|
},
|
|
},
|
|
// List: change history of a specific dashboard (two materialized column AND)
|
|
{
|
|
name: "ListByResourceKindAndID",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
|
Signal: telemetrytypes.SignalLogs,
|
|
Source: telemetrytypes.SourceAudit,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "o11y.audit.resource.kind = 'dashboard' AND o11y.audit.resource.id = '019b-5678-efgh-9012'",
|
|
},
|
|
Limit: 100,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM o11y_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'o11y.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'o11y.audit.resource.id') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM o11y_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
|
Args: []any{"dashboard", "%o11y.audit.resource.kind%", "%o11y.audit.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%o11y.audit.resource.id%", "%o11y.audit.resource.id\":\"019b-5678-efgh-9012%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
|
},
|
|
},
|
|
// List: all dashboard deletions (compliance — resource.kind + action AND)
|
|
{
|
|
name: "ListByResourceKindAndAction",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
|
Signal: telemetrytypes.SignalLogs,
|
|
Source: telemetrytypes.SourceAudit,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "o11y.audit.resource.kind = 'dashboard' AND o11y.audit.action = 'delete'",
|
|
},
|
|
Limit: 100,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM o11y_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'o11y.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM o11y_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_o11y$$audit$$action` = ? AND `attribute_string_o11y$$audit$$action_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
|
Args: []any{"dashboard", "%o11y.audit.resource.kind%", "%o11y.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
|
},
|
|
},
|
|
// List: all actions by service accounts (materialized principal.type)
|
|
{
|
|
name: "ListByPrincipalType",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
|
Signal: telemetrytypes.SignalLogs,
|
|
Source: telemetrytypes.SourceAudit,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "o11y.audit.principal.type = 'service_account'",
|
|
},
|
|
Limit: 100,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM o11y_audit.distributed_logs WHERE (`attribute_string_o11y$$audit$$principal$$type` = ? AND `attribute_string_o11y$$audit$$principal$$type_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
|
Args: []any{"service_account", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
|
},
|
|
},
|
|
// Scalar: alert — count forbidden errors (outcome + action AND)
|
|
{
|
|
name: "ScalarCountByOutcomeAndAction",
|
|
requestType: qbtypes.RequestTypeScalar,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
|
Signal: telemetrytypes.SignalLogs,
|
|
Source: telemetrytypes.SourceAudit,
|
|
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "o11y.audit.outcome = 'failure' AND o11y.audit.action = 'update'",
|
|
},
|
|
Aggregations: []qbtypes.LogAggregation{
|
|
{Expression: "count()"},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "SELECT count() AS __result_0 FROM o11y_audit.distributed_logs WHERE ((`attribute_string_o11y$$audit$$outcome` = ? AND `attribute_string_o11y$$audit$$outcome_exists` = ?) AND (`attribute_string_o11y$$audit$$action` = ? AND `attribute_string_o11y$$audit$$action_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
|
|
Args: []any{"failure", true, "update", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
|
},
|
|
},
|
|
// TimeSeries: failures grouped by principal email with top-N limit
|
|
{
|
|
name: "TimeSeriesFailuresGroupedByPrincipal",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
|
Signal: telemetrytypes.SignalLogs,
|
|
Source: telemetrytypes.SourceAudit,
|
|
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
|
Aggregations: []qbtypes.LogAggregation{
|
|
{Expression: "count()"},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "o11y.audit.outcome = 'failure'",
|
|
},
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "o11y.audit.principal.email"}},
|
|
},
|
|
Limit: 5,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __limit_cte AS (SELECT toString(multiIf(`attribute_string_o11y$$audit$$principal$$email_exists` = ?, `attribute_string_o11y$$audit$$principal$$email`, NULL)) AS `o11y.audit.principal.email`, count() AS __result_0 FROM o11y_audit.distributed_logs WHERE (`attribute_string_o11y$$audit$$outcome` = ? AND `attribute_string_o11y$$audit$$outcome_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `o11y.audit.principal.email` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_o11y$$audit$$principal$$email_exists` = ?, `attribute_string_o11y$$audit$$principal$$email`, NULL)) AS `o11y.audit.principal.email`, count() AS __result_0 FROM o11y_audit.distributed_logs WHERE (`attribute_string_o11y$$audit$$outcome` = ? AND `attribute_string_o11y$$audit$$outcome_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`o11y.audit.principal.email`) GLOBAL IN (SELECT `o11y.audit.principal.email` FROM __limit_cte) GROUP BY ts, `o11y.audit.principal.email`",
|
|
Args: []any{true, "failure", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 5, true, "failure", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
q, err := statementBuilder.Build(ctx, 1747947419000, 1747983448000, testCase.requestType, testCase.query, nil)
|
|
if testCase.expectedErr != nil {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), testCase.expectedErr.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, testCase.expected.Query, q.Query)
|
|
require.Equal(t, testCase.expected.Args, q.Args)
|
|
}
|
|
})
|
|
}
|
|
}
|