mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-24 23:10:39 +01:00
Compare commits
2 Commits
issue_5721
...
v0.134.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c6a0de8c3 | ||
|
|
c7abe0fe7b |
@@ -1496,7 +1496,6 @@ components:
|
||||
- redis
|
||||
- cloudsql_postgres
|
||||
- memorystore_redis
|
||||
- computeengine
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
|
||||
@@ -2815,7 +2815,6 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
redis = 'redis',
|
||||
cloudsql_postgres = 'cloudsql_postgres',
|
||||
memorystore_redis = 'memorystore_redis',
|
||||
computeengine = 'computeengine',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#aecbfa;}.cls-2{fill:#669df6;}.cls-3{fill:#4285f4;}</style></defs><title>Icon_24px_ComputeEngine_Color</title><g data-name="Product Icons"><rect class="cls-1" x="9" y="9" width="6" height="6"/><rect class="cls-2" x="11" y="2" width="2" height="4"/><rect class="cls-2" x="7" y="2" width="2" height="4"/><rect class="cls-2" x="15" y="2" width="2" height="4"/><rect class="cls-3" x="11" y="18" width="2" height="4"/><rect class="cls-3" x="7" y="18" width="2" height="4"/><rect class="cls-3" x="15" y="18" width="2" height="4"/><rect class="cls-3" x="19" y="10" width="2" height="4" transform="translate(8 32) rotate(-90)"/><rect class="cls-3" x="19" y="14" width="2" height="4" transform="translate(4 36) rotate(-90)"/><rect class="cls-3" x="19" y="6" width="2" height="4" transform="translate(12 28) rotate(-90)"/><rect class="cls-2" x="3" y="10" width="2" height="4" transform="translate(-8 16) rotate(-90)"/><rect class="cls-2" x="3" y="14" width="2" height="4" transform="translate(-12 20) rotate(-90)"/><rect class="cls-2" x="3" y="6" width="2" height="4" transform="translate(-4 12) rotate(-90)"/><path class="cls-1" d="M5,5V19H19V5ZM17,17H7V7H17Z"/><polygon class="cls-2" points="9 15 15 15 12 12 9 15"/><polygon class="cls-3" points="12 12 15 15 15 9 12 12"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,124 +0,0 @@
|
||||
{
|
||||
"id": "computeengine",
|
||||
"title": "GCP Compute Engine",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/uptime_total",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/cpu/utilization",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/guest/memory/bytes_used",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/guest/memory/percent_used",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "agent.googleapis.com/memory/percent_used",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/average_io_latency",
|
||||
"unit": "Microseconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/read_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/write_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/read_ops_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/write_ops_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/performance_status",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/received_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/sent_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/received_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/sent_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/firewall/dropped_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "GCP Compute Engine Overview",
|
||||
"description": "Overview of GCP Compute Engine metrics",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
### Monitor GCP Compute Engine with SigNoz
|
||||
|
||||
Collect key GCP Compute Engine metrics and view them with an out of the box dashboard.
|
||||
@@ -238,7 +238,7 @@
|
||||
}
|
||||
},
|
||||
"tags": [],
|
||||
"title": "Signoz GCP Memorystory Redis overview",
|
||||
"title": "Signoz GCP Memorystore Redis overview",
|
||||
"uploadedGrafana": false,
|
||||
"uuid": "019f85e9-3846-7dc9-9bce-b626b8a3eaff",
|
||||
"variables": {
|
||||
@@ -262,7 +262,7 @@
|
||||
"type": "DYNAMIC"
|
||||
},
|
||||
"dabcf41a-4121-4a03-8738-927576cc90bf": {
|
||||
"allSelected": true,
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"defaultValue": "",
|
||||
"description": "GCP Project ID",
|
||||
@@ -390,7 +390,7 @@
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": "rate({\"redis.googleapis.com/server/uptime\", project_id=\"$project_id\"}[5m])"
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder",
|
||||
|
||||
@@ -45,17 +45,17 @@ func (m *module) createMany(ctx context.Context, orgID valuer.UUID, kind coretyp
|
||||
return []*tagtypes.Tag{}, nil
|
||||
}
|
||||
|
||||
toCreate, matched, err := m.resolve(ctx, orgID, kind, postable)
|
||||
ordered, toCreate, err := m.resolve(ctx, orgID, kind, postable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
created, err := m.store.CreateOrGet(ctx, toCreate)
|
||||
if err != nil {
|
||||
// CreateOrGet fills new rows' IDs in place; ordered shares those pointers.
|
||||
if _, err := m.store.CreateOrGet(ctx, toCreate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(matched, created...), nil
|
||||
return ordered, nil
|
||||
}
|
||||
|
||||
func (m *module) syncLinksForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) error {
|
||||
|
||||
@@ -13,10 +13,9 @@ import (
|
||||
// the existing tags for an org. Lookup is case-insensitive on both key and
|
||||
// value (matching the storage uniqueness rule); when an existing row matches,
|
||||
// its display casing is reused. Inputs are deduped on (LOWER(key), LOWER(value));
|
||||
// the first input's casing wins on collisions. Returns:
|
||||
// - toCreate: new Tag rows the caller should insert (with pre-generated IDs)
|
||||
// - matched: existing rows the caller's input already pointed to. They
|
||||
// already carry authoritative IDs from the store.
|
||||
// the first input's casing wins on collisions. Returns the resolved tags in
|
||||
// request order (deduped) plus the new subset to insert; the new tags share
|
||||
// pointers with the ordered slice, so their IDs populate in place on insert.
|
||||
func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, []*tagtypes.Tag, error) {
|
||||
if len(postable) == 0 {
|
||||
return nil, nil, nil
|
||||
@@ -34,8 +33,8 @@ func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.
|
||||
}
|
||||
|
||||
seenInRequestAlready := make(map[string]struct{}, len(postable)) // postable can have the same tag multiple times
|
||||
ordered := make([]*tagtypes.Tag, 0, len(postable))
|
||||
toCreate := make([]*tagtypes.Tag, 0)
|
||||
matched := make([]*tagtypes.Tag, 0)
|
||||
|
||||
for _, p := range postable {
|
||||
key, value, err := tagtypes.ValidatePostableTag(p)
|
||||
@@ -49,11 +48,13 @@ func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.
|
||||
seenInRequestAlready[lookup] = struct{}{}
|
||||
|
||||
if existingTag, ok := lowercaseTagsMap[lookup]; ok {
|
||||
matched = append(matched, existingTag)
|
||||
ordered = append(ordered, existingTag)
|
||||
continue
|
||||
}
|
||||
toCreate = append(toCreate, tagtypes.NewTag(orgID, kind, key, value))
|
||||
newTag := tagtypes.NewTag(orgID, kind, key, value)
|
||||
ordered = append(ordered, newTag)
|
||||
toCreate = append(toCreate, newTag)
|
||||
}
|
||||
|
||||
return toCreate, matched, nil
|
||||
return ordered, toCreate, nil
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
toCreate, matched, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, nil)
|
||||
ordered, toCreate, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, ordered)
|
||||
assert.Empty(t, toCreate)
|
||||
assert.Empty(t, matched)
|
||||
assert.Zero(t, store.ListCallCount, "should not hit store when input is empty")
|
||||
})
|
||||
|
||||
t.Run("creates missing pairs and reuses existing", func(t *testing.T) {
|
||||
t.Run("creates missing pairs and reuses existing, in request order", func(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
dbTag := tagtypes.NewTag(orgID, testKind, "team", "Pulse")
|
||||
dbTag2 := tagtypes.NewTag(orgID, testKind, "Database", "redis")
|
||||
@@ -35,22 +35,28 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store.Tags = []*tagtypes.Tag{dbTag, dbTag2}
|
||||
m := &module{store: store}
|
||||
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "events"}, // new
|
||||
{Key: "DATABASE", Value: "REDIS"}, // case-only conflict
|
||||
{Key: "Brand", Value: "New"}, // new
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// ordered mirrors the request: new, existing (reused pointer), new.
|
||||
require.Len(t, ordered, 3)
|
||||
assert.Equal(t, "team", ordered[0].Key)
|
||||
assert.Equal(t, "events", ordered[0].Value)
|
||||
assert.Same(t, dbTag2, ordered[1], "case-only conflict reuses the existing pointer with its authoritative ID")
|
||||
assert.Equal(t, "Brand", ordered[2].Key)
|
||||
assert.Equal(t, "New", ordered[2].Value)
|
||||
|
||||
createdLowerKVs := []string{}
|
||||
for _, tg := range toCreate {
|
||||
createdLowerKVs = append(createdLowerKVs, strings.ToLower(tg.Key)+"\x00"+strings.ToLower(tg.Value))
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"team\x00events", "brand\x00new"}, createdLowerKVs,
|
||||
"only the two missing pairs should be returned for insertion")
|
||||
|
||||
require.Len(t, matched, 1, "DATABASE:REDIS should hit the existing 'Database:redis' tag")
|
||||
assert.Same(t, dbTag2, matched[0], "matched should return the existing pointer with its authoritative ID")
|
||||
assert.Same(t, ordered[0], toCreate[0], "toCreate shares pointers with ordered so inserted IDs propagate")
|
||||
})
|
||||
|
||||
t.Run("dedupes inputs that map to the same lower(key)+lower(value)", func(t *testing.T) {
|
||||
@@ -58,14 +64,14 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "Foo", Value: "Bar"},
|
||||
{Key: "foo", Value: "bar"},
|
||||
{Key: "FOO", Value: "BAR"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, matched)
|
||||
require.Len(t, ordered, 1, "duplicate inputs must collapse into a single tag")
|
||||
require.Len(t, toCreate, 1, "duplicate inputs must collapse into a single insert")
|
||||
assert.Equal(t, "Foo", toCreate[0].Key, "first input's casing wins")
|
||||
assert.Equal(t, "Bar", toCreate[0].Value, "first input's casing wins")
|
||||
@@ -78,15 +84,15 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store.Tags = []*tagtypes.Tag{dbTag}
|
||||
m := &module{store: store}
|
||||
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "PULSE"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, toCreate)
|
||||
require.Len(t, matched, 1)
|
||||
assert.Equal(t, "Team", matched[0].Key)
|
||||
assert.Equal(t, "Pulse", matched[0].Value)
|
||||
require.Len(t, ordered, 1)
|
||||
assert.Equal(t, "Team", ordered[0].Key)
|
||||
assert.Equal(t, "Pulse", ordered[0].Value)
|
||||
})
|
||||
|
||||
t.Run("propagates validation error from any input", func(t *testing.T) {
|
||||
|
||||
@@ -44,6 +44,7 @@ func (s *store) ListByResource(ctx context.Context, orgID valuer.UUID, kind core
|
||||
Where("tr.kind = ?", kind).
|
||||
Where("tr.resource_id = ?", resourceID).
|
||||
Where("tag.org_id = ?", orgID).
|
||||
OrderExpr("tr.rank ASC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -71,6 +72,7 @@ func (s *store) ListByResources(ctx context.Context, orgID valuer.UUID, kind cor
|
||||
Where("tr.kind = ?", kind).
|
||||
Where("tr.resource_id IN (?)", bun.In(resourceIDs)).
|
||||
Where("tag.org_id = ?", orgID).
|
||||
OrderExpr("tr.rank ASC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -112,11 +114,14 @@ func (s *store) CreateRelations(ctx context.Context, relations []*tagtypes.TagRe
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
// On re-link (same tag, same resource) overwrite rank with the incoming
|
||||
// position, so a reordered tag set persists its new order.
|
||||
_, err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(&relations).
|
||||
On("CONFLICT (kind, resource_id, tag_id) DO NOTHING").
|
||||
On("CONFLICT (kind, resource_id, tag_id) DO UPDATE").
|
||||
Set("rank = EXCLUDED.rank").
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddRoleTransactionGroupsFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTagUniqueIndexFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTelemetryTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddTagRelationRankFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
72
pkg/sqlmigration/102_add_tag_relation_rank.go
Normal file
72
pkg/sqlmigration/102_add_tag_relation_rank.go
Normal file
@@ -0,0 +1,72 @@
|
||||
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 addTagRelationRank struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddTagRelationRankFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_tag_relation_rank"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addTagRelationRank{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("tag_relation"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rankColumn := &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName("rank"),
|
||||
DataType: sqlschema.DataTypeInteger,
|
||||
Nullable: false,
|
||||
}
|
||||
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, rankColumn, 0)
|
||||
|
||||
// AddColumn recreates the table for a NOT NULL column on SQLite, which drops
|
||||
// standalone indices. GetTable only reports inline table constraints, so
|
||||
// restore the standalone unique index from migration 082.
|
||||
sqls = append(sqls, migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{
|
||||
TableName: sqlschema.TableName("tag_relation"),
|
||||
ColumnNames: []sqlschema.ColumnName{"kind", "resource_id", "tag_id"},
|
||||
})...)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -43,7 +43,6 @@ var (
|
||||
// GCP services.
|
||||
GCPServiceCloudSQLPostgres = ServiceID{valuer.NewString("cloudsql_postgres")}
|
||||
GCPServiceMemorystoreRedis = ServiceID{valuer.NewString("memorystore_redis")}
|
||||
GCPServiceComputeEngine = ServiceID{valuer.NewString("computeengine")}
|
||||
)
|
||||
|
||||
func (ServiceID) Enum() []any {
|
||||
@@ -77,7 +76,6 @@ func (ServiceID) Enum() []any {
|
||||
AzureServiceRedis,
|
||||
GCPServiceCloudSQLPostgres,
|
||||
GCPServiceMemorystoreRedis,
|
||||
GCPServiceComputeEngine,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +115,6 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
|
||||
CloudProviderTypeGCP: {
|
||||
GCPServiceCloudSQLPostgres,
|
||||
GCPServiceMemorystoreRedis,
|
||||
GCPServiceComputeEngine,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -16,23 +16,27 @@ type TagRelation struct {
|
||||
Kind coretypes.Kind `json:"kind" required:"true" bun:"kind,type:text,notnull"`
|
||||
ResourceID valuer.UUID `json:"resourceId" required:"true" bun:"resource_id,type:text,notnull"`
|
||||
TagID valuer.UUID `json:"tagId" required:"true" bun:"tag_id,type:text,notnull"`
|
||||
CreatedAt time.Time `json:"createdAt" bun:"created_at,notnull"`
|
||||
// Rank is the tag's position within its resource; reads order by it.
|
||||
Rank int `json:"rank" bun:"rank,notnull"`
|
||||
CreatedAt time.Time `json:"createdAt" bun:"created_at,notnull"`
|
||||
}
|
||||
|
||||
func NewTagRelation(kind coretypes.Kind, resourceID valuer.UUID, tagID valuer.UUID) *TagRelation {
|
||||
func NewTagRelation(kind coretypes.Kind, resourceID valuer.UUID, tagID valuer.UUID, rank int) *TagRelation {
|
||||
return &TagRelation{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
Kind: kind,
|
||||
ResourceID: resourceID,
|
||||
TagID: tagID,
|
||||
Rank: rank,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTagRelations ranks each tag by its position so reads preserve order.
|
||||
func NewTagRelations(kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) []*TagRelation {
|
||||
relations := make([]*TagRelation, 0, len(tagIDs))
|
||||
for _, tagID := range tagIDs {
|
||||
relations = append(relations, NewTagRelation(kind, resourceID, tagID))
|
||||
for rank, tagID := range tagIDs {
|
||||
relations = append(relations, NewTagRelation(kind, resourceID, tagID, rank))
|
||||
}
|
||||
return relations
|
||||
}
|
||||
|
||||
@@ -570,7 +570,11 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
assert alpha["schemaVersion"] == "v6"
|
||||
assert alpha["source"] == "user"
|
||||
assert alpha["locked"] is False
|
||||
assert {"key": "team", "value": "pulse"} in alpha["tags"]
|
||||
# tags round-trip in the order sent — no reordering, no drift
|
||||
assert alpha["tags"] == [
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
]
|
||||
|
||||
# ── stage 3: list everything ─────────────────────────────────────────────
|
||||
response = requests.get(
|
||||
@@ -590,6 +594,13 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
"Epsilon Metrics",
|
||||
"Zeta Overview",
|
||||
}
|
||||
# per-dashboard tags also round-trip in the order sent
|
||||
delta = next(d for d in body["data"]["dashboards"] if d["spec"]["display"]["name"] == "Delta Storage")
|
||||
assert delta["tags"] == [
|
||||
{"key": "team", "value": "storage"},
|
||||
{"key": "env", "value": "dev"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
]
|
||||
# top-level tags = org-wide distinct tag set, sorted case-insensitively
|
||||
# by (key, value). Asserting the exact list (not a set) locks in the sort.
|
||||
assert body["data"]["tags"] == [
|
||||
@@ -1013,6 +1024,99 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
assert response.json()["data"]["id"] == clone["id"]
|
||||
|
||||
|
||||
def test_dashboard_v2_tag_order_round_trips(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Tags must round-trip in the order the client sent them across every write
|
||||
path — create, update, patch — so GitOps/Terraform state never drifts."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
def tags_of(response: requests.Response) -> list[dict]:
|
||||
return response.json()["data"]["tags"]
|
||||
|
||||
# ── create with tags in a deliberate order; "env" repeats with two values ─
|
||||
created_order = [
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
]
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}}, "tags": created_order},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
dashboard_id = response.json()["data"]["id"]
|
||||
assert tags_of(response) == created_order
|
||||
|
||||
# ── get echoes the same order ────────────────────────────────────────────
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == created_order
|
||||
|
||||
# ── update reordered; the two "env" values land at non-adjacent positions ─
|
||||
reordered = [
|
||||
{"key": "tier", "value": "critical"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
]
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}}, "tags": reordered},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == reordered
|
||||
|
||||
# ── patch appends a new tag (a third "env" value); it lands at the end ────
|
||||
response = requests.patch(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json=[{"op": "add", "path": "/tags/-", "value": {"key": "env", "value": "dev"}}],
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == reordered + [{"key": "env", "value": "dev"}]
|
||||
|
||||
# ── update reorders everything and adds another new tag → all in the new order ─
|
||||
# "env" now carries three interleaved values
|
||||
new_order = [
|
||||
{"key": "env", "value": "prod"},
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "dev"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "team", "value": "storage"},
|
||||
]
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}}, "tags": new_order},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == new_order
|
||||
|
||||
# ── and the final order persists on a fresh read ─────────────────────────
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == new_order
|
||||
|
||||
|
||||
def test_dashboard_v2_pin_limit(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
|
||||
Reference in New Issue
Block a user