Compare commits

...

4 Commits

Author SHA1 Message Date
Vikrant Gupta
a4c6394542 feat(sqlstore): add support for transaction modes (#10781)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
2026-03-31 13:40:06 +00:00
Pandey
71a13b4818 feat(audit): enterprise auditor with licensing gate and OTLP HTTP export (#10776)
* feat(audit): add enterprise auditor with licensing gate and OTLP HTTP export

Implements the enterprise auditor at ee/auditor/otlphttpauditor/.
Composes auditorserver.Server for batching with licensing gate,
OTel SDK LoggerProvider for InstrumentationScope, and otlploghttp
exporter for OTLP HTTP transport.

* fix(audit): address PR review — inline factory, move body+logrecord to audittypes

- Inline NewFactory closure, remove newProviderFunc
- Move buildBody to audittypes.BuildBody
- Move eventToLogRecord to audittypes.ToLogRecord
- go mod tidy for newly direct otel/log deps

* fix(audit): address PR review round 2

- Make ToLogRecord a method on AuditEvent returning sdklog.Record
- Fold buildBody into ToLogRecord as unexported helper
- Remove accumulatingProcessor and LoggerProvider — export directly
  via otlploghttp.Exporter
- Delete body.go and processor.go

* fix(audit): address PR review round 3

- Merge export.go into provider.go
- Add severity/severityText fields to Outcome struct
- Rename buildBody to setBody method on AuditEvent
- Add appendStringIfNotEmpty helper to reduce duplication

* feat(audit): switch to plog + direct HTTP POST for OTLP export

Replace otlploghttp.Exporter + sdklog.Record with plog data model,
ProtoMarshaler, and direct HTTP POST. This properly sets
InstrumentationScope.Name = "signoz.audit" and Resource attributes
on the OTLP payload.

* fix(audit): adopt collector otlphttpexporter pattern for HTTP export

Model the send function after the OTel Collector's otlphttpexporter:
- Bounded response body reads (64KB max)
- Protobuf-encoded Status parsing from error responses
- Proper response body draining on defer
- Detailed error messages with endpoint URL and status details

* refactor(audit): split export logic into export.go, add throttle retry

- Move export, send, and HTTP response helpers to export.go
- Add exporterhelper.NewThrottleRetry for 429/503 with Retry-After
- Parse Retry-After as delay-seconds or HTTP-date per RFC 7231
- Keep provider.go focused on Auditor interface and lifecycle

* feat(audit): add partial success handler and internal retry with backoff

- Parse ExportLogsServiceResponse on 2xx for partial success, log
  warning if log records were rejected
- Internal retry loop with exponential backoff for retryable status
  codes (429, 502, 503, 504) using RetryConfig from auditor config
- Honour Retry-After header (delay-seconds and HTTP-date)
- Store full auditor.Config on provider struct
- Replace exporterhelper.NewThrottleRetry with local retryableError
  type consumed by the internal retry loop

* fix(audit): fix lint — use pkg/errors, remove stdlib errors and fmt.Errorf

* refactor(audit): use provider as receiver name instead of p

* refactor(audit): clean up enterprise auditor implementation

- Extract retry logic into retry.go
- Move NewPLogsFromAuditEvents and ToLogRecord into event.go
- Add ErrCodeAuditExportFailed to auditor package
- Add version.Build to provider for service.version attribute
- Simplify sendOnce, split response handling into onSuccess/onErr
- Use PrincipalOrgID as valuer.UUID directly
- Use OTLPHTTP.Endpoint as URL type
- Remove gzip compression handling
- Delete logrecord.go

* refactor(audit): use pkg/http/client instead of bare http.Client

Use the standard SigNoz HTTP client with OTel instrumentation.
Disable heimdall retries (count=0) since we have our own
OTLP-aware retry loop that understands Retry-After headers.

* refactor(audit): use heimdall Retriable for retry instead of manual loop

- Implement retrier with exponential backoff from auditor RetryConfig
- Compute retry count from MaxElapsedTime and backoff intervals
- Pass retrier and retry count to pkg/http/client via WithRetriable
- Remove manual retry loop, retryableError type, and Retry-After parsing
- Heimdall handles retries on >= 500 status codes automatically

* refactor(audit): rename retry.go to retrier.go
2026-03-31 13:37:20 +00:00
Vinicius Lourenço
a8e2155bb6 fix(app-routes-redirect): redirects when workspaceBlocked & onboarding having wrong redirects (#10738)
* fix(private): issues with redirect when onboarding & workspace locked

* test(app-routes): add tests for private route
2026-03-31 11:07:42 +00:00
Vinicius Lourenço
a9cbf9a4df fix(infra-monitoring): request loop when click to visualize volume (#10657)
* fix(infra-monitoring): request loop when click to visualize volume

* test(k8s-volume-list): fix tests broken due to nuqs
2026-03-31 10:56:09 +00:00
18 changed files with 2483 additions and 114 deletions

View File

@@ -85,10 +85,12 @@ sqlstore:
sqlite:
# The path to the SQLite database file.
path: /var/lib/signoz/signoz.db
# Mode is the mode to use for the sqlite database.
# The journal mode for the sqlite database. Supported values: delete, wal.
mode: delete
# BusyTimeout is the timeout for the sqlite database to wait for a lock.
# The timeout for the sqlite database to wait for a lock.
busy_timeout: 10s
# The default transaction locking behavior. Supported values: deferred, immediate, exclusive.
transaction_mode: deferred
##################### APIServer #####################
apiserver:

View File

@@ -0,0 +1,143 @@
package otlphttpauditor
import (
"bytes"
"context"
"io"
"log/slog"
"net/http"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/audittypes"
collogspb "go.opentelemetry.io/proto/otlp/collector/logs/v1"
"google.golang.org/protobuf/proto"
spb "google.golang.org/genproto/googleapis/rpc/status"
)
const (
maxHTTPResponseReadBytes int64 = 64 * 1024
protobufContentType string = "application/x-protobuf"
)
func (provider *provider) export(ctx context.Context, events []audittypes.AuditEvent) error {
logs := audittypes.NewPLogsFromAuditEvents(events, "signoz", provider.build.Version(), "signoz.audit")
request, err := provider.marshaler.MarshalLogs(logs)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "failed to marshal audit logs")
}
if err := provider.send(ctx, request); err != nil {
provider.settings.Logger().ErrorContext(ctx, "audit export failed", errors.Attr(err), slog.Int("dropped_log_records", len(events)))
return err
}
return nil
}
// Posts a protobuf-encoded OTLP request to the configured endpoint.
// Retries are handled by the underlying heimdall HTTP client.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/otlphttpexporter/otlp.go
func (provider *provider) send(ctx context.Context, body []byte) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, provider.config.OTLPHTTP.Endpoint.String(), bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", protobufContentType)
res, err := provider.httpClient.Do(req)
if err != nil {
return err
}
defer func() {
_, _ = io.CopyN(io.Discard, res.Body, maxHTTPResponseReadBytes)
res.Body.Close()
}()
if res.StatusCode >= 200 && res.StatusCode <= 299 {
provider.onSuccess(ctx, res)
return nil
}
return provider.onErr(res)
}
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L403.
func (provider *provider) onSuccess(ctx context.Context, res *http.Response) {
resBytes, err := readResponseBody(res)
if err != nil || resBytes == nil {
return
}
exportResponse := &collogspb.ExportLogsServiceResponse{}
if err := proto.Unmarshal(resBytes, exportResponse); err != nil {
return
}
ps := exportResponse.GetPartialSuccess()
if ps == nil {
return
}
if ps.GetErrorMessage() != "" || ps.GetRejectedLogRecords() != 0 {
provider.settings.Logger().WarnContext(ctx, "partial success response", slog.String("message", ps.GetErrorMessage()), slog.Int64("dropped_log_records", ps.GetRejectedLogRecords()))
}
}
func (provider *provider) onErr(res *http.Response) error {
status := readResponseStatus(res)
if status != nil {
return errors.Newf(errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "request to %s responded with status code %d, Message=%s, Details=%v", provider.config.OTLPHTTP.Endpoint.String(), res.StatusCode, status.Message, status.Details)
}
return errors.Newf(errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "request to %s responded with status code %d", provider.config.OTLPHTTP.Endpoint.String(), res.StatusCode)
}
// Reads at most maxHTTPResponseReadBytes from the response body.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L275.
func readResponseBody(resp *http.Response) ([]byte, error) {
if resp.ContentLength == 0 {
return nil, nil
}
maxRead := resp.ContentLength
if maxRead == -1 || maxRead > maxHTTPResponseReadBytes {
maxRead = maxHTTPResponseReadBytes
}
protoBytes := make([]byte, maxRead)
n, err := io.ReadFull(resp.Body, protoBytes)
if n == 0 && (err == nil || errors.Is(err, io.EOF)) {
return nil, nil
}
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
return nil, err
}
return protoBytes[:n], nil
}
// Decodes a protobuf-encoded Status from 4xx/5xx response bodies. Returns nil if the response is empty or cannot be decoded.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L310.
func readResponseStatus(resp *http.Response) *spb.Status {
if resp.StatusCode < 400 || resp.StatusCode > 599 {
return nil
}
respBytes, err := readResponseBody(resp)
if err != nil || respBytes == nil {
return nil
}
respStatus := &spb.Status{}
if err := proto.Unmarshal(respBytes, respStatus); err != nil {
return nil
}
return respStatus
}

