Compare commits

..

4 Commits

Author SHA1 Message Date
nityanandagohain
884120ba1f fix: address comments 2026-05-06 15:45:35 +05:30
nityanandagohain
a61cbf45db Merge remote-tracking branch 'origin/main' into issue_4361_1 2026-05-06 14:41:28 +05:30
nityanandagohain
c26c875cbf fix: remove nullable from fieldcontext 2026-04-29 17:41:13 +05:30
nityanandagohain
9499228398 feat: module and store for span mapper 2026-04-28 16:08:44 +05:30
26 changed files with 640 additions and 1214 deletions

View File

@@ -4919,8 +4919,6 @@ components:
type: object
SpantypesPostableSpanMapperGroup:
properties:
category:
$ref: '#/components/schemas/SpantypesSpanMapperGroupCategory'
condition:
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
enabled:
@@ -4929,7 +4927,6 @@ components:
type: string
required:
- name
- category
- condition
type: object
SpantypesSpanMapper:
@@ -4976,8 +4973,6 @@ components:
type: object
SpantypesSpanMapperGroup:
properties:
category:
$ref: '#/components/schemas/SpantypesSpanMapperGroupCategory'
condition:
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
createdAt:
@@ -5002,13 +4997,11 @@ components:
- id
- orgId
- name
- category
- condition
- enabled
type: object
SpantypesSpanMapperGroupCategory:
type: object
SpantypesSpanMapperGroupCondition:
nullable: true
properties:
attributes:
items:
@@ -10099,12 +10092,6 @@ paths:
org.
operationId: ListSpanMapperGroups
parameters:
- explode: true
in: query
name: category
schema:
$ref: '#/components/schemas/SpantypesSpanMapperGroupCategory'
style: deepObject
- in: query
name: enabled
schema:

View File

