Compare commits

...

36 Commits

Author SHA1 Message Date
SagarRajput-7
1b073e03cb fix: dummy test pr for stylus build failure test 2025-07-23 13:00:29 +05:30
Yunus M
a576982497 feat: update app loading screen and add system theme option (#8567)
* feat: update app loading screen and add system theme option

* feat: update test case
2025-07-22 20:13:07 +00:00
Alpcan Yıldız
55eadf914b fix: prevent 1Password extension from interfering with time inputs (#8486)
- Add data-1p-ignore attribute to CustomTimePicker input
- Add data-1p-ignore attribute to RangePickerModal DatePicker
- Add data-1p-ignore attribute to TimezonePicker search input

This prevents 1Password extension from automatically opening when
users interact with time-related input fields, improving UX.

Fixes #8485
2025-07-22 19:34:34 +00:00
Shaheer Kochai
b91407416b feat: add support for single step funnels while creating from span details (#8492)
* feat: add support for single step funnels while creating from span details

* fix: fix the UI for loading state
2025-07-22 13:13:09 +00:00
aniketio-ctrl
24d6d83575 fix(prom-dup-labels): added fingerprint in prom labels (#8563)
* fix(prom-dup-labels): added fingerprint in prom labels

* fix(prom-dup-labels): removed fingerprint labels from result series

* fix(prom-dup-labels): removed fingerprint labels from result series

* fix(prom-dup-labels): removed fingerprint labels from result series

* fix(prom-dup-labels): removed fingerprint labels from result series

* fix(prom-dup-labels): added test cases

* Update pkg/prometheus/clickhouseprometheus/client_query_test.go

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix(prom-dup-labels): added test cases

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-22 10:39:50 +00:00
Sahil Khan
fe95ee716a fix: added attribute check for log details filtering (#8427)
* fix: added attribute check for log details filtering

* chore: added unit tests

* chore: add the missing args to onClickHandler to fix the failing build

---------

Co-authored-by: ahmadshaheer <ashaheerki@gmail.com>
2025-07-22 09:21:07 +00:00
Vibhu Pandey
b053ce23cd ci(prereleaser): remove cron scheduler (#8584) 2025-07-22 09:03:18 +00:00
Amlan Kumar Nandy
57febd2f52 fix: navigating from infra monitoring to logs with open in explorer has missing filters (#8570) 2025-07-22 15:51:48 +07:00
Abhi kumar
ba6a1c594b fix: use only needed resource attributes in context filter (#8568)
* chore: use only needed resource attributes in context filter

* chore: moved regex to constants
2025-07-21 18:37:32 +05:30
Amlan Kumar Nandy
6afdecbd0f chore: add unit tests for metric details drawer (#8515) 2025-07-21 04:45:56 +00:00
Shaheer Kochai
41661a5e28 feat: add support for entrypoint spans toggle in top operations table (#8175)
* feat: add support for entrypoint spans toggle in top operations table

* fix: write tests for entry point toggle

* chore: entry point -> entrypoint

* fix: add info icon and tooltip for entrypoint spans toggle

* fix: fix the copy and link for entrypoint toggle in top operations

* chore: update the tooltip text

* Update frontend/src/container/MetricsApplication/TopOperationsTable.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* chore: fix the failing build

* chore: update the entry point spans docs link

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-20 11:20:13 +00:00
Nageshbansal
507dc86af2 Remove docker downloads badge (#8556) 2025-07-19 03:04:38 +00:00
Srikanth Chekuri
ff3bb04655 chore: support legacy cols usage and address several gaps (#8552) 2025-07-18 18:37:57 +05:30
Vibhu Pandey
31c4f800fc feat: add more codeowners (#8558) 2025-07-18 12:05:31 +00:00
aniketio-ctrl
51c2bbcd4b fix(dedup-prom): added check for duplicated samples data (#8502)
* fix(dedup-prom): added check for duplicated samples data

* fix(dedup-prom): added test cases for duplicated samples data

* fix(dedup-prom): added test cases for duplicated samples data
2025-07-18 08:57:00 +00:00
scout9ll
5610cb1f81 fix: retain compositeQuery during pagination and field filtering on exceptions page (#8300) 2025-07-17 15:28:28 +00:00
Yunus M
478d28eda1 feat(license): show refetch payment status button to reconcile payments (#8551) 2025-07-17 20:00:33 +05:30
Vibhu Pandey
ebb2f1fd63 feat(cmd): add cmd package (#8535) 2025-07-17 10:38:31 +00:00
Shaheer Kochai
629e502703 feat: add support for role based access in trace funnels flows (#8481)
* feat: add support for role based access in trace funnels flows

* chore: fix the failing build
2025-07-17 05:47:54 +00:00
primus-bot[bot]
cf4e44d341 chore(release): bump to v0.90.1 (#8546)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-07-16 16:47:39 +05:30
Abhi kumar
7ce1a1cbca fix: added fix for changelog refetching multiple times on tab change (#8545) 2025-07-16 16:35:25 +05:30
Priyanshu Shrivastava
e2253ec7c0 chore(release): bump SigNoz to v0.90.0, OTel Collector to v0.128.1 (#8543) 2025-07-16 12:42:59 +05:30
Yunus M
86be2869a9 fix: light mode for channels create and edit (#8542) 2025-07-16 05:03:09 +00:00
Chitransh
9ec503e302 feat: added new datasources (#8536) 2025-07-16 04:53:22 +00:00
Srikanth Chekuri
77ee201bb7 chore: remove migration notification (#8537) 2025-07-16 10:04:07 +05:30
Nageshbansal
168a7baf6c feat(statsreporter): add ecs platform detection (#8529) 2025-07-15 13:25:30 +00:00
Piyush Singariya
c36c492877 chore: update in agent version (#8457) 2025-07-15 13:01:21 +00:00
Shaheer Kochai
15332b90c1 fix: fix the layout shift issues in logs explorer column view (#8434)
* fix: fix the layout shift issue in logs list column view

* chore: overall improvements
2025-07-15 11:45:44 +00:00
Abhi kumar
53b71d7062 fix: revalidate changelog on tab visibility change (#8532)
* fix: added changes for revalidation changelog on tab change

* chore: removed four hours diff change
2025-07-15 15:17:14 +05:30
Abhi kumar
5e0bf930d6 chore: added new frontend maintainers (#8530) 2025-07-15 12:38:53 +05:30
Piyush Singariya
d6eed8e79d feat: JSON Flattening in logs pipelines (#8227)
* feat: introducing JSON Flattening

* fix: removed bug and tested

* test: removed testing test

* feat: additional severity levels, and some clearing

* chore: minor changes

* test: added tests for processJSONParser

* test: added check for OnError

* fix: review from ellipsis

* fix: variablise max flattening depth

* Update pkg/query-service/app/logparsingpipeline/pipelineBuilder.go

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update pkg/errors/errors.go

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix: quoted JSON strings fix

* test: updating otel collector for testing

* test: update collector's reference

* chore: change with new error package

* chore: set flattening depth equal to 1

* fix: fallback for depth

* fix: change in errors package

* fix: tests

* fix: test

* chore: update collector version

* fix: go.sum

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-07-14 18:48:01 +05:30
Abhi kumar
6137740907 fix: changelog ui fixes (#8526)
* chore: removed redundant scorllbar when space available

* chore: sidenav version pill can be clicked when changelog is available

* fix: removed upgrade cta for cloud users

* chore: minor ui fix
2025-07-14 17:44:08 +05:30
Nityananda Gohain
1aa6c98822 fix: check if it's castable to string (#8517) 2025-07-14 10:00:15 +00:00
Yunus M
8cf511cdb9 feat: update data on skip in onboarding (#8508) 2025-07-14 13:50:09 +05:30
Aditya Singh
a7ce6da7d1 Show new error component when saving pipeline fails (#8511)
* fix: show new error component when saving pipeline fails

* fix: minor refactor

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
2025-07-14 13:13:39 +05:30
Shaheer Kochai
ddb08b3883 feat: add support for JSON flattening in pipeline processor create and edit (#8331)
* feat: add support for JSON flattening in pipeline processor create and edit

* chore: add fallback value for mapping state

* feat: improve the UI of json flattening form and show path_prefix if enable_path is true

* fix: improve the state management

* chore: get enablePaths state using useWatch

* chore: json flattening tests

* chore: improve the test descriptions

* fix: update snapshot and adjust the existing failing test

* chore: overall improvements

* fix: update the JSON flattening keys map

* fix: destroy the new processor modal on closing to fix the unintended persistent mapping state

* chore: log event on saving pipeline if json_parser processors get modified

* chore: fix the alignment of json flattening switch

* chore: overall improvement

* refactor: improve the mapping comparison by using lodash's isEqual

* chore: update the snapshot

* refactor: improve the pipeline json_parser processor filtering logic

---------

Co-authored-by: Aditya Singh <adityasinghssj1@gmail.com>
Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-07-14 06:04:04 +00:00
218 changed files with 6804 additions and 1390 deletions

View File

@@ -40,7 +40,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
image: signoz/signoz-schema-migrator:v0.128.0
image: signoz/signoz-schema-migrator:v0.128.2
container_name: schema-migrator-sync
command:
- sync
@@ -53,7 +53,7 @@ services:
condition: service_healthy
restart: on-failure
schema-migrator-async:
image: signoz/signoz-schema-migrator:v0.128.0
image: signoz/signoz-schema-migrator:v0.128.2
container_name: schema-migrator-async
command:
- async

30
.github/CODEOWNERS vendored
View File

@@ -7,14 +7,38 @@
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
/deploy/ @SigNoz/devops
.github @SigNoz/devops
# Scaffold Owners
/pkg/config/ @grandwizard28
/pkg/errors/ @grandwizard28
/pkg/factory/ @grandwizard28
/pkg/types/ @grandwizard28
/pkg/valuer/ @grandwizard28
/cmd/ @grandwizard28
.golangci.yml @grandwizard28
# Zeus Owners
/pkg/zeus/ @vikrantgupta25
/pkg/licensing/ @vikrantgupta25
/pkg/sqlmigration/ @vikrantgupta25
/ee/zeus/ @vikrantgupta25
/pkg/licensing/ @vikrantgupta25
/ee/licensing/ @vikrantgupta25
/ee/sqlmigration/ @vikrantgupta25
# SQL Owners
/pkg/sqlmigration/ @vikrantgupta25
/ee/sqlmigration/ @vikrantgupta25
/pkg/sqlschema/ @vikrantgupta25
/ee/sqlschema/ @vikrantgupta25
# Analytics Owners
/pkg/analytics/ @vikrantgupta25
/pkg/statsreporter/ @vikrantgupta25
# Querier Owners
/pkg/querier/ @srikanthccv
/pkg/variables/ @srikanthccv
/pkg/types/querybuildertypes/ @srikanthccv
/pkg/querybuilder/ @srikanthccv
/pkg/telemetrylogs/ @srikanthccv
/pkg/telemetrymetadata/ @srikanthccv
/pkg/telemetrymetrics/ @srikanthccv
/pkg/telemetrytraces/ @srikanthccv

View File

@@ -66,7 +66,7 @@ jobs:
GO_NAME: signoz-community
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./pkg/query-service
GO_BUILD_CONTEXT: ./cmd/community
GO_BUILD_FLAGS: >-
-tags timetzdata
-ldflags='-linkmode external -extldflags \"-static\" -s -w
@@ -78,6 +78,6 @@ jobs:
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch
DOCKER_DOCKERFILE_PATH: ./cmd/community/Dockerfile.multi-arch
DOCKER_MANIFEST: true
DOCKER_PROVIDERS: dockerhub

View File

@@ -96,7 +96,7 @@ jobs:
GO_VERSION: 1.23
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./ee/query-service
GO_BUILD_CONTEXT: ./cmd/enterprise
GO_BUILD_FLAGS: >-
-tags timetzdata
-ldflags='-linkmode external -extldflags \"-static\" -s -w
@@ -112,6 +112,6 @@ jobs:
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
DOCKER_DOCKERFILE_PATH: ./cmd/enterprise/Dockerfile.multi-arch
DOCKER_MANIFEST: true
DOCKER_PROVIDERS: ${{ needs.prepare.outputs.docker_providers }}

View File

@@ -95,7 +95,7 @@ jobs:
GO_VERSION: 1.23
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./ee/query-service
GO_BUILD_CONTEXT: ./cmd/enterprise
GO_BUILD_FLAGS: >-
-tags timetzdata
-ldflags='-linkmode external -extldflags \"-static\" -s -w
@@ -111,7 +111,7 @@ jobs:
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
DOCKER_DOCKERFILE_PATH: ./cmd/enterprise/Dockerfile.multi-arch
DOCKER_MANIFEST: true
DOCKER_PROVIDERS: gcp
staging:

View File

@@ -36,7 +36,7 @@ jobs:
- ubuntu-latest
- macos-latest
env:
CONFIG_PATH: pkg/query-service/.goreleaser.yaml
CONFIG_PATH: cmd/community/.goreleaser.yaml
runs-on: ${{ matrix.os }}
steps:
- name: checkout
@@ -100,7 +100,7 @@ jobs:
needs: build
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
WORKDIR: pkg/query-service
WORKDIR: cmd/community
steps:
- name: checkout
uses: actions/checkout@v4

View File

@@ -50,7 +50,7 @@ jobs:
- ubuntu-latest
- macos-latest
env:
CONFIG_PATH: ee/query-service/.goreleaser.yaml
CONFIG_PATH: cmd/enterprise/.goreleaser.yaml
runs-on: ${{ matrix.os }}
steps:
- name: checkout

View File

@@ -20,9 +20,9 @@ jobs:
- sqlite
clickhouse-version:
- 24.1.2-alpine
- 24.12-alpine
- 25.5.6
schema-migrator-version:
- v0.128.0
- v0.128.1
postgres-version:
- 15
if: |

View File

@@ -1,10 +1,6 @@
name: prereleaser
on:
# schedule every wednesday 6:30 AM UTC (12:00 PM IST)
schedule:
- cron: '30 6 * * 3'
# allow manual triggering of the workflow by a maintainer
workflow_dispatch:
inputs:

View File

@@ -2,7 +2,7 @@ Copyright (c) 2020-present SigNoz Inc.
Portions of this software are licensed as follows:
* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
* All content that resides under the "ee/" and the "cmd/enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
* All third party components incorporated into the SigNoz Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.

View File

@@ -20,18 +20,18 @@ GO_BUILD_LDFLAG_LICENSE_SIGNOZ_IO = -X github.com/SigNoz/signoz/ee/zeus.depreca
GO_BUILD_VERSION_LDFLAGS = -X github.com/SigNoz/signoz/pkg/version.version=$(VERSION) -X github.com/SigNoz/signoz/pkg/version.hash=$(COMMIT_SHORT_SHA) -X github.com/SigNoz/signoz/pkg/version.time=$(TIMESTAMP) -X github.com/SigNoz/signoz/pkg/version.branch=$(BRANCH_NAME)
GO_BUILD_ARCHS_COMMUNITY = $(addprefix go-build-community-,$(ARCHS))
GO_BUILD_CONTEXT_COMMUNITY = $(SRC)/pkg/query-service
GO_BUILD_CONTEXT_COMMUNITY = $(SRC)/cmd/community
GO_BUILD_LDFLAGS_COMMUNITY = $(GO_BUILD_VERSION_LDFLAGS) -X github.com/SigNoz/signoz/pkg/version.variant=community
GO_BUILD_ARCHS_ENTERPRISE = $(addprefix go-build-enterprise-,$(ARCHS))
GO_BUILD_ARCHS_ENTERPRISE_RACE = $(addprefix go-build-enterprise-race-,$(ARCHS))
GO_BUILD_CONTEXT_ENTERPRISE = $(SRC)/ee/query-service
GO_BUILD_CONTEXT_ENTERPRISE = $(SRC)/cmd/enterprise
GO_BUILD_LDFLAGS_ENTERPRISE = $(GO_BUILD_VERSION_LDFLAGS) -X github.com/SigNoz/signoz/pkg/version.variant=enterprise $(GO_BUILD_LDFLAG_ZEUS_URL) $(GO_BUILD_LDFLAG_LICENSE_SIGNOZ_IO)
DOCKER_BUILD_ARCHS_COMMUNITY = $(addprefix docker-build-community-,$(ARCHS))
DOCKERFILE_COMMUNITY = $(SRC)/pkg/query-service/Dockerfile
DOCKERFILE_COMMUNITY = $(SRC)/cmd/community/Dockerfile
DOCKER_REGISTRY_COMMUNITY ?= docker.io/signoz/signoz-community
DOCKER_BUILD_ARCHS_ENTERPRISE = $(addprefix docker-build-enterprise-,$(ARCHS))
DOCKERFILE_ENTERPRISE = $(SRC)/ee/query-service/Dockerfile
DOCKERFILE_ENTERPRISE = $(SRC)/cmd/enterprise/Dockerfile
DOCKER_REGISTRY_ENTERPRISE ?= docker.io/signoz/signoz
JS_BUILD_CONTEXT = $(SRC)/frontend
@@ -74,7 +74,7 @@ go-run-enterprise: ## Runs the enterprise go backend server
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
go run -race \
$(GO_BUILD_CONTEXT_ENTERPRISE)/main.go \
$(GO_BUILD_CONTEXT_ENTERPRISE)/*.go \
--config ./conf/prometheus.yml \
--cluster cluster
@@ -92,7 +92,7 @@ go-run-community: ## Runs the community go backend server
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
go run -race \
$(GO_BUILD_CONTEXT_COMMUNITY)/main.go \
$(GO_BUILD_CONTEXT_COMMUNITY)/*.go \
--config ./conf/prometheus.yml \
--cluster cluster

View File

@@ -8,7 +8,6 @@
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
<p align="center">
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/signoz.svg?label=Docker%20Downloads"> </a>
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
@@ -231,6 +230,8 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
- [Shaheer Kochai](https://github.com/ahmadshaheer)
- [Amlan Kumar Nandy](https://github.com/amlannandy)
- [Sahil Khan](https://github.com/sawhil)
- [Aditya Singh](https://github.com/aks07)
- [Abhi Kumar](https://github.com/ahrefabhi)
#### DevOps

View File

@@ -11,7 +11,7 @@ before:
builds:
- id: signoz
binary: bin/signoz
main: pkg/query-service/main.go
main: cmd/community
env:
- CGO_ENABLED=1
- >-

View File

@@ -16,4 +16,4 @@ COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz
ENTRYPOINT ["./signoz"]
ENTRYPOINT ["./signoz", "server"]

View File

@@ -17,4 +17,4 @@ COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz-community
ENTRYPOINT ["./signoz-community"]
ENTRYPOINT ["./signoz-community", "server"]

18
cmd/community/main.go Normal file
View File

@@ -0,0 +1,18 @@
package main
import (
"log/slog"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/pkg/instrumentation"
)
func main() {
// initialize logger for logging in the cmd/ package. This logger is different from the logger used in the application.
logger := instrumentation.NewLogger(instrumentation.Config{Logs: instrumentation.LogsConfig{Level: slog.LevelInfo}})
// register a list of commands to the root command
registerServer(cmd.RootCmd, logger)
cmd.Execute(logger)
}

116
cmd/community/server.go Normal file
View File

@@ -0,0 +1,116 @@
package main
import (
"context"
"log/slog"
"time"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/SigNoz/signoz/pkg/zeus/noopzeus"
"github.com/spf13/cobra"
)
func registerServer(parentCmd *cobra.Command, logger *slog.Logger) {
var flags signoz.DeprecatedFlags
serverCmd := &cobra.Command{
Use: "server",
Short: "Run the SigNoz server",
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
RunE: func(currCmd *cobra.Command, args []string) error {
config, err := cmd.NewSigNozConfig(currCmd.Context(), flags)
if err != nil {
return err
}
return runServer(currCmd.Context(), config, logger)
},
}
flags.RegisterFlags(serverCmd)
parentCmd.AddCommand(serverCmd)
}
func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) error {
// print the version
version.Info.PrettyPrint(config.Version)
// add enterprise sqlstore factories to the community sqlstore factories
sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
return err
}
jwt := authtypes.NewJWT(cmd.NewJWTSecret(ctx, logger), 30*time.Minute, 30*24*time.Hour)
signoz, err := signoz.New(
ctx,
config,
jwt,
zeus.Config{},
noopzeus.NewProviderFactory(),
licensing.Config{},
func(_ sqlstore.SQLStore, _ zeus.Zeus, _ organization.Getter, _ analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return nooplicensing.NewFactory()
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
return signoz.NewSQLSchemaProviderFactories(sqlstore)
},
signoz.NewSQLStoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
return err
}
server, err := app.NewServer(config, signoz, jwt)
if err != nil {
logger.ErrorContext(ctx, "failed to create server", "error", err)
return err
}
if err := server.Start(ctx); err != nil {
logger.ErrorContext(ctx, "failed to start server", "error", err)
return err
}
signoz.Start(ctx)
if err := signoz.Wait(ctx); err != nil {
logger.ErrorContext(ctx, "failed to start signoz", "error", err)
return err
}
err = server.Stop(ctx)
if err != nil {
logger.ErrorContext(ctx, "failed to stop server", "error", err)
return err
}
err = signoz.Stop(ctx)
if err != nil {
logger.ErrorContext(ctx, "failed to stop signoz", "error", err)
return err
}
return nil
}

45
cmd/config.go Normal file
View File

@@ -0,0 +1,45 @@
package cmd
import (
"context"
"fmt"
"log/slog"
"os"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/config/envprovider"
"github.com/SigNoz/signoz/pkg/config/fileprovider"
"github.com/SigNoz/signoz/pkg/signoz"
)
func NewSigNozConfig(ctx context.Context, flags signoz.DeprecatedFlags) (signoz.Config, error) {
config, err := signoz.NewConfig(
ctx,
config.ResolverConfig{
Uris: []string{"env:"},
ProviderFactories: []config.ProviderFactory{
envprovider.NewFactory(),
fileprovider.NewFactory(),
},
},
flags,
)
if err != nil {
return signoz.Config{}, err
}
return config, nil
}
func NewJWTSecret(_ context.Context, _ *slog.Logger) string {
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
if len(jwtSecret) == 0 {
fmt.Println("🚨 CRITICAL SECURITY ISSUE: No JWT secret key specified!")
fmt.Println("SIGNOZ_JWT_SECRET environment variable is not set. This has dire consequences for the security of the application.")
fmt.Println("Without a JWT secret, user sessions are vulnerable to tampering and unauthorized access.")
fmt.Println("Please set the SIGNOZ_JWT_SECRET environment variable immediately.")
fmt.Println("For more information, please refer to https://github.com/SigNoz/signoz/issues/8400.")
}
return jwtSecret
}

View File

@@ -11,7 +11,7 @@ before:
builds:
- id: signoz
binary: bin/signoz
main: ee/query-service/main.go
main: cmd/enterprise
env:
- CGO_ENABLED=1
- >-

View File

@@ -16,4 +16,4 @@ COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz
ENTRYPOINT ["./signoz"]
ENTRYPOINT ["./signoz", "server"]

View File

@@ -23,6 +23,7 @@ COPY go.mod go.sum ./
RUN go mod download
COPY ./cmd/ ./cmd/
COPY ./ee/ ./ee/
COPY ./pkg/ ./pkg/
COPY ./templates/email /root/templates
@@ -33,4 +34,4 @@ RUN mv /root/linux-${TARGETARCH}/signoz /root/signoz
RUN chmod 755 /root /root/signoz
ENTRYPOINT ["/root/signoz"]
ENTRYPOINT ["/root/signoz", "server"]

View File

@@ -17,4 +17,4 @@ COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz
ENTRYPOINT ["./signoz"]
ENTRYPOINT ["./signoz", "server"]

18
cmd/enterprise/main.go Normal file
View File

@@ -0,0 +1,18 @@
package main
import (
"log/slog"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/pkg/instrumentation"
)
func main() {
// initialize logger for logging in the cmd/ package. This logger is different from the logger used in the application.
logger := instrumentation.NewLogger(instrumentation.Config{Logs: instrumentation.LogsConfig{Level: slog.LevelInfo}})
// register a list of commands to the root command
registerServer(cmd.RootCmd, logger)
cmd.Execute(logger)
}

124
cmd/enterprise/server.go Normal file
View File

@@ -0,0 +1,124 @@
package main
import (
"context"
"log/slog"
"time"
"github.com/SigNoz/signoz/cmd"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/spf13/cobra"
)
func registerServer(parentCmd *cobra.Command, logger *slog.Logger) {
var flags signoz.DeprecatedFlags
serverCmd := &cobra.Command{
Use: "server",
Short: "Run the SigNoz server",
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
RunE: func(currCmd *cobra.Command, args []string) error {
config, err := cmd.NewSigNozConfig(currCmd.Context(), flags)
if err != nil {
return err
}
return runServer(currCmd.Context(), config, logger)
},
}
flags.RegisterFlags(serverCmd)
parentCmd.AddCommand(serverCmd)
}
func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) error {
// print the version
version.Info.PrettyPrint(config.Version)
// add enterprise sqlstore factories to the community sqlstore factories
sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
return err
}
jwt := authtypes.NewJWT(cmd.NewJWTSecret(ctx, logger), 30*time.Minute, 30*24*time.Hour)
signoz, err := signoz.New(
ctx,
config,
jwt,
enterprisezeus.Config(),
httpzeus.NewProviderFactory(),
enterpriselicensing.Config(24*time.Hour, 3),
func(sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
panic(err)
}
return existingFactories
},
sqlstoreFactories,
signoz.NewTelemetryStoreProviderFactories(),
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
return err
}
server, err := enterpriseapp.NewServer(config, signoz, jwt)
if err != nil {
logger.ErrorContext(ctx, "failed to create server", "error", err)
return err
}
if err := server.Start(ctx); err != nil {
logger.ErrorContext(ctx, "failed to start server", "error", err)
return err
}
signoz.Start(ctx)
if err := signoz.Wait(ctx); err != nil {
logger.ErrorContext(ctx, "failed to start signoz", "error", err)
return err
}
err = server.Stop(ctx)
if err != nil {
logger.ErrorContext(ctx, "failed to stop server", "error", err)
return err
}
err = signoz.Stop(ctx)
if err != nil {
logger.ErrorContext(ctx, "failed to stop signoz", "error", err)
return err
}
return nil
}

33
cmd/root.go Normal file
View File

@@ -0,0 +1,33 @@
package cmd
import (
"log/slog"
"os"
"github.com/SigNoz/signoz/pkg/version"
"github.com/spf13/cobra"
"go.uber.org/zap" //nolint:depguard
)
var RootCmd = &cobra.Command{
Use: "signoz",
Short: "OpenTelemetry-Native Logs, Metrics and Traces in a single pane",
Version: version.Info.Version(),
SilenceUsage: true,
SilenceErrors: true,
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
}
func Execute(logger *slog.Logger) {
zapLogger := newZapLogger()
zap.ReplaceGlobals(zapLogger)
defer func() {
_ = zapLogger.Sync()
}()
err := RootCmd.Execute()
if err != nil {
logger.ErrorContext(RootCmd.Context(), "error running command", "error", err)
os.Exit(1)
}
}

15
cmd/zap.go Normal file
View File

@@ -0,0 +1,15 @@
package cmd
import (
"go.uber.org/zap" //nolint:depguard
"go.uber.org/zap/zapcore" //nolint:depguard
)
// Deprecated: Use `NewLogger` from `pkg/instrumentation` instead.
func newZapLogger() *zap.Logger {
config := zap.NewProductionConfig()
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
logger, _ := config.Build()
return logger
}

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.89.0
image: signoz/signoz:v0.90.1
command:
- --config=/root/config/prometheus.yml
ports:
@@ -207,7 +207,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.128.0
image: signoz/signoz-otel-collector:v0.128.2
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -231,7 +231,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.128.0
image: signoz/signoz-schema-migrator:v0.128.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -115,7 +115,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.89.0
image: signoz/signoz:v0.90.1
command:
- --config=/root/config/prometheus.yml
ports:
@@ -148,7 +148,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.128.0
image: signoz/signoz-otel-collector:v0.128.2
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -174,7 +174,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.128.0
image: signoz/signoz-schema-migrator:v0.128.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.89.0}
image: signoz/signoz:${VERSION:-v0.90.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -211,7 +211,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.0}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -237,7 +237,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-sync
command:
- sync
@@ -248,7 +248,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.89.0}
image: signoz/signoz:${VERSION:-v0.90.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -143,7 +143,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.0}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -165,7 +165,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-sync
command:
- sync
@@ -177,7 +177,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -1,4 +0,0 @@
.vscode
README.md
signoz.db
bin

View File

@@ -1,189 +0,0 @@
package main
import (
"context"
"flag"
"os"
"time"
"github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/app"
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/config/envprovider"
"github.com/SigNoz/signoz/pkg/config/fileprovider"
"github.com/SigNoz/signoz/pkg/factory"
pkglicensing "github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/version"
pkgzeus "github.com/SigNoz/signoz/pkg/zeus"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Deprecated: Please use the logger from pkg/instrumentation.
func initZapLog() *zap.Logger {
config := zap.NewProductionConfig()
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
logger, _ := config.Build()
return logger
}
func main() {
var promConfigPath, skipTopLvlOpsPath string
// disables rule execution but allows change to the rule definition
var disableRules bool
// the url used to build link in the alert messages in slack and other systems
var ruleRepoURL string
var cluster string
var useLogsNewSchema bool
var useTraceNewSchema bool
var cacheConfigPath, fluxInterval, fluxIntervalForTraceDetail string
var preferSpanMetrics bool
var maxIdleConns int
var maxOpenConns int
var dialTimeout time.Duration
var gatewayUrl string
var useLicensesV3 bool
// Deprecated
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
// Deprecated
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
// Deprecated
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
// Deprecated
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
// Deprecated
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
flag.BoolVar(&preferSpanMetrics, "prefer-span-metrics", false, "(prefer span metrics for service level metrics)")
// Deprecated
flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool.)")
// Deprecated
flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time.)")
// Deprecated
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
// Deprecated
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
// Deprecated
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
flag.StringVar(&fluxIntervalForTraceDetail, "flux-interval-trace-detail", "2m", "(the interval to exclude data from being cached to avoid incorrect cache for trace data in motion)")
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
// Deprecated
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.Parse()
loggerMgr := initZapLog()
zap.ReplaceGlobals(loggerMgr)
defer loggerMgr.Sync() // flushes buffer, if any
ctx := context.Background()
config, err := signoz.NewConfig(ctx, config.ResolverConfig{
Uris: []string{"env:"},
ProviderFactories: []config.ProviderFactory{
envprovider.NewFactory(),
fileprovider.NewFactory(),
},
}, signoz.DeprecatedFlags{
MaxIdleConns: maxIdleConns,
MaxOpenConns: maxOpenConns,
DialTimeout: dialTimeout,
Config: promConfigPath,
FluxInterval: fluxInterval,
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
Cluster: cluster,
GatewayUrl: gatewayUrl,
})
if err != nil {
zap.L().Fatal("Failed to create config", zap.Error(err))
}
version.Info.PrettyPrint(config.Version)
sqlStoreFactories := signoz.NewSQLStoreProviderFactories()
if err := sqlStoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
zap.L().Fatal("Failed to add postgressqlstore factory", zap.Error(err))
}
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
if len(jwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
}
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
signoz, err := signoz.New(
context.Background(),
config,
jwt,
zeus.Config(),
httpzeus.NewProviderFactory(),
licensing.Config(24*time.Hour, 3),
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
zap.L().Fatal("Failed to add postgressqlschema factory", zap.Error(err))
}
return existingFactories
},
sqlStoreFactories,
signoz.NewTelemetryStoreProviderFactories(),
)
if err != nil {
zap.L().Fatal("Failed to create signoz", zap.Error(err))
}
server, err := app.NewServer(config, signoz, jwt)
if err != nil {
zap.L().Fatal("Failed to create server", zap.Error(err))
}
if err := server.Start(ctx); err != nil {
zap.L().Fatal("Could not start server", zap.Error(err))
}
signoz.Start(ctx)
if err := signoz.Wait(ctx); err != nil {
zap.L().Fatal("Failed to start signoz", zap.Error(err))
}
err = server.Stop(ctx)
if err != nil {
zap.L().Fatal("Failed to stop server", zap.Error(err))
}
err = signoz.Stop(ctx)
if err != nil {
zap.L().Fatal("Failed to stop signoz", zap.Error(err))
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="a" x1="2.59" y1="10.16" x2="15.41" y2="10.16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#005ba1"/><stop offset=".07" stop-color="#0060a9"/><stop offset=".36" stop-color="#0071c8"/><stop offset=".52" stop-color="#0078d4"/><stop offset=".64" stop-color="#0074cd"/><stop offset=".82" stop-color="#006abb"/><stop offset="1" stop-color="#005ba1"/></linearGradient></defs><path d="M9 5.14c-3.54 0-6.41-1-6.41-2.32v12.36c0 1.27 2.82 2.3 6.32 2.32H9c3.54 0 6.41-1 6.41-2.32V2.82c0 1.29-2.87 2.32-6.41 2.32z" fill="url(#a)"/><path d="M15.41 2.82c0 1.29-2.87 2.32-6.41 2.32s-6.41-1-6.41-2.32S5.46.5 9 .5s6.41 1 6.41 2.32" fill="#e8e8e8"/><path d="M13.92 2.63c0 .82-2.21 1.48-4.92 1.48s-4.92-.66-4.92-1.48S6.29 1.16 9 1.16s4.92.66 4.92 1.47" fill="#50e6ff"/><path d="M9 3a11.55 11.55 0 00-3.89.57A11.42 11.42 0 009 4.11a11.15 11.15 0 003.89-.58A11.84 11.84 0 009 3z" fill="#198ab3"/><path d="M12.64 9v1.63h-1a.39.39 0 01-.29-.14V9H10v1.78a.92.92 0 001 .89h1.49l.26-.13s-.11.41-.26.43h-2.38v1h2.66A1.21 1.21 0 0014 11.7V9zM9.53 9v-.49a.7.7 0 00-.48-.77 1.74 1.74 0 00-.5-.08.94.94 0 00-.91.58l-.78 1.9-1-1.9A.93.93 0 005 7.66a1.44 1.44 0 00-.51.09c-.35.11-.43.34-.43.73v3.31h1.17V9.56l.63 1.57a1.08 1.08 0 001 .66c.44 0 .62-.26.8-.66l.67-1.51v2.15h1.18V9z" fill="#f2f2f2"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="64" height="64"><path d="M8.16 23h21.177v-5.86l-4.023-2.307-.694-.3-16.46.113z" fill="#fff"/><path d="M22.012 22.222c.197-.675.122-1.294-.206-1.754-.3-.422-.807-.666-1.416-.694l-11.545-.15c-.075 0-.14-.038-.178-.094s-.047-.13-.028-.206c.038-.113.15-.197.272-.206l11.648-.15c1.38-.066 2.88-1.182 3.404-2.55l.666-1.735a.38.38 0 0 0 .02-.225c-.75-3.395-3.78-5.927-7.4-5.927-3.34 0-6.17 2.157-7.184 5.15-.657-.488-1.5-.75-2.392-.666-1.604.16-2.9 1.444-3.048 3.048a3.58 3.58 0 0 0 .084 1.191A4.84 4.84 0 0 0 0 22.1c0 .234.02.47.047.703.02.113.113.197.225.197H21.58a.29.29 0 0 0 .272-.206l.16-.572z" fill="#f38020"/><path d="M25.688 14.803l-.32.01c-.075 0-.14.056-.17.13l-.45 1.566c-.197.675-.122 1.294.206 1.754.3.422.807.666 1.416.694l2.457.15c.075 0 .14.038.178.094s.047.14.028.206c-.038.113-.15.197-.272.206l-2.56.15c-1.388.066-2.88 1.182-3.404 2.55l-.188.478c-.038.094.028.188.13.188h8.797a.23.23 0 0 0 .225-.169A6.41 6.41 0 0 0 32 21.106a6.32 6.32 0 0 0-6.312-6.302" fill="#faae40"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><path d="M255.96 134.393c0-21.521-13.373-40.117-33.223-47.43a75.239 75.239 0 0 0 1.253-13.791c0-39.909-32.386-72.295-72.295-72.295-23.193 0-44.923 11.074-58.505 30.088-6.686-5.224-14.835-7.94-23.402-7.94-21.104 0-38.446 17.133-38.446 38.446 0 4.597.836 9.194 2.298 13.373C13.582 81.739 0 100.962 0 122.274c0 21.522 13.373 40.327 33.431 47.64-.835 4.388-1.253 8.985-1.253 13.79 0 39.7 32.386 72.087 72.086 72.087 23.402 0 44.924-11.283 58.505-30.088 6.686 5.223 15.044 8.149 23.611 8.149 21.104 0 38.446-17.134 38.446-38.446 0-4.597-.836-9.194-2.298-13.373 19.64-7.104 33.431-26.327 33.431-47.64z" fill="#FFF"/><path d="M100.085 110.364l57.043 26.119 57.669-50.565a64.312 64.312 0 0 0 1.253-12.746c0-35.52-28.834-64.355-64.355-64.355-21.313 0-41.162 10.447-53.072 27.998l-9.612 49.73 11.074 23.82z" fill="#F4BD19"/><path d="M40.953 170.75c-.835 4.179-1.253 8.567-1.253 12.955 0 35.52 29.043 64.564 64.564 64.564 21.522 0 41.372-10.656 53.49-28.208l9.403-49.729-12.746-24.238-57.251-26.118-56.207 50.774z" fill="#3CBEB1"/><path d="M40.536 71.918l39.073 9.194 8.775-44.506c-5.432-4.179-11.91-6.268-18.805-6.268-16.925 0-30.924 13.79-30.924 30.924 0 3.552.627 7.313 1.88 10.656z" fill="#E9478C"/><path d="M37.192 81.32c-17.551 5.642-29.67 22.567-29.67 40.954 0 17.97 11.074 34.059 27.79 40.327l54.953-49.73-10.03-21.52-43.043-10.03z" fill="#2C458F"/><path d="M167.784 219.852c5.432 4.18 11.91 6.478 18.596 6.478 16.925 0 30.924-13.79 30.924-30.924 0-3.761-.627-7.314-1.88-10.657l-39.073-9.193-8.567 44.296z" fill="#95C63D"/><path d="M175.724 165.317l43.043 10.03c17.551-5.85 29.67-22.566 29.67-40.954 0-17.97-11.074-33.849-27.79-40.326l-56.415 49.311 11.492 21.94z" fill="#176655"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 24 24" overflow="visible" xml:space="preserve">
<g >
<rect y="0" fill="none" width="24" height="24"/>
<g transform="translate(1.000000, 8.000000)">
<path fill-rule="evenodd" fill="#5C85DE" d="M2-1.9c-1.1,0-2.3,1.1-2.3,2.2V10H2V5.5h2.2V10h2.2V0.3c0-1.1-1.1-2.2-2.3-2.2H2
L2-1.9z M2,3.2v-3h2.2v3H2L2,3.2z"/>
<path fill-rule="evenodd" fill="#5C85DE" d="M10.3-2C9.1-2,8-0.9,8,0.2V10l2.2,0V5.5h2.2c1.1,0,2.3-1.1,2.3-2.2l0-3
c0-1.1-1.1-2.2-2.3-2.2H10.3L10.3-2z M10.2,3.2v-3h2.2v3H10.2L10.2,3.2z"/>
<polygon fill-rule="evenodd" fill="#5C85DE" points="18.5,0.3 18.5,7.8 16.2,7.8 16.2,10 23,10 23,7.8 20.8,7.8 20.8,0.3 23,0.3
23,-1.9 16.2,-1.9 16.2,0.3 "/>
<polygon fill-rule="evenodd" fill="#3367D6" points="2,5.5 2,3.2 3.5,3.2 "/>
<polygon fill-rule="evenodd" fill="#3367D6" points="10.2,5.5 10.2,3.2 11.5,3.2 "/>
<polygon fill-rule="evenodd" fill="#3367D6" points="18.5,1.8 18.5,1.8 18.5,0.3 20.8,0.3 "/>
</g>
</g>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#2088ff" d="M26.666 0C11.97 0 0 11.97 0 26.666c0 12.87 9.181 23.651 21.334 26.13v37.87c0 11.77 9.68 21.334 21.332 21.334h.195c1.302 9.023 9.1 16 18.473 16C71.612 128 80 119.612 80 109.334s-8.388-18.668-18.666-18.668c-9.372 0-17.17 6.977-18.473 16h-.195c-8.737 0-16-7.152-16-16V63.779a18.514 18.514 0 0 0 13.24 5.555h2.955c1.303 9.023 9.1 16 18.473 16 9.372 0 17.169-6.977 18.47-16h11.057c1.303 9.023 9.1 16 18.473 16 10.278 0 18.666-8.39 18.666-18.668C128 56.388 119.612 48 109.334 48c-9.373 0-17.171 6.977-18.473 16H79.805c-1.301-9.023-9.098-16-18.471-16s-17.171 6.977-18.473 16h-2.955c-6.433 0-11.793-4.589-12.988-10.672 14.58-.136 26.416-12.05 26.416-26.662C53.334 11.97 41.362 0 26.666 0zm0 5.334A21.292 21.292 0 0 1 48 26.666 21.294 21.294 0 0 1 26.666 48 21.292 21.292 0 0 1 5.334 26.666 21.29 21.29 0 0 1 26.666 5.334zm-5.215 7.541C18.67 12.889 16 15.123 16 18.166v17.043c0 4.043 4.709 6.663 8.145 4.533l13.634-8.455c3.257-2.02 3.274-7.002.032-9.045l-13.635-8.59a5.024 5.024 0 0 0-2.725-.777zm-.117 5.291 13.635 8.588-13.635 8.455V18.166zm40 35.168a13.29 13.29 0 0 1 13.332 13.332A13.293 13.293 0 0 1 61.334 80 13.294 13.294 0 0 1 48 66.666a13.293 13.293 0 0 1 13.334-13.332zm48 0a13.29 13.29 0 0 1 13.332 13.332A13.293 13.293 0 0 1 109.334 80 13.294 13.294 0 0 1 96 66.666a13.293 13.293 0 0 1 13.334-13.332zm-42.568 6.951a2.667 2.667 0 0 0-1.887.78l-6.3 6.294-2.093-2.084a2.667 2.667 0 0 0-3.771.006 2.667 2.667 0 0 0 .008 3.772l3.974 3.96a2.667 2.667 0 0 0 3.766-.001l8.185-8.174a2.667 2.667 0 0 0 .002-3.772 2.667 2.667 0 0 0-1.884-.78zm48 0a2.667 2.667 0 0 0-1.887.78l-6.3 6.294-2.093-2.084a2.667 2.667 0 0 0-3.771.006 2.667 2.667 0 0 0 .008 3.772l3.974 3.96a2.667 2.667 0 0 0 3.766-.001l8.185-8.174a2.667 2.667 0 0 0 .002-3.772 2.667 2.667 0 0 0-1.884-.78zM61.334 96a13.293 13.293 0 0 1 13.332 13.334 13.29 13.29 0 0 1-13.332 13.332A13.293 13.293 0 0 1 48 109.334 13.294 13.294 0 0 1 61.334 96zM56 105.334c-2.193 0-4 1.807-4 4 0 2.195 1.808 4 4 4s4-1.805 4-4c0-2.193-1.807-4-4-4zm10.666 0c-2.193 0-4 1.807-4 4 0 2.195 1.808 4 4 4s4-1.805 4-4c0-2.193-1.807-4-4-4zM56 108c.75 0 1.334.585 1.334 1.334 0 .753-.583 1.332-1.334 1.332-.75 0-1.334-.58-1.334-1.332 0-.75.585-1.334 1.334-1.334zm10.666 0c.75 0 1.334.585 1.334 1.334 0 .753-.583 1.332-1.334 1.332-.75 0-1.332-.58-1.332-1.332 0-.75.583-1.334 1.332-1.334z"/><path fill="#79b8ff" d="M109.334 90.666c-9.383 0-17.188 6.993-18.477 16.031a2.667 2.667 0 0 0-.265-.011l-2.7.09a2.667 2.667 0 0 0-2.578 2.751 2.667 2.667 0 0 0 2.752 2.578l2.7-.087a2.667 2.667 0 0 0 .097-.006C92.17 121.029 99.965 128 109.334 128c10.278 0 18.666-8.388 18.666-18.666s-8.388-18.668-18.666-18.668zm0 5.334a13.293 13.293 0 0 1 13.332 13.334 13.29 13.29 0 0 1-13.332 13.332A13.293 13.293 0 0 1 96 109.334 13.294 13.294 0 0 1 109.334 96z"/></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="lucide/github">
<path id="Vector" d="M15 22V18C15.1391 16.7473 14.7799 15.4901 14 14.5C17 14.5 20 12.5 20 9C20.08 7.75 19.73 6.52 19 5.5C19.28 4.35 19.28 3.15 19 2C19 2 18 2 16 3.5C13.36 3 10.64 3 8 3.5C6 2 5 2 5 2C4.7 3.15 4.7 4.35 5 5.5C4.27187 6.51588 3.91847 7.75279 4 9C4 12.5 7 14.5 10 14.5C9.61 14.99 9.32 15.55 9.15 16.15C8.98 16.75 8.93 17.38 9 18M9 18V22M9 18C4.49 20 4 16 2 16" stroke="#C0C1C3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 587 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 373.71 200"><defs><style type="text/css">.cls-1{fill:#008ec7;}.cls-2{fill:#005b9b;}.cls-3{fill:#fff;}</style></defs><title>IETF-Badge-HTTP</title><g id="Layer_2"><path class="cls-1" d="M326,0H47.73L0,100,47.73,200H326l47.73-100ZM310.05,183.36H58.22L18.43,100,58.22,16.64H310.05L349.84,100Z"/><polygon class="cls-2" points="349.84 100.01 310.05 183.37 58.22 183.37 18.43 100.01 58.22 16.64 310.05 16.64 349.84 100.01"/><path class="cls-3" d="M128.05,71.89v59.53H114.27V107h-27v24.41H73.46V71.89H87.23V95.36h27V71.89Z"/><path class="cls-3" d="M154.5,83.12H135.45V71.89h51.87V83.12h-19v48.3H154.5Z"/><path class="cls-3" d="M207.9,83.12H188.85V71.89h51.87V83.12H221.67v48.3H207.9Z"/><path class="cls-3" d="M287.62,74.53a20.45,20.45,0,0,1,9,7.48,20.67,20.67,0,0,1,3.14,11.48,20.73,20.73,0,0,1-3.14,11.44,20.06,20.06,0,0,1-9,7.48A33.55,33.55,0,0,1,273.88,115h-12v16.42H248.12V71.89h25.76A33.05,33.05,0,0,1,287.62,74.53Zm-5.06,26.57a9.33,9.33,0,0,0,3.23-7.61c0-3.34-1.08-5.91-3.23-7.69s-5.3-2.68-9.44-2.68H261.89v20.66h11.23Q279.33,103.78,282.56,101.1Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 832.8 959.8" xmlns="http://www.w3.org/2000/svg" width="2169" height="2500"><path d="M672.6 332.3l160.2-92.4v480L416.4 959.8V775.2l256.2-147.6z" fill="#00ac69"/><path d="M416.4 184.6L160.2 332.3 0 239.9 416.4 0l416.4 239.9-160.2 92.4z" fill="#1ce783"/><path d="M256.2 572.3L0 424.6V239.9l416.4 240v479.9l-160.2-92.2z" fill="#1d252c"/></svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#fff" d="M44.559 19.646a11.957 11.957 0 0 0-1.028-9.822 12.094 12.094 0 0 0-13.026-5.802A11.962 11.962 0 0 0 21.485 0 12.097 12.097 0 0 0 9.95 8.373a11.964 11.964 0 0 0-7.997 5.8A12.097 12.097 0 0 0 3.44 28.356a11.957 11.957 0 0 0 1.028 9.822 12.094 12.094 0 0 0 13.026 5.802 11.953 11.953 0 0 0 9.02 4.02 12.096 12.096 0 0 0 11.54-8.379 11.964 11.964 0 0 0 7.997-5.8 12.099 12.099 0 0 0-1.491-14.177zM26.517 44.863a8.966 8.966 0 0 1-5.759-2.082 6.85 6.85 0 0 0 .284-.16L30.6 37.1c.49-.278.79-.799.786-1.361V22.265l4.04 2.332a.141.141 0 0 1 .078.111v11.16a9.006 9.006 0 0 1-8.987 8.995zM7.191 36.608a8.957 8.957 0 0 1-1.073-6.027c.071.042.195.119.284.17l9.558 5.52a1.556 1.556 0 0 0 1.57 0l11.67-6.738v4.665a.15.15 0 0 1-.057.124l-9.662 5.579a9.006 9.006 0 0 1-12.288-3.293zM4.675 15.744a8.966 8.966 0 0 1 4.682-3.943c0 .082-.005.228-.005.33v11.042a1.555 1.555 0 0 0 .785 1.359l11.669 6.736-4.04 2.333a.143.143 0 0 1-.136.012L7.967 28.03a9.006 9.006 0 0 1-3.293-12.284zm33.19 7.724L26.196 16.73l4.04-2.331a.143.143 0 0 1 .136-.012l9.664 5.579c4.302 2.485 5.776 7.989 3.29 12.29a8.991 8.991 0 0 1-4.68 3.943V24.827a1.553 1.553 0 0 0-.78-1.36zm4.02-6.051c-.07-.044-.195-.119-.283-.17l-9.558-5.52a1.556 1.556 0 0 0-1.57 0l-11.67 6.738V13.8a.15.15 0 0 1 .057-.124l9.662-5.574a8.995 8.995 0 0 1 13.36 9.315zm-25.277 8.315-4.04-2.333a.141.141 0 0 1-.079-.11v-11.16a8.997 8.997 0 0 1 14.753-6.91c-.073.04-.2.11-.283.161L17.4 10.9a1.552 1.552 0 0 0-.786 1.36l-.006 13.469zM18.803 21l5.198-3.002 5.197 3V27l-5.197 3-5.198-3z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg height="2500" width="2500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><linearGradient id="a" x1="0%" y1="100%" y2="0%"><stop offset="0" stop-color="#1b660f"/><stop offset="1" stop-color="#6cae3e"/></linearGradient><g fill="none" fill-rule="evenodd"><path d="M0 0h80v80H0z" fill="url(#a)"/><path d="M60.836 42.893l.384-2.704c3.54 2.12 3.587 2.997 3.586 3.02-.006.006-.61.51-3.97-.316zm-1.943-.54C52.773 40.5 44.25 36.59 40.8 34.96c0-.014.004-.027.004-.041a2.406 2.406 0 0 0-2.404-2.403c-1.324 0-2.402 1.078-2.402 2.403s1.078 2.403 2.402 2.403c.582 0 1.11-.217 1.527-.562 4.058 1.92 12.515 5.774 18.68 7.594L56.17 61.56a.955.955 0 0 0-.01.14c0 1.516-6.707 4.299-17.666 4.299-11.075 0-17.853-2.783-17.853-4.298 0-.046-.003-.091-.01-.136l-5.093-37.207c4.409 3.035 13.892 4.64 22.962 4.64 9.056 0 18.523-1.6 22.94-4.625zM15 20.478C15.072 19.162 22.634 14 38.5 14c15.864 0 23.427 5.16 23.5 6.478v.449C61.13 23.877 51.33 27 38.5 27c-12.852 0-22.657-3.132-23.5-6.087zm49 .022c0-3.465-9.934-8.5-25.5-8.5S13 17.035 13 20.5l.094.754 5.548 40.524C18.775 66.31 30.86 68 38.494 68c9.472 0 19.535-2.178 19.665-6.22l2.396-16.896c1.333.319 2.43.482 3.31.482 1.184 0 1.984-.29 2.469-.867a1.95 1.95 0 0 0 .436-1.66c-.26-1.383-1.902-2.875-5.248-4.784l2.376-16.762z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#29b5e8"><path d="M9.86 15.298l13.008 7.8a3.72 3.72 0 0 0 4.589-.601 4.01 4.01 0 0 0 1.227-2.908V3.956a3.81 3.81 0 0 0-1.861-3.42 3.81 3.81 0 0 0-3.893 0 3.81 3.81 0 0 0-1.861 3.42v8.896l-7.387-4.43a3.79 3.79 0 0 0-2.922-.4c-.986.265-1.818.94-2.3 1.844-1.057 1.9-.44 4.28 1.4 5.422m31.27 7.8l13.008-7.8c1.84-1.143 2.458-3.533 1.4-5.424a3.75 3.75 0 0 0-5.22-1.452l-7.3 4.37v-8.84a3.81 3.81 0 1 0-7.615 0v15.323a4.08 4.08 0 0 0 .494 2.367c.482.903 1.314 1.57 2.3 1.844a3.71 3.71 0 0 0 2.922-.4M29.552 31.97c.013-.25.108-.5.272-.68l1.52-1.58a1.06 1.06 0 0 1 .658-.282h.057a1.05 1.05 0 0 1 .656.282l1.52 1.58a1.12 1.12 0 0 1 .272.681v.06a1.13 1.13 0 0 1-.272.683l-1.52 1.58a1.04 1.04 0 0 1-.656.284h-.057c-.246-.014-.48-.115-.658-.284l-1.52-1.58a1.13 1.13 0 0 1-.272-.683zm-4.604-.65v1.364a1.54 1.54 0 0 0 .372.93l5.16 5.357a1.42 1.42 0 0 0 .895.386h1.312a1.42 1.42 0 0 0 .895-.386l5.16-5.357a1.54 1.54 0 0 0 .372-.93V31.32a1.54 1.54 0 0 0-.372-.93l-5.16-5.357a1.42 1.42 0 0 0-.895-.386h-1.312a1.42 1.42 0 0 0-.895.386L25.32 30.4a1.55 1.55 0 0 0-.372.93M3.13 27.62l7.365 4.417L3.13 36.45a4.06 4.06 0 0 0-1.399 5.424 3.75 3.75 0 0 0 2.3 1.844c.986.274 2.042.133 2.922-.392l13.008-7.8c1.2-.762 1.9-2.078 1.9-3.492a4.16 4.16 0 0 0-1.9-3.492l-13.008-7.8a3.79 3.79 0 0 0-2.922-.4c-.986.265-1.818.94-2.3 1.844-1.057 1.9-.44 4.278 1.4 5.422m38.995 4.442a4 4 0 0 0 1.91 3.477l13 7.8c.88.524 1.934.666 2.92.392s1.817-.94 2.3-1.843a4.05 4.05 0 0 0-1.4-5.424L53.5 32.038l7.365-4.417c1.84-1.143 2.457-3.53 1.4-5.422a3.74 3.74 0 0 0-2.3-1.844c-.987-.274-2.042-.134-2.92.4l-13 7.8a4 4 0 0 0-1.91 3.507M25.48 40.508a3.7 3.7 0 0 0-2.611.464l-13.008 7.8c-1.84 1.143-2.456 3.53-1.4 5.422.483.903 1.314 1.57 2.3 1.843a3.75 3.75 0 0 0 2.922-.392l7.387-4.43v8.83a3.81 3.81 0 1 0 7.614 0V44.4a3.91 3.91 0 0 0-3.205-3.903m28.66 8.276l-13.008-7.8a3.75 3.75 0 0 0-2.922-.392 3.74 3.74 0 0 0-2.3 1.843 4.09 4.09 0 0 0-.494 2.37v15.25a3.81 3.81 0 1 0 7.614 0V51.28l7.287 4.37a3.79 3.79 0 0 0 2.922.4c.986-.265 1.818-.94 2.3-1.844 1.057-1.9.44-4.28-1.4-5.422"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
<title>Icon-Architecture/64/Arch_AWS-Simple-Notification-Service_64</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
<stop stop-color="#B0084D" offset="0%"></stop>
<stop stop-color="#FF4F8B" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Icon-Architecture/64/Arch_AWS-Simple-Notification-Service_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icon-Architecture-BG/64/Application-Integration" fill="url(#linearGradient-1)">
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
</g>
<path d="M17,38 C18.103,38 19,38.897 19,40 C19,41.103 18.103,42 17,42 C15.897,42 15,41.103 15,40 C15,38.897 15.897,38 17,38 L17,38 Z M41,64 C29.314,64 19.289,55.466 17.194,43.98 C18.965,43.894 20.427,42.659 20.857,41 L27,41 L27,39 L20.857,39 C20.427,37.342 18.966,36.107 17.195,36.02 C19.285,24.71 29.511,16 41,16 C45.313,16 49.832,17.622 54.429,20.821 L55.571,19.179 C50.633,15.743 45.73,14 41,14 C28.27,14 16.949,23.865 15.063,36.521 C13.839,37.207 13,38.5 13,40 C13,41.5 13.839,42.793 15.063,43.478 C16.97,56.341 28.056,66 41,66 C46.407,66 51.942,64.157 56.585,60.811 L55.415,59.189 C51.11,62.292 45.991,64 41,64 L41,64 Z M30.101,36.442 C31.955,36.895 34.275,37 36,37 C37.642,37 39.823,36.905 41.629,36.506 L37.105,45.553 C37.036,45.691 37,45.845 37,46 L37,50.453 C36.199,50.964 34.833,51.812 34,51.986 L34,46 C34,45.868 33.974,45.737 33.923,45.615 L30.101,36.442 Z M36,33 C40.025,33 42.174,33.604 42.841,34 C42.174,34.396 40.025,35 36,35 C31.975,35 29.826,34.396 29.159,34 C29.826,33.604 31.975,33 36,33 L36,33 Z M33,54 L34,54 C34.043,54 34.086,53.997 34.128,53.992 C35.352,53.833 36.909,52.887 38.272,52.013 L38.535,51.845 C38.824,51.661 39,51.342 39,51 L39,46.236 L44.559,35.12 C44.833,34.801 45,34.434 45,34 C45,31.39 39.361,31 36,31 C32.639,31 27,31.39 27,34 C27,34.366 27.12,34.684 27.32,34.967 L32,46.2 L32,53 C32,53.552 32.447,54 33,54 L33,54 Z M62,53 C63.103,53 64,53.897 64,55 C64,56.103 63.103,57 62,57 C60.897,57 60,56.103 60,55 C60,53.897 60.897,53 62,53 L62,53 Z M62,23 C63.103,23 64,23.897 64,25 C64,26.103 63.103,27 62,27 C60.897,27 60,26.103 60,25 C60,23.897 60.897,23 62,23 L62,23 Z M64,38 C65.103,38 66,38.897 66,40 C66,41.103 65.103,42 64,42 C62.897,42 62,41.103 62,40 C62,38.897 62.897,38 64,38 L64,38 Z M54,41 L60.143,41 C60.589,42.72 62.142,44 64,44 C66.206,44 68,42.206 68,40 C68,37.794 66.206,36 64,36 C62.142,36 60.589,37.28 60.143,39 L54,39 L54,26 L58.143,26 C58.589,27.72 60.142,29 62,29 C64.206,29 66,27.206 66,25 C66,22.794 64.206,21 62,21 C60.142,21 58.589,22.28 58.143,24 L53,24 C52.447,24 52,24.448 52,25 L52,39 L45,39 L45,41 L52,41 L52,55 C52,55.552 52.447,56 53,56 L58.143,56 C58.589,57.72 60.142,59 62,59 C64.206,59 66,57.206 66,55 C66,52.794 64.206,51 62,51 C60.142,51 58.589,52.28 58.143,54 L54,54 L54,41 Z" id="AWS-Simple-Notification-Service_Icon_64_Squid" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200">
<path fill="#fff" d="M0 0h300v200H0z"/>
<g transform="translate(30.667 -1141.475) scale(1.33333)">
<path d="M25 911.61v39h15v-6h-9v-27h9v-6zm114 0v6h9v27h-9v6h15v-39z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#201a26"/>
<path d="M92.5 931.11l27-15v30z" fill="#30d475"/>
<circle cx="70.002" cy="931.111" r="13.5" fill="#30d475"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 942 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="64" height="64" fill="#00749a"><path d="M2.26 16c0 5.45 3.13 10.145 7.7 12.348L3.478 10.435C2.725 12.174 2.26 14.03 2.26 16zm23.015-.696c0-1.68-.638-2.9-1.16-3.768-.696-1.16-1.333-2.087-1.333-3.246 0-1.275.986-2.435 2.32-2.435h.174C22.84 3.594 19.594 2.26 16 2.26A13.95 13.95 0 0 0 4.522 8.463h.87c1.45 0 3.652-.174 3.652-.174.754-.058.812 1.043.116 1.16 0 0-.754.116-1.565.116l4.986 14.84 3.014-8.986-2.145-5.855L12 9.45c-.754-.058-.638-1.16.058-1.16 0 0 2.26.174 3.594.174 1.45 0 3.652-.174 3.652-.174.754-.058.812 1.043.116 1.16 0 0-.754.116-1.565.116L22.84 24.35l1.4-4.58c.58-1.913 1.043-3.246 1.043-4.464zm-9.043 1.913L12.116 29.16c1.217.348 2.55.58 3.884.58 1.623 0 3.13-.3 4.58-.754-.058-.058-.058-.116-.116-.174zM28.058 9.45l.116 1.4c0 1.4-.232 2.957-1.043 4.928l-4.174 12.116c4.058-2.377 6.84-6.783 6.84-11.884-.058-2.377-.696-4.58-1.74-6.55zM16 0C7.188 0 0 7.188 0 16s7.188 16 16 16 16-7.188 16-16S24.812 0 16 0zm0 31.304C7.594 31.304.754 24.464.754 16A15.27 15.27 0 0 1 16 .754 15.27 15.27 0 0 1 31.246 16c0 8.464-6.84 15.304-15.246 15.304z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -8,5 +8,6 @@
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Settle your bill to continue",
"somethingWentWrong": "Something went wrong"
"somethingWentWrong": "Something went wrong",
"refreshPaymentStatus": "Refresh Status"
}

View File

@@ -8,5 +8,6 @@
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Settle your bill to continue",
"somethingWentWrong": "Something went wrong"
"somethingWentWrong": "Something went wrong",
"refreshPaymentStatus": "Refresh Status"
}

View File

@@ -3,6 +3,7 @@ import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import AppLoading from 'components/AppLoading/AppLoading';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
@@ -342,7 +343,7 @@ function App(): JSX.Element {
if (isLoggedInState) {
// if the setup calls are loading then return a spinner
if (isFetchingActiveLicense || isFetchingUser || isFetchingFeatureFlags) {
return <Spinner tip="Loading..." />;
return <AppLoading />;
}
// if the required calls fails then return a something went wrong error

View File

@@ -2,13 +2,20 @@ import axios from 'api';
import { PayloadProps, Props } from 'types/api/metrics/getTopOperations';
const getTopOperations = async (props: Props): Promise<PayloadProps> => {
const response = await axios.post(`/service/top_operations`, {
const endpoint = props.isEntryPoint
? '/service/entry_point_operations'
: '/service/top_operations';
const response = await axios.post(endpoint, {
start: `${props.start}`,
end: `${props.end}`,
service: props.service,
tags: props.selectedTags,
});
if (props.isEntryPoint) {
return response.data.data;
}
return response.data;
};

View File

@@ -1,24 +1,20 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Pipeline } from 'types/api/pipeline/def';
import { Props } from 'types/api/pipeline/post';
const post = async (
props: Props,
): Promise<SuccessResponse<Pipeline> | ErrorResponse> => {
const post = async (props: Props): Promise<SuccessResponseV2<Pipeline>> => {
try {
const response = await axios.post('/logs/pipelines', props.data);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};

View File

@@ -0,0 +1,24 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/licenses/apply';
const apply = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/licenses', {
key: props.key,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default apply;

View File

@@ -2,15 +2,11 @@ import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/licenses/apply';
import { PayloadProps } from 'types/api/licenses/apply';
const apply = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
const apply = async (): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/licenses', {
key: props.key,
});
const response = await axios.put<PayloadProps>('/licenses');
return {
httpStatusCode: response.status,

View File

@@ -0,0 +1,152 @@
.app-loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: var(--bg-ink-400, #121317); // Dark theme background
.app-loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
.brand {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 12px;
.brand-logo {
width: 40px;
height: 40px;
}
.brand-title {
font-size: 20px;
font-weight: 600;
color: var(--bg-vanilla-100, #ffffff); // White text for dark theme
margin: 0;
}
}
.brand-tagline {
margin-bottom: 24px;
.ant-typography {
color: var(--bg-vanilla-400, #c0c1c3); // Light gray text for dark theme
}
}
/* HTML: <div class="loader"></div> */
.loader {
width: 150px;
height: 12px;
border-radius: 2px;
color: var(--bg-robin-500, #4e74f8); // Primary blue color
border: 2px solid;
position: relative;
}
.loader::before {
content: '';
position: absolute;
margin: 2px;
inset: 0 100% 0 0;
border-radius: inherit;
background: currentColor;
animation: l6 2s infinite;
}
@keyframes l6 {
100% {
inset: 0;
}
}
}
}
// Light theme styles - more specific selector
.app-loading-container.lightMode {
background-color: var(
--bg-vanilla-100,
#ffffff
) !important; // White background for light theme
.app-loading-content {
.brand {
.brand-title {
color: var(--bg-ink-400, #121317) !important; // Dark text for light theme
}
}
.brand-tagline {
.ant-typography {
color: var(
--bg-ink-300,
#6b7280
) !important; // Dark gray text for light theme
}
}
.loader {
color: var(
--bg-robin-500,
#4e74f8
) !important; // Keep primary blue color for consistency
}
}
}
.perilin-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle, #fff 10%, transparent 0);
background-size: 12px 12px;
opacity: 1;
mask-image: radial-gradient(
circle at 50% 0,
rgba(11, 12, 14, 0.1) 0,
rgba(11, 12, 14, 0) 100%
);
-webkit-mask-image: radial-gradient(
circle at 50% 0,
rgba(11, 12, 14, 0.1) 0,
rgba(11, 12, 14, 0) 100%
);
}
// Dark theme styles - ensure dark theme is properly applied
.app-loading-container.dark {
background-color: var(--bg-ink-400, #121317) !important; // Dark background
.app-loading-content {
.brand {
.brand-title {
color: var(
--bg-vanilla-100,
#ffffff
) !important; // White text for dark theme
}
}
.brand-tagline {
.ant-typography {
color: var(
--bg-vanilla-400,
#c0c1c3
) !important; // Light gray text for dark theme
}
}
.loader {
color: var(--bg-robin-500, #4e74f8) !important; // Primary blue color
}
}
}

View File

@@ -0,0 +1,50 @@
import './AppLoading.styles.scss';
import { Typography } from 'antd';
import get from 'api/browser/localstorage/get';
import { LOCALSTORAGE } from 'constants/localStorage';
import { THEME_MODE } from 'hooks/useDarkMode/constant';
function AppLoading(): JSX.Element {
// Get theme from localStorage directly to avoid context dependency
const getThemeFromStorage = (): boolean => {
try {
const theme = get(LOCALSTORAGE.THEME);
return theme !== THEME_MODE.LIGHT; // Return true for dark, false for light
} catch (error) {
// If localStorage is not available, default to dark theme
return true;
}
};
const isDarkMode = getThemeFromStorage();
return (
<div className={`app-loading-container ${isDarkMode ? 'dark' : 'lightMode'}`}>
<div className="perilin-bg" />
<div className="app-loading-content">
<div className="brand">
<img
src="/Logos/signoz-brand-logo.svg"
alt="SigNoz"
className="brand-logo"
/>
<Typography.Title level={2} className="brand-title">
SigNoz
</Typography.Title>
</div>
<div className="brand-tagline">
<Typography.Text>
OpenTelemetry-Native Logs, Metrics and Traces in a single pane
</Typography.Text>
</div>
<div className="loader" />
</div>
</div>
);
}
export default AppLoading;

View File

@@ -0,0 +1,76 @@
import { render, screen } from '@testing-library/react';
import AppLoading from '../AppLoading';
// Mock the localStorage API
const mockGet = jest.fn();
jest.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: mockGet,
}));
describe('AppLoading', () => {
const SIGNOZ_TEXT = 'SigNoz';
const TAGLINE_TEXT =
'OpenTelemetry-Native Logs, Metrics and Traces in a single pane';
const CONTAINER_SELECTOR = '.app-loading-container';
beforeEach(() => {
jest.clearAllMocks();
});
it('should render loading screen with dark theme by default', () => {
// Mock localStorage to return dark theme (or undefined for default)
mockGet.mockReturnValue(undefined);
render(<AppLoading />);
// Check if main elements are rendered
expect(screen.getByAltText(SIGNOZ_TEXT)).toBeInTheDocument();
expect(screen.getByText(SIGNOZ_TEXT)).toBeInTheDocument();
expect(screen.getByText(TAGLINE_TEXT)).toBeInTheDocument();
// Check if dark theme class is applied
const container = screen.getByText(SIGNOZ_TEXT).closest(CONTAINER_SELECTOR);
expect(container).toHaveClass('dark');
expect(container).not.toHaveClass('lightMode');
});
it('should have proper structure and content', () => {
// Mock localStorage to return dark theme
mockGet.mockReturnValue(undefined);
render(<AppLoading />);
// Check for brand logo
const logo = screen.getByAltText(SIGNOZ_TEXT);
expect(logo).toBeInTheDocument();
expect(logo).toHaveAttribute('src', '/Logos/signoz-brand-logo.svg');
// Check for brand title
const title = screen.getByText(SIGNOZ_TEXT);
expect(title).toBeInTheDocument();
// Check for tagline
const tagline = screen.getByText(TAGLINE_TEXT);
expect(tagline).toBeInTheDocument();
// Check for loader
const loader = document.querySelector('.loader');
expect(loader).toBeInTheDocument();
});
it('should handle localStorage errors gracefully', () => {
// Mock localStorage to throw an error
mockGet.mockImplementation(() => {
throw new Error('localStorage not available');
});
render(<AppLoading />);
// Should still render with dark theme as fallback
expect(screen.getByText(SIGNOZ_TEXT)).toBeInTheDocument();
const container = screen.getByText(SIGNOZ_TEXT).closest(CONTAINER_SELECTOR);
expect(container).toHaveClass('dark');
});
});

View File

@@ -111,7 +111,7 @@
&-content {
max-height: calc(100vh - 300px);
overflow-y: auto;
padding: 16px;
padding: 16px 16px 18px 16px;
border: 1px solid var(--bg-slate-500, #161922);
border-top-width: 0;
border-bottom-width: 0;

View File

@@ -6,6 +6,7 @@ import updateUserPreference from 'api/v1/user/preferences/name/update';
import cx from 'classnames';
import { USER_PREFERENCES } from 'constants/userPreferences';
import dayjs from 'dayjs';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { ChevronsDown, ScrollText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -29,6 +30,8 @@ function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
'MMMM D, YYYY',
);
const { isCloudUser } = useGetTenantLicense();
const seenChangelogVersion = userPreferences?.find(
(preference) =>
preference.name === USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
@@ -121,18 +124,20 @@ function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
{changelog.features.length > 1 ? 'features' : 'feature'}
</span>
)}
<div className="changelog-modal-footer-ctas">
<Button type="default" icon={<CloseOutlined />} onClick={onClose}>
Skip for now
</Button>
<Button
type="primary"
icon={<CheckOutlined />}
onClick={onClickUpdateWorkspace}
>
Update my workspace
</Button>
</div>
{!isCloudUser && (
<div className="changelog-modal-footer-ctas">
<Button type="default" icon={<CloseOutlined />} onClick={onClose}>
Skip for now
</Button>
<Button
type="primary"
icon={<CheckOutlined />}
onClick={onClickUpdateWorkspace}
>
Update my workspace
</Button>
</div>
)}
{changelog && (
<div className="scroll-btn-container">
<button

View File

@@ -370,6 +370,7 @@ function CustomTimePicker({
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleInputChange}
data-1p-ignore
prefix={
inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />

View File

@@ -84,6 +84,7 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
date.tz(timezone.value).format(DATE_TIME_FORMATS.ISO_DATETIME)
}
onOk={onModalOkHandler}
data-1p-ignore
{...(selectedTime === 'custom' &&
!onTimeChange && {
value: rangeValue,

View File

@@ -72,6 +72,7 @@ function SearchBar({
onKeyDown={handleKeyDown}
tabIndex={0}
autoFocus
data-1p-ignore
/>
</div>
<kbd className="timezone-picker__esc-key">esc</kbd>

View File

@@ -9,6 +9,7 @@ import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
@@ -26,7 +27,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import {
BarChart2,
Braces,
@@ -71,7 +72,7 @@ function LogDetail({
const [contextQuery, setContextQuery] = useState<Query | undefined>();
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery } = useQueryBuilder();
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
@@ -88,7 +89,6 @@ function LogDetail({
const isDarkMode = useIsDarkMode();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@@ -136,10 +136,19 @@ function LogDetail({
// Go to logs explorer page with the log data
const handleOpenInExplorer = (): void => {
urlQuery.set(QueryParams.activeLogId, `"${log?.id}"`);
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
[QueryParams.endTime]: maxTime?.toString() || '',
[QueryParams.compositeQuery]: JSON.stringify(
updateAllQueriesOperators(
initialQueriesMap[DataSource.LOGS],
PANEL_TYPES.LIST,
DataSource.LOGS,
),
),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
// Only show when opened from infra monitoring page

View File

@@ -15,6 +15,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
letterSpacing: '-0.07px',
marginBottom: '0px',
minWidth: '10rem',
width: '10rem',
};
}

View File

@@ -47,6 +47,14 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const bodyColumnStyle = useMemo(
() => ({
...defaultTableStyle,
...(fields.length > 2 ? { width: '50rem' } : {}),
}),
[fields.length],
);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
@@ -136,7 +144,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultTableStyle,
style: bodyColumnStyle,
},
children: (
<TableBodyContent
@@ -166,6 +174,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
linesPerRow,
fontSize,
formatTimezoneAdjustedTimestamp,
bodyColumnStyle,
]);
return { columns, dataSource: flattenLogData };

View File

@@ -0,0 +1,56 @@
import { Button, Tooltip } from 'antd';
import refreshPaymentStatus from 'api/v3/licenses/put';
import cx from 'classnames';
import { RefreshCcw } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
function RefreshPaymentStatus({
btnShape,
type,
}: {
btnShape?: 'default' | 'round' | 'circle';
type?: 'button' | 'text' | 'tooltip';
}): JSX.Element {
const { t } = useTranslation(['failedPayment']);
const { activeLicenseRefetch } = useAppContext();
const [isLoading, setIsLoading] = useState(false);
const handleRefreshPaymentStatus = async (): Promise<void> => {
setIsLoading(true);
try {
await refreshPaymentStatus();
await Promise.all([activeLicenseRefetch()]);
} catch (e) {
console.error(e);
}
setIsLoading(false);
};
return (
<span className="refresh-payment-status-btn-wrapper">
<Tooltip title={type === 'tooltip' ? t('refreshPaymentStatus') : ''}>
<Button
type={type === 'text' ? 'text' : 'default'}
shape={btnShape}
className={cx('periscope-btn', { text: type === 'text' })}
onClick={handleRefreshPaymentStatus}
icon={<RefreshCcw size={14} />}
loading={isLoading}
>
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
</Button>
</Tooltip>
</span>
);
}
RefreshPaymentStatus.defaultProps = {
btnShape: 'default',
type: 'button',
};
export default RefreshPaymentStatus;

View File

@@ -1,6 +1,12 @@
.signoz-radio-group.ant-radio-group {
color: var(--text-vanilla-400);
&.ant-radio-group-disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
.view-title {
display: flex;
gap: var(--margin-2);
@@ -37,6 +43,22 @@
// Light mode styles
.lightMode {
.signoz-radio-group {
&.ant-radio-group-disabled {
.tab,
.selected_view {
background: var(--bg-vanilla-200) !important;
border-color: var(--bg-vanilla-400) !important;
color: var(--text-ink-400) !important;
}
.tab:hover,
.selected_view:hover {
background: var(--bg-vanilla-200) !important;
border-color: var(--bg-vanilla-400) !important;
color: var(--text-ink-400) !important;
}
}
.tab {
background: var(--bg-vanilla-100);
}

View File

@@ -13,6 +13,7 @@ interface SignozRadioGroupProps {
options: Option[];
onChange: (e: RadioChangeEvent) => void;
className?: string;
disabled?: boolean;
}
function SignozRadioGroup({
@@ -20,6 +21,7 @@ function SignozRadioGroup({
options,
onChange,
className = '',
disabled = false,
}: SignozRadioGroupProps): JSX.Element {
return (
<Radio.Group
@@ -27,6 +29,7 @@ function SignozRadioGroup({
buttonStyle="solid"
className={`signoz-radio-group ${className}`}
onChange={onChange}
disabled={disabled}
>
{options.map((option) => (
<Radio.Button
@@ -43,6 +46,7 @@ function SignozRadioGroup({
SignozRadioGroup.defaultProps = {
className: '',
disabled: false,
};
export default SignozRadioGroup;

View File

@@ -4,6 +4,7 @@ export enum LOCALSTORAGE {
AUTH_TOKEN = 'AUTH_TOKEN',
REFRESH_AUTH_TOKEN = 'REFRESH_AUTH_TOKEN',
THEME = 'THEME',
THEME_AUTO_SWITCH = 'THEME_AUTO_SWITCH',
LOGS_VIEW_MODE = 'LOGS_VIEW_MODE',
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
LOGS_LIST_OPTIONS = 'LOGS_LIST_OPTIONS',

View File

@@ -65,6 +65,7 @@ type QueryParams = {
pageSize: number;
exceptionType?: string;
serviceName?: string;
compositeQuery?: string;
};
function AllErrors(): JSX.Element {
@@ -81,6 +82,7 @@ function AllErrors(): JSX.Element {
getUpdatedPageSize,
getUpdatedExceptionType,
getUpdatedServiceName,
getUpdatedCompositeQuery,
} = useMemo(
() => ({
updatedOrder: getOrder(params.get(urlKey.order)),
@@ -89,6 +91,7 @@ function AllErrors(): JSX.Element {
getUpdatedPageSize: getUpdatePageSize(params.get(urlKey.pageSize)),
getUpdatedExceptionType: getFilterString(params.get(urlKey.exceptionType)),
getUpdatedServiceName: getFilterString(params.get(urlKey.serviceName)),
getUpdatedCompositeQuery: getFilterString(params.get(urlKey.compositeQuery)),
}),
[params],
);
@@ -203,6 +206,7 @@ function AllErrors(): JSX.Element {
offset: getUpdatedOffset,
orderParam: getUpdatedParams,
pageSize: getUpdatedPageSize,
compositeQuery: getUpdatedCompositeQuery,
};
if (exceptionFilterValue && exceptionFilterValue !== 'undefined') {
@@ -222,6 +226,7 @@ function AllErrors(): JSX.Element {
getUpdatedPageSize,
getUpdatedParams,
getUpdatedServiceName,
getUpdatedCompositeQuery,
pathname,
updatedOrder,
],
@@ -430,6 +435,7 @@ function AllErrors(): JSX.Element {
serviceName: getFilterString(params.get(urlKey.serviceName)),
exceptionType: getFilterString(params.get(urlKey.exceptionType)),
});
const compositeQuery = params.get(urlKey.compositeQuery) || '';
history.replace(
`${pathname}?${createQueryParams({
order: updatedOrder,
@@ -438,6 +444,7 @@ function AllErrors(): JSX.Element {
pageSize,
exceptionType,
serviceName,
compositeQuery,
})}`,
);
}

View File

@@ -18,6 +18,7 @@ export const urlKey = {
pageSize: 'pageSize',
exceptionType: 'exceptionType',
serviceName: 'serviceName',
compositeQuery: 'compositeQuery',
};
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy =>

View File

@@ -4,6 +4,21 @@
.app-banner-wrapper {
position: relative;
width: 100%;
.refresh-payment-status {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 4px;
.refresh-payment-status-btn-wrapper {
display: inline-block;
&:hover {
text-decoration: underline;
}
}
}
}
.app-layout {
@@ -12,24 +27,24 @@
width: 100%;
&.isWorkspaceRestricted {
height: calc(100% - 32px);
height: calc(100% - 48px);
// same styles as its either trial expired or payment failed
&.isTrialExpired {
height: calc(100% - 64px);
height: calc(100% - 96px);
}
&.isPaymentFailed {
height: calc(100% - 64px);
height: calc(100% - 96px);
}
}
&.isTrialExpired {
height: calc(100% - 32px);
height: calc(100% - 48px);
}
&.isPaymentFailed {
height: calc(100% - 32px);
height: calc(100% - 48px);
}
.app-content {
@@ -196,5 +211,5 @@
.workspace-restricted-banner,
.trial-expiry-banner,
.payment-failed-banner {
height: 32px;
height: 48px;
}

View File

@@ -16,6 +16,7 @@ import cx from 'classnames';
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -27,6 +28,7 @@ import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import useTabVisibility from 'hooks/useTabFocus';
import history from 'lib/history';
import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
@@ -154,6 +156,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
preference.name === USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
)?.value as string;
const isVisible = useTabVisibility();
const [
getUserVersionResponse,
getUserLatestVersionResponse,
@@ -177,6 +181,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
},
]);
useEffect(() => {
// refetch the changelog only when the current tab becomes active + there isn't an active request
if (!getChangelogByVersionResponse.isLoading && isVisible) {
getChangelogByVersionResponse.refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible]);
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
if (
@@ -654,6 +666,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
upgrade
</a>
to continue using SigNoz features.
<span className="refresh-payment-status">
{' '}
| Already upgraded? <RefreshPaymentStatus type="text" />
</span>
</span>
) : (
'Please contact your administrator for upgrading to a paid plan.'
@@ -680,6 +696,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
pay the bill
</a>
to continue using SigNoz features.
<span className="refresh-payment-status">
{' '}
| Already paid? <RefreshPaymentStatus type="text" />
</span>
</span>
) : (
' Please contact your administrator to pay the bill.'

View File

@@ -20,6 +20,7 @@ import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
import manageCreditCardApi from 'api/v1/portal/create';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@@ -440,14 +441,15 @@ export default function BillingContainer(): JSX.Element {
</Typography.Text>
) : null}
</Flex>
<Flex gap={20}>
<Flex gap={8}>
<Button
type="dashed"
type="default"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading || isFetchingBillingData}
onClick={handleCsvDownload}
icon={<CloudDownloadOutlined />}
className="periscope-btn"
>
Download CSV
</Button>
@@ -463,6 +465,8 @@ export default function BillingContainer(): JSX.Element {
? t('manage_billing')
: t('upgrade_plan')}
</Button>
<RefreshPaymentStatus type="tooltip" />
</Flex>
</Flex>

View File

@@ -13,3 +13,14 @@
margin-bottom: 16px;
}
}
.lightMode {
.create-alert-channels-container {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
.form-alert-channels-title {
color: var(--bg-ink-100);
}
}
}

View File

@@ -196,6 +196,7 @@ function GridCardGraph({
[requestData.query],
);
console.log('requestData', requestData);
const queryResponse = useGetQueryRange(
{
...requestData,

View File

@@ -2,7 +2,7 @@
import './Home.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Alert, Button, Popover } from 'antd';
import { Button, Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
@@ -320,8 +320,6 @@ export default function Home(): JSX.Element {
}
}, [hostData, k8sPodsData, handleUpdateChecklistDoneItem]);
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
useEffect(() => {
logEvent('Homepage: Visited', {});
}, []);
@@ -706,33 +704,6 @@ export default function Home(): JSX.Element {
)}
</div>
<div className="home-right-content">
{(isCloudUser || isEnterpriseSelfHostedUser) && (
<div className="home-notifications-container">
<div className="notification">
<Alert
message={
<>
We&apos;re updating our metric ingestion processing pipeline.
Currently, metric names and labels are normalized to replace dots and
other special characters with underscores (_). This restriction will
soon be removed. Learn more{' '}
<a
href="https://signoz.io/guides/metrics-migration-cloud-users"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</>
}
type="warning"
showIcon
/>
</div>
</div>
)}
{!isWelcomeChecklistSkipped && !loadingUserPreferences && (
<AnimatePresence initial={false}>
<Card className="checklist-card">

View File

@@ -1,5 +1,5 @@
import { Button, Form, Input } from 'antd';
import apply from 'api/v3/licenses/put';
import apply from 'api/v3/licenses/post';
import { useNotifications } from 'hooks/useNotifications';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -58,6 +58,7 @@ export interface ActionItemProps {
operator: string,
isJSON?: boolean,
dataType?: DataTypes,
fieldType?: string,
) => void;
}

View File

@@ -14,6 +14,7 @@ import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
@@ -113,6 +114,7 @@ function TableView({
fieldKey: string,
fieldValue: string,
dataType: string | undefined,
fieldType: string | undefined,
): void => {
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
if (onClickActionItem) {
@@ -122,6 +124,7 @@ function TableView({
operator,
undefined,
dataType as DataTypes,
fieldType,
);
}
};
@@ -131,8 +134,9 @@ function TableView({
fieldKey: string,
fieldValue: string,
dataType: string | undefined,
fieldType: MetricsType | undefined,
) => (): void => {
handleClick(operator, fieldKey, fieldValue, dataType);
handleClick(operator, fieldKey, fieldValue, dataType, fieldType);
if (operator === OPERATORS['=']) {
setIsFilterInLoading(true);
}

View File

@@ -11,6 +11,7 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import dompurify from 'dompurify';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
@@ -46,6 +47,7 @@ interface ITableViewActionsProps {
fieldKey: string,
fieldValue: string,
dataType: string | undefined,
logType: MetricsType | undefined,
) => () => void;
}
@@ -127,7 +129,7 @@ export default function TableViewActions(
} = props;
const { pathname } = useLocation();
const { dataType } = getFieldAttributes(record.field);
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
// there is no option for where clause in old logs explorer and live logs page
const isOldLogsExplorerOrLiveLogsPage = useMemo(
@@ -234,6 +236,7 @@ export default function TableViewActions(
fieldFilterKey,
parseFieldValue(fieldData.value),
dataType,
fieldType,
)}
/>
</Tooltip>
@@ -252,6 +255,7 @@ export default function TableViewActions(
fieldFilterKey,
parseFieldValue(fieldData.value),
dataType,
fieldType,
)}
/>
</Tooltip>
@@ -312,6 +316,7 @@ export default function TableViewActions(
fieldFilterKey,
parseFieldValue(fieldData.value),
dataType,
fieldType,
)}
/>
</Tooltip>
@@ -330,6 +335,7 @@ export default function TableViewActions(
fieldFilterKey,
parseFieldValue(fieldData.value),
dataType,
fieldType,
)}
/>
</Tooltip>

View File

@@ -1,11 +1,14 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ILog } from 'types/api/logs/log';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getFiltersFromResources } from './utils';
const RESOURCE_STARTS_WITH_REGEX = /^(k8s|cloud|host|deployment)/; // regex to filter out resources that start with the specified keywords
const RESOURCE_CONTAINS_REGEX = /(env|service|file|container|tenant)/; // regex to filter out resources that contains the spefied keywords
const useInitialQuery = (log: ILog): Query => {
const { updateAllQueriesOperators } = useQueryBuilder();
const resourcesFilters = getFiltersFromResources(log.resources_string);
@@ -16,17 +19,33 @@ const useInitialQuery = (log: ILog): Query => {
DataSource.LOGS,
);
const updateFilters = (filters: TagFilter): TagFilter => ({
...filters,
items: filters.items.filter(
(filterItem) =>
filterItem.key?.key &&
(RESOURCE_STARTS_WITH_REGEX.test(filterItem.key.key) ||
RESOURCE_CONTAINS_REGEX.test(filterItem.key.key)),
),
});
const data: Query = {
...updatedAllQueriesOperator,
builder: {
...updatedAllQueriesOperator.builder,
queryData: updatedAllQueriesOperator.builder.queryData.map((item) => ({
...item,
filters: {
queryData: updatedAllQueriesOperator.builder.queryData.map((item) => {
const filters = {
...item.filters,
items: [...item.filters.items, ...resourcesFilters],
},
})),
};
const updatedFilters = updateFilters(filters);
return {
...item,
filters: updatedFilters,
};
}),
},
};

View File

@@ -57,6 +57,10 @@ export default function TableRow({
[currentLog, handleSetActiveContextLog],
);
const hasSingleColumn =
tableColumns.filter((column) => column.key !== 'state-indicator').length ===
1;
return (
<>
{tableColumns.map((column) => {
@@ -80,9 +84,11 @@ export default function TableRow({
<TableCellStyled
$isDragColumn={false}
$isLogIndicator={column.key === 'state-indicator'}
$hasSingleColumn={hasSingleColumn}
$isDarkMode={isDarkMode}
key={column.key}
fontSize={fontSize}
columnKey={column.key as string}
>
{cloneElement(children, props)}
</TableCellStyled>

View File

@@ -135,6 +135,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
fontSize={tableViewProps?.fontSize}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: 'dragHandler' })}
columnKey={column.key as string}
>
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}
</TableHeaderCellStyled>

View File

@@ -8,13 +8,25 @@ interface TableHeaderCellStyledProps {
$isDragColumn: boolean;
$isDarkMode: boolean;
$isLogIndicator?: boolean;
$hasSingleColumn?: boolean;
fontSize?: FontSize;
columnKey?: string;
}
export const TableStyled = styled.table`
width: 100%;
`;
const getTimestampColumnWidth = (
columnKey?: string,
$hasSingleColumn?: boolean,
): string =>
columnKey === 'timestamp'
? $hasSingleColumn
? 'width: 100%;'
: 'width: 10%;'
: '';
export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
padding: 0.5rem;
${({ fontSize }): string =>
@@ -29,9 +41,12 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
${({ $isLogIndicator }): string =>
$isLogIndicator ? 'padding: 0 0 0 8px;width: 15px;' : ''}
$isLogIndicator ? 'padding: 0 0 0 8px;width: 1%;' : ''}
color: ${(props): string =>
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
${({ columnKey, $hasSingleColumn }): string =>
getTimestampColumnWidth(columnKey, $hasSingleColumn)}
`;
export const TableRowStyled = styled.tr<{
@@ -86,7 +101,11 @@ export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
: fontSize === FontSize.LARGE
? `font-size:14px; line-height:24px; padding: 0.5rem;`
: ``};
${({ $isLogIndicator }): string => ($isLogIndicator ? 'padding: 0px; ' : '')}
${({ $isLogIndicator }): string =>
$isLogIndicator ? 'padding: 0px; width: 1%;' : ''}
color: ${(props): string =>
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};
${({ columnKey, $hasSingleColumn }): string =>
getTimestampColumnWidth(columnKey, $hasSingleColumn)}
`;

View File

@@ -2,7 +2,7 @@ import getTopOperations from 'api/metrics/getTopOperations';
import TopOperationsTable from 'container/MetricsApplication/TopOperationsTable';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
@@ -20,6 +20,8 @@ function TopOperation(): JSX.Element {
}>();
const servicename = decodeURIComponent(encodedServiceName || '');
const [isEntryPoint, setIsEntryPoint] = useState<boolean>(false);
const { queries } = useResourceAttribute();
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
@@ -27,19 +29,27 @@ function TopOperation(): JSX.Element {
);
const { data, isLoading } = useQuery<PayloadProps>({
queryKey: [minTime, maxTime, servicename, selectedTags],
queryKey: [minTime, maxTime, servicename, selectedTags, isEntryPoint],
queryFn: (): Promise<PayloadProps> =>
getTopOperations({
service: servicename || '',
start: minTime,
end: maxTime,
selectedTags,
isEntryPoint,
}),
});
const topOperationData = data || [];
return <TopOperationsTable data={topOperationData} isLoading={isLoading} />;
return (
<TopOperationsTable
data={topOperationData}
isLoading={isLoading}
isEntryPoint={isEntryPoint}
onEntryPointToggle={setIsEntryPoint}
/>
);
}
export default TopOperation;

View File

@@ -1,9 +1,24 @@
.top-operation {
position: relative;
.top-operation--download {
&__controls {
display: flex;
justify-content: flex-end;
align-items: center;
position: absolute;
top: 15px;
right: 0px;
z-index: 1;
gap: 8px;
}
&__entry-point {
display: flex;
align-items: center;
gap: 6px;
}
&__download {
.ant-btn-icon {
margin: 0 !important;
}
}
}

View File

@@ -1,9 +1,10 @@
import './TopOperationsTable.styles.scss';
import { SearchOutlined } from '@ant-design/icons';
import { InputRef, Tooltip, Typography } from 'antd';
import { InputRef, Switch, Tooltip, Typography } from 'antd';
import { ColumnsType, ColumnType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip';
import Download from 'container/Download/Download';
import { filterDropdown } from 'container/ServiceApplication/Filter/FilterDropdown';
import useResourceAttribute from 'hooks/useResourceAttribute';
@@ -29,6 +30,8 @@ import {
function TopOperationsTable({
data,
isLoading,
isEntryPoint,
onEntryPointToggle,
}: TopOperationsTableProps): JSX.Element {
const searchInput = useRef<InputRef>(null);
const { servicename: encodedServiceName } = useParams<IServiceName>();
@@ -174,20 +177,45 @@ function TopOperationsTable({
hideOnSinglePage: true,
};
const entryPointSpanInfo = {
text: 'Shows the spans where requests enter new services for the first time',
url:
'https://signoz.io/docs/traces-management/guides/entry-point-spans-service-overview/',
urlText: 'Learn more about Entrypoint Spans.',
};
return (
<div className="top-operation">
<div className="top-operation--download">
<Download
data={downloadableData}
isLoading={isLoading}
fileName={`top-operations-${servicename}`}
/>
<div className="top-operation__controls">
<div className="top-operation__download">
<Download
data={downloadableData}
isLoading={isLoading}
fileName={`top-operations-${servicename}`}
/>
</div>
<div className="top-operation__entry-point">
<Switch
checked={isEntryPoint}
onChange={onEntryPointToggle}
size="small"
/>
<span className="top-operation__entry-point-label">Entrypoint Spans</span>
<TextToolTip
text={entryPointSpanInfo.text}
url={entryPointSpanInfo.url}
useFilledIcon={false}
urlText={entryPointSpanInfo.urlText}
/>
</div>
</div>
<ResizeTable
columns={columns}
loading={isLoading}
showHeader
title={(): string => 'Key Operations'}
title={(): string =>
isEntryPoint ? 'Key Entrypoint Operations' : 'Key Operations'
}
tableLayout="fixed"
dataSource={data}
rowKey="name"
@@ -209,6 +237,8 @@ export interface TopOperationList {
interface TopOperationsTableProps {
data: TopOperationList[];
isLoading: boolean;
isEntryPoint: boolean;
onEntryPointToggle: (checked: boolean) => void;
}
export default TopOperationsTable;

View File

@@ -1,3 +1,6 @@
import { QueryClient } from 'react-query';
import configureStore from 'redux-mock-store';
import { TopOperationList } from '../TopOperationsTable';
interface TopOperation {
@@ -17,3 +20,59 @@ export const getTopOperationList = ({
p95: 0,
p99: 0,
} as TopOperationList);
export const defaultApiCallExpectation = {
service: 'test-service',
start: 1640995200000,
end: 1641081600000,
selectedTags: [],
isEntryPoint: false,
};
export const mockStore = configureStore([]);
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
export const mockTopOperationsData: TopOperationList[] = [
{
name: 'GET /api/users',
p50: 1000000,
p95: 2000000,
p99: 3000000,
numCalls: 100,
errorCount: 5,
},
{
name: 'POST /api/orders',
p50: 1500000,
p95: 2500000,
p99: 3500000,
numCalls: 80,
errorCount: 2,
},
];
export const mockEntryPointData: TopOperationList[] = [
{
name: 'GET /api/health',
p50: 500000,
p95: 1000000,
p99: 1500000,
numCalls: 200,
errorCount: 0,
},
];
export const createMockStore = (): any =>
mockStore({
globalTime: {
minTime: 1640995200000,
maxTime: 1641081600000,
},
});

View File

@@ -0,0 +1,300 @@
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import {
createMockStore,
defaultApiCallExpectation,
mockEntryPointData,
mockTopOperationsData,
queryClient,
} from '../__mocks__/getTopOperation';
import TopOperation from '../Tabs/Overview/TopOperation';
// Mock dependencies
jest.mock('hooks/useResourceAttribute');
jest.mock('hooks/useResourceAttribute/utils', () => ({
convertRawQueriesToTraceSelectedTags: (): any[] => [],
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: (): { servicename: string } => ({
servicename: encodeURIComponent('test-service'),
}),
}));
// Mock the util functions that TopOperationsTable uses
jest.mock('../Tabs/util', () => ({
useGetAPMToTracesQueries: (): any => ({
builder: {
queryData: [
{
filters: {
items: [],
},
},
],
},
}),
}));
// Mock the resourceAttributesToTracesFilterItems function
jest.mock('container/TraceDetail/utils', () => ({
resourceAttributesToTracesFilterItems: (): any[] => [],
}));
const mockedUseResourceAttribute = useResourceAttribute as jest.MockedFunction<
typeof useResourceAttribute
>;
// Constants
const KEY_OPERATIONS_TEXT = 'Key Operations';
const KEY_ENTRY_POINT_OPERATIONS_TEXT = 'Key Entrypoint Operations';
const ENTRY_POINT_SPANS_TEXT = 'Entrypoint Spans';
const TOP_OPERATIONS_ENDPOINT = 'top_operations';
const ENTRY_POINT_OPERATIONS_ENDPOINT = 'entry_point_operations';
const renderComponent = (store = createMockStore()): any =>
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<TopOperation />
</QueryClientProvider>
</Provider>,
);
// Helper function to wait for initial render and verify basic functionality
const waitForInitialRender = async (): Promise<void> => {
await waitFor(() => {
expect(screen.getByText(KEY_OPERATIONS_TEXT)).toBeInTheDocument();
});
};
// Helper function to click toggle and wait for data to load
const clickToggleAndWaitForDataLoad = async (): Promise<HTMLElement> => {
const toggleSwitch = screen.getByRole('switch');
act(() => {
fireEvent.click(toggleSwitch);
});
await waitFor(() => {
expect(screen.getByText(KEY_ENTRY_POINT_OPERATIONS_TEXT)).toBeInTheDocument();
});
return toggleSwitch;
};
describe('TopOperation API Integration', () => {
let apiCalls: { endpoint: string; body: any }[] = [];
beforeEach(() => {
jest.clearAllMocks();
queryClient.clear();
apiCalls = [];
mockedUseResourceAttribute.mockReturnValue({
queries: [],
} as any);
server.use(
rest.post(
'http://localhost/api/v1/service/top_operations',
async (req, res, ctx) => {
const body = await req.json();
apiCalls.push({ endpoint: TOP_OPERATIONS_ENDPOINT, body });
return res(ctx.status(200), ctx.json(mockTopOperationsData));
},
),
rest.post(
'http://localhost/api/v1/service/entry_point_operations',
async (req, res, ctx) => {
const body = await req.json();
apiCalls.push({ endpoint: ENTRY_POINT_OPERATIONS_ENDPOINT, body });
return res(ctx.status(200), ctx.json({ data: mockEntryPointData }));
},
),
);
});
it('renders with default key operations on initial load', async () => {
renderComponent();
await waitForInitialRender();
// Verify the toggle is present and unchecked
const toggleSwitch = screen.getByRole('switch');
expect(toggleSwitch).not.toBeChecked();
expect(screen.getByText(ENTRY_POINT_SPANS_TEXT)).toBeInTheDocument();
});
it('calls top_operations API on initial render', async () => {
renderComponent();
await waitForInitialRender();
// Wait a bit more for API calls to be captured
await waitFor(() => {
expect(apiCalls.length).toBeGreaterThan(0);
});
// Verify that only the top_operations endpoint was called
expect(apiCalls).toHaveLength(1);
expect(apiCalls[0].endpoint).toBe(TOP_OPERATIONS_ENDPOINT);
expect(apiCalls[0].body).toEqual({
start: `${defaultApiCallExpectation.start}`,
end: `${defaultApiCallExpectation.end}`,
service: defaultApiCallExpectation.service,
tags: defaultApiCallExpectation.selectedTags,
});
});
it('calls entry_point_operations API when toggle is switched to entry point', async () => {
renderComponent();
// Wait for initial render
await waitForInitialRender();
// Wait for initial API call
await waitFor(() => {
expect(apiCalls.length).toBeGreaterThan(0);
});
// Clear previous API calls
apiCalls = [];
// Toggle to entry point
await clickToggleAndWaitForDataLoad();
// Wait for the API call to be captured
await waitFor(() => {
expect(apiCalls.length).toBeGreaterThan(0);
});
// Verify that the entry_point_operations endpoint was called
expect(apiCalls).toHaveLength(1);
expect(apiCalls[0].endpoint).toBe(ENTRY_POINT_OPERATIONS_ENDPOINT);
expect(apiCalls[0].body).toEqual({
start: `${defaultApiCallExpectation.start}`,
end: `${defaultApiCallExpectation.end}`,
service: defaultApiCallExpectation.service,
tags: defaultApiCallExpectation.selectedTags,
});
});
it('switches to entry point operations when toggle is clicked', async () => {
renderComponent();
// Wait for initial render
await waitForInitialRender();
// Find and click the toggle switch
const toggleSwitch = screen.getByRole('switch');
expect(toggleSwitch).not.toBeChecked();
await clickToggleAndWaitForDataLoad();
// Check that the switch is now checked and title updates
expect(toggleSwitch).toBeChecked();
expect(screen.getByText(KEY_ENTRY_POINT_OPERATIONS_TEXT)).toBeInTheDocument();
});
it('calls correct APIs when toggling multiple times', async () => {
renderComponent();
// Wait for initial render
await waitForInitialRender();
// Wait for initial API call
await waitFor(() => {
expect(apiCalls.length).toBeGreaterThan(0);
});
// Should have called top_operations initially
expect(apiCalls).toHaveLength(1);
expect(apiCalls[0].endpoint).toBe(TOP_OPERATIONS_ENDPOINT);
// Toggle to entry point
await clickToggleAndWaitForDataLoad();
// Wait for the second API call
await waitFor(() => {
expect(apiCalls.length).toBeGreaterThan(1);
});
// Should now have called entry_point_operations
expect(apiCalls).toHaveLength(2);
expect(apiCalls[1].endpoint).toBe(ENTRY_POINT_OPERATIONS_ENDPOINT);
// Toggle back to regular operations
const toggleSwitch = screen.getByRole('switch');
act(() => {
fireEvent.click(toggleSwitch);
});
await waitFor(() => {
expect(screen.getByText(KEY_OPERATIONS_TEXT)).toBeInTheDocument();
});
// Wait for the third API call
await waitFor(() => {
expect(apiCalls.length).toBeGreaterThan(2);
});
// Should have called top_operations again
expect(apiCalls).toHaveLength(3);
expect(apiCalls[2].endpoint).toBe(TOP_OPERATIONS_ENDPOINT);
expect(toggleSwitch).not.toBeChecked();
});
it('displays entry point toggle with correct label', async () => {
renderComponent();
await waitFor(() => {
expect(screen.getByText(ENTRY_POINT_SPANS_TEXT)).toBeInTheDocument();
});
const toggleSwitch = screen.getByRole('switch');
expect(toggleSwitch).toBeInTheDocument();
});
it('switches back to key operations when toggle is clicked twice', async () => {
renderComponent();
// Wait for initial render
await waitForInitialRender();
// Toggle on (to entry point)
await clickToggleAndWaitForDataLoad();
expect(screen.getByText(KEY_ENTRY_POINT_OPERATIONS_TEXT)).toBeInTheDocument();
// Toggle off (back to key operations)
const toggleSwitch = screen.getByRole('switch');
act(() => {
fireEvent.click(toggleSwitch);
});
await waitFor(() => {
expect(screen.getByText(KEY_OPERATIONS_TEXT)).toBeInTheDocument();
});
expect(toggleSwitch).not.toBeChecked();
});
});

View File

@@ -104,6 +104,7 @@ function Metadata({
if (field.key === 'metric_type') {
return (
<Select
data-testid="metric-type-select"
options={Object.entries(METRIC_TYPE_VALUES_MAP).map(([key]) => ({
value: key,
label: METRIC_TYPE_LABEL_MAP[key as MetricType],
@@ -121,6 +122,7 @@ function Metadata({
if (field.key === 'temporality') {
return (
<Select
data-testid="temporality-select"
options={Object.values(Temporality).map((key) => ({
value: key,
label: key,
@@ -137,6 +139,7 @@ function Metadata({
}
return (
<Input
data-testid="description-input"
name={field.key}
defaultValue={
metricMetadata[

View File

@@ -1,67 +0,0 @@
import { Button, Collapse, Typography } from 'antd';
import { useMemo, useState } from 'react';
import { TopAttributesProps } from './types';
function TopAttributes({
items,
title,
loadMore,
hideLoadMore,
}: TopAttributesProps): JSX.Element {
const [activeKey, setActiveKey] = useState<string | string[]>(
'top-attributes',
);
const collapseItems = useMemo(
() => [
{
label: (
<div className="metrics-accordion-header">
<Typography.Text>{title}</Typography.Text>
</div>
),
key: 'top-attributes',
children: (
<div className="top-attributes-content">
{items.map((item) => (
<div className="top-attributes-item" key={item.key}>
<div className="top-attributes-item-progress">
<div className="top-attributes-item-key">{item.key}</div>
<div className="top-attributes-item-count">{item.count}</div>
<div
className="top-attributes-item-progress-bar"
style={{ width: `${item.percentage}%` }}
/>
</div>
<div className="top-attributes-item-percentage">
{item.percentage.toFixed(2)}%
</div>
</div>
))}
{loadMore && !hideLoadMore && (
<div className="top-attributes-load-more">
<Button type="link" onClick={loadMore}>
Load more
</Button>
</div>
)}
</div>
),
},
],
[title, items, loadMore, hideLoadMore],
);
return (
<Collapse
bordered
className="metrics-accordion"
activeKey={activeKey}
onChange={(keys): void => setActiveKey(keys)}
items={collapseItems}
/>
);
}
export default TopAttributes;

View File

@@ -1,6 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
import * as reactUseHooks from 'react-use';
import { MetricDetailsAttribute } from '../../../../api/metricsExplorer/getMetricDetails';
import ROUTES from '../../../../constants/routes';
@@ -34,6 +35,11 @@ const mockAttributes: MetricDetailsAttribute[] = [
},
];
const mockUseCopyToClipboard = jest.fn();
jest
.spyOn(reactUseHooks, 'useCopyToClipboard')
.mockReturnValue([{ value: 'value1' }, mockUseCopyToClipboard] as any);
describe('AllAttributes', () => {
it('renders attributes section with title', () => {
render(
@@ -165,4 +171,42 @@ describe('AllAttributesValue', () => {
);
expect(screen.queryByText('Show More')).not.toBeInTheDocument();
});
it('copy button should copy the attribute value to the clipboard', () => {
render(
<AllAttributesValue
filterKey="attribute1"
filterValue={['value1', 'value2']}
goToMetricsExploreWithAppliedAttribute={
mockGoToMetricsExploreWithAppliedAttribute
}
/>,
);
expect(screen.getByText('value1')).toBeInTheDocument();
fireEvent.click(screen.getByText('value1'));
expect(screen.getByText('Copy Attribute')).toBeInTheDocument();
fireEvent.click(screen.getByText('Copy Attribute'));
expect(mockUseCopyToClipboard).toHaveBeenCalledWith('value1');
});
it('explorer button should go to metrics explore with the attribute filter applied', () => {
render(
<AllAttributesValue
filterKey="attribute1"
filterValue={['value1', 'value2']}
goToMetricsExploreWithAppliedAttribute={
mockGoToMetricsExploreWithAppliedAttribute
}
/>,
);
expect(screen.getByText('value1')).toBeInTheDocument();
fireEvent.click(screen.getByText('value1'));
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
fireEvent.click(screen.getByText('Open in Explorer'));
expect(mockGoToMetricsExploreWithAppliedAttribute).toHaveBeenCalledWith(
'attribute1',
'value1',
);
});
});

View File

@@ -0,0 +1,162 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query';
import * as useSafeNavigate from 'hooks/useSafeNavigate';
import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover';
const mockAlert1 = {
alert_id: '1',
alert_name: 'Alert 1',
};
const mockAlert2 = {
alert_id: '2',
alert_name: 'Alert 2',
};
const mockDashboard1 = {
dashboard_id: '1',
dashboard_name: 'Dashboard 1',
};
const mockDashboard2 = {
dashboard_id: '2',
dashboard_name: 'Dashboard 2',
};
const mockAlerts = [mockAlert1, mockAlert2];
const mockDashboards = [mockDashboard1, mockDashboard2];
const mockSafeNavigate = jest.fn();
jest.spyOn(useSafeNavigate, 'useSafeNavigate').mockReturnValue({
safeNavigate: mockSafeNavigate,
});
const mockSetQuery = jest.fn();
const mockUrlQuery = {
set: mockSetQuery,
toString: jest.fn(),
};
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: jest.fn(() => mockUrlQuery),
}));
describe('DashboardsAndAlertsPopover', () => {
it('renders the popover correctly with multiple dashboards and alerts', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={mockDashboards}
/>,
);
expect(
screen.getByText(`${mockDashboards.length} dashboards`),
).toBeInTheDocument();
expect(
screen.getByText(`${mockAlerts.length} alert rules`),
).toBeInTheDocument();
});
it('renders null with no dashboards and alerts', () => {
const { container } = render(
<DashboardsAndAlertsPopover alerts={[]} dashboards={[]} />,
);
expect(container).toBeEmptyDOMElement();
});
it('renders popover with single dashboard and alert', () => {
render(
<DashboardsAndAlertsPopover
alerts={[mockAlert1]}
dashboards={[mockDashboard1]}
/>,
);
expect(screen.getByText(`1 dashboard`)).toBeInTheDocument();
expect(screen.getByText(`1 alert rule`)).toBeInTheDocument();
});
it('renders popover with dashboard id if name is not available', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={[{ ...mockDashboard1, dashboard_name: undefined } as any]}
/>,
);
fireEvent.click(screen.getByText(`1 dashboard`));
expect(screen.getByText(mockDashboard1.dashboard_id)).toBeInTheDocument();
});
it('renders popover with alert id if name is not available', () => {
render(
<DashboardsAndAlertsPopover
alerts={[{ ...mockAlert1, alert_name: undefined } as any]}
dashboards={mockDashboards}
/>,
);
fireEvent.click(screen.getByText(`1 alert rule`));
expect(screen.getByText(mockAlert1.alert_id)).toBeInTheDocument();
});
it('navigates to the dashboard when the dashboard is clicked', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={mockDashboards}
/>,
);
// Click on 2 dashboards button
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
// Popover showing list of 2 dashboards should be visible
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
// Click on the first dashboard
fireEvent.click(screen.getByText(mockDashboard1.dashboard_name));
// Should navigate to the dashboard
expect(mockSafeNavigate).toHaveBeenCalledWith(
`/dashboard/${mockDashboard1.dashboard_id}`,
);
});
it('navigates to the alert when the alert is clicked', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={mockDashboards}
/>,
);
// Click on 2 alert rules button
fireEvent.click(screen.getByText(`${mockAlerts.length} alert rules`));
// Popover showing list of 2 alert rules should be visible
expect(screen.getByText(mockAlert1.alert_name)).toBeInTheDocument();
expect(screen.getByText(mockAlert2.alert_name)).toBeInTheDocument();
// Click on the first alert rule
fireEvent.click(screen.getByText(mockAlert1.alert_name));
// Should navigate to the alert rule
expect(mockSetQuery).toHaveBeenCalledWith(
QueryParams.ruleId,
mockAlert1.alert_id,
);
});
it('renders unique dashboards even when there are duplicates', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={[...mockDashboards, mockDashboard1]}
/>,
);
expect(
screen.getByText(`${mockDashboards.length} dashboards`),
).toBeInTheDocument();
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,222 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { fireEvent, render, screen } from '@testing-library/react';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as useUpdateMetricMetadataHooks from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import * as useNotificationsHooks from 'hooks/useNotifications';
import Metadata from '../Metadata';
const mockUseUpdateMetricMetadata = jest.fn();
jest
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
.mockReturnValue({
mutate: mockUseUpdateMetricMetadata,
isLoading: false,
} as any);
const mockErrorNotification = jest.fn();
const mockSuccessNotification = jest.fn();
jest.spyOn(useNotificationsHooks, 'useNotifications').mockReturnValue({
notifications: {
error: mockErrorNotification,
success: mockSuccessNotification,
},
} as any);
const mockMetricName = 'test_metric';
const mockMetricMetadata = {
metric_type: MetricType.GAUGE,
description: 'test_description',
unit: 'test_unit',
temporality: Temporality.DELTA,
};
const mockRefetchMetricDetails = jest.fn();
describe('Metadata', () => {
it('should render the metadata properly', () => {
render(
<Metadata
metricName={mockMetricName}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
/>,
);
expect(screen.getByText('Metric Type')).toBeInTheDocument();
expect(screen.getByText(mockMetricMetadata.metric_type)).toBeInTheDocument();
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText(mockMetricMetadata.description)).toBeInTheDocument();
expect(screen.getByText('Unit')).toBeInTheDocument();
expect(screen.getByText(mockMetricMetadata.unit)).toBeInTheDocument();
expect(screen.getByText('Temporality')).toBeInTheDocument();
expect(screen.getByText(mockMetricMetadata.temporality)).toBeInTheDocument();
});
it('editing the metadata should show the form inputs', () => {
render(
<Metadata
metricName={mockMetricName}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
/>,
);
const editButton = screen.getByText('Edit');
expect(editButton).toBeInTheDocument();
fireEvent.click(editButton);
expect(screen.getByTestId('metric-type-select')).toBeInTheDocument();
expect(screen.getByTestId('temporality-select')).toBeInTheDocument();
expect(screen.getByTestId('description-input')).toBeInTheDocument();
});
it('should update the metadata when the form is submitted', async () => {
render(
<Metadata
metricName={mockMetricName}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
/>,
);
const editButton = screen.getByText('Edit');
expect(editButton).toBeInTheDocument();
fireEvent.click(editButton);
const metricDescriptionInput = screen.getByTestId('description-input');
expect(metricDescriptionInput).toBeInTheDocument();
fireEvent.change(metricDescriptionInput, {
target: { value: 'Updated description' },
});
const saveButton = screen.getByText('Save');
expect(saveButton).toBeInTheDocument();
fireEvent.click(saveButton);
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
expect.objectContaining({
metricName: mockMetricName,
payload: expect.objectContaining({
description: 'Updated description',
}),
}),
expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}),
);
});
it('should show success notification when metadata is updated successfully', async () => {
render(
<Metadata
metricName={mockMetricName}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
/>,
);
const editButton = screen.getByText('Edit');
fireEvent.click(editButton);
const metricDescriptionInput = screen.getByTestId('description-input');
fireEvent.change(metricDescriptionInput, {
target: { value: 'Updated description' },
});
const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
const onSuccessCallback =
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
onSuccessCallback({ statusCode: 200 });
expect(mockSuccessNotification).toHaveBeenCalledWith({
message: 'Metadata updated successfully',
});
expect(mockRefetchMetricDetails).toHaveBeenCalled();
});
it('should show error notification when metadata update fails with non-200 response', async () => {
render(
<Metadata
metricName={mockMetricName}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
/>,
);
const editButton = screen.getByText('Edit');
fireEvent.click(editButton);
const metricDescriptionInput = screen.getByTestId('description-input');
fireEvent.change(metricDescriptionInput, {
target: { value: 'Updated description' },
});
const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
const onSuccessCallback =
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
onSuccessCallback({ statusCode: 500 });
expect(mockErrorNotification).toHaveBeenCalledWith({
message:
'Failed to update metadata, please try again. If the issue persists, please contact support.',
});
});
it('should show error notification when metadata update fails', async () => {
render(
<Metadata
metricName={mockMetricName}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
/>,
);
const editButton = screen.getByText('Edit');
fireEvent.click(editButton);
const metricDescriptionInput = screen.getByTestId('description-input');
fireEvent.change(metricDescriptionInput, {
target: { value: 'Updated description' },
});
const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
const error = new Error('Failed to update metadata');
onErrorCallback(error);
expect(mockErrorNotification).toHaveBeenCalledWith({
message:
'Failed to update metadata, please try again. If the issue persists, please contact support.',
});
});
it('cancel button should cancel the edit mode', () => {
render(
<Metadata
metricName={mockMetricName}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
/>,
);
const editButton = screen.getByText('Edit');
expect(editButton).toBeInTheDocument();
fireEvent.click(editButton);
const cancelButton = screen.getByText('Cancel');
expect(cancelButton).toBeInTheDocument();
fireEvent.click(cancelButton);
const editButton2 = screen.getByText('Edit');
expect(editButton2).toBeInTheDocument();
});
});

View File

@@ -1,16 +1,16 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { MetricDetails } from 'api/metricsExplorer/getMetricDetails';
import { MetricDetails as MetricDetailsType } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import ROUTES from 'constants/routes';
import * as useGetMetricDetails from 'hooks/metricsExplorer/useGetMetricDetails';
import * as useUpdateMetricMetadata from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
import MetricDetailsView from '../MetricDetails';
import MetricDetails from '../MetricDetails';
const mockMetricName = 'test-metric';
const mockMetricDescription = 'description for a test metric';
const mockMetricData: MetricDetails = {
const mockMetricData: MetricDetailsType = {
name: mockMetricName,
description: mockMetricDescription,
unit: 'count',
@@ -84,7 +84,7 @@ jest.mock('hooks/useSafeNavigate', () => ({
describe('MetricDetails', () => {
it('renders metric details correctly', () => {
render(
<MetricDetailsView
<MetricDetails
onClose={mockOnClose}
isOpen
isModalTimeSelection
@@ -114,7 +114,7 @@ describe('MetricDetails', () => {
},
} as any);
render(
<MetricDetailsView
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
@@ -143,7 +143,7 @@ describe('MetricDetails', () => {
} as any);
render(
<MetricDetailsView
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
@@ -162,7 +162,7 @@ describe('MetricDetails', () => {
} as any);
render(
<MetricDetailsView
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
@@ -179,7 +179,7 @@ describe('MetricDetails', () => {
.spyOn(useGetMetricDetails, 'useGetMetricDetails')
.mockReturnValue(mockUseGetMetricDetailsData as any);
render(
<MetricDetailsView
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
@@ -204,7 +204,7 @@ describe('MetricDetails', () => {
},
} as any);
render(
<MetricDetailsView
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}

View File

@@ -0,0 +1,259 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
determineIsMonotonic,
formatTimestampToReadableDate,
getMetricDetailsQuery,
} from '../utils';
describe('MetricDetails utils', () => {
describe('determineIsMonotonic', () => {
it('should return true for histogram metrics', () => {
expect(determineIsMonotonic(MetricType.HISTOGRAM)).toBe(true);
});
it('should return true for exponential histogram metrics', () => {
expect(determineIsMonotonic(MetricType.EXPONENTIAL_HISTOGRAM)).toBe(true);
});
it('should return false for gauge metrics', () => {
expect(determineIsMonotonic(MetricType.GAUGE)).toBe(false);
});
it('should return false for summary metrics', () => {
expect(determineIsMonotonic(MetricType.SUMMARY)).toBe(false);
});
it('should return true for sum metrics with cumulative temporality', () => {
expect(determineIsMonotonic(MetricType.SUM, Temporality.CUMULATIVE)).toBe(
true,
);
});
it('should return false for sum metrics with delta temporality', () => {
expect(determineIsMonotonic(MetricType.SUM, Temporality.DELTA)).toBe(false);
});
it('should return false by default', () => {
expect(determineIsMonotonic('' as MetricType, '' as Temporality)).toBe(
false,
);
});
});
describe('formatTimestampToReadableDate', () => {
const FEW_SECONDS_AGO = 'Few seconds ago';
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
});
afterEach(() => {
jest.useRealTimers();
});
it('should return "Few seconds ago" for timestamps less than 60 seconds ago', () => {
const timestamp = '2024-01-15T11:59:30.000Z';
expect(formatTimestampToReadableDate(timestamp)).toBe(FEW_SECONDS_AGO);
});
it('should return "1 minute ago" for exactly 1 minute ago', () => {
const timestamp = '2024-01-15T11:59:00.000Z';
expect(formatTimestampToReadableDate(timestamp)).toBe('1 minute ago');
});
it('should return "X minutes ago" for multiple minutes ago', () => {
const timestamp = '2024-01-15T11:55:00.000Z';
expect(formatTimestampToReadableDate(timestamp)).toBe('5 minutes ago');
});
it('should return "1 hour ago" for exactly 1 hour ago', () => {
const timestamp = '2024-01-15T11:00:00.000Z';
expect(formatTimestampToReadableDate(timestamp)).toBe('1 hour ago');
});
it('should return "X hours ago" for multiple hours ago', () => {
const timestamp = '2024-01-15T09:00:00.000Z';
expect(formatTimestampToReadableDate(timestamp)).toBe('3 hours ago');
});
it('should return "Yesterday at HH:MM" for exactly 1 day ago', () => {
const timestamp = '2024-01-14T12:00:00.000Z';
const expectedTime = new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
expect(formatTimestampToReadableDate(timestamp)).toBe(
`Yesterday at ${expectedTime}`,
);
});
it('should return "X days ago" for multiple days ago (less than 7 days)', () => {
const timestamp = '2024-01-12T12:00:00.000Z';
expect(formatTimestampToReadableDate(timestamp)).toBe('3 days ago');
});
it('should return localized date string for dates 7 or more days ago', () => {
const oldTimestamp = '2024-01-01T12:00:00.000Z';
const result = formatTimestampToReadableDate(oldTimestamp);
expect(result).not.toBe(FEW_SECONDS_AGO);
expect(typeof result).toBe('string');
});
it('should handle future timestamps correctly', () => {
const timestamp = '2024-01-16T12:00:00.000Z';
const result = formatTimestampToReadableDate(timestamp);
expect(result).toBe(FEW_SECONDS_AGO);
});
});
describe('getMetricDetailsQuery', () => {
const TEST_METRIC_NAME = 'test_metric';
const API_GATEWAY = 'api-gateway';
it('should create correct query for SUM metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUM);
expect(query.builder.queryData[0]?.aggregateAttribute.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute.type).toBe(
MetricType.SUM,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('rate');
expect(query.builder.queryData[0]?.timeAggregation).toBe('rate');
expect(query.builder.queryData[0]?.spaceAggregation).toBe('sum');
});
it('should create correct query for GAUGE metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.GAUGE);
expect(query.builder.queryData[0]?.aggregateAttribute.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute.type).toBe(
MetricType.GAUGE,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('avg');
expect(query.builder.queryData[0]?.timeAggregation).toBe('avg');
expect(query.builder.queryData[0]?.spaceAggregation).toBe('avg');
});
it('should create correct query for SUMMARY metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUMMARY);
expect(query.builder.queryData[0]?.aggregateAttribute.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute.type).toBe(
MetricType.SUMMARY,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
expect(query.builder.queryData[0]?.spaceAggregation).toBe('sum');
});
it('should create correct query for HISTOGRAM metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.HISTOGRAM);
expect(query.builder.queryData[0]?.aggregateAttribute.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute.type).toBe(
MetricType.HISTOGRAM,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
expect(query.builder.queryData[0]?.spaceAggregation).toBe('p90');
});
it('should create correct query for EXPONENTIAL_HISTOGRAM metric type', () => {
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetricType.EXPONENTIAL_HISTOGRAM,
);
expect(query.builder.queryData[0]?.aggregateAttribute.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute.type).toBe(
MetricType.EXPONENTIAL_HISTOGRAM,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
expect(query.builder.queryData[0]?.spaceAggregation).toBe('p90');
});
it('should create query with default values for unknown metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, undefined);
expect(query.builder.queryData[0]?.aggregateAttribute.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute.type).toBe('');
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
expect(query.builder.queryData[0]?.spaceAggregation).toBe('noop');
});
it('should include filter when provided', () => {
const filter = { key: 'service', value: API_GATEWAY };
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetricType.SUM,
filter,
);
expect(query.builder.queryData[0]?.filters.items).toHaveLength(1);
expect(query.builder.queryData[0]?.filters.items[0]?.key?.key).toBe(
'service',
);
expect(query.builder.queryData[0]?.filters.items[0]?.value).toBe(
API_GATEWAY,
);
expect(query.builder.queryData[0]?.filters.items[0]?.op).toBe('=');
});
it('should include groupBy when provided', () => {
const groupBy = 'service';
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetricType.SUM,
undefined,
groupBy,
);
expect(query.builder.queryData[0]?.groupBy).toHaveLength(1);
expect(query.builder.queryData[0]?.groupBy[0]?.key).toBe('service');
expect(query.builder.queryData[0]?.groupBy[0]?.type).toBe('tag');
});
it('should include both filter and groupBy when provided', () => {
const filter = { key: 'service', value: API_GATEWAY };
const groupBy = 'endpoint';
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetricType.SUM,
filter,
groupBy,
);
expect(query.builder.queryData[0]?.filters.items).toHaveLength(1);
expect(query.builder.queryData[0]?.groupBy).toHaveLength(1);
expect(query.builder.queryData[0]?.filters.items[0]?.key?.key).toBe(
'service',
);
expect(query.builder.queryData[0]?.groupBy[0]?.key).toBe('endpoint');
});
it('should not include filters or groupBy when not provided', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUM);
expect(query.builder.queryData[0]?.filters.items).toHaveLength(0);
expect(query.builder.queryData[0]?.groupBy).toHaveLength(0);
});
});
});

Some files were not shown because too many files have changed in this diff Show More