View File

@@ -0,0 +1,97 @@
package otlphttpauditor
import (
"context"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/auditorserver"
"github.com/SigNoz/signoz/pkg/factory"
client "github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/version"
"go.opentelemetry.io/collector/pdata/plog"
)
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
httpClient *client.Client
}
func NewFactory(licensing licensing.Licensing, build version.Build) factory.ProviderFactory[auditor.Auditor, auditor.Config] {
return factory.NewProviderFactory(factory.MustNewName("otlphttp"), 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/otlphttpauditor")
httpClient, err := client.New(
settings.Logger(),
providerSettings.TracerProvider,
providerSettings.MeterProvider,
client.WithTimeout(config.OTLPHTTP.Timeout),
client.WithRetryCount(retryCountFromConfig(config.OTLPHTTP.Retry)),
retrierOption(config.OTLPHTTP.Retry),
)
if err != nil {
return nil, err
}
provider := &provider{
settings: settings,
config: config,
licensing: licensing,
build: build,
marshaler: plog.ProtoMarshaler{},
httpClient: httpClient,
}
server, err := auditorserver.New(settings,
auditorserver.Config{
BufferSize: config.BufferSize,
BatchSize: config.BatchSize,
FlushInterval: config.FlushInterval,
},
provider.export,
)
if err != nil {
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.PrincipalOrgID.IsZero() {
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
return
}
if _, err := provider.licensing.GetActive(ctx, event.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 {
return provider.server.Stop(ctx)
}

View File

@@ -0,0 +1,52 @@
package otlphttpauditor
import (
"time"
"github.com/SigNoz/signoz/pkg/auditor"
client "github.com/SigNoz/signoz/pkg/http/client"
)
// retrier implements client.Retriable with exponential backoff
// derived from auditor.RetryConfig.
type retrier struct {
initialInterval time.Duration
maxInterval time.Duration
}
func newRetrier(cfg auditor.RetryConfig) *retrier {
return &retrier{
initialInterval: cfg.InitialInterval,
maxInterval: cfg.MaxInterval,
}
}
// NextInterval returns the backoff duration for the given retry attempt.
// Uses exponential backoff: initialInterval * 2^retry, capped at maxInterval.
func (r *retrier) NextInterval(retry int) time.Duration {
interval := r.initialInterval
for range retry {
interval *= 2
}
return min(interval, r.maxInterval)
}
func retrierOption(cfg auditor.RetryConfig) client.Option {
return client.WithRetriable(newRetrier(cfg))
}
func retryCountFromConfig(cfg auditor.RetryConfig) int {
if !cfg.Enabled || cfg.MaxElapsedTime <= 0 {
return 0
}
count := 0
elapsed := time.Duration(0)
interval := cfg.InitialInterval
for elapsed < cfg.MaxElapsedTime {
elapsed += interval
interval = min(interval*2, cfg.MaxInterval)
count++
}
return count
}

View File

@@ -101,6 +101,22 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
)?.value;
// Don't redirect to onboarding if workspace has issues (blocked, suspended, or restricted)
// User needs access to settings/billing to fix payment issues
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
const isWorkspaceAccessRestricted =
activeLicense?.state === LicenseState.TERMINATED ||
activeLicense?.state === LicenseState.EXPIRED ||
activeLicense?.state === LicenseState.CANCELLED;
const hasWorkspaceIssue =
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
if (hasWorkspaceIssue) {
return;
}
const isFirstUser = checkFirstTimeUser();
if (
isFirstUser &&
@@ -119,40 +135,36 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
orgPreferences,
usersData,
pathname,
trialInfo?.workSpaceBlock,
activeLicense?.state,
]);
const navigateToWorkSpaceBlocked = (route: any): void => {
const { path } = route;
const navigateToWorkSpaceBlocked = useCallback((): void => {
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
(path === ROUTES.SETTINGS ||
path === ROUTES.ORG_SETTINGS ||
path === ROUTES.MEMBERS_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);
(pathname === ROUTES.SETTINGS ||
pathname === ROUTES.ORG_SETTINGS ||
pathname === ROUTES.MEMBERS_SETTINGS ||
pathname === ROUTES.BILLING ||
pathname === ROUTES.MY_SETTINGS);
if (
path &&
path !== ROUTES.WORKSPACE_LOCKED &&
pathname &&
pathname !== ROUTES.WORKSPACE_LOCKED &&
!isRouteEnabledForWorkspaceBlockedState
) {
history.push(ROUTES.WORKSPACE_LOCKED);
}
};
}, [isAdmin, pathname]);
const navigateToWorkSpaceAccessRestricted = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
const navigateToWorkSpaceAccessRestricted = useCallback((): void => {
if (pathname && pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
history.push(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
}
};
}, [pathname]);
useEffect(() => {
if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
const isExpired = activeLicense.state === LicenseState.EXPIRED;
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
@@ -161,61 +173,53 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const { platform } = activeLicense;
if (
isWorkspaceAccessRestricted &&
platform === LicensePlatform.CLOUD &&
currentRoute
) {
navigateToWorkSpaceAccessRestricted(currentRoute);
if (isWorkspaceAccessRestricted && platform === LicensePlatform.CLOUD) {
navigateToWorkSpaceAccessRestricted();
}
}
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
}, [
isFetchingActiveLicense,
activeLicense,
navigateToWorkSpaceAccessRestricted,
]);
useEffect(() => {
if (!isFetchingActiveLicense) {
const currentRoute = mapRoutes.get('current');
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (
shouldBlockWorkspace &&
currentRoute &&
activeLicense?.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceBlocked(currentRoute);
navigateToWorkSpaceBlocked();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isFetchingActiveLicense,
trialInfo?.workSpaceBlock,
activeLicense?.platform,
mapRoutes,
pathname,
navigateToWorkSpaceBlocked,
]);
const navigateToWorkSpaceSuspended = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_SUSPENDED) {
const navigateToWorkSpaceSuspended = useCallback((): void => {
if (pathname && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
history.push(ROUTES.WORKSPACE_SUSPENDED);
}
};
}, [pathname]);
useEffect(() => {
if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicense.state === LicenseState.DEFAULTED;
if (
shouldSuspendWorkspace &&
currentRoute &&
activeLicense.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceSuspended(currentRoute);
navigateToWorkSpaceSuspended();
}
}
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
}, [isFetchingActiveLicense, activeLicense, navigateToWorkSpaceSuspended]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,8 @@ import {
formatDataForTable,
getK8sVolumesListColumns,
getK8sVolumesListQuery,
getVolumeListGroupedByRowDataQueryKey,
getVolumesListQueryKey,
K8sVolumesRowData,
} from './utils';
import VolumeDetails from './VolumeDetails';
@@ -167,6 +169,26 @@ function K8sVolumesList({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(
() =>
getVolumeListGroupedByRowDataQueryKey(
selectedRowData?.groupedByMeta,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
),
[
selectedRowData?.groupedByMeta,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
],
);
const {
data: groupedByRowData,
isFetching: isFetchingGroupedByRowData,
@@ -176,7 +198,7 @@ function K8sVolumesList({
} = useGetK8sVolumesList(
fetchGroupedByRowDataQuery as K8sVolumesListPayload,
{
queryKey: ['volumeList', fetchGroupedByRowDataQuery],
queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
},
undefined,
@@ -221,6 +243,28 @@ function K8sVolumesList({
return queryPayload;
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
const volumesListQueryKey = useMemo(() => {
return getVolumesListQueryKey(
selectedVolumeUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
);
}, [
selectedVolumeUID,
pageSize,
currentPage,
queryFilters,
groupBy,
orderBy,
minTime,
maxTime,
]);
const formattedGroupedByVolumesData = useMemo(
() =>
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
@@ -237,7 +281,7 @@ function K8sVolumesList({
const { data, isFetching, isLoading, isError } = useGetK8sVolumesList(
query as K8sVolumesListPayload,
{
queryKey: ['volumeList', query],
queryKey: volumesListQueryKey,
enabled: !!query,
},
undefined,

View File

@@ -77,6 +77,74 @@ export const getK8sVolumesListQuery = (): K8sVolumesListPayload => ({
orderBy: { columnName: 'usage', order: 'desc' },
});
export const getVolumeListGroupedByRowDataQueryKey = (
groupedByMeta: K8sVolumesData['meta'] | undefined,
queryFilters: IBuilderQuery['filters'],
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
groupBy: IBuilderQuery['groupBy'],
minTime: number,
maxTime: number,
): (string | undefined)[] => {
// When we have grouped by metadata defined
// We need to leave out the min/max time
// Otherwise it will cause a loop
const groupedByMetaStr = JSON.stringify(groupedByMeta || undefined) ?? '';
if (groupedByMetaStr) {
return [
'volumeList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
groupedByMetaStr,
];
}
return [
'volumeList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
groupedByMetaStr,
String(minTime),
String(maxTime),
];
};
export const getVolumesListQueryKey = (
selectedVolumeUID: string | null,
pageSize: number,
currentPage: number,
queryFilters: IBuilderQuery['filters'],
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
groupBy: IBuilderQuery['groupBy'],
minTime: number,
maxTime: number,
): (string | undefined)[] => {
// When selected volume is defined
// We need to leave out the min/max time
// Otherwise it will cause a loop
if (selectedVolumeUID) {
return [
'volumeList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'volumeList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
};
const columnsConfig = [
{
title: <div className="column-header-left pvc-name-header">PVC Name</div>,

View File

@@ -1,29 +1,93 @@
import setupCommonMocks from '../commonMocks';
setupCommonMocks();
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { MemoryRouter } from 'react-router-dom';
import { render, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom-v5-compat';
import { FeatureKeys } from 'constants/features';
import K8sVolumesList from 'container/InfraMonitoringK8s/Volumes/K8sVolumesList';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { IAppContext, IUser } from 'providers/App/types';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
// eslint-disable-next-line no-restricted-imports
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import thunk from 'redux-thunk';
import reducers from 'store/reducers';
import { act, render, screen, userEvent, waitFor } from 'tests/test-utils';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { LicenseResModel } from 'types/api/licensesV3/getActive';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
});
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../../constants';
const SERVER_URL = 'http://localhost/api';
// jsdom does not implement IntersectionObserver — provide a no-op stub
const mockObserver = {
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
};
global.IntersectionObserver = jest
.fn()
.mockImplementation(() => mockObserver) as any;
const mockVolume = {
persistentVolumeClaimName: 'test-pvc',
volumeAvailable: 1000000,
volumeCapacity: 5000000,
volumeInodes: 100,
volumeInodesFree: 50,
volumeInodesUsed: 50,
volumeUsage: 4000000,
meta: {
k8s_cluster_name: 'test-cluster',
k8s_namespace_name: 'test-namespace',
k8s_node_name: 'test-node',
k8s_persistentvolumeclaim_name: 'test-pvc',
k8s_pod_name: 'test-pod',
k8s_pod_uid: 'test-pod-uid',
k8s_statefulset_name: '',
},
};
const mockVolumesResponse = {
status: 'success',
data: {
type: '',
records: [mockVolume],
groups: null,
total: 1,
sentAnyHostMetricsData: false,
isSendingK8SAgentMetrics: false,
},
};
/** Renders K8sVolumesList with a real Redux store so dispatched actions affect state. */
function renderWithRealStore(
initialEntries?: Record<string, any>,
): { testStore: ReturnType<typeof createStore> } {
const testStore = createStore(reducers, applyMiddleware(thunk as any));
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<NuqsTestingAdapter searchParams={initialEntries}>
<QueryClientProvider client={queryClient}>
<QueryBuilderProvider>
<MemoryRouter>
<K8sVolumesList
isFiltersVisible={false}
handleFilterVisibilityChange={jest.fn()}
quickFiltersLastUpdated={-1}
/>
</MemoryRouter>
</QueryBuilderProvider>
</QueryClientProvider>
</NuqsTestingAdapter>,
);
return { testStore };
}
describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
let requestsMade: Array<{
url: string;
@@ -33,7 +97,6 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
beforeEach(() => {
requestsMade = [];
queryClient.clear();
server.use(
rest.get(`${SERVER_URL}/v3/autocomplete/attribute_keys`, (req, res, ctx) => {
@@ -79,19 +142,7 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
});
it('should call aggregate keys API with k8s_volume_capacity', async () => {
render(
<NuqsTestingAdapter>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<K8sVolumesList
isFiltersVisible={false}
handleFilterVisibilityChange={jest.fn()}
quickFiltersLastUpdated={-1}
/>
</MemoryRouter>
</QueryClientProvider>
</NuqsTestingAdapter>,
);
renderWithRealStore();
await waitFor(() => {
expect(requestsMade.length).toBeGreaterThan(0);
@@ -128,19 +179,7 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
activeLicense: (null as unknown) as LicenseResModel,
} as IAppContext);
render(
<NuqsTestingAdapter>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<K8sVolumesList
isFiltersVisible={false}
handleFilterVisibilityChange={jest.fn()}
quickFiltersLastUpdated={-1}
/>
</MemoryRouter>
</QueryClientProvider>
</NuqsTestingAdapter>,
);
renderWithRealStore();
await waitFor(() => {
expect(requestsMade.length).toBeGreaterThan(0);
@@ -159,3 +198,193 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
expect(aggregateAttribute).toBe('k8s.volume.capacity');
});
});
describe('K8sVolumesList', () => {
beforeEach(() => {
server.use(
rest.post('http://localhost/api/v1/pvcs/list', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(mockVolumesResponse)),
),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
(_req, res, ctx) =>
res(ctx.status(200), ctx.json({ data: { attributeKeys: [] } })),
),
);
});
it('renders volume rows from API response', async () => {
renderWithRealStore();
await waitFor(async () => {
const elements = await screen.findAllByText('test-pvc');
expect(elements.length).toBeGreaterThan(0);
});
});
it('opens VolumeDetails when a volume row is clicked', async () => {
const user = userEvent.setup();
renderWithRealStore();
const pvcCells = await screen.findAllByText('test-pvc');
expect(pvcCells.length).toBeGreaterThan(0);
const row = pvcCells[0].closest('tr');
expect(row).not.toBeNull();
await user.click(row!);
await waitFor(async () => {
const cells = await screen.findAllByText('test-pvc');
expect(cells.length).toBeGreaterThan(1);
});
});
it('closes VolumeDetails when the close button is clicked', async () => {
const user = userEvent.setup();
renderWithRealStore();
const pvcCells = await screen.findAllByText('test-pvc');
expect(pvcCells.length).toBeGreaterThan(0);
const row = pvcCells[0].closest('tr');
await user.click(row!);
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
});
await user.click(screen.getByRole('button', { name: 'Close' }));
await waitFor(() => {
expect(
screen.queryByRole('button', { name: 'Close' }),
).not.toBeInTheDocument();
});
});
it('does not re-fetch the volumes list when time range changes after selecting a volume', async () => {
const user = userEvent.setup();
let apiCallCount = 0;
server.use(
rest.post('http://localhost/api/v1/pvcs/list', async (_req, res, ctx) => {
apiCallCount += 1;
return res(ctx.status(200), ctx.json(mockVolumesResponse));
}),
);
const { testStore } = renderWithRealStore();
await waitFor(() => expect(apiCallCount).toBe(1));
const pvcCells = await screen.findAllByText('test-pvc');
const row = pvcCells[0].closest('tr');
await user.click(row!);
await waitFor(async () => {
const cells = await screen.findAllByText('test-pvc');
expect(cells.length).toBeGreaterThan(1);
});
// Wait for nuqs URL state to fully propagate to the component
// The selectedVolumeUID is managed via nuqs (async URL state),
// so we need to ensure the state has settled before dispatching time changes
await act(async () => {
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
});
const countAfterClick = apiCallCount;
// There's a specific component causing the min/max time to be updated
// After the volume loads, it triggers the change again
// And then the query to fetch data for the selected volume enters in a loop
act(() => {
testStore.dispatch({
type: UPDATE_TIME_INTERVAL,
payload: {
minTime: Date.now() * 1000000 - 30 * 60 * 1000 * 1000000,
maxTime: Date.now() * 1000000,
selectedTime: '30m',
},
} as any);
});
// Allow any potential re-fetch to settle
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
expect(apiCallCount).toBe(countAfterClick);
});
it('does not re-fetch groupedByRowData when time range changes after expanding a volume row with groupBy', async () => {
const user = userEvent.setup();
const groupByValue = [{ key: 'k8s_namespace_name' }];
let groupedByRowDataCallCount = 0;
server.use(
rest.post('http://localhost/api/v1/pvcs/list', async (req, res, ctx) => {
const body = await req.json();
// Check for both underscore and dot notation keys since dotMetricsEnabled
// may be true or false depending on test order
const isGroupedByRowDataRequest = body.filters?.items?.some(
(item: { key?: { key?: string }; value?: string }) =>
(item.key?.key === 'k8s_namespace_name' ||
item.key?.key === 'k8s.namespace.name') &&
item.value === 'test-namespace',
);
if (isGroupedByRowDataRequest) {
groupedByRowDataCallCount += 1;
}
return res(ctx.status(200), ctx.json(mockVolumesResponse));
}),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
(_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: {
attributeKeys: [{ key: 'k8s_namespace_name', dataType: 'string' }],
},
}),
),
),
);
const { testStore } = renderWithRealStore({
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupByValue),
});
await waitFor(async () => {
const elements = await screen.findAllByText('test-namespace');
return expect(elements.length).toBeGreaterThan(0);
});
const row = (await screen.findAllByText('test-namespace'))[0].closest('tr');
expect(row).not.toBeNull();
user.click(row as HTMLElement);
await waitFor(() => expect(groupedByRowDataCallCount).toBe(1));
const countAfterExpand = groupedByRowDataCallCount;
act(() => {
testStore.dispatch({
type: UPDATE_TIME_INTERVAL,
payload: {
minTime: Date.now() * 1000000 - 30 * 60 * 1000 * 1000000,
maxTime: Date.now() * 1000000,
selectedTime: '30m',
},
} as any);
});
// Allow any potential re-fetch to settle
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
expect(groupedByRowDataCallCount).toBe(countAfterExpand);
});
});

4
go.mod
View File

@@ -372,7 +372,7 @@ require (
go.opentelemetry.io/otel/log v0.15.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
@@ -381,7 +381,7 @@ require (
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409
google.golang.org/grpc v1.78.0 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
k8s.io/client-go v0.35.0 // indirect

View File

@@ -3,10 +3,15 @@ package auditor
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/audittypes"
)
var (
ErrCodeAuditExportFailed = errors.MustNewCode("audit_export_failed")
)
type Auditor interface {
factory.ServiceWithHealthy

View File

@@ -1,6 +1,7 @@
package auditor
import (
"net/url"
"time"
"github.com/SigNoz/signoz/pkg/errors"
@@ -10,6 +11,7 @@ import (
var _ factory.Config = (*Config)(nil)
type Config struct {
// Provider specifies the audit export implementation to use.
Provider string `mapstructure:"provider"`
// BufferSize is the async channel capacity for audit events.
@@ -28,18 +30,12 @@ type Config struct {
// OTLPHTTPConfig holds configuration for the OTLP HTTP exporter provider.
// Fields map to go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp options.
type OTLPHTTPConfig struct {
// Endpoint is the target host:port (without scheme or path).
Endpoint string `mapstructure:"endpoint"`
// URLPath overrides the default URL path (/v1/logs).
URLPath string `mapstructure:"url_path"`
// Endpoint is the target scheme://host:port of the OTLP HTTP endpoint.
Endpoint *url.URL `mapstructure:"endpoint"`
// Insecure disables TLS, using HTTP instead of HTTPS.
Insecure bool `mapstructure:"insecure"`
// Compression sets the compression strategy. Supported: "none", "gzip".
Compression string `mapstructure:"compression"`
// Timeout is the maximum duration for an export attempt.
Timeout time.Duration `mapstructure:"timeout"`
@@ -71,10 +67,12 @@ func newConfig() factory.Config {
BatchSize: 100,
FlushInterval: time.Second,
OTLPHTTP: OTLPHTTPConfig{
Endpoint: "localhost:4318",
URLPath: "/v1/logs",
Compression: "none",
Timeout: 10 * time.Second,
Endpoint: &url.URL{
Scheme: "http",
Host: "localhost:4318",
Path: "/v1/logs",
},
Timeout: 10 * time.Second,
Retry: RetryConfig{
Enabled: true,
InitialInterval: 5 * time.Second,
@@ -93,14 +91,24 @@ func (c Config) Validate() error {
if c.BufferSize <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::buffer_size must be greater than 0")
}
if c.BatchSize <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::batch_size must be greater than 0")
}
if c.FlushInterval <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::flush_interval must be greater than 0")
}
if c.BatchSize > c.BufferSize {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::batch_size must not exceed auditor::buffer_size")
}
if c.Provider == "otlphttp" {
if c.OTLPHTTP.Endpoint == nil {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::otlphttp::endpoint must be set when provider is otlphttp")
}
}
return nil
}

View File

@@ -15,12 +15,16 @@ import (
type Instrumentation interface {
// Logger returns the Slog logger.
Logger() *slog.Logger
// MeterProvider returns the OpenTelemetry meter provider.
MeterProvider() sdkmetric.MeterProvider
// TracerProvider returns the OpenTelemetry tracer provider.
TracerProvider() sdktrace.TracerProvider
// PrometheusRegisterer returns the Prometheus registerer.
PrometheusRegisterer() prometheus.Registerer
// ToProviderSettings converts instrumentation to provider settings.
ToProviderSettings() factory.ProviderSettings
}

View File

@@ -26,11 +26,14 @@ type SqliteConfig struct {
// Path is the path to the sqlite database.
Path string `mapstructure:"path"`
// Mode is the mode to use for the sqlite database.
// Mode is the journal mode for the sqlite database.
Mode string `mapstructure:"mode"`
// BusyTimeout is the timeout for the sqlite database to wait for a lock.
BusyTimeout time.Duration `mapstructure:"busy_timeout"`
// TransactionMode is the default transaction locking behavior for the sqlite database.
TransactionMode string `mapstructure:"transaction_mode"`
}
type ConnectionConfig struct {
@@ -49,9 +52,10 @@ func newConfig() factory.Config {
MaxOpenConns: 100,
},
Sqlite: SqliteConfig{
Path: "/var/lib/signoz/signoz.db",
Mode: "delete",
BusyTimeout: 10000 * time.Millisecond, // increasing the defaults from https://github.com/mattn/go-sqlite3/blob/master/sqlite3.go#L1098 because of transpilation from C to GO
Path: "/var/lib/signoz/signoz.db",
Mode: "delete",
BusyTimeout: 10000 * time.Millisecond, // increasing the defaults from https://github.com/mattn/go-sqlite3/blob/master/sqlite3.go#L1098 because of transpilation from C to GO
TransactionMode: "deferred",
},
}

View File

@@ -0,0 +1,56 @@
package sqlstore
import (
"context"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/config/envprovider"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewWithEnvProvider(t *testing.T) {
t.Setenv("SIGNOZ_SQLSTORE_PROVIDER", "sqlite")
t.Setenv("SIGNOZ_SQLSTORE_SQLITE_PATH", "/tmp/test.db")
t.Setenv("SIGNOZ_SQLSTORE_SQLITE_MODE", "wal")
t.Setenv("SIGNOZ_SQLSTORE_SQLITE_BUSY__TIMEOUT", "5s")
t.Setenv("SIGNOZ_SQLSTORE_SQLITE_TRANSACTION__MODE", "immediate")
t.Setenv("SIGNOZ_SQLSTORE_MAX__OPEN__CONNS", "50")
conf, err := config.New(
context.Background(),
config.ResolverConfig{
Uris: []string{"env:"},
ProviderFactories: []config.ProviderFactory{
envprovider.NewFactory(),
},
},
[]factory.ConfigFactory{
NewConfigFactory(),
},
)
require.NoError(t, err)
actual := &Config{}
err = conf.Unmarshal("sqlstore", actual)
require.NoError(t, err)
expected := &Config{
Provider: "sqlite",
Connection: ConnectionConfig{
MaxOpenConns: 50,
},
Sqlite: SqliteConfig{
Path: "/tmp/test.db",
Mode: "wal",
BusyTimeout: 5 * time.Second,
TransactionMode: "immediate",
},
}
assert.Equal(t, expected, actual)
assert.NoError(t, actual.Validate())
}

View File

@@ -49,6 +49,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
connectionParams.Add("_pragma", fmt.Sprintf("busy_timeout(%d)", config.Sqlite.BusyTimeout.Milliseconds()))
connectionParams.Add("_pragma", fmt.Sprintf("journal_mode(%s)", config.Sqlite.Mode))
connectionParams.Add("_pragma", "foreign_keys(1)")
connectionParams.Set("_txlock", config.Sqlite.TransactionMode)
sqldb, err := sql.Open("sqlite", "file:"+config.Sqlite.Path+"?"+connectionParams.Encode())
if err != nil {
return nil, err

View File

@@ -1,8 +1,14 @@
package audittypes
import (
"context"
"encoding/hex"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/valuer"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/plog"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
)
// AuditEvent represents a single audit log event.
@@ -16,10 +22,10 @@ type AuditEvent struct {
EventName EventName `json:"eventName"`
// Audit attributes — Principal (Who)
PrincipalID string `json:"principalId"`
PrincipalEmail string `json:"principalEmail"`
PrincipalID valuer.UUID `json:"principalId"`
PrincipalEmail valuer.Email `json:"principalEmail"`
PrincipalType PrincipalType `json:"principalType"`
PrincipalOrgID string `json:"principalOrgId"`
PrincipalOrgID valuer.UUID `json:"principalOrgId"`
IdentNProvider string `json:"identnProvider,omitempty"`
// Audit attributes — Action (What)
@@ -45,7 +51,105 @@ type AuditEvent struct {
UserAgent string `json:"userAgent,omitempty"`
}
// Store is the minimal interface for emitting audit events.
type Store interface {
Emit(ctx context.Context, event AuditEvent) error
func NewPLogsFromAuditEvents(events []AuditEvent, name string, version string, scope string) plog.Logs {
logs := plog.NewLogs()
resourceLogs := logs.ResourceLogs().AppendEmpty()
resourceLogs.Resource().Attributes().PutStr(string(semconv.ServiceNameKey), name)
resourceLogs.Resource().Attributes().PutStr(string(semconv.ServiceVersionKey), version)
scopeLogs := resourceLogs.ScopeLogs().AppendEmpty()
scopeLogs.Scope().SetName(scope)
for i := range events {
events[i].ToLogRecord(scopeLogs.LogRecords().AppendEmpty())
}
return logs
}
func (event AuditEvent) ToLogRecord(dest plog.LogRecord) {
dest.SetTimestamp(pcommon.NewTimestampFromTime(event.Timestamp))
dest.SetObservedTimestamp(pcommon.NewTimestampFromTime(event.Timestamp))
dest.Body().SetStr(event.setBody())
dest.SetEventName(event.EventName.String())
dest.SetSeverityNumber(event.Outcome.Severity())
dest.SetSeverityText(event.Outcome.SeverityText())
if tid, ok := parseTraceID(event.TraceID); ok {
dest.SetTraceID(tid)
}
if sid, ok := parseSpanID(event.SpanID); ok {
dest.SetSpanID(sid)
}
attrs := dest.Attributes()
// Principal attributes
attrs.PutStr("signoz.audit.principal.id", event.PrincipalID.StringValue())
attrs.PutStr("signoz.audit.principal.email", event.PrincipalEmail.String())
attrs.PutStr("signoz.audit.principal.type", event.PrincipalType.StringValue())
attrs.PutStr("signoz.audit.principal.org_id", event.PrincipalOrgID.StringValue())
putStrIfNotEmpty(attrs, "signoz.audit.identn_provider", event.IdentNProvider)
// Action attributes
attrs.PutStr("signoz.audit.action", event.Action.StringValue())
attrs.PutStr("signoz.audit.action_category", event.ActionCategory.StringValue())
attrs.PutStr("signoz.audit.outcome", event.Outcome.StringValue())
// Resource attributes
attrs.PutStr("signoz.audit.resource.name", event.ResourceName)
putStrIfNotEmpty(attrs, "signoz.audit.resource.id", event.ResourceID)
// Error attributes (on failure)
putStrIfNotEmpty(attrs, "signoz.audit.error.type", event.ErrorType)
putStrIfNotEmpty(attrs, "signoz.audit.error.code", event.ErrorCode)
putStrIfNotEmpty(attrs, "signoz.audit.error.message", event.ErrorMessage)
// Transport context attributes
putStrIfNotEmpty(attrs, "http.request.method", event.HTTPMethod)
putStrIfNotEmpty(attrs, "http.route", event.HTTPRoute)
if event.HTTPStatusCode != 0 {
attrs.PutInt("http.response.status_code", int64(event.HTTPStatusCode))
}
putStrIfNotEmpty(attrs, "url.path", event.URLPath)
putStrIfNotEmpty(attrs, "client.address", event.ClientAddress)
putStrIfNotEmpty(attrs, "user_agent.original", event.UserAgent)
}
func (event AuditEvent) setBody() string {
if event.Outcome == OutcomeSuccess {
return fmt.Sprintf("%s (%s) %s %s %s", event.PrincipalEmail, event.PrincipalID, event.Action.PastTense(), event.ResourceName, event.ResourceID)
}
return fmt.Sprintf("%s (%s) failed to %s %s %s: %s (%s)", event.PrincipalEmail, event.PrincipalID, event.Action.StringValue(), event.ResourceName, event.ResourceID, event.ErrorType, event.ErrorCode)
}
func putStrIfNotEmpty(attrs pcommon.Map, key, value string) {
if value != "" {
attrs.PutStr(key, value)
}
}
func parseTraceID(s string) (pcommon.TraceID, bool) {
b, err := hex.DecodeString(s)
if err != nil || len(b) != 16 {
return pcommon.TraceID{}, false
}
var tid pcommon.TraceID
copy(tid[:], b)
return tid, true
}
func parseSpanID(s string) (pcommon.SpanID, bool) {
b, err := hex.DecodeString(s)
if err != nil || len(b) != 8 {
return pcommon.SpanID{}, false
}
var sid pcommon.SpanID
copy(sid[:], b)
return sid, true
}

View File

@@ -1,13 +1,20 @@
package audittypes
import "github.com/SigNoz/signoz/pkg/valuer"
import (
"github.com/SigNoz/signoz/pkg/valuer"
"go.opentelemetry.io/collector/pdata/plog"
)
// Outcome represents the result of an audited operation.
type Outcome struct{ valuer.String }
type Outcome struct {
valuer.String
severity plog.SeverityNumber
severityText string
}
var (
OutcomeSuccess = Outcome{valuer.NewString("success")}
OutcomeFailure = Outcome{valuer.NewString("failure")}
OutcomeSuccess = Outcome{valuer.NewString("success"), plog.SeverityNumberInfo, "INFO"}
OutcomeFailure = Outcome{valuer.NewString("failure"), plog.SeverityNumberError, "ERROR"}
)
func (Outcome) Enum() []any {
@@ -16,3 +23,13 @@ func (Outcome) Enum() []any {
OutcomeFailure,
}
}
// Severity returns the plog severity number for this outcome.
func (o Outcome) Severity() plog.SeverityNumber {
return o.severity
}
// SeverityText returns the severity text for this outcome.
func (o Outcome) SeverityText() string {
return o.severityText
}