@@ -7200,7 +7200,6 @@ export interface SpantypesPostableSpanMapperDTO {
}
export interface SpantypesPostableSpanMapperGroupDTO {
category: SpantypesSpanMapperGroupCategoryDTO;
condition: SpantypesSpanMapperGroupConditionDTO;
/**
* @type boolean
@@ -7260,7 +7259,6 @@ export interface SpantypesSpanMapperConfigDTO {
}
export interface SpantypesSpanMapperGroupDTO {
category: SpantypesSpanMapperGroupCategoryDTO;
condition: SpantypesSpanMapperGroupConditionDTO;
/**
* @type string
@@ -7298,11 +7296,10 @@ export interface SpantypesSpanMapperGroupDTO {
updatedBy?: string;
}
export interface SpantypesSpanMapperGroupCategoryDTO {
[key: string]: unknown;
}
export interface SpantypesSpanMapperGroupConditionDTO {
/**
* @nullable
*/
export type SpantypesSpanMapperGroupConditionDTO = {
/**
* @type array
* @nullable true
@@ -7313,7 +7310,7 @@ export interface SpantypesSpanMapperGroupConditionDTO {
* @nullable true
*/
resource: string[] | null;
}
} | null;
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
@@ -8887,10 +8884,6 @@ export type GetMyServiceAccount200 = {
};
export type ListSpanMapperGroupsParams = {
/**
* @description undefined
*/
category?: SpantypesSpanMapperGroupCategoryDTO;
/**
* @type boolean
* @nullable true

View File

@@ -1,158 +0,0 @@
import { act, render } from '@testing-library/react';
import { Modal } from 'antd';
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useTabVisibility from 'hooks/useTabFocus';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { useDashboardQuery } from './useDashboardQuery';
const mockDispatch = jest.fn();
const mockSetDashboardData = jest.fn();
const mockSetLayouts = jest.fn();
const mockSetPanelMap = jest.fn();
const mockResetDashboardStore = jest.fn();
const mockGetUrlVariables = jest.fn();
const mockUpdateUrlVariable = jest.fn();
const mockRefetch = jest.fn();
let mockGlobalTime = {
selectedTime: 'custom',
minTime: 1710000000000000000,
maxTime: 1710000300000000000,
isAutoRefreshDisabled: true,
};
let currentQueryData: unknown;
jest.mock('react-i18next', () => ({
useTranslation: (): { t: (key: string) => string } => ({
t: (key: string): string => key,
}),
}));
jest.mock('react-redux', () => ({
useDispatch: jest.fn(() => mockDispatch),
useSelector: jest.fn(
(
selectorFn: (state: { globalTime: typeof mockGlobalTime }) => unknown,
): unknown => selectorFn({ globalTime: mockGlobalTime }),
),
}));
jest.mock('hooks/useTabFocus', () => jest.fn(() => true));
jest.mock('hooks/dashboard/useDashboardVariablesSync', () => ({
useDashboardVariablesSync: jest.fn(),
}));
jest.mock('./useDashboardQuery', () => ({
useDashboardQuery: jest.fn(),
}));
jest.mock('hooks/dashboard/useTransformDashboardVariables', () => ({
useTransformDashboardVariables: jest.fn(),
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: jest.fn(),
}));
jest.mock('providers/Dashboard/initializeDefaultVariables', () => ({
initializeDefaultVariables: jest.fn(),
}));
jest.mock('lib/dashboard/getUpdatedLayout', () => ({
getUpdatedLayout: jest.fn(() => []),
}));
jest.mock('providers/Dashboard/util', () => ({
sortLayout: jest.fn((layout) => layout),
}));
jest.mock('lib/getMinMax', () => ({
getMinMaxForSelectedTime: jest.fn(),
}));
function TestComponent({ confirm }: { confirm: typeof Modal.confirm }): null {
useDashboardBootstrap('dashboard-1', { confirm });
return null;
}
describe('useDashboardBootstrap', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGlobalTime = {
selectedTime: 'custom',
minTime: 1710000000000000000,
maxTime: 1710000300000000000,
isAutoRefreshDisabled: true,
};
jest.mocked(useDashboardStore as unknown as jest.Mock).mockReturnValue({
setDashboardData: mockSetDashboardData,
setLayouts: mockSetLayouts,
setPanelMap: mockSetPanelMap,
resetDashboardStore: mockResetDashboardStore,
});
jest
.mocked(useTransformDashboardVariables as unknown as jest.Mock)
.mockReturnValue({
getUrlVariables: mockGetUrlVariables,
updateUrlVariable: mockUpdateUrlVariable,
transformDashboardVariables: <T,>(data: T): T => data,
});
jest.mocked(useTabVisibility as unknown as jest.Mock).mockReturnValue(true);
jest
.mocked(useDashboardQuery as unknown as jest.Mock)
.mockImplementation(() => ({
data: currentQueryData,
isLoading: false,
isError: false,
isFetching: false,
error: null,
refetch: mockRefetch,
}));
});
it('keeps minTime and maxTime unchanged for custom range on refresh confirm', () => {
const initialDashboard = {
id: 'dashboard-1',
updatedAt: '2024-01-01T00:00:00.000Z',
data: { layout: [], panelMap: {}, variables: {} },
};
const updatedDashboard = {
id: 'dashboard-1',
updatedAt: '2024-01-01T01:00:00.000Z',
data: { layout: [], panelMap: {}, variables: {} },
};
const mockConfirm = jest.fn<
ReturnType<typeof Modal.confirm>,
Parameters<typeof Modal.confirm>
>(() => ({ destroy: jest.fn(), update: jest.fn() }));
currentQueryData = { data: initialDashboard };
const { rerender } = render(<TestComponent confirm={mockConfirm} />);
expect(mockConfirm).not.toHaveBeenCalled();
currentQueryData = { data: updatedDashboard };
rerender(<TestComponent confirm={mockConfirm} />);
expect(mockConfirm).toHaveBeenCalledTimes(1);
const firstCall = mockConfirm.mock.calls[0];
expect(firstCall).toBeDefined();
const [confirmProps] = firstCall as Parameters<typeof Modal.confirm>;
act(() => {
confirmProps.onOk?.();
});
expect(getMinMaxForSelectedTime).not.toHaveBeenCalled();
expect(mockDispatch).toHaveBeenCalledWith({
type: 'UPDATE_TIME_INTERVAL',
payload: {
selectedTime: 'custom',
minTime: mockGlobalTime.minTime,
maxTime: mockGlobalTime.maxTime,
},
});
});
});

View File

@@ -102,19 +102,11 @@ export function useDashboardBootstrap(
onOk() {
setDashboardData(updatedDashboardData);
const { maxTime, minTime } =
globalTime.selectedTime === 'custom'
? {
// For custom ranges, min/max are already stored in nanoseconds.
// Recomputing via getMinMaxForSelectedTime would multiply them again.
maxTime: globalTime.maxTime,
minTime: globalTime.minTime,
}
: getMinMaxForSelectedTime(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
const { maxTime, minTime } = getMinMaxForSelectedTime(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
dispatch({
type: UPDATE_TIME_INTERVAL,
payload: { maxTime, minTime, selectedTime: globalTime.selectedTime },

View File

@@ -8,7 +8,7 @@ import afterLogin from 'AppRoutes/utils';
import AuthError from 'components/AuthError/AuthError';
import AuthPageContainer from 'components/AuthPageContainer';
import { useNotifications } from 'hooks/useNotifications';
import { ArrowRight } from 'lucide-react';
import { ArrowRight, CircleAlert } from 'lucide-react';
import APIError from 'types/api/error';
import tvUrl from '@/assets/svgs/tv.svg';
@@ -28,8 +28,9 @@ type FormValues = {
function SignUp(): JSX.Element {
const [loading, setLoading] = useState(false);
const [confirmPasswordTouched, setConfirmPasswordTouched] = useState(false);
const [confirmPasswordError, setConfirmPasswordError] =
useState<boolean>(false);
const [formError, setFormError] = useState<APIError | null>();
const { notifications } = useNotifications();
@@ -83,10 +84,35 @@ function SignUp(): JSX.Element {
})();
};
const isPasswordMismatch =
Boolean(confirmPassword) && password !== confirmPassword;
const handleValuesChange: (changedValues: Partial<FormValues>) => void = (
changedValues,
) => {
// Clear error if passwords match while typing (but don't set error until blur)
if ('password' in changedValues || 'confirmPassword' in changedValues) {
const { password, confirmPassword } = form.getFieldsValue();
const showPasswordMismatchError = confirmPasswordTouched && isPasswordMismatch;
if (password && confirmPassword && password === confirmPassword) {
setConfirmPasswordError(false);
}
}
};
const handlePasswordBlur = (): void => {
const { password, confirmPassword } = form.getFieldsValue();
// Only validate if confirm password has a value
if (confirmPassword) {
const isSamePassword = password === confirmPassword;
setConfirmPasswordError(!isSamePassword);
}
};
const handleConfirmPasswordBlur = (): void => {
const { password, confirmPassword } = form.getFieldsValue();
if (password && confirmPassword) {
const isSamePassword = password === confirmPassword;
setConfirmPasswordError(!isSamePassword);
}
};
const isValidForm = useMemo(
(): boolean =>
@@ -94,8 +120,8 @@ function SignUp(): JSX.Element {
Boolean(email?.trim()) &&
Boolean(password?.trim()) &&
Boolean(confirmPassword?.trim()) &&
password === confirmPassword,
[loading, email, password, confirmPassword],
!confirmPasswordError,
[loading, email, password, confirmPassword, confirmPasswordError],
);
return (
@@ -114,7 +140,12 @@ function SignUp(): JSX.Element {
</Typography.Paragraph>
</div>
<FormContainer onFinish={handleSubmit} form={form} className="signup-form">
<FormContainer
onFinish={handleSubmit}
onValuesChange={handleValuesChange}
form={form}
className="signup-form"
>
<div className="signup-form-container">
<div className="signup-form-fields">
<div className="signup-field-container">
@@ -144,6 +175,7 @@ function SignUp(): JSX.Element {
placeholder="Enter new password"
disabled={loading}
className="signup-antd-input"
onBlur={handlePasswordBlur}
/>
</FormContainer.Item>
</div>
@@ -153,12 +185,6 @@ function SignUp(): JSX.Element {
<FormContainer.Item
name="confirmPassword"
validateTrigger="onBlur"
validateStatus={showPasswordMismatchError ? 'error' : undefined}
help={
showPasswordMismatchError
? "Passwords don't match. Please try again."
: undefined
}
rules={[{ required: true, message: 'Please enter confirm password!' }]}
>
<AntdInput.Password
@@ -167,7 +193,7 @@ function SignUp(): JSX.Element {
placeholder="Confirm your new password"
disabled={loading}
className="signup-antd-input"
onBlur={() => setConfirmPasswordTouched(true)}
onBlur={handleConfirmPasswordBlur}
/>
</FormContainer.Item>
</div>
@@ -179,7 +205,19 @@ function SignUp(): JSX.Element {
your admin for an invite link
</Callout>
{formError && <AuthError error={formError} />}
{confirmPasswordError && (
<Callout
type="error"
size="small"
showIcon
icon={<CircleAlert size={12} />}
className="signup-error-callout"
>
Passwords don&apos;t match. Please try again.
</Callout>
)}
{formError && !confirmPasswordError && <AuthError error={formError} />}
<div className="signup-form-actions">
<Button

View File

@@ -7,12 +7,7 @@ export const topTracesTableColumns = [
dataIndex: 'trace_id',
key: 'trace_id',
render: (traceId: string): JSX.Element => (
<Link
to={`/trace/${traceId}`}
className="trace-id-cell"
target="_blank"
rel="noopener noreferrer"
>
<Link to={`/trace/${traceId}`} className="trace-id-cell">
{traceId}
</Link>
),

View File

@@ -1,99 +0,0 @@
package v2
import (
"regexp"
"sync"
"time"
)
// Code is a dotted, hierarchical identifier registered at process start. It
// encodes domain (subsystem), op (verb), optional sub (qualifier), and a
// terminal reason. Codes are values; two Codes with the same string are equal
// by value and safe to compare with ==.
type Code struct{ s string }
// String returns the dotted code as it appears on the wire. Empty for the
// zero value.
func (c Code) String() string { return c.s }
// codePattern allows 2-4 dotted segments, each starting with a lowercase
// letter and continuing with [a-z0-9_]. One segment is too broad (use a
// domain prefix); five or more means the domain should be split.
var codePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$`)
// Meta is the per-code default envelope applied by constructors before
// per-call options. Every field has a natural per-code default — an auth
// code always wants Reauthenticate, every documented code wants its docs
// URL — so the registry is the right place to declare them once.
type Meta struct {
Category Category
Fault Fault
Retry Retry
Remediation Remediation
Refs map[RefKind]string
}
// Retry tells the caller how and when to retry. After is meaningful only
// when Policy == RetryAfter.
type Retry struct {
Policy RetryPolicy
After time.Duration
}
var (
registryMu sync.RWMutex
registry = map[string]Meta{}
)
// Register installs a code with its default Meta and returns the Code value.
// It panics on a malformed code string or a duplicate registration — both
// indicate a programming error that must be caught at boot, not at first
// failure.
//
// Call from the owning domain's package init or top-level var block:
//
// var CodeUnknownFunction = errors.Register("query.parse.unknown_function", errors.Meta{
// Category: errors.CategoryInvalidInput,
// Fault: errors.FaultCaller,
// Retry: errors.Retry{Policy: errors.RetryAfterFix},
// })
func Register(s string, meta Meta) Code {
if !codePattern.MatchString(s) {
panic("errors/v2: malformed code: " + s)
}
registryMu.Lock()
defer registryMu.Unlock()
if _, ok := registry[s]; ok {
panic("errors/v2: duplicate code: " + s)
}
registry[s] = meta
return Code{s: s}
}
// MetaOf returns the Meta a code was registered with. Returns the zero Meta
// and false for unregistered or zero codes.
func MetaOf(c Code) (Meta, bool) {
if c.s == "" {
return Meta{}, false
}
registryMu.RLock()
defer registryMu.RUnlock()
m, ok := registry[c.s]
return m, ok
}
// registerOrGet is the internal idempotent register used by adapters that
// may see the same code (e.g. legacy.<v1>) more than once across the process
// lifetime. It panics on malformed codes — duplicate codes silently keep the
// existing Meta.
func registerOrGet(s string, meta Meta) Code {
if !codePattern.MatchString(s) {
panic("errors/v2: malformed code: " + s)
}
registryMu.Lock()
defer registryMu.Unlock()
if _, ok := registry[s]; !ok {
registry[s] = meta
}
return Code{s: s}
}

View File

@@ -1,93 +0,0 @@
package v2
// The enums in this file are closed sets. Each value is a package-level var of
// an unexported-field struct, so external code cannot synthesize new values —
// it must reference one of the defined ones. String() returns the stable
// snake_case wire name; once shipped, those names are append-only.
// Category groups errors by what kind of failure occurred. It is the coarsest
// branch-worthy axis and is intended to be a superset of gRPC status codes
// extended with cases SigNoz cares about (e.g. license issues land under
// FailedDependency or ResourceExhausted depending on context).
type Category struct{ s string }
func (c Category) String() string { return c.s }
var (
CategoryInvalidInput = Category{"invalid_input"} // request was malformed or violated a documented constraint.
CategoryNotFound = Category{"not_found"} // referenced resource does not exist.
CategoryAlreadyExists = Category{"already_exists"} // resource creation conflicts with an existing one.
CategoryConflict = Category{"conflict"} // concurrent modification or state mismatch (e.g. stale revision).
CategoryPrecondition = Category{"precondition"} // a required precondition (system or caller-asserted) was not met.
CategoryUnauthenticated = Category{"unauthenticated"} // credentials are missing or invalid.
CategoryForbidden = Category{"forbidden"} // authenticated but not authorized for this action.
CategoryResourceExhausted = Category{"resource_exhausted"} // quota, rate limit, or other budget exceeded.
CategoryFailedDependency = Category{"failed_dependency"} // an upstream service we depend on failed (db, license, etc.).
CategoryUnavailable = Category{"unavailable"} // service is temporarily down; retry with backoff.
CategoryTimeout = Category{"timeout"} // deadline exceeded before the operation completed.
CategoryCanceled = Category{"canceled"} // caller or context canceled the operation.
CategoryUnimplemented = Category{"unimplemented"} // operation is not supported (or not yet) by this server.
CategoryDataLoss = Category{"data_loss"} // unrecoverable data corruption or loss detected.
CategoryInternal = Category{"internal"} // bug — invariant broken; should not occur in normal operation.
)
// Fault attributes responsibility. An agent uses this to decide whether to
// fix the request (Caller), retry/escalate (Server, Upstream), or page a
// human (Operator).
type Fault struct{ s string }
func (f Fault) String() string { return f.s }
var (
FaultCaller = Fault{"caller"}
FaultServer = Fault{"server"}
FaultUpstream = Fault{"upstream"}
FaultOperator = Fault{"operator"}
)
// RetryPolicy tells the caller how to behave on retry. Backoff implies the
// caller should use its own backoff schedule; After means honor Retry.After
// exactly; AfterFix and AfterAuth signal that retry is pointless until the
// caller fixes the request or re-authenticates.
type RetryPolicy struct{ s string }
func (r RetryPolicy) String() string { return r.s }
var (
RetryNever = RetryPolicy{"never"}
RetryImmediate = RetryPolicy{"immediate"}
RetryBackoff = RetryPolicy{"backoff"}
RetryAfter = RetryPolicy{"after"}
RetryAfterFix = RetryPolicy{"after_fix"}
RetryAfterAuth = RetryPolicy{"after_auth"}
)
// Remediation names the single recommended next action. It does not execute.
type Remediation struct{ s string }
func (r Remediation) String() string { return r.s }
var (
RemediationNone = Remediation{"none"}
RemediationFixInput = Remediation{"fix_input"}
RemediationReauthenticate = Remediation{"reauthenticate"}
RemediationWaitAndRetry = Remediation{"wait_and_retry"}
RemediationFailover = Remediation{"failover"}
RemediationContactOperator = Remediation{"contact_operator"}
RemediationFileBug = Remediation{"file_bug"}
RemediationUpgradeLicense = Remediation{"upgrade_license"}
)
// RefKind classifies a reference URL attached to the error.
type RefKind struct{ s string }
func (r RefKind) String() string { return r.s }
var (
RefDocs = RefKind{"docs"}
RefRunbook = RefKind{"runbook"}
RefDashboard = RefKind{"dashboard"}
RefTrace = RefKind{"trace"}
RefSource = RefKind{"source"}
RefIssue = RefKind{"issue"}
)

View File

@@ -1,248 +0,0 @@
// Package v2 is the redesigned pkg/errors.
//
// Every branch-worthy field on the Error struct is a closed enum and every
// variable part is a typed key/value. The intent is to make errors first-class
// data for programmatic consumers — SDK clients, UI surfaces, alerting, and
// LLM agents — without sacrificing human readability.
//
// Domain and op are encoded into Code (e.g. "query.parse.unknown_function")
// rather than carried as separate struct fields. Frames[0] is the
// authoritative call-site location, captured at construction time.
package v2
import (
stderrors "errors"
"fmt"
"io"
"sort"
"strconv"
"strings"
)
// Error is the redesigned error value. *Error is the canonical form passed
// around — the zero value is unused, construct via New / Newf / Wrap / Wrapf.
//
// Frames are intentionally not a struct field: resolving captured PCs into
// func/file/line is the dominant construction cost, so we capture PCs eagerly
// at construction time (so the snapshot is faithful to the call site) and
// resolve them lazily via Frames() only when something actually inspects them.
type Error struct {
// WHAT
Category Category
Code Code
Title string
Detail string
// WHY / WHO
Cause error
Fault Fault
// WHAT NEXT
Retry Retry
Remediation Remediation
Refs map[RefKind]string
// CONTEXT
Attrs map[string]any
TraceID string
SpanID string
// stack is the captured PCs plus a memoized []Frame; never read directly,
// always go through Frames().
stack *frameStack
}
// Frames returns the captured stack, resolved to func/file/line on first
// access. Frames[0] is the constructor's caller. Safe for concurrent use.
func (e *Error) Frames() []Frame {
if e == nil {
return nil
}
return e.stack.frames()
}
// New creates an Error for a registered Code. Defaults from the registered
// Meta are applied first; opts override per call site.
func New(code Code, title string, opts ...Option) *Error {
e := &Error{Code: code, Title: title, stack: captureStack(3)}
applyMeta(e)
for _, opt := range opts {
opt(e)
}
return e
}
// Newf is New with fmt.Sprintf-style formatting for the title.
func Newf(code Code, format string, args ...any) *Error {
e := &Error{Code: code, Title: fmt.Sprintf(format, args...), stack: captureStack(3)}
applyMeta(e)
return e
}
// Wrap creates an Error that wraps cause. The new error's Title is the
// caller-supplied title (not the cause's message), so Error() reports what
// went wrong at this layer — the cause is reachable via Unwrap.
func Wrap(cause error, code Code, title string, opts ...Option) *Error {
e := &Error{Code: code, Title: title, Cause: cause, stack: captureStack(3)}
applyMeta(e)
for _, opt := range opts {
opt(e)
}
return e
}
// Wrapf is Wrap with fmt.Sprintf-style formatting for the title.
func Wrapf(cause error, code Code, format string, args ...any) *Error {
e := &Error{Code: code, Title: fmt.Sprintf(format, args...), Cause: cause, stack: captureStack(3)}
applyMeta(e)
return e
}
// applyMeta copies default values from the registered Meta into a fresh
// Error. It runs before per-call options so options win.
func applyMeta(e *Error) {
meta, ok := MetaOf(e.Code)
if !ok {
return
}
if (e.Category == Category{}) {
e.Category = meta.Category
}
if (e.Fault == Fault{}) {
e.Fault = meta.Fault
}
if (e.Retry == Retry{}) {
e.Retry = meta.Retry
}
if (e.Remediation == Remediation{}) {
e.Remediation = meta.Remediation
}
if len(meta.Refs) > 0 {
if e.Refs == nil {
e.Refs = make(map[RefKind]string, len(meta.Refs))
}
for k, v := range meta.Refs {
if _, exists := e.Refs[k]; !exists {
e.Refs[k] = v
}
}
}
}
// Error returns the Title (the message specifically attached at this wrap
// site), not the cause's message. This fixes the v1 surprise where Error()
// returned the wrapped cause's text.
func (e *Error) Error() string {
if e == nil {
return "<nil>"
}
return e.Title
}
// Unwrap returns the wrapped cause, enabling errors.Is / errors.As.
func (e *Error) Unwrap() error {
if e == nil {
return nil
}
return e.Cause
}
// Format implements fmt.Formatter.
//
// %s, %v → Title only
// %+v → full chain: code, title, frames, attrs, recursive cause
func (e *Error) Format(f fmt.State, verb rune) {
switch verb {
case 's':
_, _ = io.WriteString(f, e.Title)
case 'v':
if f.Flag('+') {
_, _ = io.WriteString(f, e.fullString())
return
}
_, _ = io.WriteString(f, e.Title)
case 'q':
fmt.Fprintf(f, "%q", e.Title)
default:
fmt.Fprintf(f, "%%!%c(*errors/v2.Error)", verb)
}
}
// fullString produces the %+v rendering. Format is intentionally
// human-readable rather than machine-parseable; consumers that want structure
// should marshal to JSON.
func (e *Error) fullString() string {
var b strings.Builder
e.appendFull(&b, 0)
return b.String()
}
func (e *Error) appendFull(b *strings.Builder, depth int) {
indent := strings.Repeat(" ", depth)
fmt.Fprintf(b, "%s[%s] %s\n", indent, e.Code.s, e.Title)
if e.Detail != "" {
fmt.Fprintf(b, "%s detail: %s\n", indent, e.Detail)
}
if len(e.Attrs) > 0 {
// Stable key order for deterministic output.
keys := make([]string, 0, len(e.Attrs))
for k := range e.Attrs {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Fprintf(b, "%s attrs:\n", indent)
for _, k := range keys {
fmt.Fprintf(b, "%s %s=%v\n", indent, k, e.Attrs[k])
}
}
if frames := e.Frames(); len(frames) > 0 {
fmt.Fprintf(b, "%s frames:\n", indent)
for _, fr := range frames {
fmt.Fprintf(b, "%s %s\n%s %s:%s\n", indent, fr.Func, indent, fr.File, strconv.Itoa(fr.Line))
}
}
if e.Cause != nil {
fmt.Fprintf(b, "%scaused by:\n", indent)
var ce *Error
if stderrors.As(e.Cause, &ce) && ce != nil {
ce.appendFull(b, depth+1)
} else {
fmt.Fprintf(b, "%s %s\n", indent, e.Cause.Error())
}
}
}
// AsError extracts a *Error from anywhere in err's wrap chain. It is the
// common shortcut around errors.As for code that always wants this package's
// type.
func AsError(err error) (*Error, bool) {
if err == nil {
return nil, false
}
var e *Error
if stderrors.As(err, &e) {
return e, true
}
return nil, false
}
// Is reports whether err or any error in its chain has the given Code.
// Convenience wrapper that's friendlier than errors.As at call sites that
// only care about code identity.
func Is(err error, code Code) bool {
e, ok := AsError(err)
if !ok {
return false
}
for e != nil {
if e.Code == code {
return true
}
next, ok := AsError(e.Cause)
if !ok {
return false
}
e = next
}
return false
}

View File

@@ -1,90 +0,0 @@
package v2
// This file is a self-contained walkthrough of how a domain integrates with
// pkg/errors/v2. It mirrors what a real pkg/<domain>/errors.go looks like in
// practice — registering codes, constructing typed errors at failure sites,
// and consuming them at API boundaries. The "example.*" namespace is reserved
// for these demo codes so they never collide with a real domain's
// registrations.
// 1. Register codes at package init time. Each Register call panics on
// malformed code or duplicate registration, so misconfiguration is caught
// at process boot, not at first failure.
var (
// A caller-fault, fix-the-input error: rejected before any work happens.
exampleCodeInvalidQuery = Register("example.query.invalid_filter", Meta{
Category: CategoryInvalidInput,
Fault: FaultCaller,
Remediation: RemediationFixInput,
Retry: Retry{Policy: RetryAfterFix},
Refs: map[RefKind]string{
RefDocs: "https://signoz.io/docs/query/filters",
},
})
// A quota error: the caller's request was well-formed but their plan
// doesn't allow it. The recommended remediation is structural (upgrade),
// not "try again later."
exampleCodeQuotaExceeded = Register("example.billing.quota_exceeded", Meta{
Category: CategoryResourceExhausted,
Fault: FaultCaller,
Remediation: RemediationUpgradeLicense,
Retry: Retry{Policy: RetryNever},
})
)
// 2. Construct errors at the failure site. Notice that variable parts of
// the message (the offending field, the limits) live in typed Attrs, not in
// the title prose — a downstream agent can read them without parsing English.
func exampleRejectInvalidFilter(field string) *Error {
return New(exampleCodeInvalidQuery, "filter is not supported",
WithAttr("field", field),
)
}
// 3. Consume errors at the API boundary. Branching on Category gives the
// HTTP status; Retry tells an SDK how to behave; Fault drives logging
// classification (caller errors are warnings, server/upstream errors page).
func exampleClassifyForHTTP(err error) (status int, retry RetryPolicy) {
e, ok := AsError(err)
if !ok {
return 500, RetryNever
}
switch e.Category {
case CategoryInvalidInput, CategoryPrecondition:
status = 400
case CategoryUnauthenticated:
status = 401
case CategoryForbidden:
status = 403
case CategoryNotFound:
status = 404
case CategoryConflict, CategoryAlreadyExists:
status = 409
case CategoryResourceExhausted:
status = 429
case CategoryUnavailable, CategoryTimeout:
status = 503
case CategoryUnimplemented:
status = 501
default:
status = 500
}
return status, e.Retry.Policy
}
// 4. Identify a specific failure mode by Code. Is walks the cause chain so
// a wrapper at the HTTP layer still matches when the root cause was raised
// deep in the call graph.
func exampleIsQuotaExceeded(err error) bool {
return Is(err, exampleCodeQuotaExceeded)
}
// The example helpers are reference-only: they exist to document call-site
// patterns, not to be called from anywhere in the binary. This anchor keeps
// them visible to readers (and the linter) without exporting demo code.
var _ = []any{
exampleRejectInvalidFilter,
exampleClassifyForHTTP,
exampleIsQuotaExceeded,
}

View File

@@ -1,65 +0,0 @@
package v2
import (
"runtime"
"sync"
)
// Frame is a single line in the call stack. Frames[0] is the constructor's
// caller — the authoritative "where this error came from" — and downstream
// consumers can filter (e.g. "frames inside our code") without regex
// reparsing of a pre-formatted stack string.
type Frame struct {
Func string `json:"func,omitempty"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
}
// frameStack carries the PCs captured at construction plus the resolved
// []Frame slice, behind a sync.Once. Resolving frames into func/file/line is
// expensive (runtime.CallersFrames walks the symbol table); the vast majority
// of errors are constructed and never inspected, so we only pay that cost
// when a consumer actually asks for frames (Frames()/MarshalJSON/%+v).
//
// The PC capture itself is cheap and happens at construction so that
// Frames[0] is a faithful "where" record of the original call site.
type frameStack struct {
pcs []uintptr
once sync.Once
resolved []Frame
}
// captureStack is called by every constructor. skip drops runtime.Callers,
// captureStack itself, and the constructor frame so that the first PC is the
// user code that invoked the constructor.
func captureStack(skip int) *frameStack {
const depth = 32
pcs := make([]uintptr, depth)
n := runtime.Callers(skip, pcs)
if n == 0 {
return nil
}
return &frameStack{pcs: pcs[:n:n]}
}
// frames resolves the captured PCs into []Frame. The resolution is memoized
// — concurrent calls are safe and only one of them does the work.
func (s *frameStack) frames() []Frame {
if s == nil {
return nil
}
s.once.Do(func() {
cf := runtime.CallersFrames(s.pcs)
out := make([]Frame, 0, len(s.pcs))
for {
f, more := cf.Next()
out = append(out, Frame{Func: f.Function, File: f.File, Line: f.Line})
if !more {
break
}
}
s.resolved = out
})
return s.resolved
}

View File

@@ -1,175 +0,0 @@
package v2
import (
"encoding/json"
"net/url"
)
// CodeUnknown is the sentinel returned when AsJSON / AsURLValues are called
// on a non-*Error. A consumer that sees this on the wire should read it as
// "the producer did not raise a v2 Error and we projected it through the
// fallback path" — i.e. somewhere upstream is still using std errors or v1.
var CodeUnknown = Register("unknown.unset", Meta{
Category: CategoryInternal,
Fault: FaultServer,
Retry: Retry{Policy: RetryNever},
})
// JSON is the wire envelope for an Error. It is intentionally a superset of
// v1's pkg/errors.JSON: SDK clients that only read v1's {code, message, url,
// errors[]} keep working, while v2 consumers can branch on the new typed
// fields (category, fault, retry, remediation, attrs, refs, cause).
type JSON struct {
Code string `json:"code" required:"true"`
Title string `json:"title" required:"true"`
Detail string `json:"detail,omitempty"`
Category string `json:"category,omitempty"`
Fault string `json:"fault,omitempty"`
Retry *RetryJSON `json:"retry,omitempty"`
Remediation string `json:"remediation,omitempty"`
Attrs map[string]any `json:"attrs,omitempty"`
Refs map[string]string `json:"refs,omitempty"`
Frames []Frame `json:"frames,omitempty"`
TraceID string `json:"trace_id,omitempty"`
SpanID string `json:"span_id,omitempty"`
Cause *CauseJSON `json:"cause,omitempty"`
}
// RetryJSON renders Retry as an object so consumers can branch on policy
// before consulting AfterMS. AfterMS is omitted unless policy is "after".
type RetryJSON struct {
Policy string `json:"policy"`
AfterMS int64 `json:"after_ms,omitempty"`
}
// CauseJSON is the thin recursive shape for a cause chain. Only code, title,
// and a nested cause are guaranteed — producers may add more, consumers must
// not rely on it.
type CauseJSON struct {
Code string `json:"code,omitempty"`
Title string `json:"title"`
Cause *CauseJSON `json:"cause,omitempty"`
}
// AsJSON projects any error onto the v2 wire envelope. If cause is a
// *Error (anywhere in its wrap chain) every field is filled from it;
// otherwise the result is a CodeUnknown envelope with Title=cause.Error()
// so the wire shape is always valid and never panics.
func AsJSON(cause error) *JSON {
if cause == nil {
return nil
}
e, ok := AsError(cause)
if !ok {
return &JSON{
Code: CodeUnknown.s,
Title: cause.Error(),
Category: CategoryInternal.s,
Fault: FaultServer.s,
}
}
return errorToJSON(e)
}
func errorToJSON(e *Error) *JSON {
out := &JSON{
Code: e.Code.s,
Title: e.Title,
Detail: e.Detail,
Category: e.Category.s,
Fault: e.Fault.s,
Remediation: e.Remediation.s,
Attrs: e.Attrs,
TraceID: e.TraceID,
SpanID: e.SpanID,
}
if (e.Retry.Policy != RetryPolicy{}) {
out.Retry = &RetryJSON{Policy: e.Retry.Policy.s}
if e.Retry.Policy == RetryAfter && e.Retry.After > 0 {
out.Retry.AfterMS = e.Retry.After.Milliseconds()
}
}
if len(e.Refs) > 0 {
out.Refs = make(map[string]string, len(e.Refs))
for k, v := range e.Refs {
out.Refs[k.s] = v
}
}
if frames := e.Frames(); len(frames) > 0 {
out.Frames = frames
}
if e.Cause != nil {
out.Cause = causeToJSON(e.Cause)
}
return out
}
func causeToJSON(err error) *CauseJSON {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
c := &CauseJSON{Code: e.Code.s, Title: e.Title}
if e.Cause != nil {
c.Cause = causeToJSON(e.Cause)
}
return c
}
// Non-*Error leaf: only Title is set, no Code.
return &CauseJSON{Title: err.Error()}
}
// AsURLValues projects an error onto a flat url.Values, matching v1's shape
// for callers (e.g. OAuth/SSO redirects) that smuggle errors back through a
// query string. Complex fields (attrs, refs, retry, frames, cause) are
// JSON-marshaled into a single value rather than spread across multiple
// keys, since query strings have no good representation for nested data.
func AsURLValues(cause error) url.Values {
j := AsJSON(cause)
if j == nil {
return url.Values{}
}
v := url.Values{
"code": {j.Code},
"title": {j.Title},
}
if j.Detail != "" {
v.Set("detail", j.Detail)
}
if j.Category != "" {
v.Set("category", j.Category)
}
if j.Fault != "" {
v.Set("fault", j.Fault)
}
if j.Remediation != "" {
v.Set("remediation", j.Remediation)
}
if j.TraceID != "" {
v.Set("trace_id", j.TraceID)
}
if j.SpanID != "" {
v.Set("span_id", j.SpanID)
}
if j.Retry != nil {
if b, err := json.Marshal(j.Retry); err == nil {
v.Set("retry", string(b))
}
}
if len(j.Refs) > 0 {
if b, err := json.Marshal(j.Refs); err == nil {
v.Set("refs", string(b))
}
}
if len(j.Attrs) > 0 {
if b, err := json.Marshal(j.Attrs); err == nil {
v.Set("attrs", string(b))
}
}
if j.Cause != nil {
if b, err := json.Marshal(j.Cause); err == nil {
v.Set("cause", string(b))
}
}
return v
}

View File

@@ -1,85 +0,0 @@
package v2
import "time"
// Option mutates an Error during construction. Options are applied after the
// registered Meta defaults so a per-call WithFault wins over the code's
// default Fault.
type Option func(*Error)
// WithTitle overrides the title (used when Newf's formatted string is not
// what you want, or after a Wrap that took its title from the cause).
func WithTitle(s string) Option { return func(e *Error) { e.Title = s } }
// WithDetail adds a long, user-safe explanation. Detail must never include
// raw cause text; the cause is already in the chain.
func WithDetail(s string) Option { return func(e *Error) { e.Detail = s } }
// WithCategory overrides the registered Category.
func WithCategory(c Category) Option { return func(e *Error) { e.Category = c } }
// WithFault overrides the registered Fault.
func WithFault(f Fault) Option { return func(e *Error) { e.Fault = f } }
// WithRetry overrides the registered Retry.
func WithRetry(r Retry) Option { return func(e *Error) { e.Retry = r } }
// WithRetryAfter is a convenience for the common RetryAfter case.
func WithRetryAfter(d time.Duration) Option {
return func(e *Error) { e.Retry = Retry{Policy: RetryAfter, After: d} }
}
// WithRemediation overrides the registered Remediation.
//
// Convention for "did you mean" hints: stash a []string under
// Attrs["suggestions"], ranked best-first. Each element should be a complete,
// copy-pasteable replacement — not an explanation of what went wrong (use
// WithDetail for that). Once 3-4 domains adopt the convention identically,
// promote to a first-class field.
func WithRemediation(r Remediation) Option { return func(e *Error) { e.Remediation = r } }
// WithRef adds (or replaces) a single reference URL keyed by kind.
func WithRef(kind RefKind, url string) Option {
return func(e *Error) {
if e.Refs == nil {
e.Refs = make(map[RefKind]string, 1)
}
e.Refs[kind] = url
}
}
// WithAttr sets a single typed attribute. Prefer typed per-domain helpers
// (e.g. WithQueryAttrs(q Query)) over raw WithAttr at call sites — they keep
// the attr keys consistent and let the compiler reject typos.
func WithAttr(key string, value any) Option {
return func(e *Error) {
if e.Attrs == nil {
e.Attrs = make(map[string]any, 1)
}
e.Attrs[key] = value
}
}
// WithAttrs merges a map of attributes; later keys win.
func WithAttrs(attrs map[string]any) Option {
return func(e *Error) {
if len(attrs) == 0 {
return
}
if e.Attrs == nil {
e.Attrs = make(map[string]any, len(attrs))
}
for k, v := range attrs {
e.Attrs[k] = v
}
}
}
// WithTrace stamps the error with OTel trace and span IDs so the JSON
// response can link back to the originating span.
func WithTrace(traceID, spanID string) Option {
return func(e *Error) {
e.TraceID = traceID
e.SpanID = spanID
}
}

View File

@@ -6,7 +6,6 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
@@ -17,12 +16,11 @@ import (
)
type handler struct {
module spanmapper.Module
providerSettings factory.ProviderSettings
module spanmapper.Module
}
func NewHandler(module spanmapper.Module, providerSettings factory.ProviderSettings) spanmapper.Handler {
return &handler{module: module, providerSettings: providerSettings}
func NewHandler(module spanmapper.Module) spanmapper.Handler {
return &handler{module: module}
}
// ListGroups handles GET /api/v1/span_mapper_groups.
@@ -72,10 +70,9 @@ func (h *handler) CreateGroup(rw http.ResponseWriter, r *http.Request) {
return
}
group := spantypes.NewSpanMapperGroupFromPostable(req)
group := spantypes.NewSpanMapperGroup(orgID, claims.Email, req)
err = h.module.CreateGroup(ctx, orgID, claims.Email, group)
if err != nil {
if err := h.module.CreateGroup(ctx, orgID, group); err != nil {
render.Error(rw, err)
return
}
@@ -107,11 +104,17 @@ func (h *handler) UpdateGroup(rw http.ResponseWriter, r *http.Request) {
return
}
err = h.module.UpdateGroup(ctx, orgID, id, claims.Email, spantypes.NewSpanMapperGroupFromUpdatable(req))
group, err := h.module.GetGroup(ctx, orgID, id)
if err != nil {
render.Error(rw, err)
return
}
group.Update(req, claims.Email)
if err := h.module.UpdateGroup(ctx, orgID, id, group); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
@@ -195,10 +198,9 @@ func (h *handler) CreateMapper(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, err)
return
}
mapper := spantypes.NewSpanMapperFromPostable(req)
mapper := spantypes.NewSpanMapper(groupID, claims.Email, req)
err = h.module.CreateMapper(ctx, orgID, groupID, claims.Email, mapper)
if err != nil {
if err := h.module.CreateMapper(ctx, orgID, groupID, mapper); err != nil {
render.Error(rw, err)
return
}
@@ -237,11 +239,17 @@ func (h *handler) UpdateMapper(rw http.ResponseWriter, r *http.Request) {
return
}
err = h.module.UpdateMapper(ctx, orgID, groupID, mapperID, claims.Email, spantypes.NewSpanMapperFromUpdatable(req))
mapper, err := h.module.GetMapper(ctx, orgID, groupID, mapperID)
if err != nil {
render.Error(rw, err)
return
}
mapper.Update(req, claims.Email)
if err := h.module.UpdateMapper(ctx, orgID, groupID, mapperID, mapper); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@@ -0,0 +1,64 @@
package implspanmapper
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store spantypes.SpanMapperStore
}
func NewModule(store spantypes.SpanMapperStore) spanmapper.Module {
return &module{store: store}
}
func (module *module) ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error) {
return module.store.ListGroups(ctx, orgID, q)
}
func (module *module) GetGroup(ctx context.Context, orgID, id valuer.UUID) (*spantypes.SpanMapperGroup, error) {
return module.store.GetGroup(ctx, orgID, id)
}
func (module *module) CreateGroup(ctx context.Context, orgID valuer.UUID, group *spantypes.SpanMapperGroup) error {
return module.store.CreateGroup(ctx, group)
}
func (module *module) UpdateGroup(ctx context.Context, orgID, id valuer.UUID, group *spantypes.SpanMapperGroup) error {
return module.store.UpdateGroup(ctx, group)
}
func (module *module) DeleteGroup(ctx context.Context, orgID, id valuer.UUID) error {
return module.store.DeleteGroup(ctx, orgID, id)
}
func (module *module) ListMappers(ctx context.Context, orgID, groupID valuer.UUID) ([]*spantypes.SpanMapper, error) {
return module.store.ListMappers(ctx, orgID, groupID)
}
func (module *module) GetMapper(ctx context.Context, orgID, groupID, id valuer.UUID) (*spantypes.SpanMapper, error) {
return module.store.GetMapper(ctx, orgID, groupID, id)
}
func (module *module) CreateMapper(ctx context.Context, orgID, groupID valuer.UUID, mapper *spantypes.SpanMapper) error {
// Ensure the group belongs to the org before inserting the child row.
if _, err := module.store.GetGroup(ctx, orgID, groupID); err != nil {
return err
}
return module.store.CreateMapper(ctx, mapper)
}
func (module *module) UpdateMapper(ctx context.Context, orgID, groupID, id valuer.UUID, mapper *spantypes.SpanMapper) error {
if _, err := module.store.GetGroup(ctx, orgID, groupID); err != nil {
return err
}
return module.store.UpdateMapper(ctx, mapper)
}
func (module *module) DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error {
return module.store.DeleteMapper(ctx, orgID, groupID, id)
}

View File

@@ -0,0 +1,227 @@
package implspanmapper
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) spantypes.SpanMapperStore {
return &store{sqlstore: sqlstore}
}
func (s *store) CreateGroup(ctx context.Context, group *spantypes.SpanMapperGroup) error {
storable := group.ToStorable()
_, err := s.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(storable).
Exec(ctx)
if err != nil {
return s.sqlstore.WrapAlreadyExistsErrf(err, spantypes.ErrCodeMappingGroupAlreadyExists, "span mapper group %q already exists", group.Name)
}
return nil
}
func (s *store) GetGroup(ctx context.Context, orgID, id valuer.UUID) (*spantypes.SpanMapperGroup, error) {
storable := new(spantypes.StorableSpanMapperGroup)
err := s.sqlstore.
BunDB().
NewSelect().
Model(storable).
Where("org_id = ?", orgID).
Where("id = ?", id).
Scan(ctx)
if err != nil {
return nil, s.sqlstore.WrapNotFoundErrf(err, spantypes.ErrCodeMappingGroupNotFound, "span mapper group %s not found", id)
}
return storable.ToSpanMapperGroup(), nil
}
func (s *store) ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error) {
storables := make([]*spantypes.StorableSpanMapperGroup, 0)
sel := s.sqlstore.
BunDB().
NewSelect().
Model(&storables).
Where("org_id = ?", orgID)
if q != nil {
if q.Enabled != nil {
sel = sel.Where("enabled = ?", *q.Enabled)
}
}
if err := sel.Order("created_at DESC").Scan(ctx); err != nil {
return nil, err
}
return spantypes.NewSpanMapperGroupsFromStorable(storables), nil
}
func (s *store) UpdateGroup(ctx context.Context, group *spantypes.SpanMapperGroup) error {
storable := group.ToStorable()
res, err := s.sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(storable).
Where("org_id = ?", storable.OrgID).
Where("id = ?", storable.ID).
Exec(ctx)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return errors.Newf(errors.TypeNotFound, spantypes.ErrCodeMappingGroupNotFound, "span mapper group %s not found", group.ID)
}
return nil
}
func (s *store) DeleteGroup(ctx context.Context, orgID, id valuer.UUID) error {
tx, err := s.sqlstore.BunDBCtx(ctx).BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
// Cascade: remove mappers belonging to this group first.
if _, err := tx.NewDelete().
Model((*spantypes.StorableSpanMapper)(nil)).
Where("group_id = ?", id).
Exec(ctx); err != nil {
return err
}
res, err := tx.NewDelete().
Model((*spantypes.StorableSpanMapperGroup)(nil)).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return errors.Newf(errors.TypeNotFound, spantypes.ErrCodeMappingGroupNotFound, "span mapper group %s not found", id)
}
return tx.Commit()
}
func (s *store) CreateMapper(ctx context.Context, mapper *spantypes.SpanMapper) error {
storable := mapper.ToStorable()
_, err := s.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(storable).
Exec(ctx)
if err != nil {
return s.sqlstore.WrapAlreadyExistsErrf(err, spantypes.ErrCodeMapperAlreadyExists, "span mapper %q already exists", mapper.Name)
}
return nil
}
func (s *store) GetMapper(ctx context.Context, orgID, groupID, id valuer.UUID) (*spantypes.SpanMapper, error) {
// Ensure the group belongs to the org.
if _, err := s.GetGroup(ctx, orgID, groupID); err != nil {
return nil, err
}
storable := new(spantypes.StorableSpanMapper)
err := s.sqlstore.
BunDB().
NewSelect().
Model(storable).
Where("group_id = ?", groupID).
Where("id = ?", id).
Scan(ctx)
if err != nil {
return nil, s.sqlstore.WrapNotFoundErrf(err, spantypes.ErrCodeMapperNotFound, "span mapper %s not found", id)
}
return storable.ToSpanMapper(), nil
}
func (s *store) ListMappers(ctx context.Context, orgID, groupID valuer.UUID) ([]*spantypes.SpanMapper, error) {
// Scope by org via the parent group's org_id.
if _, err := s.GetGroup(ctx, orgID, groupID); err != nil {
return nil, err
}
storables := make([]*spantypes.StorableSpanMapper, 0)
if err := s.sqlstore.
BunDB().
NewSelect().
Model(&storables).
Where("group_id = ?", groupID).
Order("created_at DESC").
Scan(ctx); err != nil {
return nil, err
}
return spantypes.NewSpanMappersFromStorable(storables), nil
}
func (s *store) UpdateMapper(ctx context.Context, mapper *spantypes.SpanMapper) error {
storable := mapper.ToStorable()
res, err := s.sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(storable).
Where("group_id = ?", storable.GroupID).
Where("id = ?", storable.ID).
Exec(ctx)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return errors.Newf(errors.TypeNotFound, spantypes.ErrCodeMapperNotFound, "span mapper %s not found", mapper.ID)
}
return nil
}
func (s *store) DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error {
if _, err := s.GetGroup(ctx, orgID, groupID); err != nil {
return err
}
res, err := s.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model((*spantypes.StorableSpanMapper)(nil)).
Where("group_id = ?", groupID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return errors.Newf(errors.TypeNotFound, spantypes.ErrCodeMapperNotFound, "span mapper %s not found", id)
}
return nil
}

View File

@@ -12,17 +12,17 @@ import (
type Module interface {
// Group operations
ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error)
GetGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*spantypes.SpanMapperGroup, error)
CreateGroup(ctx context.Context, orgID valuer.UUID, createdBy string, group *spantypes.SpanMapperGroup) error
UpdateGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, group *spantypes.SpanMapperGroup) error
DeleteGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
GetGroup(ctx context.Context, orgID, id valuer.UUID) (*spantypes.SpanMapperGroup, error)
CreateGroup(ctx context.Context, orgID valuer.UUID, group *spantypes.SpanMapperGroup) error
UpdateGroup(ctx context.Context, orgID, id valuer.UUID, group *spantypes.SpanMapperGroup) error
DeleteGroup(ctx context.Context, orgID, id valuer.UUID) error
// Mapper operations
ListMappers(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID) ([]*spantypes.SpanMapper, error)
GetMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID) (*spantypes.SpanMapper, error)
CreateMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, createdBy string, mapper *spantypes.SpanMapper) error
UpdateMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID, updatedBy string, mapper *spantypes.SpanMapper) error
DeleteMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID) error
ListMappers(ctx context.Context, orgID, groupID valuer.UUID) ([]*spantypes.SpanMapper, error)
GetMapper(ctx context.Context, orgID, groupID, id valuer.UUID) (*spantypes.SpanMapper, error)
CreateMapper(ctx context.Context, orgID, groupID valuer.UUID, mapper *spantypes.SpanMapper) error
UpdateMapper(ctx context.Context, orgID, groupID, id valuer.UUID, mapper *spantypes.SpanMapper) error
DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error
}
// Handler defines the HTTP handler interface for mapping group and mapper endpoints.

View File

@@ -1,40 +1,15 @@
package querybuilder
import (
"fmt"
"sort"
"strings"
errors "github.com/SigNoz/signoz/pkg/errors/v2"
"github.com/SigNoz/signoz/pkg/errors"
grammar "github.com/SigNoz/signoz/pkg/parser/havingexpression/grammar"
"github.com/antlr4-go/antlr/v4"
"github.com/huandu/go-sqlbuilder"
)
// HAVING-expression validator codes. All three are caller-fault, fix-the-input
// errors — the user wrote an expression we cannot turn into SQL — so retry
// is pointless until the expression itself changes.
var (
codeHavingStringLiteral = errors.Register("querybuilder.having.string_literal", errors.Meta{
Category: errors.CategoryInvalidInput,
Fault: errors.FaultCaller,
Retry: errors.Retry{Policy: errors.RetryAfterFix},
Remediation: errors.RemediationFixInput,
})
codeHavingInvalidReference = errors.Register("querybuilder.having.invalid_reference", errors.Meta{
Category: errors.CategoryInvalidInput,
Fault: errors.FaultCaller,
Retry: errors.Retry{Policy: errors.RetryAfterFix},
Remediation: errors.RemediationFixInput,
})
codeHavingSyntaxError = errors.Register("querybuilder.having.syntax_error", errors.Meta{
Category: errors.CategoryInvalidInput,
Fault: errors.FaultCaller,
Retry: errors.Retry{Policy: errors.RetryAfterFix},
Remediation: errors.RemediationFixInput,
})
)
// havingExpressionRewriteVisitor walks the parse tree of a HavingExpression in a single
// pass, simultaneously rewriting user-facing references to their SQL column names and
// collecting any references that could not be resolved.
@@ -306,10 +281,10 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
// This is checked before invalid references so that "contains string literals" takes
// priority when a bare string literal is also an unresolvable operand.
if v.hasStringLiteral {
return "", errors.New(codeHavingStringLiteral,
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"`Having` expression contains string literals",
errors.WithDetail("Aggregator results are numeric"),
)
).WithAdditional("Aggregator results are numeric")
}
if len(v.invalid) > 0 {
@@ -319,10 +294,7 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
validKeys = append(validKeys, k)
}
sort.Strings(validKeys)
opts := []errors.Option{
errors.WithAttr("invalid_refs", v.invalid),
errors.WithAttr("valid_refs", validKeys),
}
additional := []string{"Valid references are: [" + strings.Join(validKeys, ", ") + "]"}
if len(v.invalid) == 1 {
inv := v.invalid[0]
// Only suggest for plain identifier typos, not for unresolved function
@@ -331,13 +303,15 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
// a simple string substitution produce a corrupt expression.
isFuncCall := strings.Contains(original, inv+"(")
if match, dist := closestMatch(inv, validKeys); !isFuncCall && !strings.Contains(match, "(") && dist <= 3 {
opts = append(opts, errors.WithAttr("suggestions", []string{strings.ReplaceAll(original, inv, match)}))
corrected := strings.ReplaceAll(original, inv, match)
additional = append(additional, "Suggestion: `"+corrected+"`")
}
}
return "", errors.New(codeHavingInvalidReference,
fmt.Sprintf("Invalid references in `Having` expression: [%s]", strings.Join(v.invalid, ", ")),
opts...,
)
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"Invalid references in `Having` expression: [%s]",
strings.Join(v.invalid, ", "),
).WithAdditional(additional...)
}
// Layer 3 ANTLR syntax errors. We parse the original expression, so error messages
@@ -354,20 +328,17 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
if detail == "" {
detail = "check the expression syntax"
}
opts := []errors.Option{
errors.WithDetail(detail),
errors.WithAttr("syntax_errors", msgs),
}
additional := []string{detail}
// For single-error expressions, try to produce an actionable suggestion.
if len(allSyntaxErrors) == 1 {
if s := havingSuggestion(allSyntaxErrors[0], original); s != "" {
opts = append(opts, errors.WithAttr("suggestions", []string{s}))
additional = append(additional, "Suggestion: `"+s+"`")
}
}
return "", errors.New(codeHavingSyntaxError,
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"Syntax error in `Having` expression",
opts...,
)
).WithAdditional(additional...)
}
return result, nil

View File

@@ -120,7 +120,7 @@ func NewHandlers(
RegistryHandler: registryHandler,
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
SpanMapperHandler: implspanmapper.NewHandler(nil, providerSettings), // todo(nitya): will update this in future PR
SpanMapperHandler: implspanmapper.NewHandler(modules.SpanMapper),
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
RulerHandler: signozruler.NewHandler(rulerService),

View File

@@ -38,6 +38,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/services/implservices"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/session/implsession"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/modules/spanmapper/implspanmapper"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
@@ -80,6 +82,7 @@ type Modules struct {
CloudIntegration cloudintegration.Module
RuleStateHistory rulestatehistory.Module
TraceDetail tracedetail.Module
SpanMapper spanmapper.Module
}
func NewModules(
@@ -133,5 +136,6 @@ func NewModules(
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)),
CloudIntegration: cloudIntegrationModule,
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore)),
}
}

View File

@@ -195,6 +195,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewServiceAccountAuthzactory(sqlstore),
sqlmigration.NewDropUserDeletedAtFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateAWSAllRegionsFactory(sqlstore),
sqlmigration.NewAddSpanMapperFactory(sqlstore, sqlschema),
)
}

View File

@@ -0,0 +1,118 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addSpanMapper struct {
sqlschema sqlschema.SQLSchema
sqlstore sqlstore.SQLStore
}
func NewAddSpanMapperFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_span_mapper"), func(_ context.Context, _ factory.ProviderSettings, _ Config) (SQLMigration, error) {
return &addSpanMapper{sqlschema: sqlschema, sqlstore: sqlstore}, nil
})
}
func (migration *addSpanMapper) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addSpanMapper) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
sqls := [][]byte{}
groupSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "span_mapper_group",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "created_by", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "updated_by", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "condition", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "enabled", DataType: sqlschema.DataTypeBoolean, Nullable: false, Default: "true"},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("org_id"),
ReferencedTableName: sqlschema.TableName("organizations"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, groupSQLs...)
groupIdxSQLs := migration.sqlschema.Operator().CreateIndex(
&sqlschema.UniqueIndex{
TableName: "span_mapper_group",
ColumnNames: []sqlschema.ColumnName{"org_id", "name"},
})
sqls = append(sqls, groupIdxSQLs...)
mapperSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "span_mapper",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "created_by", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "updated_by", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "group_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "field_context", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "config", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "enabled", DataType: sqlschema.DataTypeBoolean, Nullable: false, Default: "true"},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("group_id"),
ReferencedTableName: sqlschema.TableName("span_mapper_group"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, mapperSQLs...)
mapperIdxSQLs := migration.sqlschema.Operator().CreateIndex(
&sqlschema.UniqueIndex{
TableName: "span_mapper",
ColumnNames: []sqlschema.ColumnName{"group_id", "name"},
})
sqls = append(sqls, mapperIdxSQLs...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *addSpanMapper) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -1,6 +1,8 @@
package spantypes
import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -69,9 +71,9 @@ type PostableSpanMapper struct {
// UpdatableSpanMapper is the HTTP request body for updating a span mapper.
// All fields are optional; only non-nil fields are applied.
type UpdatableSpanMapper struct {
FieldContext FieldContext `json:"field_context,omitempty"`
Config *SpanMapperConfig `json:"config,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
FieldContext FieldContext `json:"field_context"`
Config *SpanMapperConfig `json:"config"`
Enabled *bool `json:"enabled"`
}
type GettableSpanMapper = SpanMapper
@@ -88,7 +90,53 @@ func (SpanMapperOperation) Enum() []any {
return []any{SpanMapperOperationMove, SpanMapperOperationCopy}
}
func NewSpanMapperFromStorable(s *StorableSpanMapper) *SpanMapper {
func NewSpanMapper(groupID valuer.UUID, createdBy string, p *PostableSpanMapper) *SpanMapper {
now := time.Now()
return &SpanMapper{
ID: valuer.GenerateUUID(),
GroupID: groupID,
Name: p.Name,
FieldContext: p.FieldContext,
Config: p.Config,
Enabled: p.Enabled,
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
UpdatedAt: now,
},
UserAuditable: types.UserAuditable{
CreatedBy: createdBy,
UpdatedBy: createdBy,
},
}
}
func (m *SpanMapper) Update(u *UpdatableSpanMapper, updatedBy string) {
m.FieldContext = u.FieldContext
if u.Config != nil {
m.Config = *u.Config
}
if u.Enabled != nil {
m.Enabled = *u.Enabled
}
m.UpdatedAt = time.Now()
m.UpdatedBy = updatedBy
}
func (m *SpanMapper) ToStorable() *StorableSpanMapper {
return &StorableSpanMapper{
Identifiable: types.Identifiable{ID: m.ID},
TimeAuditable: m.TimeAuditable,
UserAuditable: m.UserAuditable,
GroupID: m.GroupID,
Name: m.Name,
FieldContext: m.FieldContext,
Config: m.Config,
Enabled: m.Enabled,
}
}
func (s *StorableSpanMapper) ToSpanMapper() *SpanMapper {
return &SpanMapper{
TimeAuditable: s.TimeAuditable,
UserAuditable: s.UserAuditable,
@@ -101,33 +149,10 @@ func NewSpanMapperFromStorable(s *StorableSpanMapper) *SpanMapper {
}
}
func NewSpanMapperFromPostable(req *PostableSpanMapper) *SpanMapper {
return &SpanMapper{
Name: req.Name,
FieldContext: req.FieldContext,
Config: req.Config,
Enabled: req.Enabled,
}
}
func NewSpanMapperFromUpdatable(req *UpdatableSpanMapper) *SpanMapper {
m := &SpanMapper{}
if req.FieldContext != (FieldContext{}) {
m.FieldContext = req.FieldContext
}
if req.Config != nil {
m.Config = *req.Config
}
if req.Enabled != nil {
m.Enabled = *req.Enabled
}
return m
}
func NewSpanMappersFromStorableSpanMappers(ss []*StorableSpanMapper) []*SpanMapper {
func NewSpanMappersFromStorable(ss []*StorableSpanMapper) []*SpanMapper {
mappers := make([]*SpanMapper, len(ss))
for i, s := range ss {
mappers[i] = NewSpanMapperFromStorable(s)
mappers[i] = s.ToSpanMapper()
}
return mappers
}

View File

@@ -1,6 +1,8 @@
package spantypes
import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -11,9 +13,6 @@ var (
ErrCodeMappingGroupAlreadyExists = errors.MustNewCode("span_attribute_mapping_group_already_exists")
)
// SpanMapperGroupCategory defaults will be llm, tool, agent but user can configure more as they want.
type SpanMapperGroupCategory valuer.String
// A group runs when any of the listed attribute/resource key patterns match.
type SpanMapperGroupCondition struct {
Attributes []string `json:"attributes" required:"true" nullable:"true"`
@@ -28,7 +27,6 @@ type SpanMapperGroup struct {
ID valuer.UUID `json:"id" required:"true"`
OrgID valuer.UUID `json:"orgId" required:"true"`
Name string `json:"name" required:"true"`
Category SpanMapperGroupCategory `json:"category" required:"true"`
Condition SpanMapperGroupCondition `json:"condition" required:"true"`
Enabled bool `json:"enabled" required:"true"`
}
@@ -38,7 +36,6 @@ type GettableSpanMapperGroup = SpanMapperGroup
type PostableSpanMapperGroup struct {
Name string `json:"name" required:"true"`
Category SpanMapperGroupCategory `json:"category" required:"true"`
Condition SpanMapperGroupCondition `json:"condition" required:"true"`
Enabled bool `json:"enabled"`
}
@@ -46,44 +43,39 @@ type PostableSpanMapperGroup struct {
// UpdatableSpanMapperGroup is the HTTP request body for updating a mapping group.
// All fields are optional; only non-nil fields are applied.
type UpdatableSpanMapperGroup struct {
Name *string `json:"name,omitempty"`
Condition *SpanMapperGroupCondition `json:"condition,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Name *string `json:"name" nullable:"true"`
Condition *SpanMapperGroupCondition `json:"condition" nullable:"true"`
Enabled *bool `json:"enabled" nullable:"true"`
}
type ListSpanMapperGroupsQuery struct {
Category *SpanMapperGroupCategory `query:"category"`
Enabled *bool `query:"enabled"`
Enabled *bool `query:"enabled"`
}
type GettableSpanMapperGroups struct {
Items []*GettableSpanMapperGroup `json:"items" required:"true" nullable:"false"`
}
func NewSpanMapperGroupFromStorable(s *StorableSpanMapperGroup) *SpanMapperGroup {
return &SpanMapperGroup{
TimeAuditable: s.TimeAuditable,
UserAuditable: s.UserAuditable,
ID: s.ID,
OrgID: s.OrgID,
Name: s.Name,
Category: s.Category,
Condition: s.Condition,
Enabled: s.Enabled,
}
}
func NewSpanMapperGroupFromPostable(p *PostableSpanMapperGroup) *SpanMapperGroup {
func NewSpanMapperGroup(orgID valuer.UUID, createdBy string, p *PostableSpanMapperGroup) *SpanMapperGroup {
now := time.Now()
return &SpanMapperGroup{
ID: valuer.GenerateUUID(),
OrgID: orgID,
Name: p.Name,
Category: p.Category,
Condition: p.Condition,
Enabled: p.Enabled,
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
UpdatedAt: now,
},
UserAuditable: types.UserAuditable{
CreatedBy: createdBy,
UpdatedBy: createdBy,
},
}
}
func NewSpanMapperGroupFromUpdatable(u *UpdatableSpanMapperGroup) *SpanMapperGroup {
g := &SpanMapperGroup{}
func (g *SpanMapperGroup) Update(u *UpdatableSpanMapperGroup, updatedBy string) {
if u.Name != nil {
g.Name = *u.Name
}
@@ -93,13 +85,38 @@ func NewSpanMapperGroupFromUpdatable(u *UpdatableSpanMapperGroup) *SpanMapperGro
if u.Enabled != nil {
g.Enabled = *u.Enabled
}
return g
g.UpdatedAt = time.Now()
g.UpdatedBy = updatedBy
}
func NewSpanMapperGroupsFromStorableGroups(ss []*StorableSpanMapperGroup) []*SpanMapperGroup {
func (g *SpanMapperGroup) ToStorable() *StorableSpanMapperGroup {
return &StorableSpanMapperGroup{
Identifiable: types.Identifiable{ID: g.ID},
TimeAuditable: g.TimeAuditable,
UserAuditable: g.UserAuditable,
OrgID: g.OrgID,
Name: g.Name,
Condition: g.Condition,
Enabled: g.Enabled,
}
}
func (s *StorableSpanMapperGroup) ToSpanMapperGroup() *SpanMapperGroup {
return &SpanMapperGroup{
TimeAuditable: s.TimeAuditable,
UserAuditable: s.UserAuditable,
ID: s.ID,
OrgID: s.OrgID,
Name: s.Name,
Condition: s.Condition,
Enabled: s.Enabled,
}
}
func NewSpanMapperGroupsFromStorable(ss []*StorableSpanMapperGroup) []*SpanMapperGroup {
groups := make([]*SpanMapperGroup, len(ss))
for i, s := range ss {
groups[i] = NewSpanMapperGroupFromStorable(s)
groups[i] = s.ToSpanMapperGroup()
}
return groups
}

View File

@@ -19,7 +19,6 @@ type StorableSpanMapperGroup struct {
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
Name string `bun:"name,type:text,notnull"`
Category SpanMapperGroupCategory `bun:"category,type:text,notnull"`
Condition SpanMapperGroupCondition `bun:"condition,type:jsonb,notnull"`
Enabled bool `bun:"enabled,notnull,default:true"`
}

View File

@@ -6,18 +6,18 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
type SpanMapperStore interface {
// Group operations
ListSpanMapperGroups(ctx context.Context, orgID valuer.UUID, q *ListSpanMapperGroupsQuery) ([]*StorableSpanMapperGroup, error)
GetSpanMapperGroup(ctx context.Context, orgID, id valuer.UUID) (*StorableSpanMapperGroup, error)
CreateSpanMapperGroup(ctx context.Context, group *StorableSpanMapperGroup) error
UpdateSpanMapperGroup(ctx context.Context, group *StorableSpanMapperGroup) error
DeleteSpanMapperGroup(ctx context.Context, orgID, id valuer.UUID) error
ListGroups(ctx context.Context, orgID valuer.UUID, q *ListSpanMapperGroupsQuery) ([]*SpanMapperGroup, error)
GetGroup(ctx context.Context, orgID, id valuer.UUID) (*SpanMapperGroup, error)
CreateGroup(ctx context.Context, group *SpanMapperGroup) error
UpdateGroup(ctx context.Context, group *SpanMapperGroup) error
DeleteGroup(ctx context.Context, orgID, id valuer.UUID) error
// Mapper operations
ListSpanMappers(ctx context.Context, orgID, groupID valuer.UUID) ([]*StorableSpanMapper, error)
GetSpanMapper(ctx context.Context, orgID, groupID, id valuer.UUID) (*StorableSpanMapper, error)
CreateSpanMapper(ctx context.Context, mapper *StorableSpanMapper) error
UpdateSpanMapper(ctx context.Context, mapper *StorableSpanMapper) error
DeleteSpanMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error
ListMappers(ctx context.Context, orgID, groupID valuer.UUID) ([]*SpanMapper, error)
GetMapper(ctx context.Context, orgID, groupID, id valuer.UUID) (*SpanMapper, error)
CreateMapper(ctx context.Context, mapper *SpanMapper) error
UpdateMapper(ctx context.Context, mapper *SpanMapper) error
DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error
}