Compare commits

..

1 Commits

Author SHA1 Message Date
Nikhil Soni
eb95364aba feat(alerts): add docs and agent skill info banner to ClickHouse query editor (#11262)
Some checks are pending
Release Drafter / update_release_draft (push) Waiting to run
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
* feat(alerts): add docs and agent skill info banner to ClickHouse query editor

Shows a contextual info banner when creating alert rules using ClickHouse
query mode, with doc links that vary by alert type (logs/traces/metrics).
Agent skill link is shown for logs and traces but skipped for metrics.

* chore: change allow referrer and add noopener
2026-05-14 19:21:55 +00:00
24 changed files with 82 additions and 292 deletions

View File

@@ -5,7 +5,6 @@ import (
"context"
"os"
"sort"
"strings"
"text/template"
"github.com/SigNoz/signoz/pkg/types/coretypes"
@@ -24,7 +23,6 @@ export default {
{
kind: '{{ .Kind }}',
type: '{{ .Type }}',
{{ .FormattedAllowedVerbs }}
},
{{- end }}
],
@@ -43,9 +41,8 @@ type permissionsTypeRelation struct {
}
type permissionsTypeResource struct {
Kind string
Type string
FormattedAllowedVerbs string
Kind string
Type string
}
type permissionsTypeData struct {
@@ -53,30 +50,6 @@ type permissionsTypeData struct {
Relations []permissionsTypeRelation
}
// formatAllowedVerbs returns a prettier-compatible formatted allowedVerbs line.
// indentLevel is the number of tabs for the property (matching kind/type indent).
// printWidth is prettier's printWidth; tabWidth is assumed to be 1 (each \t = 1 char).
func formatAllowedVerbs(verbs []string, indentLevel int, printWidth int) string {
quoted := make([]string, len(verbs))
for i, v := range verbs {
quoted[i] = "'" + v + "'"
}
indent := strings.Repeat("\t", indentLevel)
oneLine := indent + "allowedVerbs: [" + strings.Join(quoted, ", ") + "],"
if len(oneLine) <= printWidth {
return oneLine
}
var b strings.Builder
b.WriteString(indent + "allowedVerbs: [\n")
for _, q := range quoted {
b.WriteString(indent + "\t" + q + ",\n")
}
b.WriteString(indent + "],")
return b.String()
}
func registerGenerateAuthz(parentCmd *cobra.Command) {
authzCmd := &cobra.Command{
Use: "authz",
@@ -93,8 +66,8 @@ func runGenerateAuthz(_ context.Context) error {
registry := coretypes.NewRegistry()
allowedResources := map[string]bool{
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
}
@@ -107,23 +80,9 @@ func runGenerateAuthz(_ context.Context) error {
continue
}
allowedTypes[ref.Type.StringValue()] = true
resource, err := coretypes.NewResourceFromTypeAndKind(ref.Type, ref.Kind)
if err != nil {
return err
}
verbs := resource.AllowedVerbs()
allowedVerbStrings := make([]string, 0, len(verbs))
for _, verb := range verbs {
allowedVerbStrings = append(allowedVerbStrings, verb.StringValue())
}
sort.Strings(allowedVerbStrings)
resources = append(resources, permissionsTypeResource{
Kind: ref.Kind.String(),
Type: ref.Type.StringValue(),
FormattedAllowedVerbs: formatAllowedVerbs(allowedVerbStrings, 4, 80),
Kind: ref.Kind.String(),
Type: ref.Type.StringValue(),
})
}

View File

@@ -54,9 +54,6 @@ type metaresource
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
define detach: [user, serviceaccount, role#assignee]
define block: [user, serviceaccount, role#assignee]

View File

@@ -1,16 +1,65 @@
import { Callout } from '@signozhq/ui/callout';
import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/query';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import DOCLINKS from 'utils/docLinks';
function ChQuerySection(): JSX.Element {
import 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/ClickHouse.styles.scss';
const ALERT_TYPE_DOC_LINK: Partial<Record<AlertTypes, string>> = {
[AlertTypes.LOGS_BASED_ALERT]: DOCLINKS.QUERY_CLICKHOUSE_LOGS,
[AlertTypes.TRACES_BASED_ALERT]: DOCLINKS.QUERY_CLICKHOUSE_TRACES,
[AlertTypes.EXCEPTIONS_BASED_ALERT]: DOCLINKS.QUERY_CLICKHOUSE_TRACES,
[AlertTypes.METRICS_BASED_ALERT]: DOCLINKS.QUERY_CLICKHOUSE_METRICS,
};
const ALERT_TYPES_WITH_AGENT_SKILL: AlertTypes[] = [
AlertTypes.LOGS_BASED_ALERT,
AlertTypes.TRACES_BASED_ALERT,
AlertTypes.EXCEPTIONS_BASED_ALERT,
];
interface ChQuerySectionProps {
alertType: AlertTypes;
}
function ChQuerySection({ alertType }: ChQuerySectionProps): JSX.Element {
const { currentQuery } = useQueryBuilder();
const docLink = ALERT_TYPE_DOC_LINK[alertType];
const showAgentSkill = ALERT_TYPES_WITH_AGENT_SKILL.includes(alertType);
return (
<ClickHouseQueryBuilder
key="A"
queryIndex={0}
queryData={currentQuery.clickhouse_sql[0]}
deletable={false}
/>
<>
{docLink && (
<div className="info-banner-wrapper">
<Callout
type="info"
showIcon
title={
<span>
<a href={docLink} target="_blank" rel="noopener">
Learn to write faster, optimized queries
</a>
{showAgentSkill && (
<>
{' · Using AI? '}
<a href={DOCLINKS.AGENT_SKILL_INSTALL} target="_blank" rel="noopener">
Install the SigNoz ClickHouse query agent skill
</a>
</>
)}
</span>
}
/>
</div>
)}
<ClickHouseQueryBuilder
key="A"
queryIndex={0}
queryData={currentQuery.clickhouse_sql[0]}
deletable={false}
/>
</>
);
}

View File

@@ -56,7 +56,9 @@ function QuerySection({
const renderPromqlUI = (): JSX.Element => <PromqlSection />;
const renderChQueryUI = (): JSX.Element => <ChQuerySection />;
const renderChQueryUI = (): JSX.Element => (
<ChQuerySection alertType={alertType} />
);
const isDarkMode = useIsDarkMode();

View File

@@ -26,12 +26,12 @@ function ClickHouseQueryContainer(): JSX.Element | null {
<a
href={DOCLINKS.QUERY_CLICKHOUSE_TRACES}
target="_blank"
rel="noreferrer"
rel="noopener"
>
Learn to write faster, optimized queries
</a>
{' · Using AI? '}
<a href={DOCLINKS.AGENT_SKILL_INSTALL} target="_blank" rel="noreferrer">
<a href={DOCLINKS.AGENT_SKILL_INSTALL} target="_blank" rel="noopener">
Install the SigNoz ClickHouse query agent skill
</a>
</span>

View File

@@ -118,14 +118,10 @@ export function buildPatchPayload({
for (const res of resources) {
const initial = initialConfig[res.id];
const current = newConfig[res.id];
const found = authzRes.resources.find((r) => r.kind === res.id);
if (!found) {
const resourceDef = authzRes.resources.find((r) => r.kind === res.id);
if (!resourceDef) {
continue;
}
const resourceDef: CoretypesResourceRefDTO = {
kind: found.kind,
type: found.type,
};
const initialScope = initial?.scope ?? PermissionScope.ONLY_SELECTED;
const currentScope = current?.scope ?? PermissionScope.ONLY_SELECTED;

View File

@@ -6,34 +6,14 @@ export default {
{
kind: 'factor-api-key',
type: 'metaresource',
allowedVerbs: ['create', 'delete', 'list', 'read', 'update'],
},
{
kind: 'role',
type: 'role',
allowedVerbs: [
'assignee',
'attach',
'create',
'delete',
'detach',
'list',
'read',
'update',
],
},
{
kind: 'serviceaccount',
type: 'serviceaccount',
allowedVerbs: [
'attach',
'create',
'delete',
'detach',
'list',
'read',
'update',
],
},
],
relations: {

View File

@@ -10,6 +10,10 @@ const DOCLINKS = {
'https://signoz.io/docs/external-api-monitoring/overview/',
QUERY_CLICKHOUSE_TRACES:
'https://signoz.io/docs/userguide/writing-clickhouse-traces-query/#timestamp-bucketing-for-distributed_signoz_index_v3',
QUERY_CLICKHOUSE_LOGS:
'https://signoz.io/docs/userguide/logs_clickhouse_queries/',
QUERY_CLICKHOUSE_METRICS:
'https://signoz.io/docs/userguide/write-a-metrics-clickhouse-query/',
AGENT_SKILL_INSTALL: 'https://signoz.io/docs/ai/agent-skills/#installation',
};

View File

@@ -202,7 +202,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddLLMPricingRulesFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateMetaresourcesTuplesFactory(sqlstore),
sqlmigration.NewAddTagsFactory(sqlstore, sqlschema),
sqlmigration.NewAddRoleCRUDTuplesFactory(sqlstore),
)
}

View File

@@ -1,139 +0,0 @@
package sqlmigration
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addRoleCRUDTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddRoleCRUDTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_role_crud_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addRoleCRUDTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addRoleCRUDTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addRoleCRUDTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
var orgIDs []string
rows, err := tx.QueryContext(ctx, `SELECT id FROM organizations`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var orgID string
if err := rows.Scan(&orgID); err != nil {
return err
}
orgIDs = append(orgIDs, orgID)
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
// Migration 081 moved role tuples from "metaresources" to "role" type but
// only inserted create and list. The read, update, and delete tuples were
// lost in the migration. Re-add them here.
tuples := []migrationTuple{
{authtypes.SigNozAdminRoleName, "role", "role", "read"},
{authtypes.SigNozAdminRoleName, "role", "role", "update"},
{authtypes.SigNozAdminRoleName, "role", "role", "delete"},
}
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "TUPLE_OPERATION_WRITE", tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
return tx.Commit()
}
func (migration *addRoleCRUDTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -25,7 +25,7 @@ type TransactionWithAuthorization struct {
}
func NewTransaction(relation Relation, object coretypes.Object) (*Transaction, error) {
if err := coretypes.ErrIfVerbNotValidForResource(relation.Verb, object.Resource); err != nil {
if err := coretypes.ErrIfVerbNotValidForType(relation.Verb, object.Resource.Type); err != nil {
return nil, err
}

View File

@@ -129,13 +129,13 @@ func NewPatchableObjects(additions []*ObjectGroup, deletions []*ObjectGroup, ver
}
for _, objectGroup := range additions {
if err := ErrIfVerbNotValidForResource(verb, objectGroup.Resource); err != nil {
if err := ErrIfVerbNotValidForType(verb, objectGroup.Resource.Type); err != nil {
return nil, nil, err
}
}
for _, objectGroup := range deletions {
if err := ErrIfVerbNotValidForResource(verb, objectGroup.Resource); err != nil {
if err := ErrIfVerbNotValidForType(verb, objectGroup.Resource.Type); err != nil {
return nil, nil, err
}
}

View File

@@ -68,7 +68,7 @@ var (
ResourceMetaResourceSavedView = NewResourceMetaResource(KindSavedView)
ResourceMetaResourceTraceFunnel = NewResourceMetaResource(KindTraceFunnel)
ResourceMetaResourceFactorPassword = NewResourceMetaResource(KindFactorPassword)
ResourceMetaResourceFactorAPIKey = NewResourceMetaResource(KindFactorAPIKey, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete)
ResourceMetaResourceFactorAPIKey = NewResourceMetaResource(KindFactorAPIKey)
ResourceMetaResourceLicense = NewResourceMetaResource(KindLicense)
ResourceMetaResourceSubscription = NewResourceMetaResource(KindSubscription)
ResourceTelemetryResourceLogs = NewResourceTelemetryResource(KindLogs)

View File

@@ -21,7 +21,7 @@ var (
TypeServiceAccount = Type{valuer.NewString("serviceaccount"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
TypeAnonymous = Type{valuer.NewString("anonymous"), regexp.MustCompile(`^\*$`), []Verb{}}
TypeRole = Type{valuer.NewString("role"), regexp.MustCompile(`^([a-z-]{1,50}|\*)$`), []Verb{VerbAssignee, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
TypeOrganization = Type{valuer.NewString("organization"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbRead, VerbUpdate}}
TypeOrganization = Type{valuer.NewString("organization"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbRead, VerbUpdate, VerbDelete}}
TypeMetaResource = Type{valuer.NewString("metaresource"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
TypeTelemetryResource = Type{valuer.NewString("telemetryresource"), regexp.MustCompile(`^\*$`), []Verb{VerbRead}}
)

View File

@@ -19,12 +19,6 @@ type Resource interface {
// Scope of the resource.
Scope(verb Verb) string
// AllowedVerbs returns the verbs that are valid for this resource.
// By default, this delegates to the type's allowed verbs, but specific
// resources can restrict the set further (e.g., some metaresource kinds
// may not support attach/detach).
AllowedVerbs() []Verb
}
type ResourceRef struct {

View File

@@ -34,7 +34,3 @@ func (resourceAnonymous *resourceAnonymous) Object(orgID valuer.UUID, selector s
func (resourceAnonymous *resourceAnonymous) Scope(verb Verb) string {
return resourceAnonymous.Kind().String() + ":" + verb.StringValue()
}
func (*resourceAnonymous) AllowedVerbs() []Verb {
return TypeAnonymous.AllowedVerbs()
}

View File

@@ -5,15 +5,11 @@ import (
)
type resourceMetaResource struct {
kind Kind
allowedVerbs []Verb
kind Kind
}
func NewResourceMetaResource(kind Kind, allowedVerbs ...Verb) Resource {
if len(allowedVerbs) == 0 {
allowedVerbs = TypeMetaResource.AllowedVerbs()
}
return &resourceMetaResource{kind: kind, allowedVerbs: allowedVerbs}
func NewResourceMetaResource(kind Kind) Resource {
return &resourceMetaResource{kind: kind}
}
func (*resourceMetaResource) Type() Type {
@@ -36,7 +32,3 @@ func (resourceMetaResource *resourceMetaResource) Object(orgID valuer.UUID, sele
func (resourceMetaResource *resourceMetaResource) Scope(verb Verb) string {
return resourceMetaResource.Kind().String() + ":" + verb.StringValue()
}
func (resourceMetaResource *resourceMetaResource) AllowedVerbs() []Verb {
return resourceMetaResource.allowedVerbs
}

View File

@@ -33,7 +33,3 @@ func (resourceOrganization *resourceOrganization) Object(orgID valuer.UUID, sele
func (resourceOrganization *resourceOrganization) Scope(verb Verb) string {
return resourceOrganization.Kind().String() + ":" + verb.StringValue()
}
func (*resourceOrganization) AllowedVerbs() []Verb {
return TypeOrganization.AllowedVerbs()
}

View File

@@ -34,7 +34,3 @@ func (resourceRole *resourceRole) Object(orgID valuer.UUID, selector string) str
func (resourceRole *resourceRole) Scope(verb Verb) string {
return resourceRole.Kind().String() + ":" + verb.StringValue()
}
func (*resourceRole) AllowedVerbs() []Verb {
return TypeRole.AllowedVerbs()
}

View File

@@ -34,7 +34,3 @@ func (resourceServiceAccount *resourceServiceAccount) Object(orgID valuer.UUID,
func (resourceServiceAccount *resourceServiceAccount) Scope(verb Verb) string {
return resourceServiceAccount.Kind().String() + ":" + verb.StringValue()
}
func (*resourceServiceAccount) AllowedVerbs() []Verb {
return TypeServiceAccount.AllowedVerbs()
}

View File

@@ -32,7 +32,3 @@ func (resourceTelemetryResource *resourceTelemetryResource) Object(orgID valuer.
func (resourceTelemetryResource *resourceTelemetryResource) Scope(verb Verb) string {
return resourceTelemetryResource.Kind().String() + ":" + verb.StringValue()
}
func (*resourceTelemetryResource) AllowedVerbs() []Verb {
return TypeTelemetryResource.AllowedVerbs()
}

View File

@@ -34,7 +34,3 @@ func (resourceUser *resourceUser) Object(orgID valuer.UUID, selector string) str
func (resourceUser *resourceUser) Scope(verb Verb) string {
return resourceUser.Kind().String() + ":" + verb.StringValue()
}
func (*resourceUser) AllowedVerbs() []Verb {
return TypeUser.AllowedVerbs()
}

View File

@@ -6,7 +6,7 @@ type Transaction struct {
}
func NewTransaction(verb Verb, object Object) (*Transaction, error) {
if err := ErrIfVerbNotValidForResource(verb, object.Resource); err != nil {
if err := ErrIfVerbNotValidForType(verb, object.Resource.Type); err != nil {
return nil, err
}

View File

@@ -56,25 +56,6 @@ func ErrIfVerbNotValidForType(verb Verb, typed Type) error {
return nil
}
func ErrIfVerbNotValidForResource(verb Verb, ref ResourceRef) error {
if err := ErrIfVerbNotValidForType(verb, ref.Type); err != nil {
return err
}
resource, err := NewResourceFromTypeAndKind(ref.Type, ref.Kind)
if err != nil {
return err
}
for _, allowed := range resource.AllowedVerbs() {
if verb == allowed {
return nil
}
}
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidVerbForType, "verb %s is not valid for resource %s:%s", verb.StringValue(), ref.Type.StringValue(), ref.Kind.String())
}
func (typed *Type) UnmarshalJSON(data []byte) error {
str := ""
err := json.Unmarshal(data, &str)