mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-11 10:51:01 +00:00
Compare commits
4 Commits
fix/genera
...
fix/axios-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b01571ebcf | ||
|
|
9139da3762 | ||
|
|
da8d66b5b9 | ||
|
|
f1d6c988c9 |
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/cmd"
|
||||
"github.com/SigNoz/signoz/ee/auditor/fileauditor"
|
||||
"github.com/SigNoz/signoz/ee/auditor/kafkaauditor"
|
||||
"github.com/SigNoz/signoz/ee/auditor/otlphttpauditor"
|
||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
|
||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
||||
@@ -156,6 +157,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
if err := factories.Add(fileauditor.NewFactory(licensing, version.Info)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := factories.Add(kafkaauditor.NewFactory(licensing, version.Info)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return factories
|
||||
},
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, flagger pkgflagger.Flagger, licensing licensing.Licensing, telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter, orgGetter organization.Getter, zeus zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string) {
|
||||
|
||||
@@ -436,6 +436,7 @@ auditor:
|
||||
# Specifies the auditor provider to use.
|
||||
# noop: discards all audit events (community default).
|
||||
# otlphttp: exports audit events via OTLP HTTP (enterprise).
|
||||
# kafka: exports audit events to a Kafka topic as OTLP protobuf (enterprise).
|
||||
provider: noop
|
||||
# The async channel capacity for audit events. Events are dropped when full (fail-open).
|
||||
buffer_size: 1000
|
||||
@@ -461,6 +462,18 @@ auditor:
|
||||
max_interval: 30s
|
||||
# The total maximum time spent retrying.
|
||||
max_elapsed_time: 60s
|
||||
kafka:
|
||||
# The seed broker addresses (host:port) used to discover the cluster.
|
||||
brokers:
|
||||
- localhost:9092
|
||||
# The topic audit events are written to. It must already exist.
|
||||
topic: ""
|
||||
# The client id the producer reports to the brokers.
|
||||
client_id: signoz
|
||||
# The compression codec for produced batches: none, gzip, snappy, lz4 or zstd.
|
||||
compression: lz4
|
||||
# The maximum time to drain buffered events on shutdown. Undelivered events are dropped after it.
|
||||
shutdown_timeout: 30s
|
||||
|
||||
##################### Cloud Integration #####################
|
||||
cloudintegration:
|
||||
|
||||
31
ee/auditor/kafkaauditor/export.go
Normal file
31
ee/auditor/kafkaauditor/export.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package kafkaauditor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/twmb/franz-go/pkg/kgo"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
)
|
||||
|
||||
// Produces the whole batch as a single record so the collector's otlp_proto unmarshaler reads it as one ExportLogsServiceRequest.
|
||||
func (provider *provider) export(ctx context.Context, events []audittypes.AuditEvent) error {
|
||||
logs := audittypes.NewPLogsFromAuditEvents(events, "signoz", provider.build.Version(), "signoz.audit")
|
||||
|
||||
value, err := provider.marshaler.MarshalLogs(logs)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "failed to marshal audit logs")
|
||||
}
|
||||
|
||||
record := &kgo.Record{Topic: provider.config.Kafka.Topic, Value: value}
|
||||
if err := provider.client.ProduceSync(ctx, record).FirstErr(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "audit export failed", errors.Attr(err), slog.Int("dropped_log_records", len(events)))
|
||||
|
||||
return errors.Wrapf(err, errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "failed to produce audit logs to topic %q", provider.config.Kafka.Topic)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
132
ee/auditor/kafkaauditor/provider.go
Normal file
132
ee/auditor/kafkaauditor/provider.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package kafkaauditor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/twmb/franz-go/pkg/kgo"
|
||||
"github.com/twmb/franz-go/plugin/kslog"
|
||||
"go.opentelemetry.io/collector/pdata/plog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/auditor/auditorserver"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
)
|
||||
|
||||
var _ auditor.Auditor = (*provider)(nil)
|
||||
|
||||
type provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
config auditor.Config
|
||||
licensing licensing.Licensing
|
||||
build version.Build
|
||||
server *auditorserver.Server
|
||||
marshaler plog.ProtoMarshaler
|
||||
client *kgo.Client
|
||||
}
|
||||
|
||||
func NewFactory(licensing licensing.Licensing, build version.Build) factory.ProviderFactory[auditor.Auditor, auditor.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("kafka"), func(ctx context.Context, providerSettings factory.ProviderSettings, config auditor.Config) (auditor.Auditor, error) {
|
||||
return newProvider(ctx, providerSettings, config, licensing, build)
|
||||
})
|
||||
}
|
||||
|
||||
func newProvider(_ context.Context, providerSettings factory.ProviderSettings, config auditor.Config, licensing licensing.Licensing, build version.Build) (auditor.Auditor, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/auditor/kafkaauditor")
|
||||
|
||||
// Acks from all in-sync replicas, idempotent writes and unbounded record retries are franz-go defaults and are kept as is.
|
||||
// A delivery timeout or retry limit lets franz-go < v1.22.2 ack a record that was never written (twmb/franz-go#1484).
|
||||
client, err := kgo.NewClient(
|
||||
kgo.SeedBrokers(config.Kafka.Brokers...),
|
||||
kgo.ClientID(config.Kafka.ClientID),
|
||||
kgo.ProducerBatchCompression(compressionCodec(config.Kafka.Compression)),
|
||||
kgo.ProducerLinger(0),
|
||||
kgo.WithLogger(kslog.New(settings.Logger())),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := &provider{
|
||||
settings: settings,
|
||||
config: config,
|
||||
licensing: licensing,
|
||||
build: build,
|
||||
marshaler: plog.ProtoMarshaler{},
|
||||
client: client,
|
||||
}
|
||||
|
||||
server, err := auditorserver.New(settings,
|
||||
auditorserver.Config{
|
||||
BufferSize: config.BufferSize,
|
||||
BatchSize: config.BatchSize,
|
||||
FlushInterval: config.FlushInterval,
|
||||
},
|
||||
provider.export,
|
||||
)
|
||||
if err != nil {
|
||||
client.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider.server = server
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Start(ctx context.Context) error {
|
||||
return provider.server.Start(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) {
|
||||
if event.PrincipalAttributes.PrincipalOrgID.IsZero() {
|
||||
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := provider.licensing.GetActive(ctx, event.PrincipalAttributes.PrincipalOrgID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
provider.server.Add(ctx, event)
|
||||
}
|
||||
|
||||
func (provider *provider) Healthy() <-chan struct{} {
|
||||
return provider.server.Healthy()
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, provider.config.Kafka.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
stoppedC := make(chan error, 1)
|
||||
go func() { stoppedC <- provider.server.Stop(ctx) }()
|
||||
|
||||
select {
|
||||
case err := <-stoppedC:
|
||||
provider.client.Close()
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
provider.settings.Logger().WarnContext(ctx, "audit drain timed out, closing kafka client", slog.Duration("shutdown_timeout", provider.config.Kafka.ShutdownTimeout))
|
||||
// Closing fails buffered and in-flight records, so the blocked export and the rest of the drain return immediately.
|
||||
provider.client.Close()
|
||||
return <-stoppedC
|
||||
}
|
||||
}
|
||||
|
||||
func compressionCodec(name string) kgo.CompressionCodec {
|
||||
switch name {
|
||||
case "gzip":
|
||||
return kgo.GzipCompression()
|
||||
case "snappy":
|
||||
return kgo.SnappyCompression()
|
||||
case "lz4":
|
||||
return kgo.Lz4Compression()
|
||||
case "zstd":
|
||||
return kgo.ZstdCompression()
|
||||
default:
|
||||
return kgo.NoCompression()
|
||||
}
|
||||
}
|
||||
227
ee/auditor/kafkaauditor/provider_test.go
Normal file
227
ee/auditor/kafkaauditor/provider_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package kafkaauditor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/twmb/franz-go/pkg/kerr"
|
||||
"github.com/twmb/franz-go/pkg/kfake"
|
||||
"github.com/twmb/franz-go/pkg/kgo"
|
||||
"go.opentelemetry.io/collector/pdata/plog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
)
|
||||
|
||||
func newTestCluster(t *testing.T, topics ...string) *kfake.Cluster {
|
||||
t.Helper()
|
||||
|
||||
cluster, err := kfake.NewCluster(kfake.NumBrokers(1), kfake.SeedTopics(1, topics...))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(cluster.Close)
|
||||
|
||||
return cluster
|
||||
}
|
||||
|
||||
func newTestProvider(t *testing.T, brokers []string, topic string, compression string) *provider {
|
||||
t.Helper()
|
||||
|
||||
config := auditor.Config{
|
||||
Provider: "kafka",
|
||||
BufferSize: 10,
|
||||
BatchSize: 5,
|
||||
FlushInterval: 50 * time.Millisecond,
|
||||
Kafka: auditor.KafkaConfig{
|
||||
Brokers: brokers,
|
||||
Topic: topic,
|
||||
ClientID: "signoz-test",
|
||||
Compression: compression,
|
||||
ShutdownTimeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
auditor, err := newProvider(context.Background(), instrumentationtest.New().ToProviderSettings(), config, nil, version.Info)
|
||||
require.NoError(t, err)
|
||||
|
||||
return auditor.(*provider)
|
||||
}
|
||||
|
||||
func newTestEvent(resource coretypes.Resource, action coretypes.Verb, body string) audittypes.AuditEvent {
|
||||
return audittypes.AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
Body: body,
|
||||
EventName: audittypes.NewEventName(action),
|
||||
AuditAttributes: audittypes.AuditAttributes{
|
||||
Action: action,
|
||||
Outcome: audittypes.OutcomeSuccess,
|
||||
},
|
||||
ResourceAttributes: audittypes.ResourceAttributes{
|
||||
Resource: resource,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Polls the topic until the consumed records hold wantLogRecords audit log records in total.
|
||||
func consumeRecords(t *testing.T, brokers []string, topic string, wantLogRecords int) []*kgo.Record {
|
||||
t.Helper()
|
||||
|
||||
client, err := kgo.NewClient(
|
||||
kgo.SeedBrokers(brokers...),
|
||||
kgo.ConsumeTopics(topic),
|
||||
kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var records []*kgo.Record
|
||||
gotLogRecords := 0
|
||||
for gotLogRecords < wantLogRecords {
|
||||
fetches := client.PollFetches(ctx)
|
||||
require.NoError(t, ctx.Err(), "timed out waiting for %d log records, got %d", wantLogRecords, gotLogRecords)
|
||||
require.Empty(t, fetches.Errors())
|
||||
|
||||
for _, record := range fetches.Records() {
|
||||
logs, err := (&plog.ProtoUnmarshaler{}).UnmarshalLogs(record.Value)
|
||||
require.NoError(t, err)
|
||||
gotLogRecords += logs.LogRecordCount()
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
func TestExport(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
compression string
|
||||
compressionType uint8
|
||||
}{
|
||||
{name: "CompressionNone", compression: "none", compressionType: 0},
|
||||
{name: "CompressionGzip", compression: "gzip", compressionType: 1},
|
||||
{name: "CompressionSnappy", compression: "snappy", compressionType: 2},
|
||||
{name: "CompressionLz4", compression: "lz4", compressionType: 3},
|
||||
{name: "CompressionZstd", compression: "zstd", compressionType: 4},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
cluster := newTestCluster(t, "tenant_audit")
|
||||
provider := newTestProvider(t, cluster.ListenAddrs(), "tenant_audit", testCase.compression)
|
||||
defer provider.client.Close()
|
||||
|
||||
events := []audittypes.AuditEvent{
|
||||
newTestEvent(coretypes.ResourceUser, coretypes.VerbUpdate, "user updated"),
|
||||
newTestEvent(coretypes.ResourceMetaResourceDashboard, coretypes.VerbCreate, "dashboard created"),
|
||||
newTestEvent(coretypes.ResourceMetaResourceDashboard, coretypes.VerbDelete, "dashboard deleted"),
|
||||
}
|
||||
require.NoError(t, provider.export(context.Background(), events))
|
||||
|
||||
records := consumeRecords(t, cluster.ListenAddrs(), "tenant_audit", len(events))
|
||||
require.Len(t, records, 1)
|
||||
assert.Equal(t, "tenant_audit", records[0].Topic)
|
||||
assert.Equal(t, testCase.compressionType, records[0].Attrs.CompressionType())
|
||||
|
||||
logs, err := (&plog.ProtoUnmarshaler{}).UnmarshalLogs(records[0].Value)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(events), logs.LogRecordCount())
|
||||
|
||||
serviceName, ok := logs.ResourceLogs().At(0).Resource().Attributes().Get("service.name")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "signoz", serviceName.Str())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExport_RecordTooLarge(t *testing.T) {
|
||||
cluster := newTestCluster(t, "large_audit")
|
||||
provider := newTestProvider(t, cluster.ListenAddrs(), "large_audit", "none")
|
||||
defer provider.client.Close()
|
||||
|
||||
event := newTestEvent(coretypes.ResourceUser, coretypes.VerbCreate, strings.Repeat("a", 2<<20))
|
||||
|
||||
err := provider.export(context.Background(), []audittypes.AuditEvent{event})
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, kerr.MessageTooLarge))
|
||||
}
|
||||
|
||||
func TestExport_UnknownTopic(t *testing.T) {
|
||||
cluster := newTestCluster(t)
|
||||
provider := newTestProvider(t, cluster.ListenAddrs(), "missing_audit", "lz4")
|
||||
defer provider.client.Close()
|
||||
|
||||
err := provider.export(context.Background(), []audittypes.AuditEvent{newTestEvent(coretypes.ResourceUser, coretypes.VerbCreate, "user created")})
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, kerr.UnknownTopicOrPartition))
|
||||
}
|
||||
|
||||
func TestExport_BrokerDown_RetriesUntilContextDone(t *testing.T) {
|
||||
cluster := newTestCluster(t, "down_audit")
|
||||
brokers := cluster.ListenAddrs()
|
||||
cluster.Close()
|
||||
|
||||
provider := newTestProvider(t, brokers, "down_audit", "lz4")
|
||||
defer provider.client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
err := provider.export(ctx, []audittypes.AuditEvent{newTestEvent(coretypes.ResourceUser, coretypes.VerbCreate, "user created")})
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, context.DeadlineExceeded))
|
||||
assert.GreaterOrEqual(t, time.Since(start), 500*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestStop_DrainsQueuedEvents(t *testing.T) {
|
||||
cluster := newTestCluster(t, "drain_audit")
|
||||
provider := newTestProvider(t, cluster.ListenAddrs(), "drain_audit", "lz4")
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- provider.Start(context.Background()) }()
|
||||
<-provider.Healthy()
|
||||
|
||||
for range 7 {
|
||||
provider.server.Add(context.Background(), newTestEvent(coretypes.ResourceUser, coretypes.VerbUpdate, "user updated"))
|
||||
}
|
||||
|
||||
require.NoError(t, provider.Stop(context.Background()))
|
||||
require.NoError(t, <-done)
|
||||
|
||||
records := consumeRecords(t, cluster.ListenAddrs(), "drain_audit", 7)
|
||||
assert.NotEmpty(t, records)
|
||||
}
|
||||
|
||||
func TestStop_BrokerDown_ClosesAfterShutdownTimeout(t *testing.T) {
|
||||
cluster := newTestCluster(t, "stuck_audit")
|
||||
brokers := cluster.ListenAddrs()
|
||||
cluster.Close()
|
||||
|
||||
provider := newTestProvider(t, brokers, "stuck_audit", "lz4")
|
||||
provider.config.Kafka.ShutdownTimeout = 500 * time.Millisecond
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- provider.Start(context.Background()) }()
|
||||
<-provider.Healthy()
|
||||
|
||||
for range 7 {
|
||||
provider.server.Add(context.Background(), newTestEvent(coretypes.ResourceUser, coretypes.VerbUpdate, "user updated"))
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
require.NoError(t, provider.Stop(context.Background()))
|
||||
require.NoError(t, <-done)
|
||||
assert.GreaterOrEqual(t, time.Since(start), 500*time.Millisecond)
|
||||
assert.Less(t, time.Since(start), 5*time.Second)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ErrorEvent, StackFrame } from '@sentry/react';
|
||||
import { AxiosError, AxiosHeaders } from 'axios';
|
||||
|
||||
import {
|
||||
hasNoAppFrames,
|
||||
isExpectedRequestFailure,
|
||||
isExternalIntegrationError,
|
||||
SENTRY_IGNORE_ERRORS,
|
||||
} from '../sentryFilters';
|
||||
@@ -138,3 +140,36 @@ describe('SENTRY_IGNORE_ERRORS', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExpectedRequestFailure', () => {
|
||||
function axiosErrorWithStatus(status: number): AxiosError {
|
||||
const config = { headers: new AxiosHeaders() };
|
||||
return new AxiosError(
|
||||
`Request failed with status code ${status}`,
|
||||
AxiosError.ERR_BAD_REQUEST,
|
||||
config,
|
||||
undefined,
|
||||
{ status, statusText: '', headers: {}, config, data: {} },
|
||||
);
|
||||
}
|
||||
|
||||
it.each([401, 403])('drops a %i response', (status) => {
|
||||
expect(isExpectedRequestFailure(axiosErrorWithStatus(status))).toBe(true);
|
||||
});
|
||||
|
||||
it('drops a network error', () => {
|
||||
expect(
|
||||
isExpectedRequestFailure(
|
||||
new AxiosError('Network Error', AxiosError.ERR_NETWORK),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([400, 404, 500, 503])('keeps a %i response', (status) => {
|
||||
expect(isExpectedRequestFailure(axiosErrorWithStatus(status))).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps an error that is not from axios', () => {
|
||||
expect(isExpectedRequestFailure(new Error('Network Error'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ import defaultRoutes, {
|
||||
} from './routes';
|
||||
import {
|
||||
hasNoAppFrames,
|
||||
isExpectedRequestFailure,
|
||||
isExternalIntegrationError,
|
||||
SENTRY_IGNORE_ERRORS,
|
||||
} from './sentryFilters';
|
||||
@@ -341,6 +342,9 @@ function App(): JSX.Element {
|
||||
if (error?.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
if (isExpectedRequestFailure(hint?.originalException)) {
|
||||
return null;
|
||||
}
|
||||
// Ignore benign Monaco cancellation errors (name 'Canceled').
|
||||
if (error?.name === 'Canceled') {
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ErrorEvent } from '@sentry/react';
|
||||
import { AxiosError, isAxiosError } from 'axios';
|
||||
|
||||
export const SENTRY_IGNORE_ERRORS = [
|
||||
// A tab that outlived a deploy requests hashed assets the new build no longer
|
||||
@@ -14,6 +15,18 @@ export const SENTRY_IGNORE_ERRORS = [
|
||||
/Called on script loaded before session recording is available/,
|
||||
];
|
||||
|
||||
// Not UI bugs: 401/403 come from the session or role, ERR_NETWORK from a
|
||||
// dropped connection.
|
||||
export function isExpectedRequestFailure(error: unknown): boolean {
|
||||
if (!isAxiosError(error)) {
|
||||
return false;
|
||||
}
|
||||
const status = error.response?.status;
|
||||
return (
|
||||
error.code === AxiosError.ERR_NETWORK || status === 401 || status === 403
|
||||
);
|
||||
}
|
||||
|
||||
const APP_ASSETS_PATH = '/assets/';
|
||||
|
||||
// Frames without a position are `[native code]`, which says nothing about the
|
||||
|
||||
@@ -11,8 +11,6 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
// generated API Instance
|
||||
const generatedAPIAxiosInstance = axios.create({
|
||||
baseURL: ENVIRONMENT.baseURL,
|
||||
// The backend binds repeated keys (`states=a&states=b`); axios' default sends `states[]=`.
|
||||
paramsSerializer: { indexes: null },
|
||||
});
|
||||
|
||||
let generatedAPIQueryKeyHeaderContext: Record<string, unknown> | undefined;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useListRulesV3 } from 'api/generated/services/rules';
|
||||
import {
|
||||
type ListRulesV3200,
|
||||
type ListRulesV3Params,
|
||||
RuletypesAlertStateDTO,
|
||||
type RuletypesListableRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
|
||||
|
||||
const HOME_ALERT_RULES_LIMIT = 5;
|
||||
|
||||
@@ -21,7 +23,18 @@ interface HomeAlertRules {
|
||||
|
||||
export function useHomeAlertRules(): HomeAlertRules {
|
||||
const firing = useListRulesV3(FIRING_RULES_PARAMS, {
|
||||
query: { cacheTime: 0 },
|
||||
query: {
|
||||
cacheTime: 0,
|
||||
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
|
||||
queryFn: ({ signal }) =>
|
||||
GeneratedAPIInstance<ListRulesV3200>({
|
||||
url: '/api/v3/rules',
|
||||
method: 'GET',
|
||||
params: FIRING_RULES_PARAMS,
|
||||
paramsSerializer: { indexes: null },
|
||||
signal,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const firingRules = firing.data?.data.rules;
|
||||
const latest = useListRulesV3(
|
||||
|
||||
@@ -3,11 +3,13 @@ import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useListRulesV3 } from 'api/generated/services/rules';
|
||||
import type {
|
||||
ListRulesV3200,
|
||||
ListRulesV3Params,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesLabelPairDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
|
||||
import type { AlertRule } from './types';
|
||||
@@ -33,7 +35,18 @@ export function useAlertRulesData(
|
||||
const hasLoggedEvent = useRef(false);
|
||||
|
||||
const rulesResponse = useListRulesV3(params, {
|
||||
query: { keepPreviousData: true },
|
||||
query: {
|
||||
keepPreviousData: true,
|
||||
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
|
||||
queryFn: ({ signal }) =>
|
||||
GeneratedAPIInstance<ListRulesV3200>({
|
||||
url: '/api/v3/rules',
|
||||
method: 'GET',
|
||||
params,
|
||||
paramsSerializer: { indexes: null },
|
||||
signal,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const rules = useMemo(
|
||||
|
||||
9
go.mod
9
go.mod
@@ -10,6 +10,7 @@ require (
|
||||
github.com/SigNoz/clickhouse-go-mock v0.14.0
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.6
|
||||
github.com/SigNoz/signoz-semantic-conventions/gen/go v0.0.1
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
@@ -61,6 +62,9 @@ require (
|
||||
github.com/swaggest/jsonschema-go v0.3.78
|
||||
github.com/swaggest/rest v0.2.75
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/twmb/franz-go v1.21.7
|
||||
github.com/twmb/franz-go/pkg/kfake v0.0.0-20260421215025-4e7a1e1569ac
|
||||
github.com/twmb/franz-go/plugin/kslog v1.0.0
|
||||
github.com/uptrace/bun v1.2.9
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
||||
@@ -144,6 +148,7 @@ require (
|
||||
github.com/swaggest/refl v1.4.0 // indirect
|
||||
github.com/swaggest/usecase v1.3.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/twmb/franz-go/pkg/kmsg v1.13.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -243,7 +248,7 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/compress v1.19.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
@@ -272,7 +277,7 @@ require (
|
||||
github.com/openfga/openfga v1.14.1
|
||||
github.com/paulmach/orb v0.12.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pressly/goose/v3 v3.27.0 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -102,6 +102,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.6 h1:AgblCIDlLKGtssS4ywvyEByH9fbJ7hFqZvPvUVJKwp8=
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.6/go.mod h1:YRQw6ETaq7fm8ts4Itgb6wC0bVhc0+nC6PFI0MaiH9k=
|
||||
github.com/SigNoz/signoz-semantic-conventions/gen/go v0.0.1 h1:eKJ4vmSLYIfufgh+HaiJxXZts12Xz4jr81HMYYoQvMU=
|
||||
github.com/SigNoz/signoz-semantic-conventions/gen/go v0.0.1/go.mod h1:RSsdmH4/6H+EHeNPyqJ59dGdpKS0hl2p3T5g1NbehOU=
|
||||
github.com/Yiling-J/theine-go v0.6.2 h1:1GeoXeQ0O0AUkiwj2S9Jc0Mzx+hpqzmqsJ4kIC4M9AY=
|
||||
github.com/Yiling-J/theine-go v0.6.2/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
@@ -675,8 +677,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
|
||||
@@ -853,8 +855,8 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
|
||||
github.com/perses/spec v0.1.2 h1:yGoygcR3ZusuGDCmRMwsVXCMvMwi1qZndKV6NYNreEw=
|
||||
github.com/perses/spec v0.1.2/go.mod h1:NoGI5jmGwRdkdPgyYSZJTBL4/Py+dqIPKS2QV8NOvGE=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -1055,6 +1057,16 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GH
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/twmb/franz-go v1.21.7 h1:/DkA/o8wQN55gZWtpj2QNb9SIdxwFR7M+NecQWMdmc0=
|
||||
github.com/twmb/franz-go v1.21.7/go.mod h1:89kLt1uhE1GkyossLHGdpAMFNK9mV8GYk1lfWu9FiNs=
|
||||
github.com/twmb/franz-go/pkg/kadm v1.18.0 h1:WRf/LZmDdcDXwX7WMbtDU++v+b3NzYh2bCGoPMmzirw=
|
||||
github.com/twmb/franz-go/pkg/kadm v1.18.0/go.mod h1:XeLhGoLXLFzK8/ryv5FfpxPxGwj4oFEGpPJMB/x6KDE=
|
||||
github.com/twmb/franz-go/pkg/kfake v0.0.0-20260421215025-4e7a1e1569ac h1:6Dn8at7J4+5A8H7NgVLcPj4dfVm4pRtUZMHxXmBKxJ4=
|
||||
github.com/twmb/franz-go/pkg/kfake v0.0.0-20260421215025-4e7a1e1569ac/go.mod h1:6ofUZL6jAhajJ5WAaJTX9rK3B1ozGDOwKyZczdGK8uE=
|
||||
github.com/twmb/franz-go/pkg/kmsg v1.13.1 h1:fG5kItwysTk5UXqVwb64EpQEy3TydF3vYYK21nUQ+bI=
|
||||
github.com/twmb/franz-go/pkg/kmsg v1.13.1/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY=
|
||||
github.com/twmb/franz-go/plugin/kslog v1.0.0 h1:I64oEmF+0PDvmyLgwrlOtg4mfpSE9GwlcLxM4af2t60=
|
||||
github.com/twmb/franz-go/plugin/kslog v1.0.0/go.mod h1:8pMjK3OJJJNNYddBSbnXZkIK5dCKFIk9GcVVCDgvnQc=
|
||||
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
|
||||
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6 h1:SIKIoA4e/5Y9ZOl0DCe3eVMLPOQzJxgZpfdHHeauNTM=
|
||||
|
||||
@@ -23,7 +23,7 @@ func newTestSettings() factory.ScopedProviderSettings {
|
||||
func newTestEvent(resource coretypes.Resource, action coretypes.Verb) audittypes.AuditEvent {
|
||||
return audittypes.AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
EventName: audittypes.NewEventName(resource.Kind(), action),
|
||||
EventName: audittypes.NewEventName(action),
|
||||
AuditAttributes: audittypes.AuditAttributes{
|
||||
Action: action,
|
||||
Outcome: audittypes.OutcomeSuccess,
|
||||
|
||||
@@ -27,6 +27,8 @@ type Config struct {
|
||||
OTLPHTTP OTLPHTTPConfig `mapstructure:"otlphttp"`
|
||||
|
||||
File FileConfig `mapstructure:"file"`
|
||||
|
||||
Kafka KafkaConfig `mapstructure:"kafka"`
|
||||
}
|
||||
|
||||
// OTLPHTTPConfig holds configuration for the OTLP HTTP exporter provider.
|
||||
@@ -54,6 +56,23 @@ type FileConfig struct {
|
||||
Path string `mapstructure:"path"`
|
||||
}
|
||||
|
||||
type KafkaConfig struct {
|
||||
// Brokers are the seed broker addresses (host:port) used to discover the cluster.
|
||||
Brokers []string `mapstructure:"brokers"`
|
||||
|
||||
// Topic must already exist; the producer does not create it.
|
||||
Topic string `mapstructure:"topic"`
|
||||
|
||||
// ClientID identifies this producer in broker logs and metrics.
|
||||
ClientID string `mapstructure:"client_id"`
|
||||
|
||||
// Compression is the codec for produced batches: none, gzip, snappy, lz4 or zstd.
|
||||
Compression string `mapstructure:"compression"`
|
||||
|
||||
// ShutdownTimeout bounds draining buffered events on stop; events still undelivered when it expires are dropped.
|
||||
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
|
||||
}
|
||||
|
||||
// RetryConfig configures exponential backoff for the OTLP HTTP exporter.
|
||||
type RetryConfig struct {
|
||||
// Enabled controls whether retries are attempted on transient failures.
|
||||
@@ -89,6 +108,12 @@ func newConfig() factory.Config {
|
||||
MaxElapsedTime: time.Minute,
|
||||
},
|
||||
},
|
||||
Kafka: KafkaConfig{
|
||||
Brokers: []string{"localhost:9092"},
|
||||
ClientID: "signoz",
|
||||
Compression: "lz4",
|
||||
ShutdownTimeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,5 +150,25 @@ func (c Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Provider == "kafka" {
|
||||
if len(c.Kafka.Brokers) == 0 {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::kafka::brokers must be set when provider is kafka")
|
||||
}
|
||||
|
||||
if c.Kafka.Topic == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::kafka::topic must be set when provider is kafka")
|
||||
}
|
||||
|
||||
switch c.Kafka.Compression {
|
||||
case "none", "gzip", "snappy", "lz4", "zstd":
|
||||
default:
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::kafka::compression must be one of none, gzip, snappy, lz4 or zstd, got %q", c.Kafka.Compression)
|
||||
}
|
||||
|
||||
if c.Kafka.ShutdownTimeout <= 0 {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::kafka::shutdown_timeout must be greater than 0")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -109,10 +109,9 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
statusCode := writer.StatusCode()
|
||||
span := trace.SpanFromContext(req.Context())
|
||||
|
||||
var errorType, errorCode string
|
||||
var errorType string
|
||||
if statusCode >= 400 {
|
||||
errorType = render.ErrorTypeFromStatusCode(statusCode)
|
||||
errorCode = render.ErrorCodeFromBody(writer.BodyBytes())
|
||||
}
|
||||
|
||||
extractorCtx := coretypes.ExtractorContext{Request: req, ResponseBody: writer.BodyBytes()}
|
||||
@@ -122,14 +121,18 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
continue
|
||||
}
|
||||
|
||||
resource.ResolveResponse(extractorCtx)
|
||||
verb, category := resource.Verb(), resource.Category()
|
||||
if !audittypes.IsAudited(verb) {
|
||||
continue
|
||||
}
|
||||
|
||||
resource.ResolveResponse(extractorCtx)
|
||||
|
||||
switch typed := resource.(type) {
|
||||
case coretypes.ResolvedResourceWithTargetResource:
|
||||
for _, sourceID := range typed.SourceIDs() {
|
||||
for _, targetID := range typed.TargetIDs() {
|
||||
attributesList := []audittypes.ResourceAttributes{
|
||||
attributesList := []audittypes.RelatedResourceAttributes{
|
||||
audittypes.NewRelatedResourceAttributes(
|
||||
typed.SourceResource(),
|
||||
sourceID,
|
||||
@@ -158,9 +161,9 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
verb,
|
||||
category,
|
||||
claims,
|
||||
attributes,
|
||||
attributes.Resource,
|
||||
attributes.Target,
|
||||
errorType,
|
||||
errorCode,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -179,8 +182,8 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
category,
|
||||
claims,
|
||||
attributes,
|
||||
audittypes.TargetAttributes{},
|
||||
errorType,
|
||||
errorCode,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,18 +44,6 @@ func Success(rw http.ResponseWriter, httpCode int, data interface{}) {
|
||||
_, _ = rw.Write(body)
|
||||
}
|
||||
|
||||
func ErrorCodeFromBody(body []byte) string {
|
||||
code := gjson.GetBytes(body, "error.code").String()
|
||||
|
||||
// This should never return empty since we only call this function on responses that were generated by us.
|
||||
// If it does return empty, the codebase has failed to use render package for error responses somewhere, and we should fix that instead of trying to handle it here.
|
||||
if code == "" {
|
||||
return errors.CodeUnset.String()
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
func ErrorTypeFromStatusCode(statusCode int) string {
|
||||
// We are losing the exact type information here, but we can at least capture the error code and message for better observability.
|
||||
// To get the exact type, we would need some changes in the render package to include the error type in the response, which we can consider in the future if there is a need for it.
|
||||
|
||||
@@ -60,31 +60,6 @@ func TestSuccess(t *testing.T) {
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestErrorCodeFromBody(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body []byte
|
||||
wantCode string
|
||||
}{
|
||||
{
|
||||
name: "ValidErrorResponse",
|
||||
body: []byte(`{"status":"error","error":{"code":"authz_forbidden","message":"only admins can access this resource"}}`),
|
||||
wantCode: "authz_forbidden",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON",
|
||||
body: []byte(`not json`),
|
||||
wantCode: "unset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.wantCode, ErrorCodeFromBody(testCase.body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -35,16 +35,15 @@ func auditFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
||||
|
||||
return map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"service.name": {key("service.name", res, str, false)},
|
||||
"signoz.audit.action": {key("signoz.audit.action", attr, str, true)},
|
||||
"signoz.audit.verb": {key("signoz.audit.verb", attr, str, true)},
|
||||
"signoz.audit.outcome": {key("signoz.audit.outcome", attr, str, true)},
|
||||
"signoz.audit.principal.email": {key("signoz.audit.principal.email", attr, str, true)},
|
||||
"signoz.audit.principal.id": {key("signoz.audit.principal.id", attr, str, true)},
|
||||
"signoz.audit.principal.type": {key("signoz.audit.principal.type", attr, str, true)},
|
||||
"signoz.audit.resource.kind": {key("signoz.audit.resource.kind", res, str, false)},
|
||||
"signoz.audit.resource.id": {key("signoz.audit.resource.id", res, str, false)},
|
||||
"signoz.principal.email": {key("signoz.principal.email", attr, str, true)},
|
||||
"signoz.principal.id": {key("signoz.principal.id", attr, str, true)},
|
||||
"signoz.principal.type": {key("signoz.principal.type", attr, str, true)},
|
||||
"signoz.resource.kind": {key("signoz.resource.kind", res, str, false)},
|
||||
"signoz.resource.id": {key("signoz.resource.id", res, str, false)},
|
||||
"signoz.audit.action_category": {key("signoz.audit.action_category", attr, str, false)},
|
||||
"signoz.audit.error.type": {key("signoz.audit.error.type", attr, str, false)},
|
||||
"signoz.audit.error.code": {key("signoz.audit.error.code", attr, str, false)},
|
||||
"exception.type": {key("exception.type", 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)},
|
||||
}
|
||||
@@ -88,12 +87,12 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.principal.id = '019a-1234-abcd-5678'",
|
||||
Expression: "signoz.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 signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$principal$$id` = ? AND `attribute_string_signoz$$audit$$principal$$id_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
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 signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$principal$$id` = ? AND `attribute_string_signoz$$principal$$id_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"019a-1234-abcd-5678", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
@@ -122,13 +121,13 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.resource.kind = 'dashboard' AND signoz.audit.resource.id = '019b-5678-efgh-9012'",
|
||||
Expression: "signoz.resource.kind = 'dashboard' AND signoz.resource.id = '019b-5678-efgh-9012'",
|
||||
},
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'signoz.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 signoz_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", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%signoz.audit.resource.id%", "%signoz.audit.resource.id\":\"019b-5678-efgh-9012%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'signoz.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 signoz_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", "%signoz.resource.kind%", "%signoz.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%signoz.resource.id%", "%signoz.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)
|
||||
@@ -139,13 +138,13 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.resource.kind = 'dashboard' AND signoz.audit.action = 'delete'",
|
||||
Expression: "signoz.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
|
||||
},
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.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 signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.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 signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$verb` = ? AND `attribute_string_signoz$$audit$$verb_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.resource.kind%", "%signoz.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
// List: all actions by service accounts (materialized principal.type)
|
||||
@@ -156,12 +155,12 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.principal.type = 'service_account'",
|
||||
Expression: "signoz.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 signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$principal$$type` = ? AND `attribute_string_signoz$$audit$$principal$$type_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
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 signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$principal$$type` = ? AND `attribute_string_signoz$$principal$$type_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"service_account", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
@@ -174,14 +173,14 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.outcome = 'failure' AND signoz.audit.action = 'update'",
|
||||
Expression: "signoz.audit.outcome = 'failure' AND signoz.audit.verb = 'update'",
|
||||
},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT count() AS __result_0 FROM signoz_audit.distributed_logs WHERE ((`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = true)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
|
||||
Query: "SELECT count() AS __result_0 FROM signoz_audit.distributed_logs WHERE ((`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND (`attribute_string_signoz$$audit$$verb` = ? AND `attribute_string_signoz$$audit$$verb_exists` = true)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
|
||||
Args: []any{"failure", "update", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
},
|
||||
@@ -200,12 +199,12 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Expression: "signoz.audit.outcome = 'failure'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "signoz.audit.principal.email"}},
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "signoz.principal.email"}},
|
||||
},
|
||||
Limit: 5,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __limit_cte AS (SELECT toString(multiIf(`attribute_string_signoz$$audit$$principal$$email_exists` = true, `attribute_string_signoz$$audit$$principal$$email`, NULL)) AS `signoz.audit.principal.email`, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `signoz.audit.principal.email` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_signoz$$audit$$principal$$email_exists` = true, `attribute_string_signoz$$audit$$principal$$email`, NULL)) AS `signoz.audit.principal.email`, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`signoz.audit.principal.email`) GLOBAL IN (SELECT `signoz.audit.principal.email` FROM __limit_cte) GROUP BY ts, `signoz.audit.principal.email`",
|
||||
Query: "WITH __limit_cte AS (SELECT toString(multiIf(`attribute_string_signoz$$principal$$email_exists` = true, `attribute_string_signoz$$principal$$email`, NULL)) AS `signoz.principal.email`, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `signoz.principal.email` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_signoz$$principal$$email_exists` = true, `attribute_string_signoz$$principal$$email`, NULL)) AS `signoz.principal.email`, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`signoz.principal.email`) GLOBAL IN (SELECT `signoz.principal.email` FROM __limit_cte) GROUP BY ts, `signoz.principal.email`",
|
||||
Args: []any{"failure", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 5, "failure", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
},
|
||||
@@ -243,13 +242,13 @@ func TestStatementBuilderHeatmap(t *testing.T) {
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "http.response.status_code"}},
|
||||
Filter: &qbtypes.Filter{Expression: "signoz.audit.resource.kind = 'dashboard'"},
|
||||
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "signoz.audit.action"}}},
|
||||
Filter: &qbtypes.Filter{Expression: "signoz.resource.kind = 'dashboard'"},
|
||||
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "signoz.audit.verb"}}},
|
||||
BucketOptions: &qbtypes.BucketOptions{Kind: qbtypes.BucketsKindLinear, Spec: qbtypes.LinearBucketsSpec{MaxValue: 500, NumBuckets: 5}},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), ifNull(multiIf(mapContains(attributes_number, 'http.response.status_code'), toFloat64(attributes_number['http.response.status_code']), NULL), 0) AS __bucket_value SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_signoz$$audit$$action_exists` = true, `attribute_string_signoz$$audit$$action`, NULL)) AS `signoz.audit.action`, multiIf(__bucket_value <= 0, toFloat64('-Inf'), __bucket_value > 500, toFloat64(500), (least(greatest(ceil(__bucket_value * 5 / 500), 1), 5) - 1) * 500 / 5) AS __bucket_min, multiIf(__bucket_value <= 0, toFloat64(0), __bucket_value > 500, toFloat64('+Inf'), least(greatest(ceil(__bucket_value * 5 / 500), 1), 5) * 500 / 5) AS __bucket_max, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND mapContains(attributes_number, 'http.response.status_code') AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts, `signoz.audit.action`, __bucket_min, __bucket_max",
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), ifNull(multiIf(mapContains(attributes_number, 'http.response.status_code'), toFloat64(attributes_number['http.response.status_code']), NULL), 0) AS __bucket_value SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_signoz$$audit$$verb_exists` = true, `attribute_string_signoz$$audit$$verb`, NULL)) AS `signoz.audit.verb`, multiIf(__bucket_value <= 0, toFloat64('-Inf'), __bucket_value > 500, toFloat64(500), (least(greatest(ceil(__bucket_value * 5 / 500), 1), 5) - 1) * 500 / 5) AS __bucket_min, multiIf(__bucket_value <= 0, toFloat64(0), __bucket_value > 500, toFloat64('+Inf'), least(greatest(ceil(__bucket_value * 5 / 500), 1), 5) * 500 / 5) AS __bucket_max, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND mapContains(attributes_number, 'http.response.status_code') AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts, `signoz.audit.verb`, __bucket_min, __bucket_max",
|
||||
Args: []any{"dashboard", "%signoz.resource.kind%", "%signoz.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -260,13 +259,13 @@ func TestStatementBuilderHeatmap(t *testing.T) {
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "http.response.status_code"}},
|
||||
Filter: &qbtypes.Filter{Expression: "signoz.audit.resource.kind = 'dashboard'"},
|
||||
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "signoz.audit.action"}}},
|
||||
Filter: &qbtypes.Filter{Expression: "signoz.resource.kind = 'dashboard'"},
|
||||
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "signoz.audit.verb"}}},
|
||||
BucketOptions: &qbtypes.BucketOptions{Kind: qbtypes.BucketsKindLinear, Spec: qbtypes.LinearBucketsSpec{MaxValue: 500, NumBuckets: 5}},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), ifNull(multiIf(mapContains(attributes_number, 'http.response.status_code'), toFloat64(attributes_number['http.response.status_code']), NULL), 0) AS __bucket_value SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_signoz$$audit$$action_exists` = true, `attribute_string_signoz$$audit$$action`, NULL)) AS `signoz.audit.action`, multiIf(__bucket_value <= 0, toFloat64('-Inf'), __bucket_value > 500, toFloat64(500), (least(greatest(ceil(__bucket_value * 5 / 500), 1), 5) - 1) * 500 / 5) AS __bucket_min, multiIf(__bucket_value <= 0, toFloat64(0), __bucket_value > 500, toFloat64('+Inf'), least(greatest(ceil(__bucket_value * 5 / 500), 1), 5) * 500 / 5) AS __bucket_max, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND mapContains(attributes_number, 'http.response.status_code') AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts, `signoz.audit.action`, __bucket_min, __bucket_max",
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), ifNull(multiIf(mapContains(attributes_number, 'http.response.status_code'), toFloat64(attributes_number['http.response.status_code']), NULL), 0) AS __bucket_value SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_signoz$$audit$$verb_exists` = true, `attribute_string_signoz$$audit$$verb`, NULL)) AS `signoz.audit.verb`, multiIf(__bucket_value <= 0, toFloat64('-Inf'), __bucket_value > 500, toFloat64(500), (least(greatest(ceil(__bucket_value * 5 / 500), 1), 5) - 1) * 500 / 5) AS __bucket_min, multiIf(__bucket_value <= 0, toFloat64(0), __bucket_value > 500, toFloat64('+Inf'), least(greatest(ceil(__bucket_value * 5 / 500), 1), 5) * 500 / 5) AS __bucket_max, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND mapContains(attributes_number, 'http.response.status_code') AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts, `signoz.audit.verb`, __bucket_min, __bucket_max",
|
||||
Args: []any{"dashboard", "%signoz.resource.kind%", "%signoz.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package audittypes
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz-semantic-conventions/gen/go/sigsemconv"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -16,10 +19,9 @@ type AuditAttributes struct {
|
||||
Action coretypes.Verb // guaranteed to be present
|
||||
ActionCategory coretypes.ActionCategory // guaranteed to be present
|
||||
Outcome Outcome // guaranteed to be present
|
||||
IdentNProvider authtypes.IdentNProvider
|
||||
}
|
||||
|
||||
func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category coretypes.ActionCategory, claims authtypes.Claims) AuditAttributes {
|
||||
func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category coretypes.ActionCategory) AuditAttributes {
|
||||
outcome := OutcomeFailure
|
||||
if statusCode >= 200 && statusCode < 400 {
|
||||
outcome = OutcomeSuccess
|
||||
@@ -29,15 +31,13 @@ func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category
|
||||
Action: action,
|
||||
ActionCategory: category,
|
||||
Outcome: outcome,
|
||||
IdentNProvider: claims.IdentNProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (attributes AuditAttributes) Put(dest pcommon.Map) {
|
||||
dest.PutStr("signoz.audit.action", attributes.Action.StringValue())
|
||||
dest.PutStr("signoz.audit.action_category", attributes.ActionCategory.StringValue())
|
||||
dest.PutStr("signoz.audit.outcome", attributes.Outcome.StringValue())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.identn_provider", attributes.IdentNProvider.StringValue())
|
||||
dest.PutStr(string(sigsemconv.SigNozAuditVerbKey), attributes.Action.StringValue())
|
||||
dest.PutStr(string(sigsemconv.SigNozAuditActionCategoryKey), attributes.ActionCategory.StringValue())
|
||||
dest.PutStr(string(sigsemconv.SigNozAuditOutcomeKey), attributes.Outcome.StringValue())
|
||||
}
|
||||
|
||||
// Audit attributes — Principal (Who).
|
||||
@@ -50,6 +50,9 @@ type PrincipalAttributes struct {
|
||||
|
||||
func NewPrincipalAttributesFromClaims(claims authtypes.Claims) PrincipalAttributes {
|
||||
principalID, _ := valuer.NewUUID(claims.UserID)
|
||||
if claims.Principal == authtypes.PrincipalServiceAccount {
|
||||
principalID, _ = valuer.NewUUID(claims.ServiceAccountID)
|
||||
}
|
||||
principalEmail, _ := valuer.NewEmail(claims.Email)
|
||||
principalOrgID, _ := valuer.NewUUID(claims.OrgID)
|
||||
|
||||
@@ -62,10 +65,12 @@ func NewPrincipalAttributesFromClaims(claims authtypes.Claims) PrincipalAttribut
|
||||
}
|
||||
|
||||
func (attributes PrincipalAttributes) Put(dest pcommon.Map) {
|
||||
dest.PutStr("signoz.audit.principal.id", attributes.PrincipalID.StringValue())
|
||||
dest.PutStr("signoz.audit.principal.email", attributes.PrincipalEmail.String())
|
||||
dest.PutStr("signoz.audit.principal.type", attributes.PrincipalType.StringValue())
|
||||
dest.PutStr("signoz.audit.principal.org_id", attributes.PrincipalOrgID.StringValue())
|
||||
if !attributes.PrincipalID.IsZero() {
|
||||
dest.PutStr(string(sigsemconv.SigNozPrincipalIDKey), attributes.PrincipalID.StringValue())
|
||||
putStrIfNotEmpty(dest, string(sigsemconv.SigNozPrincipalTypeKey), attributes.PrincipalType.StringValue())
|
||||
}
|
||||
putStrIfNotEmpty(dest, string(sigsemconv.SigNozPrincipalEmailKey), attributes.PrincipalEmail.String())
|
||||
dest.PutStr(string(sigsemconv.SigNozOrganizationIDKey), attributes.PrincipalOrgID.StringValue())
|
||||
}
|
||||
|
||||
// Audit attributes — Resource (On What).
|
||||
@@ -73,11 +78,6 @@ func (attributes PrincipalAttributes) Put(dest pcommon.Map) {
|
||||
type ResourceAttributes struct {
|
||||
Resource coretypes.Resource // guaranteed to be present
|
||||
ResourceID string
|
||||
|
||||
// TargetResource names the counterpart of an attach/detach event (audit
|
||||
// context only). nil when there is no relationship.
|
||||
TargetResource coretypes.Resource
|
||||
TargetResourceID string
|
||||
}
|
||||
|
||||
func NewResourceAttributes(resource coretypes.Resource, resourceID string) ResourceAttributes {
|
||||
@@ -87,55 +87,75 @@ func NewResourceAttributes(resource coretypes.Resource, resourceID string) Resou
|
||||
}
|
||||
}
|
||||
|
||||
// NewAttachResourceAttributes builds resource attributes that additionally name
|
||||
// the target counterpart (used for attach/detach audit events).
|
||||
func NewRelatedResourceAttributes(resource coretypes.Resource, resourceID string, targetResource coretypes.Resource, targetResourceID string) ResourceAttributes {
|
||||
return ResourceAttributes{
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
// Put writes the resource attributes to an OTel Resource's attribute map.
|
||||
// These are resource-level attributes (stored in the resource JSON column),
|
||||
// not event-level attributes (stored in attributes_string).
|
||||
func (attributes ResourceAttributes) Put(orgID valuer.UUID, dest pcommon.Map) {
|
||||
putStrIfNotEmpty(dest, string(sigsemconv.SigNozResourceKindKey), attributes.Resource.Kind().String())
|
||||
putStrIfNotEmpty(dest, string(sigsemconv.SigNozResourceIDKey), attributes.ResourceID)
|
||||
if attributes.ResourceID != "" {
|
||||
putStrIfNotEmpty(dest, string(sigsemconv.SigNozResourceObjectKey), attributes.Resource.Object(orgID, attributes.ResourceID))
|
||||
}
|
||||
}
|
||||
|
||||
// Audit attributes — Target (To/From What).
|
||||
// The counterpart of an attach/detach event, placed on the log record.
|
||||
type TargetAttributes struct {
|
||||
// nil when there is no relationship.
|
||||
TargetResource coretypes.Resource
|
||||
TargetResourceID string
|
||||
}
|
||||
|
||||
func NewTargetAttributes(targetResource coretypes.Resource, targetResourceID string) TargetAttributes {
|
||||
return TargetAttributes{
|
||||
TargetResource: targetResource,
|
||||
TargetResourceID: targetResourceID,
|
||||
}
|
||||
}
|
||||
|
||||
// PutResource writes the resource attributes to an OTel Resource's attribute map.
|
||||
// These are resource-level attributes (stored in the resource JSON column),
|
||||
// not event-level attributes (stored in attributes_string).
|
||||
func (attributes ResourceAttributes) PutResource(orgID valuer.UUID, dest pcommon.Map) {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.kind", attributes.Resource.Kind().String())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.id", attributes.ResourceID)
|
||||
if attributes.ResourceID != "" {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.object", attributes.Resource.Object(orgID, attributes.ResourceID))
|
||||
func (attributes TargetAttributes) Put(orgID valuer.UUID, dest pcommon.Map) {
|
||||
if attributes.TargetResource == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if attributes.TargetResource != nil {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.target.kind", attributes.TargetResource.Kind().String())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.target.id", attributes.TargetResourceID)
|
||||
if attributes.TargetResourceID != "" {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.target.object", attributes.TargetResource.Object(orgID, attributes.TargetResourceID))
|
||||
}
|
||||
putStrIfNotEmpty(dest, string(sigsemconv.SigNozResourceTargetKindKey), attributes.TargetResource.Kind().String())
|
||||
putStrIfNotEmpty(dest, string(sigsemconv.SigNozResourceTargetIDKey), attributes.TargetResourceID)
|
||||
if attributes.TargetResourceID != "" {
|
||||
putStrIfNotEmpty(dest, string(sigsemconv.SigNozResourceTargetObjectKey), attributes.TargetResource.Object(orgID, attributes.TargetResourceID))
|
||||
}
|
||||
}
|
||||
|
||||
// Audit attributes: Relation (attach/detach).
|
||||
// One side of a relation: the resource the event is recorded from, placed on
|
||||
// the Resource, and its counterpart, placed on the log record.
|
||||
type RelatedResourceAttributes struct {
|
||||
Resource ResourceAttributes
|
||||
Target TargetAttributes
|
||||
}
|
||||
|
||||
func NewRelatedResourceAttributes(resource coretypes.Resource, resourceID string, targetResource coretypes.Resource, targetResourceID string) RelatedResourceAttributes {
|
||||
return RelatedResourceAttributes{
|
||||
Resource: NewResourceAttributes(resource, resourceID),
|
||||
Target: NewTargetAttributes(targetResource, targetResourceID),
|
||||
}
|
||||
}
|
||||
|
||||
// Audit attributes — Error (When outcome is failure)
|
||||
// Error messages are intentionally excluded to avoid leaking sensitive or
|
||||
// PII data into audit logs. The error type and code are sufficient for
|
||||
// filtering and alerting; investigators can correlate via trace ID.
|
||||
// PII data into audit logs. The error type is sufficient for filtering and
|
||||
// alerting; investigators can correlate via trace ID.
|
||||
type ErrorAttributes struct {
|
||||
ErrorType string
|
||||
ErrorCode string
|
||||
}
|
||||
|
||||
func NewErrorAttributes(errorType, errorCode string) ErrorAttributes {
|
||||
func NewErrorAttributes(errorType string) ErrorAttributes {
|
||||
return ErrorAttributes{
|
||||
ErrorType: errorType,
|
||||
ErrorCode: errorCode,
|
||||
}
|
||||
}
|
||||
|
||||
func (attributes ErrorAttributes) Put(dest pcommon.Map) {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.error.type", attributes.ErrorType)
|
||||
putStrIfNotEmpty(dest, "signoz.audit.error.code", attributes.ErrorCode)
|
||||
putStrIfNotEmpty(dest, string(semconv.ExceptionTypeKey), attributes.ErrorType)
|
||||
}
|
||||
|
||||
// Audit attributes — Transport Context (Where/How).
|
||||
@@ -148,6 +168,7 @@ type TransportAttributes struct {
|
||||
HTTPStatusCode int
|
||||
URLPath string
|
||||
ClientAddress string
|
||||
ClientPort int
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
@@ -157,15 +178,18 @@ func NewTransportAttributesFromHTTP(req *http.Request, route string, statusCode
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
clientAddress, clientPort := splitHostPort(req.RemoteAddr)
|
||||
|
||||
return TransportAttributes{
|
||||
NetworkProtocolName: "http",
|
||||
NetworkProtocolVersion: req.Proto,
|
||||
NetworkProtocolVersion: networkProtocolVersion(req),
|
||||
URLScheme: scheme,
|
||||
HTTPMethod: req.Method,
|
||||
HTTPRoute: route,
|
||||
HTTPStatusCode: statusCode,
|
||||
URLPath: req.URL.Path,
|
||||
ClientAddress: req.RemoteAddr,
|
||||
ClientAddress: clientAddress,
|
||||
ClientPort: clientPort,
|
||||
UserAgent: req.UserAgent(),
|
||||
}
|
||||
}
|
||||
@@ -181,16 +205,43 @@ func (attributes TransportAttributes) Put(dest pcommon.Map) {
|
||||
}
|
||||
putStrIfNotEmpty(dest, string(semconv.URLPathKey), attributes.URLPath)
|
||||
putStrIfNotEmpty(dest, string(semconv.ClientAddressKey), attributes.ClientAddress)
|
||||
if attributes.ClientPort != 0 {
|
||||
dest.PutInt(string(semconv.ClientPortKey), int64(attributes.ClientPort))
|
||||
}
|
||||
putStrIfNotEmpty(dest, string(semconv.UserAgentOriginalKey), attributes.UserAgent)
|
||||
}
|
||||
|
||||
// networkProtocolVersion drops the HTTP/ prefix, and the .0 of HTTP/2 and later.
|
||||
func networkProtocolVersion(req *http.Request) string {
|
||||
if req.ProtoMajor >= 2 && req.ProtoMinor == 0 {
|
||||
return strconv.Itoa(req.ProtoMajor)
|
||||
}
|
||||
|
||||
return strconv.Itoa(req.ProtoMajor) + "." + strconv.Itoa(req.ProtoMinor)
|
||||
}
|
||||
|
||||
// splitHostPort returns addr unchanged with port 0 when it has no valid port.
|
||||
func splitHostPort(addr string) (string, int) {
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return addr, 0
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return addr, 0
|
||||
}
|
||||
|
||||
return host, port
|
||||
}
|
||||
|
||||
func putStrIfNotEmpty(attrs pcommon.Map, key, value string) {
|
||||
if value != "" {
|
||||
attrs.PutStr(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttributes, resourceAttributes ResourceAttributes, errorAttributes ErrorAttributes) string {
|
||||
func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttributes, resourceAttributes ResourceAttributes, targetAttributes TargetAttributes, errorAttributes ErrorAttributes) string {
|
||||
var b strings.Builder
|
||||
|
||||
// Principal: "email (id)" or "id" or "email" or omitted.
|
||||
@@ -227,36 +278,25 @@ func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttri
|
||||
b.WriteString(")")
|
||||
}
|
||||
|
||||
// Target (attach/detach context): " · target kind (id)" or " · target kind".
|
||||
if resourceAttributes.TargetResource != nil {
|
||||
b.WriteString(" to ")
|
||||
b.WriteString(resourceAttributes.TargetResource.Kind().String())
|
||||
if resourceAttributes.TargetResourceID != "" {
|
||||
// Target (attach/detach context): " to target kind (id)" or " from target kind" for detach.
|
||||
if targetAttributes.TargetResource != nil {
|
||||
if auditAttributes.Action == coretypes.VerbDetach {
|
||||
b.WriteString(" from ")
|
||||
} else {
|
||||
b.WriteString(" to ")
|
||||
}
|
||||
b.WriteString(targetAttributes.TargetResource.Kind().String())
|
||||
if targetAttributes.TargetResourceID != "" {
|
||||
b.WriteString(" (")
|
||||
b.WriteString(resourceAttributes.TargetResourceID)
|
||||
b.WriteString(targetAttributes.TargetResourceID)
|
||||
b.WriteString(")")
|
||||
}
|
||||
}
|
||||
|
||||
// Error suffix (failure only): ": type (code)" or ": type" or ": (code)" or omitted.
|
||||
if auditAttributes.Outcome == OutcomeFailure {
|
||||
errorType := errorAttributes.ErrorType
|
||||
errorCode := errorAttributes.ErrorCode
|
||||
if errorType != "" || errorCode != "" {
|
||||
b.WriteString(": ")
|
||||
if errorType != "" && errorCode != "" {
|
||||
b.WriteString(errorType)
|
||||
b.WriteString(" (")
|
||||
b.WriteString(errorCode)
|
||||
b.WriteString(")")
|
||||
} else if errorType != "" {
|
||||
b.WriteString(errorType)
|
||||
} else {
|
||||
b.WriteString("(")
|
||||
b.WriteString(errorCode)
|
||||
b.WriteString(")")
|
||||
}
|
||||
}
|
||||
// Error suffix (failure only): ": type" or omitted.
|
||||
if auditAttributes.Outcome == OutcomeFailure && errorAttributes.ErrorType != "" {
|
||||
b.WriteString(": ")
|
||||
b.WriteString(errorAttributes.ErrorType)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
|
||||
@@ -3,15 +3,12 @@ package audittypes
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewAuditAttributesFromHTTP_OutcomeBoundary(t *testing.T) {
|
||||
claims := authtypes.Claims{IdentNProvider: authtypes.IdentNProviderTokenizer}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
@@ -36,7 +33,7 @@ func TestNewAuditAttributesFromHTTP_OutcomeBoundary(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
attrs := NewAuditAttributesFromHTTP(testCase.statusCode, coretypes.VerbUpdate, coretypes.ActionCategoryConfigurationChange, claims)
|
||||
attrs := NewAuditAttributesFromHTTP(testCase.statusCode, coretypes.VerbUpdate, coretypes.ActionCategoryConfigurationChange)
|
||||
assert.Equal(t, testCase.expectedOutcome, attrs.Outcome)
|
||||
})
|
||||
}
|
||||
@@ -48,6 +45,7 @@ func TestNewBody(t *testing.T) {
|
||||
auditAttributes AuditAttributes
|
||||
principalAttributes PrincipalAttributes
|
||||
resourceAttributes ResourceAttributes
|
||||
targetAttributes TargetAttributes
|
||||
errorAttributes ErrorAttributes
|
||||
expectedBody string
|
||||
}{
|
||||
@@ -154,9 +152,8 @@ func TestNewBody(t *testing.T) {
|
||||
},
|
||||
errorAttributes: ErrorAttributes{
|
||||
ErrorType: "forbidden",
|
||||
ErrorCode: "authz_forbidden",
|
||||
},
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678): forbidden (authz_forbidden)",
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678): forbidden",
|
||||
},
|
||||
{
|
||||
name: "Failure_ErrorTypeOnly",
|
||||
@@ -193,11 +190,47 @@ func TestNewBody(t *testing.T) {
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to create dashboard (019b-5678)",
|
||||
},
|
||||
{
|
||||
name: "Success_Attach",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbAttach,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{},
|
||||
resourceAttributes: NewResourceAttributes(coretypes.ResourceRole, "role-1"),
|
||||
targetAttributes: NewTargetAttributes(coretypes.ResourceServiceAccount, "sa-1"),
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "attached role (role-1) to serviceaccount (sa-1)",
|
||||
},
|
||||
{
|
||||
name: "Success_Detach",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbDetach,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{},
|
||||
resourceAttributes: NewResourceAttributes(coretypes.ResourceRole, "role-1"),
|
||||
targetAttributes: NewTargetAttributes(coretypes.ResourceServiceAccount, "sa-1"),
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "detached role (role-1) from serviceaccount (sa-1)",
|
||||
},
|
||||
{
|
||||
name: "Failure_Detach",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbDetach,
|
||||
Outcome: OutcomeFailure,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{},
|
||||
resourceAttributes: NewResourceAttributes(coretypes.ResourceRole, "role-1"),
|
||||
targetAttributes: NewTargetAttributes(coretypes.ResourceServiceAccount, "sa-1"),
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "failed to detach role (role-1) from serviceaccount (sa-1)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
body := newBody(testCase.auditAttributes, testCase.principalAttributes, testCase.resourceAttributes, testCase.errorAttributes)
|
||||
body := newBody(testCase.auditAttributes, testCase.principalAttributes, testCase.resourceAttributes, testCase.targetAttributes, testCase.errorAttributes)
|
||||
assert.Equal(t, testCase.expectedBody, body)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ type AuditEvent struct {
|
||||
// Custom Audit Attributes - Resource
|
||||
ResourceAttributes ResourceAttributes
|
||||
|
||||
// Custom Audit Attributes - Target
|
||||
TargetAttributes TargetAttributes
|
||||
|
||||
// Custom Audit Attributes - Error
|
||||
ErrorAttributes ErrorAttributes
|
||||
|
||||
@@ -56,23 +59,24 @@ func NewAuditEventFromHTTPRequest(
|
||||
actionCategory coretypes.ActionCategory,
|
||||
claims authtypes.Claims,
|
||||
resourceAttributes ResourceAttributes,
|
||||
targetAttributes TargetAttributes,
|
||||
errorType string,
|
||||
errorCode string,
|
||||
) AuditEvent {
|
||||
auditAttributes := NewAuditAttributesFromHTTP(statusCode, action, actionCategory, claims)
|
||||
auditAttributes := NewAuditAttributesFromHTTP(statusCode, action, actionCategory)
|
||||
principalAttributes := NewPrincipalAttributesFromClaims(claims)
|
||||
errorAttributes := NewErrorAttributes(errorType, errorCode)
|
||||
errorAttributes := NewErrorAttributes(errorType)
|
||||
transportAttributes := NewTransportAttributesFromHTTP(req, route, statusCode)
|
||||
|
||||
return AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
TraceID: traceID,
|
||||
SpanID: spanID,
|
||||
Body: newBody(auditAttributes, principalAttributes, resourceAttributes, errorAttributes),
|
||||
EventName: NewEventName(resourceAttributes.Resource.Kind(), auditAttributes.Action),
|
||||
Body: newBody(auditAttributes, principalAttributes, resourceAttributes, targetAttributes, errorAttributes),
|
||||
EventName: NewEventName(auditAttributes.Action),
|
||||
AuditAttributes: auditAttributes,
|
||||
PrincipalAttributes: principalAttributes,
|
||||
ResourceAttributes: resourceAttributes,
|
||||
TargetAttributes: targetAttributes,
|
||||
ErrorAttributes: errorAttributes,
|
||||
TransportAttributes: transportAttributes,
|
||||
}
|
||||
@@ -102,7 +106,7 @@ func NewPLogsFromAuditEvents(events []AuditEvent, name string, version string, s
|
||||
resourceAttrs.PutStr(string(semconv.ServiceNameKey), name)
|
||||
resourceAttrs.PutStr(string(semconv.ServiceVersionKey), version)
|
||||
head := events[groups[key][0]]
|
||||
head.ResourceAttributes.PutResource(head.PrincipalAttributes.PrincipalOrgID, resourceAttrs)
|
||||
head.ResourceAttributes.Put(head.PrincipalAttributes.PrincipalOrgID, resourceAttrs)
|
||||
|
||||
scopeLogs := resourceLogs.ScopeLogs().AppendEmpty()
|
||||
scopeLogs.Scope().SetName(scope)
|
||||
@@ -148,6 +152,9 @@ func (event AuditEvent) ToLogRecord(dest plog.LogRecord) {
|
||||
// Resource attributes are set at the ResourceLogs level, not per-LogRecord.
|
||||
// See NewPLogsFromAuditEvents.
|
||||
|
||||
// Target attributes
|
||||
event.TargetAttributes.Put(event.PrincipalAttributes.PrincipalOrgID, attrs)
|
||||
|
||||
// Error attributes
|
||||
event.ErrorAttributes.Put(attrs)
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
package audittypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
import (
|
||||
"github.com/SigNoz/signoz-semantic-conventions/gen/go/sigsemconv"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
)
|
||||
|
||||
var eventNames = map[coretypes.Verb]EventName{
|
||||
coretypes.VerbCreate: {s: sigsemconv.SigNozAuditCreateEventName},
|
||||
coretypes.VerbUpdate: {s: sigsemconv.SigNozAuditUpdateEventName},
|
||||
coretypes.VerbDelete: {s: sigsemconv.SigNozAuditDeleteEventName},
|
||||
coretypes.VerbAttach: {s: sigsemconv.SigNozAuditAttachEventName},
|
||||
coretypes.VerbDetach: {s: sigsemconv.SigNozAuditDetachEventName},
|
||||
}
|
||||
|
||||
// EventName is a typed wrapper for audit event names, ensuring not every
|
||||
// string qualifies as an event name.
|
||||
@@ -8,15 +19,16 @@ type EventName struct {
|
||||
s string
|
||||
}
|
||||
|
||||
// NewEventName derives the audit event name from a resource kind and action.
|
||||
// Format: {resource_kind}.{pastTense(action)}
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// NewEventName("dashboard", coretypes.VerbCreate) → "dashboard.created"
|
||||
// NewEventName("dashboard", coretypes.VerbUpdate) → "dashboard.updated"
|
||||
func NewEventName(resourceKind coretypes.Kind, action coretypes.Verb) EventName {
|
||||
return EventName{s: resourceKind.String() + "." + action.PastTense()}
|
||||
// NewEventName returns the audit event name of an action, signoz.audit.{action}.
|
||||
// It is zero for an action that is not audited; see IsAudited.
|
||||
func NewEventName(action coretypes.Verb) EventName {
|
||||
return eventNames[action]
|
||||
}
|
||||
|
||||
// IsAudited reports whether an action records an audit event.
|
||||
func IsAudited(action coretypes.Verb) bool {
|
||||
_, ok := eventNames[action]
|
||||
return ok
|
||||
}
|
||||
|
||||
// String returns the string representation of the event name.
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/collector/pdata/plog"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
@@ -31,7 +34,6 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
resource coretypes.Resource
|
||||
resourceID string
|
||||
errorType string
|
||||
errorCode string
|
||||
expectedOutcome Outcome
|
||||
expectedBody string
|
||||
}{
|
||||
@@ -61,9 +63,8 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
resource: testDashboardResource,
|
||||
resourceID: "019b-5678-efgh-9012",
|
||||
errorType: "forbidden",
|
||||
errorCode: "authz_forbidden",
|
||||
expectedOutcome: OutcomeFailure,
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678-efgh-9012): forbidden (authz_forbidden)",
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678-efgh-9012): forbidden",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,8 +82,8 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
testCase.category,
|
||||
testCase.claims,
|
||||
NewResourceAttributes(testCase.resource, testCase.resourceID),
|
||||
TargetAttributes{},
|
||||
testCase.errorType,
|
||||
testCase.errorCode,
|
||||
)
|
||||
|
||||
assert.Equal(t, testCase.expectedOutcome, event.AuditAttributes.Outcome)
|
||||
@@ -97,7 +98,6 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
assert.Equal(t, traceID, event.TraceID)
|
||||
assert.Equal(t, spanID, event.SpanID)
|
||||
assert.Equal(t, testCase.errorType, event.ErrorAttributes.ErrorType)
|
||||
assert.Equal(t, testCase.errorCode, event.ErrorAttributes.ErrorCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
func newTestEvent(resource coretypes.Resource, resourceID string, action coretypes.Verb) AuditEvent {
|
||||
return AuditEvent{
|
||||
Body: resource.Kind().String() + "." + action.PastTense(),
|
||||
EventName: NewEventName(resource.Kind(), action),
|
||||
EventName: NewEventName(action),
|
||||
AuditAttributes: AuditAttributes{
|
||||
Action: action,
|
||||
ActionCategory: coretypes.ActionCategoryConfigurationChange,
|
||||
@@ -210,11 +210,11 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "0.90.0", serviceVersion.Str())
|
||||
|
||||
kind, exists := resourceAttrs.Get("signoz.audit.resource.kind")
|
||||
kind, exists := resourceAttrs.Get("signoz.resource.kind")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, testCase.expectedResourceKinds[i], kind.Str())
|
||||
|
||||
id, exists := resourceAttrs.Get("signoz.audit.resource.id")
|
||||
id, exists := resourceAttrs.Get("signoz.resource.id")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, testCase.expectedResourceIDs[i], id.Str())
|
||||
|
||||
@@ -225,8 +225,8 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
|
||||
|
||||
for j := 0; j < resourceLogs.ScopeLogs().At(0).LogRecords().Len(); j++ {
|
||||
recordAttrs := resourceLogs.ScopeLogs().At(0).LogRecords().At(j).Attributes()
|
||||
_, hasKind := recordAttrs.Get("signoz.audit.resource.kind")
|
||||
_, hasID := recordAttrs.Get("signoz.audit.resource.id")
|
||||
_, hasKind := recordAttrs.Get("signoz.resource.kind")
|
||||
_, hasID := recordAttrs.Get("signoz.resource.id")
|
||||
assert.False(t, hasKind, "resource.kind must not be in log record attributes")
|
||||
assert.False(t, hasID, "resource.id must not be in log record attributes")
|
||||
}
|
||||
@@ -234,3 +234,276 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEventName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
action coretypes.Verb
|
||||
expected string
|
||||
expectedAudited bool
|
||||
}{
|
||||
{name: "Create", action: coretypes.VerbCreate, expected: "signoz.audit.create", expectedAudited: true},
|
||||
{name: "Detach", action: coretypes.VerbDetach, expected: "signoz.audit.detach", expectedAudited: true},
|
||||
{name: "Read_NotAudited", action: coretypes.VerbRead, expected: "", expectedAudited: false},
|
||||
{name: "List_NotAudited", action: coretypes.VerbList, expected: "", expectedAudited: false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expected, NewEventName(testCase.action).String())
|
||||
assert.Equal(t, testCase.expectedAudited, IsAudited(testCase.action))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditEventToLogRecord(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
claims authtypes.Claims
|
||||
resourceAttributes ResourceAttributes
|
||||
targetAttributes TargetAttributes
|
||||
statusCode int
|
||||
errorType string
|
||||
remoteAddr string
|
||||
proto string
|
||||
protoMajor int
|
||||
protoMinor int
|
||||
expectedAttributes map[string]any
|
||||
absentAttributes []string
|
||||
}{
|
||||
{
|
||||
name: "Success_User_HTTP11",
|
||||
claims: authtypes.Claims{UserID: "019a1234-abcd-7000-8000-567800000001", Principal: authtypes.PrincipalUser, Email: "alice@acme.com", OrgID: "019a1234-abcd-7000-8000-567800000002"},
|
||||
resourceAttributes: NewResourceAttributes(testDashboardResource, "d-001"),
|
||||
statusCode: http.StatusOK,
|
||||
remoteAddr: "10.0.0.1:52341",
|
||||
proto: "HTTP/1.1",
|
||||
protoMajor: 1,
|
||||
protoMinor: 1,
|
||||
expectedAttributes: map[string]any{
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.principal.id": "019a1234-abcd-7000-8000-567800000001",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.organization.id": "019a1234-abcd-7000-8000-567800000002",
|
||||
"network.protocol.version": "1.1",
|
||||
"client.address": "10.0.0.1",
|
||||
"client.port": int64(52341),
|
||||
"url.path": "/api/v1/resource",
|
||||
},
|
||||
absentAttributes: []string{"exception.type", "signoz.workspace.key.id", "signoz.resource.target.kind", "signoz.audit.identn_provider"},
|
||||
},
|
||||
{
|
||||
name: "ServiceAccount_HTTP2",
|
||||
claims: authtypes.Claims{UserID: "00000000-0000-0000-0000-000000000000", ServiceAccountID: "019a1234-abcd-7000-8000-567800000003", Principal: authtypes.PrincipalServiceAccount, OrgID: "019a1234-abcd-7000-8000-567800000002"},
|
||||
resourceAttributes: NewResourceAttributes(testDashboardResource, "d-002"),
|
||||
statusCode: http.StatusOK,
|
||||
remoteAddr: "10.0.0.3:443",
|
||||
proto: "HTTP/2.0",
|
||||
protoMajor: 2,
|
||||
protoMinor: 0,
|
||||
expectedAttributes: map[string]any{
|
||||
"signoz.principal.id": "019a1234-abcd-7000-8000-567800000003",
|
||||
"signoz.principal.type": "service_account",
|
||||
"network.protocol.version": "2",
|
||||
},
|
||||
absentAttributes: []string{"signoz.principal.email"},
|
||||
},
|
||||
{
|
||||
name: "Failure_Anonymous_IngestionKey_AddressWithoutPort",
|
||||
claims: authtypes.Claims{OrgID: "019a1234-abcd-7000-8000-567800000002"},
|
||||
resourceAttributes: NewResourceAttributes(coretypes.ResourceMetaResourceIngestionKey, "key-001"),
|
||||
statusCode: http.StatusForbidden,
|
||||
errorType: "forbidden",
|
||||
remoteAddr: "10.0.0.2",
|
||||
proto: "HTTP/1.1",
|
||||
protoMajor: 1,
|
||||
protoMinor: 1,
|
||||
expectedAttributes: map[string]any{
|
||||
"signoz.audit.outcome": "failure",
|
||||
"exception.type": "forbidden",
|
||||
"client.address": "10.0.0.2",
|
||||
},
|
||||
absentAttributes: []string{"client.port", "signoz.principal.id", "signoz.principal.type", "signoz.principal.email", "signoz.workspace.key.id"},
|
||||
},
|
||||
{
|
||||
name: "Attach_IngestionLimitToKey",
|
||||
claims: authtypes.Claims{UserID: "019a1234-abcd-7000-8000-567800000001", Principal: authtypes.PrincipalUser, OrgID: "019a1234-abcd-7000-8000-567800000002"},
|
||||
resourceAttributes: NewResourceAttributes(coretypes.ResourceMetaResourceIngestionLimit, "limit-001"),
|
||||
targetAttributes: NewTargetAttributes(coretypes.ResourceMetaResourceIngestionKey, "key-002"),
|
||||
statusCode: http.StatusOK,
|
||||
remoteAddr: "10.0.0.4:1234",
|
||||
proto: "HTTP/1.1",
|
||||
protoMajor: 1,
|
||||
protoMinor: 1,
|
||||
expectedAttributes: map[string]any{
|
||||
"signoz.resource.target.kind": "ingestion-key",
|
||||
"signoz.resource.target.id": "key-002",
|
||||
},
|
||||
absentAttributes: []string{"signoz.resource.kind", "signoz.resource.id", "signoz.workspace.key.id"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/resource", nil)
|
||||
req.RemoteAddr = testCase.remoteAddr
|
||||
req.Proto, req.ProtoMajor, req.ProtoMinor = testCase.proto, testCase.protoMajor, testCase.protoMinor
|
||||
|
||||
event := NewAuditEventFromHTTPRequest(req, "/api/v1/resource", testCase.statusCode, oteltrace.TraceID{}, oteltrace.SpanID{}, coretypes.VerbCreate, coretypes.ActionCategoryConfigurationChange, testCase.claims, testCase.resourceAttributes, testCase.targetAttributes, testCase.errorType)
|
||||
|
||||
record := plog.NewLogRecord()
|
||||
event.ToLogRecord(record)
|
||||
attrs := record.Attributes().AsRaw()
|
||||
|
||||
for key, value := range testCase.expectedAttributes {
|
||||
assert.Equal(t, value, attrs[key], key)
|
||||
}
|
||||
|
||||
for _, key := range testCase.absentAttributes {
|
||||
assert.NotContains(t, attrs, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPLogsFromAuditEvents_TargetPerRecord(t *testing.T) {
|
||||
first := NewAuditEventFromHTTPRequest(httptest.NewRequest(http.MethodPost, "/", nil), "/", http.StatusOK, oteltrace.TraceID{}, oteltrace.SpanID{}, coretypes.VerbAttach, coretypes.ActionCategoryConfigurationChange, authtypes.Claims{}, NewResourceAttributes(testDashboardResource, "d-001"), NewTargetAttributes(coretypes.ResourceMetaResourceRule, "r-001"), "")
|
||||
second := NewAuditEventFromHTTPRequest(httptest.NewRequest(http.MethodPost, "/", nil), "/", http.StatusOK, oteltrace.TraceID{}, oteltrace.SpanID{}, coretypes.VerbAttach, coretypes.ActionCategoryConfigurationChange, authtypes.Claims{}, NewResourceAttributes(testDashboardResource, "d-001"), NewTargetAttributes(coretypes.ResourceMetaResourceRule, "r-002"), "")
|
||||
|
||||
logs := NewPLogsFromAuditEvents([]AuditEvent{first, second}, "signoz", "0.90.0", "signoz.audit")
|
||||
require.Equal(t, 1, logs.ResourceLogs().Len())
|
||||
|
||||
resourceLogs := logs.ResourceLogs().At(0)
|
||||
assert.NotContains(t, resourceLogs.Resource().Attributes().AsRaw(), "signoz.resource.target.id")
|
||||
|
||||
records := resourceLogs.ScopeLogs().At(0).LogRecords()
|
||||
require.Equal(t, 2, records.Len())
|
||||
|
||||
targetIDs := []any{}
|
||||
for i := 0; i < records.Len(); i++ {
|
||||
targetIDs = append(targetIDs, records.At(i).Attributes().AsRaw()["signoz.resource.target.id"])
|
||||
}
|
||||
|
||||
assert.Equal(t, []any{"r-001", "r-002"}, targetIDs)
|
||||
}
|
||||
|
||||
func TestNewPLogsFromAuditEvents_Attributes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
route string
|
||||
statusCode int
|
||||
action coretypes.Verb
|
||||
category coretypes.ActionCategory
|
||||
claims authtypes.Claims
|
||||
resourceAttributes ResourceAttributes
|
||||
targetAttributes TargetAttributes
|
||||
errorType string
|
||||
expectedEventName string
|
||||
expectedResourceAttributes map[string]any
|
||||
expectedRecordAttributes map[string]any
|
||||
}{
|
||||
{
|
||||
name: "Success_DashboardCreated",
|
||||
method: http.MethodPost,
|
||||
path: "/api/v1/dashboards",
|
||||
route: "/api/v1/dashboards",
|
||||
statusCode: http.StatusCreated,
|
||||
action: coretypes.VerbCreate,
|
||||
category: coretypes.ActionCategoryConfigurationChange,
|
||||
claims: authtypes.Claims{UserID: "019a1234-abcd-7000-8000-567800000001", Email: "alice@acme.com", OrgID: "019a0000-0000-7000-8000-000000000001", Principal: authtypes.PrincipalUser},
|
||||
resourceAttributes: NewResourceAttributes(testDashboardResource, "019b5678-0000-7000-8000-000000009012"),
|
||||
expectedEventName: "signoz.audit.create",
|
||||
expectedResourceAttributes: map[string]any{
|
||||
"service.name": "signoz",
|
||||
"service.version": "0.90.0",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "019b5678-0000-7000-8000-000000009012",
|
||||
"signoz.resource.object": testDashboardResource.Object(valuer.MustNewUUID("019a0000-0000-7000-8000-000000000001"), "019b5678-0000-7000-8000-000000009012"),
|
||||
},
|
||||
expectedRecordAttributes: map[string]any{
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.principal.id": "019a1234-abcd-7000-8000-567800000001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.organization.id": "019a0000-0000-7000-8000-000000000001",
|
||||
"network.protocol.name": "http",
|
||||
"network.protocol.version": "1.1",
|
||||
"url.scheme": "http",
|
||||
"url.path": "/api/v1/dashboards",
|
||||
"http.request.method": "POST",
|
||||
"http.route": "/api/v1/dashboards",
|
||||
"http.response.status_code": int64(201),
|
||||
"client.address": "192.0.2.1",
|
||||
"client.port": int64(1234),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Failure_ForbiddenRoleAttachedToUser",
|
||||
method: http.MethodPost,
|
||||
path: "/api/v1/users/019c1111-0000-7000-8000-000000000020/roles",
|
||||
route: "/api/v1/users/{id}/roles",
|
||||
statusCode: http.StatusForbidden,
|
||||
action: coretypes.VerbAttach,
|
||||
category: coretypes.ActionCategoryAccessControl,
|
||||
claims: authtypes.Claims{ServiceAccountID: "019aaaaa-bbbb-7000-8000-cccc00000002", OrgID: "019a0000-0000-7000-8000-000000000002", Principal: authtypes.PrincipalServiceAccount},
|
||||
resourceAttributes: NewResourceAttributes(coretypes.ResourceUser, "019c1111-0000-7000-8000-000000000020"),
|
||||
targetAttributes: NewTargetAttributes(coretypes.ResourceRole, "019d2222-0000-7000-8000-000000000030"),
|
||||
errorType: "forbidden",
|
||||
expectedEventName: "signoz.audit.attach",
|
||||
expectedResourceAttributes: map[string]any{
|
||||
"service.name": "signoz",
|
||||
"service.version": "0.90.0",
|
||||
"signoz.resource.kind": "user",
|
||||
"signoz.resource.id": "019c1111-0000-7000-8000-000000000020",
|
||||
"signoz.resource.object": coretypes.ResourceUser.Object(valuer.MustNewUUID("019a0000-0000-7000-8000-000000000002"), "019c1111-0000-7000-8000-000000000020"),
|
||||
},
|
||||
expectedRecordAttributes: map[string]any{
|
||||
"signoz.audit.verb": "attach",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "failure",
|
||||
"signoz.principal.id": "019aaaaa-bbbb-7000-8000-cccc00000002",
|
||||
"signoz.principal.type": "service_account",
|
||||
"signoz.organization.id": "019a0000-0000-7000-8000-000000000002",
|
||||
"signoz.resource.target.kind": "role",
|
||||
"signoz.resource.target.id": "019d2222-0000-7000-8000-000000000030",
|
||||
"signoz.resource.target.object": coretypes.ResourceRole.Object(valuer.MustNewUUID("019a0000-0000-7000-8000-000000000002"), "019d2222-0000-7000-8000-000000000030"),
|
||||
"exception.type": "forbidden",
|
||||
"network.protocol.name": "http",
|
||||
"network.protocol.version": "1.1",
|
||||
"url.scheme": "http",
|
||||
"url.path": "/api/v1/users/019c1111-0000-7000-8000-000000000020/roles",
|
||||
"http.request.method": "POST",
|
||||
"http.route": "/api/v1/users/{id}/roles",
|
||||
"http.response.status_code": int64(403),
|
||||
"client.address": "192.0.2.1",
|
||||
"client.port": int64(1234),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
event := NewAuditEventFromHTTPRequest(httptest.NewRequest(testCase.method, testCase.path, nil), testCase.route, testCase.statusCode, oteltrace.TraceID{}, oteltrace.SpanID{}, testCase.action, testCase.category, testCase.claims, testCase.resourceAttributes, testCase.targetAttributes, testCase.errorType)
|
||||
|
||||
logs := NewPLogsFromAuditEvents([]AuditEvent{event}, "signoz", "0.90.0", "signoz.audit")
|
||||
require.Equal(t, 1, logs.ResourceLogs().Len())
|
||||
|
||||
resourceLogs := logs.ResourceLogs().At(0)
|
||||
assert.Equal(t, testCase.expectedResourceAttributes, resourceLogs.Resource().Attributes().AsRaw())
|
||||
|
||||
require.Equal(t, 1, resourceLogs.ScopeLogs().Len())
|
||||
require.Equal(t, 1, resourceLogs.ScopeLogs().At(0).LogRecords().Len())
|
||||
|
||||
record := resourceLogs.ScopeLogs().At(0).LogRecords().At(0)
|
||||
assert.Equal(t, testCase.expectedEventName, record.EventName())
|
||||
assert.Equal(t, testCase.expectedRecordAttributes, record.Attributes().AsRaw())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package audittypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz-semantic-conventions/gen/go/sigsemconv"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"go.opentelemetry.io/collector/pdata/plog"
|
||||
)
|
||||
@@ -13,8 +14,8 @@ type Outcome struct {
|
||||
}
|
||||
|
||||
var (
|
||||
OutcomeSuccess = Outcome{valuer.NewString("success"), plog.SeverityNumberInfo, "INFO"}
|
||||
OutcomeFailure = Outcome{valuer.NewString("failure"), plog.SeverityNumberError, "ERROR"}
|
||||
OutcomeSuccess = Outcome{valuer.NewString(sigsemconv.SigNozAuditOutcomeSuccess.Value.AsString()), plog.SeverityNumberInfo, "INFO"}
|
||||
OutcomeFailure = Outcome{valuer.NewString(sigsemconv.SigNozAuditOutcomeFailure.Value.AsString()), plog.SeverityNumberError, "ERROR"}
|
||||
)
|
||||
|
||||
func (Outcome) Enum() []any {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package coretypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
import (
|
||||
"github.com/SigNoz/signoz-semantic-conventions/gen/go/sigsemconv"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var (
|
||||
ActionCategoryAccessControl = ActionCategory{valuer.NewString("access_control")}
|
||||
ActionCategoryConfigurationChange = ActionCategory{valuer.NewString("configuration_change")}
|
||||
ActionCategoryAccessControl = ActionCategory{valuer.NewString(sigsemconv.SigNozAuditActionCategoryAccessControl.Value.AsString())}
|
||||
ActionCategoryConfigurationChange = ActionCategory{valuer.NewString(sigsemconv.SigNozAuditActionCategoryConfigurationChanges.Value.AsString())}
|
||||
ActionCategoryDataAccess = ActionCategory{valuer.NewString("data_access")}
|
||||
ActionCategorySystemEvent = ActionCategory{valuer.NewString("system_event")}
|
||||
)
|
||||
|
||||
@@ -30,55 +30,55 @@ def test_audit_list_all(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "alert-rule",
|
||||
"signoz.audit.resource.id": "alert-001",
|
||||
"signoz.resource.kind": "alert-rule",
|
||||
"signoz.resource.id": "alert-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "create",
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) created alert-rule (alert-001)",
|
||||
event_name="alert-rule.created",
|
||||
event_name="signoz.audit.create",
|
||||
severity_text="INFO",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "saved-view",
|
||||
"signoz.audit.resource.id": "view-001",
|
||||
"signoz.resource.kind": "saved-view",
|
||||
"signoz.resource.id": "view-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) updated saved-view (view-001)",
|
||||
event_name="saved-view.updated",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="INFO",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "user",
|
||||
"signoz.audit.resource.id": "user-020",
|
||||
"signoz.resource.kind": "user",
|
||||
"signoz.resource.id": "user-020",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) updated user (user-020)",
|
||||
event_name="user.role.changed",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="INFO",
|
||||
),
|
||||
]
|
||||
@@ -109,9 +109,12 @@ def test_audit_list_all(
|
||||
assert len(rows) == 3
|
||||
|
||||
# Most recent first
|
||||
assert rows[0]["data"]["event_name"] == "user.role.changed"
|
||||
assert rows[1]["data"]["event_name"] == "saved-view.updated"
|
||||
assert rows[2]["data"]["event_name"] == "alert-rule.created"
|
||||
assert [row["data"]["event_name"] for row in rows] == ["signoz.audit.update", "signoz.audit.update", "signoz.audit.create"]
|
||||
assert [row["data"]["body"] for row in rows] == [
|
||||
"ops@acme.com (user-010) updated user (user-020)",
|
||||
"ops@acme.com (user-010) updated saved-view (view-001)",
|
||||
"ops@acme.com (user-010) created alert-rule (alert-001)",
|
||||
]
|
||||
|
||||
# Verify event_name and body are present
|
||||
assert rows[0]["data"]["body"] == "ops@acme.com (user-010) updated user (user-020)"
|
||||
@@ -122,33 +125,33 @@ def test_audit_list_all(
|
||||
"filter_expression,expected_count,expected_event_names",
|
||||
[
|
||||
pytest.param(
|
||||
"signoz.audit.principal.id = 'user-001'",
|
||||
"signoz.principal.id = 'user-001'",
|
||||
3,
|
||||
{"session.login", "dashboard.updated", "dashboard.created"},
|
||||
{"signoz.audit.update", "signoz.audit.create"},
|
||||
id="filter_by_principal_id",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.outcome = 'failure'",
|
||||
1,
|
||||
{"dashboard.deleted"},
|
||||
{"signoz.audit.delete"},
|
||||
id="filter_by_outcome_failure",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.resource.kind = 'dashboard' AND signoz.audit.resource.id = 'dash-001'",
|
||||
"signoz.resource.kind = 'dashboard' AND signoz.resource.id = 'dash-001'",
|
||||
3,
|
||||
{"dashboard.deleted", "dashboard.updated", "dashboard.created"},
|
||||
{"signoz.audit.delete", "signoz.audit.update", "signoz.audit.create"},
|
||||
id="filter_by_resource_kind_and_id",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.principal.type = 'service_account'",
|
||||
"signoz.principal.type = 'service_account'",
|
||||
1,
|
||||
{"serviceaccount.apikey.created"},
|
||||
{"signoz.audit.create"},
|
||||
id="filter_by_principal_type",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.resource.kind = 'dashboard' AND signoz.audit.action = 'delete'",
|
||||
"signoz.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
|
||||
1,
|
||||
{"dashboard.deleted"},
|
||||
{"signoz.audit.delete"},
|
||||
id="filter_by_resource_kind_and_action",
|
||||
),
|
||||
],
|
||||
@@ -170,94 +173,92 @@ def test_audit_filter(
|
||||
timestamp=now - timedelta(seconds=5),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-001",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "create",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="alice@acme.com created dashboard",
|
||||
event_name="dashboard.created",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=4),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-001",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="alice@acme.com updated dashboard",
|
||||
event_name="dashboard.updated",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-001",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-002",
|
||||
"signoz.audit.principal.email": "viewer@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "delete",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"signoz.principal.id": "user-002",
|
||||
"signoz.principal.email": "viewer@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "failure",
|
||||
"signoz.audit.error.type": "forbidden",
|
||||
"signoz.audit.error.code": "authz_forbidden",
|
||||
"exception.type": "forbidden",
|
||||
},
|
||||
body="viewer@acme.com failed to delete dashboard",
|
||||
event_name="dashboard.deleted",
|
||||
event_name="signoz.audit.delete",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "serviceaccount",
|
||||
"signoz.audit.resource.id": "sa-001",
|
||||
"signoz.resource.kind": "serviceaccount",
|
||||
"signoz.resource.id": "sa-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "sa-001",
|
||||
"signoz.audit.principal.email": "",
|
||||
"signoz.audit.principal.type": "service_account",
|
||||
"signoz.audit.action": "create",
|
||||
"signoz.principal.id": "sa-001",
|
||||
"signoz.principal.type": "service_account",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="sa-001 created serviceaccount",
|
||||
event_name="serviceaccount.apikey.created",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "session",
|
||||
"signoz.audit.resource.id": "*",
|
||||
"signoz.resource.kind": "session",
|
||||
"signoz.resource.id": "*",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "login",
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="alice@acme.com login session",
|
||||
event_name="session.login",
|
||||
body="alice@acme.com created session",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -304,51 +305,51 @@ def test_audit_scalar_count_failures(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-100",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-100",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-050",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "delete",
|
||||
"signoz.principal.id": "user-050",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.outcome": "failure",
|
||||
},
|
||||
body="user-050 failed to delete dashboard",
|
||||
event_name="dashboard.deleted",
|
||||
event_name="signoz.audit.delete",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "alert-rule",
|
||||
"signoz.audit.resource.id": "alert-200",
|
||||
"signoz.resource.kind": "alert-rule",
|
||||
"signoz.resource.id": "alert-200",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-060",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.principal.id": "user-060",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "failure",
|
||||
},
|
||||
body="user-060 failed to update alert-rule",
|
||||
event_name="alert-rule.updated",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-100",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-100",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-050",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.principal.id": "user-050",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-050 updated dashboard",
|
||||
event_name="dashboard.updated",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -394,17 +395,17 @@ def test_audit_does_not_leak_into_logs(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "organization",
|
||||
"signoz.audit.resource.id": "org-999",
|
||||
"signoz.resource.kind": "organization",
|
||||
"signoz.resource.id": "org-999",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-admin",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.principal.id": "user-admin",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-admin updated organization (org-999)",
|
||||
event_name="organization.updated",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -430,5 +431,5 @@ def test_audit_does_not_leak_into_logs(
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
|
||||
|
||||
audit_bodies = [row["data"]["body"] for row in rows if "signoz.audit" in row["data"].get("attributes_string", {}).get("signoz.audit.action", "")]
|
||||
audit_bodies = [row["data"]["body"] for row in rows if "signoz.audit" in row["data"].get("attributes_string", {}).get("signoz.audit.verb", "")]
|
||||
assert len(audit_bodies) == 0
|
||||
|
||||
@@ -32,9 +32,9 @@ def test_audit_heatmap(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=index + 1),
|
||||
resources={"service.name": "signoz", "signoz.audit.resource.kind": "dashboard"},
|
||||
attributes={"signoz.audit.action": action, "signoz.audit.fields_changed": fields_changed},
|
||||
event_name=f"dashboard.{action}",
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "dashboard"},
|
||||
attributes={"signoz.audit.verb": action, "signoz.audit.fields_changed": fields_changed},
|
||||
event_name=f"signoz.audit.{action}",
|
||||
)
|
||||
for action, values in changed_by_action.items()
|
||||
for index, fields_changed in enumerate(values)
|
||||
@@ -43,9 +43,9 @@ def test_audit_heatmap(
|
||||
+ [
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=5),
|
||||
resources={"service.name": "signoz", "signoz.audit.resource.kind": "dashboard"},
|
||||
attributes={"signoz.audit.action": "update"},
|
||||
event_name="dashboard.update",
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "dashboard"},
|
||||
attributes={"signoz.audit.verb": "update"},
|
||||
event_name="signoz.audit.update",
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -59,8 +59,8 @@ def test_audit_heatmap(
|
||||
"logs",
|
||||
"signoz.audit.fields_changed",
|
||||
source="audit",
|
||||
group_by=[build_group_by_field("signoz.audit.action", "string", "attribute")],
|
||||
filter_expression="signoz.audit.resource.kind = 'dashboard'",
|
||||
group_by=[build_group_by_field("signoz.audit.verb", "string", "attribute")],
|
||||
filter_expression="signoz.resource.kind = 'dashboard'",
|
||||
bucket_options=build_linear_bucket_options(20, 4),
|
||||
)
|
||||
]
|
||||
@@ -75,7 +75,7 @@ def test_audit_heatmap(
|
||||
buckets = get_buckets(data, "A")
|
||||
assert buckets == pytest.approx([0.0, 5.0, 10.0])
|
||||
|
||||
series_by_action = index_series_by_label(get_all_series(data, "A"), "signoz.audit.action")
|
||||
series_by_action = index_series_by_label(get_all_series(data, "A"), "signoz.audit.verb")
|
||||
totals_by_action = {action: [sum(column["values"][slot] for column in series["values"]) for slot in range(len(buckets) + 1)] for action, series in series_by_action.items()}
|
||||
assert totals_by_action == {
|
||||
"update": [0, 1, 1, 0],
|
||||
|
||||
Reference in New Issue
Block a user