Compare commits
269 Commits
fix/metada
...
testing-fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cd7489280 | ||
|
|
3f69d5cdc2 | ||
|
|
ff0736532d | ||
|
|
98617b5120 | ||
|
|
df30852296 | ||
|
|
b9beabb425 | ||
|
|
ed812b7c16 | ||
|
|
14cfc31c88 | ||
|
|
376d650a8c | ||
|
|
0ab179b5df | ||
|
|
a9b1dd8510 | ||
|
|
6e28f4dd84 | ||
|
|
43933f3a33 | ||
|
|
d2e1a24b20 | ||
|
|
887b3c7b3a | ||
|
|
476fe7a29a | ||
|
|
c1d38d86f1 | ||
|
|
4f2594c31d | ||
|
|
c9985b56bc | ||
|
|
f9868e2221 | ||
|
|
72b0398eaf | ||
|
|
5b75a39777 | ||
|
|
6948b69012 | ||
|
|
bc9701397e | ||
|
|
396cf3194e | ||
|
|
8be96a0ded | ||
|
|
82c54b1d36 | ||
|
|
39f5fb7290 | ||
|
|
6ec2989e5c | ||
|
|
016da679b9 | ||
|
|
ff028e366b | ||
|
|
c579614d56 | ||
|
|
78ba2ba356 | ||
|
|
7fd4762e2a | ||
|
|
4e4c9ce5af | ||
|
|
7605775a38 | ||
|
|
cb1a2a8a13 | ||
|
|
1a5d37b25a | ||
|
|
bc4273f2f8 | ||
|
|
77fdd28e93 | ||
|
|
8e08a42617 | ||
|
|
2c3042304a | ||
|
|
c9da09256e | ||
|
|
e8ed22cafb | ||
|
|
4658232025 | ||
|
|
e8add5942e | ||
|
|
ddecf05d9f | ||
|
|
bf13b26a37 | ||
|
|
0796e7b388 | ||
|
|
25ff140324 | ||
|
|
465e07de83 | ||
|
|
c04f664e2f | ||
|
|
e406b0bb61 | ||
|
|
c143e0b130 | ||
|
|
0dd42ec076 | ||
|
|
34ba5bab28 | ||
|
|
678f015e0b | ||
|
|
5a69f16410 | ||
|
|
07afef5c5e | ||
|
|
dcae722b53 | ||
|
|
92b07d15ea | ||
|
|
a0dad1602e | ||
|
|
5cf5b70aca | ||
|
|
db51b23e3d | ||
|
|
80c46b3414 | ||
|
|
2b929421a1 | ||
|
|
2792e20aa2 | ||
|
|
473be1b174 | ||
|
|
6d0c13f9a7 | ||
|
|
5cc562ba35 | ||
|
|
22099962be | ||
|
|
2559b52bb1 | ||
|
|
7523596043 | ||
|
|
9f4d9eef46 | ||
|
|
d9cddbfb42 | ||
|
|
0828c13f7c | ||
|
|
2c558b9bf7 | ||
|
|
48b588fe48 | ||
|
|
5c86b80682 | ||
|
|
75512a81c6 | ||
|
|
6aaea79b73 | ||
|
|
04643264ff | ||
|
|
3aa0d8a7fd | ||
|
|
7f1d350ffe | ||
|
|
1d3134959d | ||
|
|
b86bd24dd9 | ||
|
|
4c49d45cbf | ||
|
|
9b3d3453b1 | ||
|
|
9d981d8a13 | ||
|
|
6de4520a95 | ||
|
|
f566909320 | ||
|
|
aa39db8ac2 | ||
|
|
eb2c6b78c8 | ||
|
|
2d2d0c3d9f | ||
|
|
8a4544cbac | ||
|
|
8898f02698 | ||
|
|
f277009ff8 | ||
|
|
17c6b79d79 | ||
|
|
76d6c23217 | ||
|
|
82dffdda56 | ||
|
|
8f38398863 | ||
|
|
eb39772d3c | ||
|
|
df72c897f9 | ||
|
|
4bbe5ead07 | ||
|
|
e36689ecba | ||
|
|
2c948ef9f6 | ||
|
|
3c30114642 | ||
|
|
d042fad1e3 | ||
|
|
235c606b44 | ||
|
|
a49d7e1662 | ||
|
|
b3e41b5520 | ||
|
|
83bb97cc58 | ||
|
|
68ea28cf6b | ||
|
|
3726c0aac1 | ||
|
|
dae2d3239b | ||
|
|
0c660f8618 | ||
|
|
97cffbc20a | ||
|
|
8317eb1735 | ||
|
|
b1789ea3f7 | ||
|
|
3b41d0a731 | ||
|
|
a171f7122f | ||
|
|
4a20e93b20 | ||
|
|
d4dc709aa5 | ||
|
|
cd014652a1 | ||
|
|
538351131f | ||
|
|
a3bd72ad86 | ||
|
|
786070d90b | ||
|
|
e699ad8122 | ||
|
|
a1cc05848c | ||
|
|
6d78df2275 | ||
|
|
df49484bea | ||
|
|
72b0f27494 | ||
|
|
e36b647bc7 | ||
|
|
b491772eaa | ||
|
|
128497f27a | ||
|
|
9e466b56b2 | ||
|
|
4ad0baa2a2 | ||
|
|
24b588bfba | ||
|
|
e5867cc2ad | ||
|
|
b420ca494e | ||
|
|
e4693ce64c | ||
|
|
ca9b3a910a | ||
|
|
9dc7d2389a | ||
|
|
da5860297f | ||
|
|
54fca5ba44 | ||
|
|
66f4c3d6ec | ||
|
|
a0f407a848 | ||
|
|
3562de8fbb | ||
|
|
ef80fb39fd | ||
|
|
594d4dc737 | ||
|
|
01415b58be | ||
|
|
f7728c9019 | ||
|
|
3fffe6e198 | ||
|
|
4594f4ffe3 | ||
|
|
b660dc8573 | ||
|
|
c7b9c0d36c | ||
|
|
4433d22c09 | ||
|
|
9443c42334 | ||
|
|
9387c96023 | ||
|
|
ff2555d8dd | ||
|
|
85953c9a4c | ||
|
|
0155cc38cb | ||
|
|
c217cc96c3 | ||
|
|
580cf32eb5 | ||
|
|
6d3580cbfa | ||
|
|
6c5d36caa9 | ||
|
|
c4a6c7e277 | ||
|
|
c9cd974dca | ||
|
|
5b3f121431 | ||
|
|
c79373314a | ||
|
|
858cd287fa | ||
|
|
afdb674068 | ||
|
|
30a6721472 | ||
|
|
518dfcbe59 | ||
|
|
424127c27c | ||
|
|
2dcb817de1 | ||
|
|
f6f8c78aaf | ||
|
|
3c99dfdfa5 | ||
|
|
6ed72519b8 | ||
|
|
fe910aaa0f | ||
|
|
0d362b3ba8 | ||
|
|
51e9ffb847 | ||
|
|
f497a154a2 | ||
|
|
659fa361ef | ||
|
|
84e77182f6 | ||
|
|
32619869e7 | ||
|
|
e9093126b2 | ||
|
|
195f44802d | ||
|
|
948bdb7881 | ||
|
|
7274d51236 | ||
|
|
5b580f8e63 | ||
|
|
5ce1f0b855 | ||
|
|
291e6a2af8 | ||
|
|
3a34fb4813 | ||
|
|
189781748a | ||
|
|
e7c812e07f | ||
|
|
c0e2dad3b5 | ||
|
|
bb3ccfae3b | ||
|
|
08f261b0f0 | ||
|
|
9b4fd46d2c | ||
|
|
f348f47951 | ||
|
|
9d700563c1 | ||
|
|
54b80c3949 | ||
|
|
1c815b130c | ||
|
|
9e9879fc8d | ||
|
|
af87c2e80b | ||
|
|
e1ac992e5a | ||
|
|
15161c09e8 | ||
|
|
ee5fbe41eb | ||
|
|
f2f3a7b24a | ||
|
|
dd0738ac70 | ||
|
|
1c4dfc931f | ||
|
|
605d6ba17d | ||
|
|
f017b07525 | ||
|
|
b5901ac174 | ||
|
|
caacbc086c | ||
|
|
9d06ccab48 | ||
|
|
de45292782 | ||
|
|
9f38305e5a | ||
|
|
e1c8b68cd2 | ||
|
|
76ec089a43 | ||
|
|
4117c7442b | ||
|
|
4153515767 | ||
|
|
6a0ee32616 | ||
|
|
1f13b60703 | ||
|
|
0865d2edaf | ||
|
|
8629c959f0 | ||
|
|
10760e6e1b | ||
|
|
4f45645b32 | ||
|
|
1417e22ae4 | ||
|
|
3051d442c0 | ||
|
|
ea15ce4e04 | ||
|
|
865a7a5a31 | ||
|
|
de4ca50a40 | ||
|
|
8cabaafc58 | ||
|
|
e9d66b8094 | ||
|
|
26d3d6b1e4 | ||
|
|
36d6debeab | ||
|
|
445b0cace8 | ||
|
|
132f10f8a3 | ||
|
|
14011bc277 | ||
|
|
f17a332c23 | ||
|
|
5ae7a464e6 | ||
|
|
51c3628f6e | ||
|
|
6a69076828 | ||
|
|
edd04e2f07 | ||
|
|
ee734cf78c | ||
|
|
6d137bcdff | ||
|
|
444161671d | ||
|
|
31e9e896ec | ||
|
|
325974292f | ||
|
|
7c1a531d01 | ||
|
|
5a45532a72 | ||
|
|
e9501d2e0f | ||
|
|
c306e66bcd | ||
|
|
767a0cc28e | ||
|
|
4f9efcc133 | ||
|
|
bf2dd612e0 | ||
|
|
d3f15022a4 | ||
|
|
a5c021e96c | ||
|
|
8b9fcae0cb | ||
|
|
e75abcb108 | ||
|
|
ffa15f9ec1 | ||
|
|
87562f5387 | ||
|
|
11a7512a25 | ||
|
|
f925a15f30 | ||
|
|
a3fe2c2589 | ||
|
|
2719c9b6a7 | ||
|
|
b4282be3ac |
@@ -41,31 +41,23 @@ services:
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
schema-migrator-sync:
|
||||
image: signoz/signoz-schema-migrator:v0.129.12
|
||||
container_name: schema-migrator-sync
|
||||
telemetrystore-migrator:
|
||||
image: signoz/signoz-otel-collector:v0.142.0
|
||||
container_name: telemetrystore-migrator
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- sync
|
||||
- --cluster-name=cluster
|
||||
- --dsn=tcp://clickhouse:9000
|
||||
- --replication=true
|
||||
- --up=
|
||||
- -c
|
||||
- |
|
||||
/signoz-otel-collector migrate bootstrap &&
|
||||
/signoz-otel-collector migrate sync up &&
|
||||
/signoz-otel-collector migrate async up
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
image: signoz/signoz-schema-migrator:v0.129.12
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
- --cluster-name=cluster
|
||||
- --dsn=tcp://clickhouse:9000
|
||||
- --replication=true
|
||||
- --up=
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
restart: on-failure
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
services:
|
||||
signoz-otel-collector:
|
||||
image: signoz/signoz-otel-collector:v0.129.6
|
||||
image: signoz/signoz-otel-collector:v0.142.0
|
||||
container_name: signoz-otel-collector-dev
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
- -c
|
||||
- |
|
||||
/signoz-otel-collector migrate sync check &&
|
||||
/signoz-otel-collector --config=/etc/otel-collector-config.yaml
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
ports:
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
|
||||
80
.github/CODEOWNERS
vendored
@@ -16,13 +16,13 @@
|
||||
|
||||
# Scaffold Owners
|
||||
|
||||
/pkg/config/ @therealpandey
|
||||
/pkg/errors/ @therealpandey
|
||||
/pkg/factory/ @therealpandey
|
||||
/pkg/types/ @therealpandey
|
||||
/pkg/valuer/ @therealpandey
|
||||
/cmd/ @therealpandey
|
||||
.golangci.yml @therealpandey
|
||||
/pkg/config/ @vikrantgupta25
|
||||
/pkg/errors/ @vikrantgupta25
|
||||
/pkg/factory/ @vikrantgupta25
|
||||
/pkg/types/ @vikrantgupta25
|
||||
/pkg/valuer/ @vikrantgupta25
|
||||
/cmd/ @vikrantgupta25
|
||||
.golangci.yml @vikrantgupta25
|
||||
|
||||
# Zeus Owners
|
||||
|
||||
@@ -43,28 +43,84 @@
|
||||
/pkg/analytics/ @vikrantgupta25
|
||||
/pkg/statsreporter/ @vikrantgupta25
|
||||
|
||||
# Emailing Owners
|
||||
|
||||
/pkg/emailing/ @vikrantgupta25
|
||||
/pkg/types/emailtypes/ @vikrantgupta25
|
||||
/templates/email/ @vikrantgupta25
|
||||
|
||||
# Querier Owners
|
||||
|
||||
/pkg/querier/ @srikanthccv
|
||||
/pkg/variables/ @srikanthccv
|
||||
/pkg/types/querybuildertypes/ @srikanthccv
|
||||
/pkg/types/telemetrytypes/ @srikanthccv
|
||||
/pkg/querybuilder/ @srikanthccv
|
||||
/pkg/telemetrylogs/ @srikanthccv
|
||||
/pkg/telemetrymetadata/ @srikanthccv
|
||||
/pkg/telemetrymetrics/ @srikanthccv
|
||||
/pkg/telemetrytraces/ @srikanthccv
|
||||
|
||||
# Metrics
|
||||
|
||||
/pkg/types/metrictypes/ @srikanthccv
|
||||
/pkg/types/metricsexplorertypes/ @srikanthccv
|
||||
/pkg/modules/metricsexplorer/ @srikanthccv
|
||||
/pkg/prometheus/ @srikanthccv
|
||||
|
||||
# APM
|
||||
|
||||
/pkg/types/servicetypes/ @srikanthccv
|
||||
/pkg/types/apdextypes/ @srikanthccv
|
||||
/pkg/modules/apdex/ @srikanthccv
|
||||
/pkg/modules/services/ @srikanthccv
|
||||
|
||||
# Dashboard
|
||||
|
||||
/pkg/types/dashboardtypes/ @srikanthccv
|
||||
/pkg/modules/dashboard/ @srikanthccv
|
||||
|
||||
# Rule/Alertmanager
|
||||
|
||||
/pkg/types/ruletypes/ @srikanthccv
|
||||
/pkg/types/alertmanagertypes @srikanthccv
|
||||
/pkg/alertmanager/ @srikanthccv
|
||||
/pkg/ruler/ @srikanthccv
|
||||
|
||||
# Correlation-adjacent
|
||||
|
||||
/pkg/contextlinks/ @srikanthccv
|
||||
/pkg/types/parsertypes/ @srikanthccv
|
||||
/pkg/queryparser/ @srikanthccv
|
||||
|
||||
# AuthN / AuthZ Owners
|
||||
|
||||
/pkg/authz/ @vikrantgupta25 @therealpandey
|
||||
/pkg/authz/ @vikrantgupta25
|
||||
/ee/authz/ @vikrantgupta25
|
||||
/pkg/authn/ @vikrantgupta25
|
||||
/ee/authn/ @vikrantgupta25
|
||||
/pkg/modules/user/ @vikrantgupta25
|
||||
/pkg/modules/session/ @vikrantgupta25
|
||||
/pkg/modules/organization/ @vikrantgupta25
|
||||
/pkg/modules/authdomain/ @vikrantgupta25
|
||||
/pkg/modules/role/ @vikrantgupta25
|
||||
|
||||
# Integration tests
|
||||
|
||||
/tests/integration/ @therealpandey
|
||||
/tests/integration/ @vikrantgupta25
|
||||
|
||||
# OpenAPI types generator
|
||||
|
||||
/frontend/src/api @SigNoz/frontend-maintainers
|
||||
|
||||
# Dashboard Owners
|
||||
|
||||
/frontend/src/hooks/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/providers/Dashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Types
|
||||
|
||||
/frontend/src/api/types/dashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard List
|
||||
|
||||
@@ -82,3 +138,9 @@
|
||||
|
||||
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Libs + Components
|
||||
/frontend/src/lib/uPlotV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
|
||||
|
||||
103
.github/pull_request_template.md
vendored
@@ -1,86 +1,85 @@
|
||||
## 📄 Summary
|
||||
|
||||
<!-- Describe the purpose of the PR in a few sentences. What does it fix/add/update? -->
|
||||
## Pull Request
|
||||
|
||||
---
|
||||
|
||||
## ✅ Changes
|
||||
### 📄 Summary
|
||||
> Why does this change exist?
|
||||
> What problem does it solve, and why is this the right approach?
|
||||
|
||||
- [ ] Feature: Brief description
|
||||
- [ ] Bug fix: Brief description
|
||||
|
||||
|
||||
#### Screenshots / Screen Recordings (if applicable)
|
||||
> Include screenshots or screen recordings that clearly show the behavior before the change and the result after the change. This helps reviewers quickly understand the impact and verify the update.
|
||||
|
||||
|
||||
#### Issues closed by this PR
|
||||
> Reference issues using `Closes #issue-number` to enable automatic closure on merge.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Change Type
|
||||
_Select all that apply_
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
> Fill this only if the change affects users, APIs, UI, or documented behavior.
|
||||
Mention as N/A for internal refactors or non-user-visible changes.
|
||||
|
||||
**Deployment Type:** Cloud / OSS / Enterprise
|
||||
|
||||
**Type:** Feature / Bug Fix / Maintenance
|
||||
|
||||
**Description:** Short, user-facing summary of the change
|
||||
- [ ] ✨ Feature
|
||||
- [ ] 🐛 Bug fix
|
||||
- [ ] ♻️ Refactor
|
||||
- [ ] 🛠️ Infra / Tooling
|
||||
- [ ] 🧪 Test-only
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ Required: Add Relevant Labels
|
||||
### 🐛 Bug Context
|
||||
> Required if this PR fixes a bug
|
||||
|
||||
> ⚠️ **Manually add appropriate labels in the PR sidebar**
|
||||
Please select one or more labels (as applicable):
|
||||
#### Root Cause
|
||||
> What caused the issue?
|
||||
> Regression, faulty assumption, edge case, refactor, etc.
|
||||
|
||||
ex:
|
||||
|
||||
- `frontend`
|
||||
- `backend`
|
||||
- `devops`
|
||||
- `bug`
|
||||
- `enhancement`
|
||||
- `ui`
|
||||
- `test`
|
||||
#### Fix Strategy
|
||||
> How does this PR address the root cause?
|
||||
|
||||
---
|
||||
|
||||
## 👥 Reviewers
|
||||
### 🧪 Testing Strategy
|
||||
> How was this change validated?
|
||||
|
||||
> Tag the relevant teams for review:
|
||||
|
||||
- frontend / backend / devops
|
||||
- Tests added/updated:
|
||||
- Manual verification:
|
||||
- Edge cases covered:
|
||||
|
||||
---
|
||||
|
||||
## 🧪 How to Test
|
||||
### ⚠️ Risk & Impact Assessment
|
||||
> What could break? How do we recover?
|
||||
|
||||
<!-- Describe how reviewers can test this PR -->
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
- Blast radius:
|
||||
- Potential regressions:
|
||||
- Rollback plan:
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Related Issues
|
||||
### 📝 Changelog
|
||||
> Fill only if this affects users, APIs, UI, or documented behavior
|
||||
> Use **N/A** for internal or non-user-facing changes
|
||||
|
||||
<!-- Reference any related issues (e.g. Fixes #123, Closes #456) -->
|
||||
Closes #
|
||||
| Field | Value |
|
||||
|------|-------|
|
||||
| Deployment Type | Cloud / OSS / Enterprise |
|
||||
| Change Type | Feature / Bug Fix / Maintenance |
|
||||
| Description | User-facing summary |
|
||||
|
||||
---
|
||||
|
||||
## 📸 Screenshots / Screen Recording (if applicable / mandatory for UI related changes)
|
||||
|
||||
<!-- Add screenshots or GIFs to help visualize changes -->
|
||||
|
||||
---
|
||||
|
||||
## 📋 Checklist
|
||||
|
||||
- [ ] Dev Review
|
||||
- [ ] Test cases added (Unit/ Integration / E2E)
|
||||
- [ ] Manually tested the changes
|
||||
|
||||
### 📋 Checklist
|
||||
- [ ] Tests added or explicitly not required
|
||||
- [ ] Manually tested
|
||||
- [ ] Breaking changes documented
|
||||
- [ ] Backward compatibility considered
|
||||
|
||||
---
|
||||
|
||||
## 👀 Notes for Reviewers
|
||||
|
||||
<!-- Anything reviewers should keep in mind while reviewing -->
|
||||
|
||||
---
|
||||
|
||||
1
.github/workflows/build-enterprise.yaml
vendored
@@ -70,6 +70,7 @@ jobs:
|
||||
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
|
||||
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
|
||||
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
|
||||
echo 'DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
|
||||
- name: cache-dotenv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
1
.github/workflows/build-staging.yaml
vendored
@@ -69,6 +69,7 @@ jobs:
|
||||
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
|
||||
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
|
||||
echo 'PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
|
||||
echo 'DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
|
||||
- name: cache-dotenv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
7
.github/workflows/commitci.yaml
vendored
@@ -25,3 +25,10 @@ jobs:
|
||||
else
|
||||
echo "No references to 'ee' packages found in 'pkg' directory"
|
||||
fi
|
||||
|
||||
if grep -R --include="*.go" '.*/ee/.*' cmd/community/; then
|
||||
echo "Error: Found references to 'ee' packages in 'cmd/community' directory"
|
||||
exit 1
|
||||
else
|
||||
echo "No references to 'ee' packages found in 'cmd/community' directory"
|
||||
fi
|
||||
|
||||
14
.github/workflows/goci.yaml
vendored
@@ -65,6 +65,10 @@ jobs:
|
||||
set -ex
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools
|
||||
- name: node-install
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: docker-community
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -89,3 +93,13 @@ jobs:
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate openapi
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
|
||||
- name: node-install
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: install-frontend
|
||||
run: cd frontend && yarn install
|
||||
- name: generate-api-clients
|
||||
run: |
|
||||
cd frontend && yarn generate:api
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run yarn generate:api in frontend/ locally and commit."; exit 1)
|
||||
|
||||
12
.github/workflows/gor-signoz-community.yaml
vendored
@@ -3,8 +3,8 @@ name: gor-signoz-community
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -21,6 +21,10 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
- name: node-setup
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: build-frontend
|
||||
run: make js-build
|
||||
- name: upload-frontend-artifact
|
||||
@@ -89,7 +93,7 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: '~> v2'
|
||||
version: "~> v2"
|
||||
args: release --config ${{ env.CONFIG_PATH }} --clean --split
|
||||
workdir: .
|
||||
env:
|
||||
@@ -147,7 +151,7 @@ jobs:
|
||||
if: steps.cache-linux.outputs.cache-hit == 'true' && steps.cache-darwin.outputs.cache-hit == 'true' # only run if caches hit
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: '~> v2'
|
||||
version: "~> v2"
|
||||
args: continue --merge
|
||||
workdir: .
|
||||
env:
|
||||
|
||||
15
.github/workflows/gor-signoz.yaml
vendored
@@ -3,8 +3,8 @@ name: gor-signoz
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -36,8 +36,13 @@ jobs:
|
||||
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
|
||||
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
|
||||
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
|
||||
echo 'DOCS_BASE_URL="https://signoz.io"' >> .env
|
||||
- name: node-setup
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: build-frontend
|
||||
run: make js-build
|
||||
run: make js-build
|
||||
- name: upload-frontend-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -104,7 +109,7 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: '~> v2'
|
||||
version: "~> v2"
|
||||
args: release --config ${{ env.CONFIG_PATH }} --clean --split
|
||||
workdir: .
|
||||
env:
|
||||
@@ -161,7 +166,7 @@ jobs:
|
||||
if: steps.cache-linux.outputs.cache-hit == 'true' && steps.cache-darwin.outputs.cache-hit == 'true' # only run if caches hit
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: '~> v2'
|
||||
version: "~> v2"
|
||||
args: continue --merge
|
||||
workdir: .
|
||||
env:
|
||||
|
||||
18
.github/workflows/integrationci.yaml
vendored
@@ -42,18 +42,21 @@ jobs:
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
- dashboard
|
||||
- querier
|
||||
- ttl
|
||||
- preference
|
||||
- logspipelines
|
||||
- preference
|
||||
- querier
|
||||
- role
|
||||
- ttl
|
||||
- alerts
|
||||
- ingestionkeys
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
clickhouse-version:
|
||||
- 25.5.6
|
||||
- 25.10.1
|
||||
- 25.12.5
|
||||
schema-migrator-version:
|
||||
- v0.129.7
|
||||
- v0.142.0
|
||||
postgres-version:
|
||||
- 15
|
||||
if: |
|
||||
@@ -84,8 +87,11 @@ jobs:
|
||||
sudo rm /etc/apt/sources.list.d/google-chrome.list
|
||||
export CHROMEDRIVER_VERSION=`curl -s https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}`
|
||||
curl -L -O "https://storage.googleapis.com/chrome-for-testing-public/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip"
|
||||
unzip chromedriver-linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin
|
||||
unzip chromedriver-linux64.zip
|
||||
chmod +x chromedriver-linux64/chromedriver
|
||||
sudo mv chromedriver-linux64/chromedriver /usr/local/bin/chromedriver
|
||||
chromedriver -version
|
||||
google-chrome-stable --version
|
||||
- name: run
|
||||
run: |
|
||||
cd tests/integration && \
|
||||
|
||||
13
.github/workflows/jsci.yaml
vendored
@@ -17,10 +17,23 @@ jobs:
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: install
|
||||
run: cd frontend && yarn install
|
||||
- name: tsc
|
||||
run: cd frontend && yarn tsc
|
||||
tsc2:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||
uses: signoz/primus.workflows/.github/workflows/js-tsc.yaml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
JS_SRC: frontend
|
||||
test:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
|
||||
14
.gitignore
vendored
@@ -1,6 +1,12 @@
|
||||
|
||||
node_modules
|
||||
|
||||
# editor
|
||||
.vscode
|
||||
!.vscode/settings.json
|
||||
.zed
|
||||
.idea
|
||||
|
||||
deploy/docker/environment_tiny/common_test
|
||||
frontend/node_modules
|
||||
frontend/.pnp
|
||||
@@ -12,7 +18,6 @@ frontend/coverage
|
||||
|
||||
# production
|
||||
frontend/build
|
||||
frontend/.vscode
|
||||
frontend/.yarnclean
|
||||
frontend/.temp_cache
|
||||
frontend/test-results
|
||||
@@ -29,9 +34,6 @@ frontend/yarn-debug.log*
|
||||
frontend/yarn-error.log*
|
||||
frontend/src/constants/env.ts
|
||||
|
||||
.idea
|
||||
|
||||
**/.vscode
|
||||
**/build
|
||||
**/storage
|
||||
**/locust-scripts/__pycache__/
|
||||
@@ -56,7 +58,6 @@ bin/
|
||||
.local/
|
||||
*/query-service/queries.active
|
||||
ee/query-service/db
|
||||
|
||||
# e2e
|
||||
|
||||
e2e/node_modules/
|
||||
@@ -106,7 +107,6 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
@@ -230,5 +230,3 @@ cython_debug/
|
||||
pyrightconfig.json
|
||||
|
||||
|
||||
# cursor files
|
||||
frontend/.cursor/
|
||||
|
||||
@@ -12,6 +12,7 @@ linters:
|
||||
- misspell
|
||||
- nilnil
|
||||
- sloglint
|
||||
- wastedassign
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
|
||||
13
.mockery.yml
@@ -4,7 +4,14 @@ packages:
|
||||
github.com/SigNoz/signoz/pkg/alertmanager:
|
||||
config:
|
||||
all: true
|
||||
dir: '{{.InterfaceDir}}/mocks'
|
||||
filename: "mocks.go"
|
||||
dir: '{{.InterfaceDir}}/alertmanagertest'
|
||||
filename: "alertmanager.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}mock'
|
||||
pkgname: '{{.SrcPackageName}}test'
|
||||
github.com/SigNoz/signoz/pkg/tokenizer:
|
||||
config:
|
||||
all: true
|
||||
dir: '{{.InterfaceDir}}/tokenizertest'
|
||||
filename: "tokenizer.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}test'
|
||||
|
||||
21
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"eslint.workingDirectories": [
|
||||
"./frontend"
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"prettier.requireConfig": true,
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[sql]": {
|
||||
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
}
|
||||
}
|
||||
13
Makefile
@@ -86,7 +86,7 @@ go-run-enterprise: ## Runs the enterprise go backend server
|
||||
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
|
||||
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
|
||||
SIGNOZ_WEB_ENABLED=false \
|
||||
SIGNOZ_JWT_SECRET=secret \
|
||||
SIGNOZ_TOKENIZER_JWT_SECRET=secret \
|
||||
SIGNOZ_ALERTMANAGER_PROVIDER=signoz \
|
||||
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
|
||||
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
|
||||
@@ -103,7 +103,7 @@ go-run-community: ## Runs the community go backend server
|
||||
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
|
||||
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
|
||||
SIGNOZ_WEB_ENABLED=false \
|
||||
SIGNOZ_JWT_SECRET=secret \
|
||||
SIGNOZ_TOKENIZER_JWT_SECRET=secret \
|
||||
SIGNOZ_ALERTMANAGER_PROVIDER=signoz \
|
||||
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
|
||||
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
|
||||
@@ -230,3 +230,12 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
|
||||
@find tests -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
@find tests -type f -name "*.pyo" -delete 2>/dev/null || true
|
||||
@echo ">> python cache cleaned"
|
||||
|
||||
|
||||
##############################################################
|
||||
# generate commands
|
||||
##############################################################
|
||||
.PHONY: gen-mocks
|
||||
gen-mocks:
|
||||
@echo ">> Generating mocks"
|
||||
@mockery --config .mockery.yml
|
||||
@@ -176,25 +176,6 @@ Wir haben Benchmarks veröffentlicht, die Loki mit SigNoz vergleichen. Schauen S
|
||||
Wir ❤️ Beiträge zum Projekt, egal ob große oder kleine. Bitte lies dir zuerst die [CONTRIBUTING.md](CONTRIBUTING.md), durch, bevor du anfängst, Beiträge zu SigNoz zu machen.
|
||||
Du bist dir nicht sicher, wie du anfangen sollst? Schreib uns einfach auf dem #contributing Kanal in unserer [slack community](https://signoz.io/slack)
|
||||
|
||||
### Unsere Projektbetreuer
|
||||
|
||||
#### Backend
|
||||
|
||||
- [Ankit Nayan](https://github.com/ankitnayan)
|
||||
- [Nityananda Gohain](https://github.com/nityanandagohain)
|
||||
- [Srikanth Chekuri](https://github.com/srikanthccv)
|
||||
- [Vishal Sharma](https://github.com/makeavish)
|
||||
|
||||
#### Frontend
|
||||
|
||||
- [Palash Gupta](https://github.com/palashgdev)
|
||||
- [Yunus M](https://github.com/YounixM)
|
||||
- [Rajat Dabade](https://github.com/Rajat-Dabade)
|
||||
|
||||
#### DevOps
|
||||
|
||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Dokumentation
|
||||
|
||||
39
README.md
@@ -66,6 +66,17 @@ Read [more](https://signoz.io/metrics-and-dashboards/).
|
||||
|
||||

|
||||
|
||||
### LLM Observability
|
||||
|
||||
Monitor and debug your LLM applications with comprehensive observability. Track LLM calls, analyze token usage, monitor performance, and gain insights into your AI application's behavior in production.
|
||||
|
||||
SigNoz LLM observability helps you understand how your language models are performing, identify issues with prompts and responses, track token usage and costs, and optimize your AI applications for better performance and reliability.
|
||||
|
||||
[Get started with LLM Observability →](https://signoz.io/docs/llm-observability/)
|
||||
|
||||

|
||||
|
||||
|
||||
### Alerts
|
||||
|
||||
Use alerts in SigNoz to get notified when anything unusual happens in your application. You can set alerts on any type of telemetry signal (logs, metrics, traces), create thresholds and set up a notification channel to get notified. Advanced features like alert history and anomaly detection can help you create smarter alerts.
|
||||
@@ -210,34 +221,6 @@ We ❤️ contributions big or small. Please read [CONTRIBUTING.md](CONTRIBUTING
|
||||
|
||||
Not sure how to get started? Just ping us on `#contributing` in our [slack community](https://signoz.io/slack)
|
||||
|
||||
### Project maintainers
|
||||
|
||||
#### Backend
|
||||
|
||||
- [Ankit Nayan](https://github.com/ankitnayan)
|
||||
- [Nityananda Gohain](https://github.com/nityanandagohain)
|
||||
- [Srikanth Chekuri](https://github.com/srikanthccv)
|
||||
- [Vishal Sharma](https://github.com/makeavish)
|
||||
- [Shivanshu Raj Shrivastava](https://github.com/shivanshuraj1333)
|
||||
- [Ekansh Gupta](https://github.com/eKuG)
|
||||
- [Aniket Agarwal](https://github.com/aniketio-ctrl)
|
||||
|
||||
#### Frontend
|
||||
|
||||
- [Yunus M](https://github.com/YounixM)
|
||||
- [Vikrant Gupta](https://github.com/vikrantgupta25)
|
||||
- [Sagar Rajput](https://github.com/SagarRajput-7)
|
||||
- [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
|
||||
|
||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||
- [Vibhu Pandey](https://github.com/therealpandey)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
|
||||
@@ -187,25 +187,6 @@ Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metric
|
||||
|
||||
如果你不知道如何开始? 只需要在 [slack 社区](https://signoz.io/slack) 通过 `#contributing` 频道联系我们。
|
||||
|
||||
### 项目维护人员
|
||||
|
||||
#### 后端
|
||||
|
||||
- [Ankit Nayan](https://github.com/ankitnayan)
|
||||
- [Nityananda Gohain](https://github.com/nityanandagohain)
|
||||
- [Srikanth Chekuri](https://github.com/srikanthccv)
|
||||
- [Vishal Sharma](https://github.com/makeavish)
|
||||
|
||||
#### 前端
|
||||
|
||||
- [Palash Gupta](https://github.com/palashgdev)
|
||||
- [Yunus M](https://github.com/YounixM)
|
||||
- [Rajat Dabade](https://github.com/Rajat-Dabade)
|
||||
|
||||
#### 运维开发
|
||||
|
||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 文档
|
||||
|
||||
@@ -5,26 +5,25 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/cmd"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/role"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"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"
|
||||
@@ -57,13 +56,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
// 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
|
||||
}
|
||||
|
||||
signoz, err := signoz.New(
|
||||
ctx,
|
||||
config,
|
||||
@@ -84,12 +76,18 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, _ role.Module, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
|
||||
},
|
||||
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return noopgateway.NewProviderFactory()
|
||||
},
|
||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||
return querier.NewHandler(ps, q, a)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
|
||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
||||
eequerier "github.com/SigNoz/signoz/ee/querier"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
|
||||
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
|
||||
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
|
||||
@@ -22,11 +24,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/role"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
@@ -114,13 +116,21 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return authNs, nil
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, dashboardModule dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), licensing, dashboardModule)
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, role, queryParser, querier, licensing)
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
},
|
||||
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return httpgateway.NewProviderFactory(licensing)
|
||||
},
|
||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||
communityHandler := querier.NewHandler(ps, q, a)
|
||||
return eequerier.NewHandler(ps, q, communityHandler)
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
||||
return err
|
||||
|
||||
@@ -193,6 +193,15 @@ emailing:
|
||||
templates:
|
||||
# The directory containing the email templates. This directory should contain a list of files defined at pkg/types/emailtypes/template.go.
|
||||
directory: /opt/signoz/conf/templates/email
|
||||
format:
|
||||
header:
|
||||
enabled: false
|
||||
logo_url: ""
|
||||
help:
|
||||
enabled: false
|
||||
email: ""
|
||||
footer:
|
||||
enabled: false
|
||||
smtp:
|
||||
# The SMTP server address.
|
||||
address: localhost:25
|
||||
@@ -285,9 +294,30 @@ flagger:
|
||||
config:
|
||||
boolean:
|
||||
use_span_metrics: true
|
||||
interpolation_enabled: false
|
||||
kafka_span_eval: false
|
||||
string:
|
||||
float:
|
||||
integer:
|
||||
object:
|
||||
|
||||
##################### User #####################
|
||||
user:
|
||||
password:
|
||||
reset:
|
||||
# Whether to allow users to reset their password themselves.
|
||||
allow_self: true
|
||||
# The duration within which a user can reset their password.
|
||||
max_token_lifetime: 6h
|
||||
root:
|
||||
# Whether to enable the root user. When enabled, a root user is provisioned
|
||||
# on startup using the email and password below. The root user cannot be
|
||||
# deleted, updated, or have their password changed through the UI.
|
||||
enabled: false
|
||||
# The email address of the root user.
|
||||
email: ""
|
||||
# The password of the root user. Must meet password requirements.
|
||||
password: ""
|
||||
# The name of the organization to create or look up for the root user.
|
||||
org:
|
||||
name: default
|
||||
id: 00000000-0000-0000-0000-000000000000
|
||||
|
||||
4
context7.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"url": "https://context7.com/signoz/signoz",
|
||||
"public_key": "pk_6g9GfjdkuPEIDuTGAxnol"
|
||||
}
|
||||
@@ -61,7 +61,6 @@ x-db-depend: &db-depend
|
||||
- clickhouse
|
||||
- clickhouse-2
|
||||
- clickhouse-3
|
||||
- schema-migrator
|
||||
services:
|
||||
init-clickhouse:
|
||||
!!merge <<: *common
|
||||
@@ -136,12 +135,17 @@ services:
|
||||
# - "9000:9000"
|
||||
# - "8123:8123"
|
||||
# - "9181:9181"
|
||||
configs:
|
||||
- source: clickhouse-config
|
||||
target: /etc/clickhouse-server/config.xml
|
||||
- source: clickhouse-users
|
||||
target: /etc/clickhouse-server/users.xml
|
||||
- source: clickhouse-custom-function
|
||||
target: /etc/clickhouse-server/custom-function.xml
|
||||
- source: clickhouse-cluster
|
||||
target: /etc/clickhouse-server/config.d/cluster.ha.xml
|
||||
volumes:
|
||||
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
|
||||
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
|
||||
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
||||
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||
- ./clickhouse-setup/data/clickhouse/:/var/lib/clickhouse/
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
clickhouse-2:
|
||||
@@ -151,12 +155,17 @@ services:
|
||||
# - "9001:9000"
|
||||
# - "8124:8123"
|
||||
# - "9182:9181"
|
||||
configs:
|
||||
- source: clickhouse-config
|
||||
target: /etc/clickhouse-server/config.xml
|
||||
- source: clickhouse-users
|
||||
target: /etc/clickhouse-server/users.xml
|
||||
- source: clickhouse-custom-function
|
||||
target: /etc/clickhouse-server/custom-function.xml
|
||||
- source: clickhouse-cluster
|
||||
target: /etc/clickhouse-server/config.d/cluster.ha.xml
|
||||
volumes:
|
||||
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
|
||||
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
|
||||
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
||||
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||
- ./clickhouse-setup/data/clickhouse-2/:/var/lib/clickhouse/
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
clickhouse-3:
|
||||
@@ -166,37 +175,32 @@ services:
|
||||
# - "9002:9000"
|
||||
# - "8125:8123"
|
||||
# - "9183:9181"
|
||||
configs:
|
||||
- source: clickhouse-config
|
||||
target: /etc/clickhouse-server/config.xml
|
||||
- source: clickhouse-users
|
||||
target: /etc/clickhouse-server/users.xml
|
||||
- source: clickhouse-custom-function
|
||||
target: /etc/clickhouse-server/custom-function.xml
|
||||
- source: clickhouse-cluster
|
||||
target: /etc/clickhouse-server/config.d/cluster.ha.xml
|
||||
volumes:
|
||||
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
|
||||
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
|
||||
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
||||
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||
- ./clickhouse-setup/data/clickhouse-3/:/var/lib/clickhouse/
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.106.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
image: signoz/signoz:v0.113.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
volumes:
|
||||
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
|
||||
- ../common/dashboards:/root/config/dashboards
|
||||
- ./clickhouse-setup/data/signoz/:/var/lib/signoz/
|
||||
environment:
|
||||
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
||||
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
|
||||
- DASHBOARDS_PATH=/root/config/dashboards
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-swarm
|
||||
- SIGNOZ_JWT_SECRET=secret
|
||||
- DOT_METRICS_ENABLED=true
|
||||
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -209,41 +213,48 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.129.12
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
- -c
|
||||
- |
|
||||
/signoz-otel-collector migrate sync check &&
|
||||
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
|
||||
configs:
|
||||
- source: otel-collector-config
|
||||
target: /etc/otel-collector-config.yaml
|
||||
- source: otel-manager-config
|
||||
target: /etc/manager-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
deploy:
|
||||
replicas: 3
|
||||
depends_on:
|
||||
- clickhouse
|
||||
- schema-migrator
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.129.12
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
entrypoint: sh
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- -c
|
||||
- "/signoz-schema-migrator sync --dsn=tcp://clickhouse:9000 --up= && /signoz-schema-migrator async --dsn=tcp://clickhouse:9000 --up="
|
||||
depends_on:
|
||||
- clickhouse
|
||||
- |
|
||||
/signoz-otel-collector migrate bootstrap &&
|
||||
/signoz-otel-collector migrate sync up &&
|
||||
/signoz-otel-collector migrate async up
|
||||
networks:
|
||||
signoz-net:
|
||||
name: signoz-net
|
||||
@@ -262,3 +273,16 @@ volumes:
|
||||
name: signoz-zookeeper-2
|
||||
zookeeper-3:
|
||||
name: signoz-zookeeper-3
|
||||
configs:
|
||||
clickhouse-config:
|
||||
file: ../common/clickhouse/config.xml
|
||||
clickhouse-users:
|
||||
file: ../common/clickhouse/users.xml
|
||||
clickhouse-custom-function:
|
||||
file: ../common/clickhouse/custom-function.xml
|
||||
clickhouse-cluster:
|
||||
file: ../common/clickhouse/cluster.ha.xml
|
||||
otel-collector-config:
|
||||
file: ./otel-collector-config.yaml
|
||||
otel-manager-config:
|
||||
file: ../common/signoz/otel-collector-opamp-config.yaml
|
||||
|
||||
@@ -58,7 +58,6 @@ x-db-depend: &db-depend
|
||||
!!merge <<: *common
|
||||
depends_on:
|
||||
- clickhouse
|
||||
- schema-migrator
|
||||
services:
|
||||
init-clickhouse:
|
||||
!!merge <<: *common
|
||||
@@ -114,30 +113,20 @@ services:
|
||||
target: /etc/clickhouse-server/config.d/cluster.xml
|
||||
volumes:
|
||||
- clickhouse:/var/lib/clickhouse/
|
||||
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.106.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
image: signoz/signoz:v0.113.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
volumes:
|
||||
- sqlite:/var/lib/signoz/
|
||||
configs:
|
||||
- source: signoz-prometheus-config
|
||||
target: /root/config/prometheus.yml
|
||||
environment:
|
||||
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
||||
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
|
||||
- DASHBOARDS_PATH=/root/config/dashboards
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-swarm
|
||||
- DOT_METRICS_ENABLED=true
|
||||
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -150,12 +139,14 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.129.12
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
- -c
|
||||
- |
|
||||
/signoz-otel-collector migrate sync check &&
|
||||
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
|
||||
configs:
|
||||
- source: otel-collector-config
|
||||
target: /etc/otel-collector-config.yaml
|
||||
@@ -164,29 +155,32 @@ services:
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
deploy:
|
||||
replicas: 3
|
||||
depends_on:
|
||||
- clickhouse
|
||||
- schema-migrator
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.129.12
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
entrypoint: sh
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- -c
|
||||
- "/signoz-schema-migrator sync --dsn=tcp://clickhouse:9000 --up= && /signoz-schema-migrator async --dsn=tcp://clickhouse:9000 --up="
|
||||
depends_on:
|
||||
- clickhouse
|
||||
- |
|
||||
/signoz-otel-collector migrate bootstrap &&
|
||||
/signoz-otel-collector migrate sync up &&
|
||||
/signoz-otel-collector migrate async up
|
||||
networks:
|
||||
signoz-net:
|
||||
name: signoz-net
|
||||
@@ -206,14 +200,6 @@ configs:
|
||||
file: ../common/clickhouse/custom-function.xml
|
||||
clickhouse-cluster:
|
||||
file: ../common/clickhouse/cluster.xml
|
||||
signoz-prometheus-config:
|
||||
file: ../common/signoz/prometheus.yml
|
||||
# If you have multiple dashboard files, you can list them individually:
|
||||
# dashboard-foo:
|
||||
# file: ../common/dashboards/foo.json
|
||||
# dashboard-bar:
|
||||
# file: ../common/dashboards/bar.json
|
||||
|
||||
otel-collector-config:
|
||||
file: ./otel-collector-config.yaml
|
||||
otel-manager-config:
|
||||
|
||||
@@ -82,6 +82,12 @@ exporters:
|
||||
timeout: 45s
|
||||
sending_queue:
|
||||
enabled: false
|
||||
metadataexporter:
|
||||
cache:
|
||||
provider: in_memory
|
||||
dsn: tcp://clickhouse:9000/signoz_metadata
|
||||
enabled: true
|
||||
timeout: 45s
|
||||
service:
|
||||
telemetry:
|
||||
logs:
|
||||
@@ -93,19 +99,19 @@ service:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [signozspanmetrics/delta, batch]
|
||||
exporters: [clickhousetraces, signozmeter]
|
||||
exporters: [clickhousetraces, metadataexporter, signozmeter]
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [signozclickhousemetrics, signozmeter]
|
||||
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
||||
metrics/prometheus:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [signozclickhousemetrics, signozmeter]
|
||||
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhouselogsexporter, signozmeter]
|
||||
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
|
||||
metrics/meter:
|
||||
receivers: [signozmeter]
|
||||
processors: [batch/meter]
|
||||
|
||||
@@ -62,8 +62,10 @@ x-db-depend: &db-depend
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
clickhouse-2:
|
||||
condition: service_healthy
|
||||
clickhouse-3:
|
||||
condition: service_healthy
|
||||
services:
|
||||
init-clickhouse:
|
||||
!!merge <<: *common
|
||||
@@ -179,27 +181,17 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.106.0}
|
||||
image: signoz/signoz:${VERSION:-v0.113.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
volumes:
|
||||
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
|
||||
- ../common/dashboards:/root/config/dashboards
|
||||
- sqlite:/var/lib/signoz/
|
||||
environment:
|
||||
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
||||
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
|
||||
- DASHBOARDS_PATH=/root/config/dashboards
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- DOT_METRICS_ENABLED=true
|
||||
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -210,52 +202,48 @@ services:
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.12}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
container_name: signoz-otel-collector
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
- -c
|
||||
- |
|
||||
/signoz-otel-collector migrate sync check &&
|
||||
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
signoz:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.12}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
- --dsn=tcp://clickhouse:9000
|
||||
- --up=
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
schema-migrator-async:
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.12}
|
||||
container_name: schema-migrator-async
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
container_name: signoz-telemetrystore-migrator
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- async
|
||||
- --dsn=tcp://clickhouse:9000
|
||||
- --up=
|
||||
- -c
|
||||
- |
|
||||
/signoz-otel-collector migrate bootstrap &&
|
||||
/signoz-otel-collector migrate sync up &&
|
||||
/signoz-otel-collector migrate async up
|
||||
restart: on-failure
|
||||
networks:
|
||||
signoz-net:
|
||||
|
||||
@@ -57,8 +57,6 @@ x-db-depend: &db-depend
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
services:
|
||||
init-clickhouse:
|
||||
!!merge <<: *common
|
||||
@@ -111,27 +109,17 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.106.0}
|
||||
image: signoz/signoz:${VERSION:-v0.113.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
volumes:
|
||||
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
|
||||
- ../common/dashboards:/root/config/dashboards
|
||||
- sqlite:/var/lib/signoz/
|
||||
environment:
|
||||
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
||||
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
|
||||
- DASHBOARDS_PATH=/root/config/dashboards
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- DOT_METRICS_ENABLED=true
|
||||
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -144,46 +132,46 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.12}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
container_name: signoz-otel-collector
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
- -c
|
||||
- |
|
||||
/signoz-otel-collector migrate sync check &&
|
||||
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
depends_on:
|
||||
signoz:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.12}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
- --dsn=tcp://clickhouse:9000
|
||||
- --up=
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.12}
|
||||
container_name: schema-migrator-async
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
container_name: signoz-telemetrystore-migrator
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
|
||||
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- async
|
||||
- --dsn=tcp://clickhouse:9000
|
||||
- --up=
|
||||
- -c
|
||||
- |
|
||||
/signoz-otel-collector migrate bootstrap &&
|
||||
/signoz-otel-collector migrate sync up &&
|
||||
/signoz-otel-collector migrate async up
|
||||
restart: on-failure
|
||||
networks:
|
||||
signoz-net:
|
||||
|
||||
@@ -82,6 +82,12 @@ exporters:
|
||||
timeout: 45s
|
||||
sending_queue:
|
||||
enabled: false
|
||||
metadataexporter:
|
||||
cache:
|
||||
provider: in_memory
|
||||
dsn: tcp://clickhouse:9000/signoz_metadata
|
||||
enabled: true
|
||||
timeout: 45s
|
||||
service:
|
||||
telemetry:
|
||||
logs:
|
||||
@@ -93,19 +99,19 @@ service:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [signozspanmetrics/delta, batch]
|
||||
exporters: [clickhousetraces, signozmeter]
|
||||
exporters: [clickhousetraces, metadataexporter, signozmeter]
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [signozclickhousemetrics, signozmeter]
|
||||
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
||||
metrics/prometheus:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [signozclickhousemetrics, signozmeter]
|
||||
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhouselogsexporter, signozmeter]
|
||||
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
|
||||
metrics/meter:
|
||||
receivers: [signozmeter]
|
||||
processors: [batch/meter]
|
||||
|
||||
5295
docs/api/openapi.yml
127
docs/contributing/go/abstractions.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Abstractions
|
||||
|
||||
This document provides rules for deciding when a new type, interface, or intermediate representation is warranted in Go code. The goal is to keep the codebase navigable by ensuring every abstraction earns its place.
|
||||
|
||||
## The cost of a new abstraction
|
||||
|
||||
Every exported type, interface, or wrapper is a permanent commitment. It must be named, documented, tested, and understood by every future contributor. It creates a new concept in the codebase vocabulary. Before introducing one, verify that the cost is justified by a concrete benefit that cannot be achieved with existing mechanisms.
|
||||
|
||||
## Before you introduce anything new
|
||||
|
||||
Answer these four questions. If writing a PR, include the answers in the description.
|
||||
|
||||
1. **What already exists?** Name the specific type, function, interface, or library that covers this ground today.
|
||||
2. **What does the new abstraction add?** Name the concrete operation, guarantee, or capability. "Cleaner" or "more reusable" are not sufficient; name what the caller can do that it could not do before.
|
||||
3. **What does the new abstraction drop?** If it wraps or mirrors an existing structure, list what it cannot represent. Every gap must be either justified or handled with an explicit error.
|
||||
4. **Who consumes it?** List the call sites. If there is only one producer and one consumer in the same call chain, you likely need a function, not a type.
|
||||
|
||||
## Rules
|
||||
|
||||
### 1. Prefer functions over types
|
||||
|
||||
If a piece of logic has one input and one output, write a function. Do not create a struct to hold intermediate state that is built in one place and read in one place. A function is easier to test, easier to inline, and does not expand the vocabulary of the codebase.
|
||||
|
||||
```go
|
||||
// Prefer this:
|
||||
func ConvertConfig(src ExternalConfig) (InternalConfig, error)
|
||||
|
||||
// Over this:
|
||||
type ConfigAdapter struct { ... }
|
||||
func NewConfigAdapter(src ExternalConfig) *ConfigAdapter
|
||||
func (a *ConfigAdapter) ToInternal() (InternalConfig, error)
|
||||
```
|
||||
|
||||
The two-step version is only justified when `ConfigAdapter` has multiple distinct consumers that use it in different ways.
|
||||
|
||||
### 2. Do not duplicate structures you do not own
|
||||
|
||||
When a library or external package produces a structured output, operate on that output directly. Do not create a parallel type that mirrors a subset of its fields.
|
||||
|
||||
A partial copy will:
|
||||
- **Silently lose data** when the source has fields or variants the copy does not account for.
|
||||
- **Drift** when the source evolves and the copy is not updated in lockstep.
|
||||
- **Add a conversion step** that doubles the code surface and the opportunity for bugs.
|
||||
|
||||
If you need to shield consumers from a dependency, define a narrow interface over the dependency's type rather than copying its shape into a new struct.
|
||||
|
||||
### 3. Never silently discard input
|
||||
|
||||
If your code receives structured input and cannot handle part of it, return an error. Do not silently return nil, skip the element, or produce a partial result. Silent data loss is the hardest class of bug to detect because the code appears to work, it just produces wrong results.
|
||||
|
||||
```go
|
||||
// Wrong: silently ignores the unrecognized case.
|
||||
default:
|
||||
return nil
|
||||
|
||||
// Right: makes the gap visible.
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported %T value: %v", v, v)
|
||||
```
|
||||
|
||||
This applies broadly: type switches, format conversions, data migrations, enum mappings, configuration parsing. Anywhere a `default` or `else` branch can swallow input, it should surface an error instead.
|
||||
|
||||
### 4. Do not expose methods that lose information
|
||||
|
||||
A method on a structured type should not strip meaning from the structure it belongs to. If a caller needs to iterate over elements for a specific purpose (validation, aggregation, logging), write that logic as a standalone function that operates on the structure with full context, rather than adding a method that returns a reduced view.
|
||||
|
||||
```go
|
||||
// Problematic: callers cannot distinguish how items were related.
|
||||
func (o *Order) AllLineItems() []LineItem { ... }
|
||||
|
||||
// Better: the validation logic operates on the full structure.
|
||||
func ValidateOrder(o *Order) error { ... }
|
||||
```
|
||||
|
||||
Public methods shape how a type is used. Once a lossy accessor exists, callers will depend on it, and the lost information becomes unrecoverable at those call sites.
|
||||
|
||||
### 5. Interfaces should be discovered, not predicted
|
||||
|
||||
Do not define an interface before you have at least two concrete implementations that need it. An interface with one implementation is not abstraction; it is indirection that makes it harder to navigate from call site to implementation.
|
||||
|
||||
The exception is interfaces required for testing (e.g., for mocking an external dependency). In that case, define the interface in the **consuming** package, not the providing package, following the Go convention of [accepting interfaces and returning structs](https://go.dev/wiki/CodeReviewComments#interfaces).
|
||||
|
||||
### 6. Wrappers must add semantics, not just rename
|
||||
|
||||
A wrapper type is justified when it adds meaning, validation, or invariants that the underlying type does not carry. It is not justified when it merely renames fields or reorganizes the same data into a different shape.
|
||||
|
||||
```go
|
||||
// Justified: adds validation that the underlying string does not carry.
|
||||
type OrgID struct{ value string }
|
||||
func NewOrgID(s string) (OrgID, error) { /* validates format */ }
|
||||
|
||||
// Not justified: renames fields with no new invariant or behavior.
|
||||
type UserInfo struct {
|
||||
Name string // same as source.Name
|
||||
Email string // same as source.Email
|
||||
}
|
||||
```
|
||||
|
||||
Ask: what does the wrapper guarantee that the underlying type does not? If the answer is nothing, use the underlying type directly.
|
||||
|
||||
## When a new type IS warranted
|
||||
|
||||
A new type earns its place when it meets **at least one** of these criteria:
|
||||
|
||||
- **Serialization boundary**: It must be persisted, sent over the wire, or written to config. The source type is unsuitable (unexported fields, function pointers, cycles).
|
||||
- **Invariant enforcement**: The constructor or methods enforce constraints that raw data does not carry (e.g., non-empty, validated format, bounded range).
|
||||
- **Multiple distinct consumers**: Three or more call sites use the type in meaningfully different ways. The type is the shared vocabulary between them.
|
||||
- **Dependency firewall**: The type lives in a lightweight package so that consumers avoid importing a heavy dependency.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- A function is almost always simpler than a type. Start with a function; promote to a type only when you have evidence of need.
|
||||
- Never silently drop data. If you cannot handle it, error.
|
||||
- If your new type mirrors an existing one, you need a strong reason beyond "nicer to work with".
|
||||
- If your type has one producer and one consumer, it is indirection, not abstraction.
|
||||
- Interfaces come from need (multiple implementations), not from prediction.
|
||||
- When in doubt, do not add it. It is easier to add an abstraction later when the need is clear than to remove one after it has spread through the codebase.
|
||||
|
||||
## Further reading
|
||||
|
||||
These works and our own lessions shaped the above guidelines
|
||||
|
||||
- [The Wrong Abstraction](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction) - Sandi Metz. The wrong abstraction is worse than duplication. If you find yourself passing parameters and adding conditional paths through shared code, inline it back into every caller and let the duplication show you what the right abstraction is.
|
||||
- [Write code that is easy to delete, not easy to extend](https://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to) - tef. Every abstraction is a bet on the future. Optimize for how cheaply you can remove code when the bet is wrong, not for how easily you can extend it when the bet is right.
|
||||
- [Goodbye, Clean Code](https://overreacted.io/goodbye-clean-code/) - Dan Abramov. A refactoring that removes duplication can look cleaner while making the code harder to change. Clean-looking and easy-to-change are not the same thing.
|
||||
- [A Philosophy of Software Design](https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201) - John Ousterhout. Good abstractions are deep: simple interface, complex implementation. A "false abstraction" omits important details while appearing simple, and is worse than no abstraction at all. ([Summary by Pragmatic Engineer](https://blog.pragmaticengineer.com/a-philosophy-of-software-design-review/))
|
||||
- [Simplicity is Complicated](https://go.dev/talks/2015/simplicity-is-complicated.slide) - Rob Pike. Go-specific. Fewer orthogonal concepts that compose predictably beat many overlapping ones. Features were left out of Go deliberately; the same discipline applies to your own code.
|
||||
@@ -155,6 +155,7 @@ The `handler.New` function ties the HTTP handler to OpenAPI metadata via `OpenAP
|
||||
- **Request / RequestContentType**:
|
||||
- `Request` is a Go type that describes the request body or form.
|
||||
- `RequestContentType` is usually `"application/json"` or `"application/x-www-form-urlencoded"` (for callbacks like SAML).
|
||||
- **RequestExamples**: An array of `handler.OpenAPIExample` that provide concrete request payloads in the generated spec. See [Adding request examples](#adding-request-examples) below.
|
||||
- **Response / ResponseContentType**:
|
||||
- `Response` is the Go type for the successful response payload.
|
||||
- `ResponseContentType` is usually `"application/json"`; use `""` for responses without a body.
|
||||
@@ -172,8 +173,170 @@ See existing examples in:
|
||||
- `addUserRoutes` (for typical JSON request/response)
|
||||
- `addSessionRoutes` (for form-encoded and redirect flows)
|
||||
|
||||
## OpenAPI schema details for request/response types
|
||||
|
||||
The OpenAPI spec is generated from the Go types you pass as `Request` and `Response` in `OpenAPIDef`. The following struct tags and interfaces control how those types appear in the generated schema.
|
||||
|
||||
### Adding request examples
|
||||
|
||||
Use the `RequestExamples` field in `OpenAPIDef` to provide concrete request payloads. Each example is a `handler.OpenAPIExample`:
|
||||
|
||||
```go
|
||||
type OpenAPIExample struct {
|
||||
Name string // unique key for the example (e.g. "traces_time_series")
|
||||
Summary string // short description shown in docs (e.g. "Time series: count spans grouped by service")
|
||||
Description string // optional longer description
|
||||
Value any // the example payload, typically map[string]any
|
||||
}
|
||||
```
|
||||
|
||||
For reference, see `pkg/apiserver/signozapiserver/querier.go` which defines examples inline for the `/api/v5/query_range` endpoint:
|
||||
|
||||
```go
|
||||
if err := router.Handle("/api/v5/query_range", handler.New(provider.authZ.ViewAccess(provider.querierHandler.QueryRange), handler.OpenAPIDef{
|
||||
ID: "QueryRangeV5",
|
||||
Tags: []string{"querier"},
|
||||
Summary: "Query range",
|
||||
Description: "Execute a composite query over a time range.",
|
||||
Request: new(qbtypes.QueryRangeRequest),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: []handler.OpenAPIExample{
|
||||
{
|
||||
Name: "traces_time_series",
|
||||
Summary: "Time series: count spans grouped by service",
|
||||
Value: map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
// ...
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// ... more examples
|
||||
},
|
||||
// ...
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
### `required` tag
|
||||
|
||||
Use `required:"true"` on struct fields where the property is expected to be **present** in the JSON payload. This is different from the zero value, a field can have its zero value (e.g. `0`, `""`, `false`) and still be required. The `required` tag means the key itself must exist in the JSON object.
|
||||
|
||||
```go
|
||||
type ListItem struct {
|
||||
...
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
List []ListItem `json:"list" required:"true" nullable:"true"`
|
||||
Total uint64 `json:"total" required:"true"`
|
||||
}
|
||||
```
|
||||
|
||||
In this example, a response like `{"list": null, "total": 0}` is valid. Both keys are present (satisfying `required`), `total` has its zero value, and `list` is null (allowed by `nullable`). But `{"total": 0}` would violate the schema because the `list` key is missing.
|
||||
|
||||
### `nullable` tag
|
||||
|
||||
Use `nullable:"true"` on struct fields that can be `null` in the JSON payload. This is especially important for **slice and map fields** because in Go, the zero value for these types is `nil`, which serializes to `null` in JSON (not `[]` or `{}`).
|
||||
|
||||
Be explicit about the distinction:
|
||||
|
||||
- **Nullable list** (`nullable:"true"`): the field can be `null`. Use this when the Go code may return `nil` for the slice.
|
||||
- **Non-nullable list** (no `nullable` tag): the field is always an array, never `null`. Ensure the Go code initializes it to an empty slice (e.g. `make([]T, 0)`) before serializing.
|
||||
|
||||
```go
|
||||
// Non-nullable: Go code must ensure this is always an initialized slice.
|
||||
type NonNullableExample struct {
|
||||
Items []Item `json:"items" required:"true"`
|
||||
}
|
||||
```
|
||||
|
||||
When defining your types, ask yourself: "Can this field be `null` in the JSON response, or is it always an array/object?" If the Go code ever returns a `nil` slice or map, mark it `nullable:"true"`.
|
||||
|
||||
### `Enum()` method
|
||||
|
||||
For types that have a fixed set of acceptable values, implement the `Enum() []any` method. This generates an `enum` constraint in the JSON schema so the OpenAPI spec accurately restricts the values.
|
||||
|
||||
```go
|
||||
type Signal struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
SignalTraces = Signal{valuer.NewString("traces")}
|
||||
SignalLogs = Signal{valuer.NewString("logs")}
|
||||
SignalMetrics = Signal{valuer.NewString("metrics")}
|
||||
)
|
||||
|
||||
func (Signal) Enum() []any {
|
||||
return []any{
|
||||
SignalTraces,
|
||||
SignalLogs,
|
||||
SignalMetrics,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This produces the following in the generated OpenAPI spec:
|
||||
|
||||
```yaml
|
||||
Signal:
|
||||
enum:
|
||||
- traces
|
||||
- logs
|
||||
- metrics
|
||||
type: string
|
||||
```
|
||||
|
||||
Every type with a known set of values **must** implement `Enum()`. Without it, the JSON schema will only show the base type (e.g. `string`) with no value constraints.
|
||||
|
||||
### `JSONSchema()` method (custom schema)
|
||||
|
||||
For types that need a completely custom JSON schema (for example, a field that accepts either a string or a number), implement the `jsonschema.Exposer` interface:
|
||||
|
||||
```go
|
||||
var _ jsonschema.Exposer = Step{}
|
||||
|
||||
func (Step) JSONSchema() (jsonschema.Schema, error) {
|
||||
s := jsonschema.Schema{}
|
||||
s.WithDescription("Step interval. Accepts a duration string or seconds.")
|
||||
|
||||
strSchema := jsonschema.Schema{}
|
||||
strSchema.WithType(jsonschema.String.Type())
|
||||
strSchema.WithExamples("60s", "5m", "1h")
|
||||
|
||||
numSchema := jsonschema.Schema{}
|
||||
numSchema.WithType(jsonschema.Number.Type())
|
||||
numSchema.WithExamples(60, 300, 3600)
|
||||
|
||||
s.OneOf = []jsonschema.SchemaOrBool{
|
||||
strSchema.ToSchemaOrBool(),
|
||||
numSchema.ToSchemaOrBool(),
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- **Keep handlers thin**: focus on HTTP concerns and delegate logic to modules/services.
|
||||
- **Always register routes through `signozapiserver`** using `handler.New` and a complete `OpenAPIDef`.
|
||||
- **Choose accurate request/response types** from the `types` packages so OpenAPI schemas are correct.
|
||||
- **Add `required:"true"`** on fields where the key must be present in the JSON (this is about key presence, not about the zero value).
|
||||
- **Add `nullable:"true"`** on fields that can be `null`. Pay special attention to slices and maps -- in Go these default to `nil` which serializes to `null`. If the field should always be an array, initialize it and do not mark it nullable.
|
||||
- **Implement `Enum()`** on every type that has a fixed set of acceptable values so the JSON schema generates proper `enum` constraints.
|
||||
- **Add request examples** via `RequestExamples` in `OpenAPIDef` for any non-trivial endpoint. See `pkg/apiserver/signozapiserver/querier.go` for reference.
|
||||
|
||||
192
docs/contributing/go/packages.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Packages
|
||||
|
||||
All shared Go code in SigNoz lives under `pkg/`. Each package represents a distinct domain concept and exposes a clear public interface. This guide covers the conventions for creating, naming, and organising packages so the codebase stays consistent as it grows.
|
||||
|
||||
## How should I name a package?
|
||||
|
||||
Use short, lowercase, single-word names. No underscores or camelCase (`querier`, `cache`, `authz`, not `query_builder` or `dataStore`).
|
||||
|
||||
Names must be **domain-specific**. A package name should tell you what problem domain it deals with, not what data structure it wraps. Prefer `alertmanager` over `manager`, `licensing` over `checker`.
|
||||
|
||||
Avoid generic names like `util`, `helpers`, `common`, `misc`, or `base`. If you can't name it, the code probably belongs in an existing package.
|
||||
|
||||
## When should I create a new package?
|
||||
|
||||
Create a new package when:
|
||||
|
||||
- The functionality represents a **distinct domain concept** (e.g., `authz`, `licensing`, `cache`).
|
||||
- Two or more other packages would import it; it serves as shared infrastructure.
|
||||
- The code has a clear public interface that can stand on its own.
|
||||
|
||||
Do **not** create a new package when:
|
||||
|
||||
- There is already a package that covers the same domain. Extend the existing package instead.
|
||||
- The code is only used in one place. Keep it local to the caller.
|
||||
- You are splitting purely for file size. Use multiple files within the same package instead.
|
||||
|
||||
## How should I lay out a package?
|
||||
|
||||
A typical package looks like:
|
||||
|
||||
```
|
||||
pkg/cache/
|
||||
├── cache.go # Public interface + exported types
|
||||
├── config.go # Configuration types if needed
|
||||
├── memorycache/ # Implementation sub-package
|
||||
├── rediscache/ # Another implementation
|
||||
└── cachetest/ # Test helpers for consumers
|
||||
```
|
||||
|
||||
Follow these rules:
|
||||
|
||||
1. **Interface-first file**: The file matching the package name (e.g., `cache.go` in `pkg/cache/`) should define the public interface and core exported types. Keep implementation details out of this file.
|
||||
|
||||
2. **One responsibility per file**: Name files after what they contain (`config.go`, `handler.go`, `service.go`), not after the package name. If a package merges two concerns, prefix files to group them (e.g., `memory_store.go`, `redis_store.go` in a storage package).
|
||||
|
||||
3. **Sub-packages for implementations**: When a package defines an interface with multiple implementations, put each implementation in its own sub-package (`memorycache/`, `rediscache/`). This keeps the parent package import-free of implementation dependencies.
|
||||
|
||||
4. **Test helpers in `{pkg}test/`**: If consumers need test mocks or builders, put them in a `{pkg}test/` sub-package (e.g., `cachetest/`, `sqlstoretest/`). This avoids polluting the main package with test-only code.
|
||||
|
||||
5. **Test files stay alongside source**: Unit tests go in `_test.go` files next to the code they test, in the same package.
|
||||
|
||||
## How should I order code within a file?
|
||||
|
||||
Within a single `.go` file, declarations should follow this order:
|
||||
|
||||
1. Constants
|
||||
2. Variables
|
||||
3. Types (structs, interfaces)
|
||||
4. Constructor functions (`New...`)
|
||||
5. Exported methods and functions
|
||||
6. Unexported methods and functions
|
||||
|
||||
```go
|
||||
// 1. Constants
|
||||
const defaultTimeout = 30 * time.Second
|
||||
|
||||
// 2. Variables
|
||||
var ErrNotFound = errors.New(errors.TypeNotFound, errors.CodeNotFound, "resource not found")
|
||||
|
||||
// 3. Types
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// 4. Constructors
|
||||
func NewStore(db *sql.DB) *Store {
|
||||
return &Store{db: db}
|
||||
}
|
||||
|
||||
// 5. Exported methods
|
||||
func (s *Store) Get(ctx context.Context, id string) (*Resource, error) { ... }
|
||||
|
||||
// 6. Unexported methods
|
||||
func (s *Store) buildQuery(id string) string { ... }
|
||||
```
|
||||
|
||||
This ordering makes files predictable. A reader scanning from top to bottom sees the contract (constants, types, constructors) before the implementation (methods), and exported behavior before internal helpers.
|
||||
|
||||
## How should I name symbols?
|
||||
|
||||
### Exported symbols
|
||||
|
||||
- **Interfaces**: For single-method interfaces, follow the standard `-er` suffix convention (`Reader`, `Writer`, `Closer`). For multi-method interfaces, use clear nouns (`Cache`, `Store`, `Provider`).
|
||||
- **Constructors**: `New<Type>(...)` (e.g., `NewMemoryCache()`).
|
||||
- **Avoid stutter**: Since callers qualify with the package name, don't repeat it. Write `cache.Cache`, not `cache.CacheInterface`. Write `authz.FromRole`, not `authz.AuthzFromRole`.
|
||||
|
||||
### Unexported symbols
|
||||
|
||||
- Struct receivers: one or two characters (`c`, `f`, `br`).
|
||||
- Helper functions: descriptive lowercase names (`parseToken`, `buildQuery`).
|
||||
|
||||
### Constants
|
||||
|
||||
- Use `PascalCase` for exported constants.
|
||||
- When merging files from different origins into one package, watch out for **name collisions** across files. Prefix to disambiguate when two types share a natural name.
|
||||
|
||||
## How should I organise imports?
|
||||
|
||||
Group imports in three blocks separated by blank lines:
|
||||
|
||||
```go
|
||||
import (
|
||||
// 1. Standard library
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
// 2. External dependencies
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
// 3. Internal
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
```
|
||||
|
||||
Never introduce circular imports. If package A needs package B and B needs A, extract the shared types into a third package (often under `pkg/types/`).
|
||||
|
||||
## Where do shared types go?
|
||||
|
||||
Most types belong in `pkg/types/` under a domain-specific sub-package (e.g., `pkg/types/ruletypes`, `pkg/types/authtypes`).
|
||||
|
||||
Do not put domain logic in `pkg/types/`. Only data structures, constants, and simple methods.
|
||||
|
||||
## How do I merge or move packages?
|
||||
|
||||
When two packages are tightly coupled (one imports the other's constants, they cover the same domain), merge them:
|
||||
|
||||
1. Pick a domain-specific name for the combined package.
|
||||
2. Prefix files to preserve origin (e.g., `memory_store.go`, `redis_store.go`).
|
||||
3. Resolve symbol conflicts explicitly; rename with a prefix rather than silently shadowing.
|
||||
4. Update all consumers in a single change.
|
||||
5. Delete the old packages. Do not leave behind re-export shims.
|
||||
6. Verify with `go build ./...`, `go test ./<new-pkg>/...`, and `go vet ./...`.
|
||||
|
||||
## When should I use valuer types?
|
||||
|
||||
The `pkg/valuer` package provides typed wrappers for common domain values: `valuer.String`, `valuer.Email`, `valuer.UUID`, and `valuer.TextDuration`. These types carry validation, normalization, and consistent serialization (JSON, SQL, text) that raw Go primitives do not.
|
||||
|
||||
Use a valuer type instead of a raw primitive when the value represents a domain concept with any of:
|
||||
|
||||
- **Enums**: All enums in the codebase must be backed by `valuer.String`. Do not use raw `string` constants or `iota`-based `int` enums. A struct embedding `valuer.String` with predefined variables gives you normalization, serialization, and an `Enum()` method for OpenAPI schema generation in one place.
|
||||
- **Validation**: emails must match a format, UUIDs must be parseable, durations must be valid.
|
||||
- **Normalization**: `valuer.String` lowercases and trims input, so comparisons are consistent throughout the system.
|
||||
- **Serialization boundary**: the value is stored in a database, sent over the wire, or bound from an HTTP parameter. Valuer types implement `Scan`, `Value`, `MarshalJSON`, `UnmarshalJSON`, and `UnmarshalParam` consistently.
|
||||
|
||||
```go
|
||||
// Wrong: raw string constant with no validation or normalization.
|
||||
const SignalTraces = "traces"
|
||||
|
||||
// Right: valuer-backed type that normalizes and serializes consistently.
|
||||
type Signal struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var SignalTraces = Signal{valuer.NewString("traces")}
|
||||
```
|
||||
|
||||
Only primitive domain types that serve as shared infrastructure belong in `pkg/valuer`. If you need a new base type (like `Email` or `TextDuration`) that multiple packages will embed for validation and serialization, add it there. Domain-specific types that build on top of a valuer (like `Signal` embedding `valuer.String`) belong in their own domain package, not in `pkg/valuer`.
|
||||
|
||||
## When should I add documentation?
|
||||
|
||||
Add a `doc.go` with a package-level comment for any package that is non-trivial or has multiple consumers. Keep it to 1–3 sentences:
|
||||
|
||||
```go
|
||||
// Package cache provides a caching interface with pluggable backends
|
||||
// for in-memory and Redis-based storage.
|
||||
package cache
|
||||
```
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Package names are domain-specific and lowercase. Never generic names like `util` or `common`.
|
||||
- The file matching the package name (e.g., `cache.go`) defines the public interface. Implementation details go elsewhere.
|
||||
- Within a file, order declarations: constants, variables, types, constructors, exported functions, unexported functions.
|
||||
- Segregate types across files by responsibility. A file with 5 unrelated types is harder to navigate than 5 files with one type each.
|
||||
- Use valuer types (`valuer.String`, `valuer.Email`, `valuer.UUID`, `valuer.TextDuration`) for domain values that need validation, normalization, or cross-boundary serialization. Do not use raw string constants where a valuer type exists.
|
||||
- Avoid `init()` functions. If you need to initialize a variable, use a package-level `var` with a function call or a `sync.Once`. `init()` hides execution order, makes testing harder, and has caused subtle bugs in large codebases.
|
||||
- Never introduce circular imports. Extract shared types into `pkg/types/` when needed.
|
||||
- Watch for symbol name collisions when merging packages, prefix to disambiguate.
|
||||
- Put test helpers in a `{pkg}test/` sub-package, not in the main package.
|
||||
- Before submitting, verify with `go build ./...`, `go test ./<your-pkg>/...`, and `go vet ./...`.
|
||||
- Update all consumers when you rename or move symbols.
|
||||
@@ -8,4 +8,40 @@ We adhere to three primary style guides as our foundation:
|
||||
- [Code Review Comments](https://go.dev/wiki/CodeReviewComments) - For understanding common comments in code reviews
|
||||
- [Google Style Guide](https://google.github.io/styleguide/go/) - Additional practices from Google
|
||||
|
||||
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package.
|
||||
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development.
|
||||
|
||||
**Discover before inventing.** Before writing new code, search the codebase for existing solutions. SigNoz has established patterns for common problems: `pkg/valuer` for typed domain values, `pkg/errors` for structured errors, `pkg/factory` for provider wiring, `{pkg}test/` sub-packages for test helpers, and shared fixtures for integration tests. Duplicating what already exists creates drift and maintenance burden. When you find an existing pattern, use it. When you don't find one, check with the maintainers before building your own.
|
||||
|
||||
## How to approach a feature
|
||||
|
||||
Building a feature is not one task, it is a sequence of concerns that build on each other. Work through them in this order:
|
||||
|
||||
1. **Domain design (types).** Define the types that represent your domain. What are the entities, what are their relationships, what are the constraints? This is where you decide your data model. Get this right first because everything else depends on it. See [Packages](packages.md) and [Abstractions](abstractions.md).
|
||||
|
||||
2. **Structure (services / modules / handlers).** Place your code in the right layer given the current infrastructure. If the current structure does not work for your feature, that is the time to open a discussion and write a technical document, not to silently reshape things in the same PR. See [Handler](handler.md) and [Provider](provider.md).
|
||||
|
||||
3. **HTTP endpoints (paths, status codes, errors).** Pay close attention to detail here. Paths, methods, request/response shapes, status codes, error codes. These are the contract with consumers and are expensive to change after release. See [Endpoint](endpoint.md) and [Handler](handler.md).
|
||||
|
||||
4. **Database constraints (org_id, foreign keys, migrations).** Ensure org scoping, schema consistency, and migration correctness. See [SQL](sql.md).
|
||||
|
||||
5. **Business logic (module layer).** With the types, structure, endpoints, and storage in place, the focus narrows to the actual logic. This is where review should concentrate on correctness, edge cases, and error handling.
|
||||
|
||||
This ordering also gives you a natural way to split PRs. Each layer affects a different area and requires a different lens for review. A PR that mixes refactoring with new feature logic is hard to review and risky to ship. Separate them.
|
||||
|
||||
For large refactors or features that touch multiple subsystems, write a short technical document outlining the design and get relevant stakeholders aligned before starting implementation. This saves significant back-and-forth during review.
|
||||
|
||||
## Area-specific guides
|
||||
|
||||
In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
|
||||
|
||||
- [Abstractions](abstractions.md) - When to introduce new types and intermediate representations
|
||||
- [Errors](errors.md) - Structured error handling
|
||||
- [Endpoint](endpoint.md) - HTTP endpoint patterns
|
||||
- [Flagger](flagger.md) - Feature flag patterns
|
||||
- [Handler](handler.md) - HTTP handler patterns
|
||||
- [Integration](integration.md) - Integration testing
|
||||
- [Provider](provider.md) - Dependency injection and provider patterns
|
||||
- [Packages](packages.md) - Naming, layout, and conventions for `pkg/` packages
|
||||
- [Service](service.md) - Managed service lifecycle with `factory.Service`
|
||||
- [SQL](sql.md) - Database and SQL patterns
|
||||
- [Testing](testing.md) - Writing tests that catch bugs without becoming a maintenance burden
|
||||
|
||||
269
docs/contributing/go/service.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Service
|
||||
|
||||
A service is a component with a managed lifecycle: it starts, runs for the lifetime of the application, and stops gracefully.
|
||||
|
||||
Services are distinct from [providers](provider.md). A provider adapts an external dependency behind an interface. A service has a managed lifecycle that is tied to the lifetime of the application.
|
||||
|
||||
## When do you need a service?
|
||||
|
||||
You need a service when your component needs to do work that outlives a single method call:
|
||||
|
||||
- **Periodic work**: polling an external system, garbage-collecting expired data, syncing state on an interval.
|
||||
- **Graceful shutdown**: holding resources (connections, caches, buffers) that must be flushed or closed before the process exits.
|
||||
- **Blocking on readiness**: waiting for an external dependency to become available before the application can proceed.
|
||||
|
||||
If your component only responds to calls and holds no state that requires cleanup, it is a provider, not a service. If it does both (responds to calls *and* needs a lifecycle), embed `factory.Service` in the provider interface; see [How to create a service](#how-to-create-a-service).
|
||||
|
||||
## The interface
|
||||
|
||||
The `factory.Service` interface in `pkg/factory/service.go` defines two methods:
|
||||
|
||||
```go
|
||||
type Service interface {
|
||||
// Starts a service. It should block and should not return until the service is stopped or it fails.
|
||||
Start(context.Context) error
|
||||
// Stops a service.
|
||||
Stop(context.Context) error
|
||||
}
|
||||
```
|
||||
|
||||
`Start` **must block**. It should not return until the service is stopped (returning `nil`) or something goes wrong (returning an error). If `Start` returns an error, the entire application shuts down.
|
||||
|
||||
`Stop` should cause `Start` to unblock and return. It must be safe to call from a different goroutine than the one running `Start`.
|
||||
|
||||
## Shutdown coordination
|
||||
|
||||
Every service uses a `stopC chan struct{}` to coordinate shutdown:
|
||||
|
||||
- **Constructor**: `stopC: make(chan struct{})`
|
||||
- **Start**: blocks on `<-stopC` (or uses it in a `select` loop)
|
||||
- **Stop**: `close(stopC)` to unblock `Start`
|
||||
|
||||
This is the standard pattern. Do not use `context.WithCancel` or other mechanisms for service-level shutdown coordination. See the examples in the next section.
|
||||
|
||||
## Service shapes
|
||||
|
||||
Two shapes recur across the codebase (these are not exhaustive, if a new shape is needed, bring it up for discussion before going ahead with the implementation), implemented by convention rather than base classes.
|
||||
|
||||
### Idle service
|
||||
|
||||
The service does work during startup or shutdown but has nothing to do while running. `Start` blocks on `<-stopC`. `Stop` closes `stopC` and optionally does cleanup.
|
||||
|
||||
The JWT tokenizer (`pkg/tokenizer/jwttokenizer/provider.go`) is a good example. It validates and creates tokens on demand via method calls, but has no periodic work to do. It still needs the service lifecycle so the registry can manage its lifetime:
|
||||
|
||||
```go
|
||||
// pkg/tokenizer/jwttokenizer/provider.go
|
||||
|
||||
func (provider *provider) Start(ctx context.Context) error {
|
||||
<-provider.stopC
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
close(provider.stopC)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
The instrumentation SDK (`pkg/instrumentation/sdk.go`) is idle while running but does real cleanup in `Stop` shutting down its OpenTelemetry tracer and meter providers:
|
||||
|
||||
```go
|
||||
// pkg/instrumentation/sdk.go
|
||||
|
||||
func (i *SDK) Start(ctx context.Context) error {
|
||||
<-i.startCh
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *SDK) Stop(ctx context.Context) error {
|
||||
close(i.startCh)
|
||||
return errors.Join(
|
||||
i.sdk.Shutdown(ctx),
|
||||
i.meterProviderShutdownFunc(ctx),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Scheduled service
|
||||
|
||||
The service runs an operation repeatedly on a fixed interval. `Start` runs a ticker loop with a `select` on `stopC` and the ticker channel.
|
||||
|
||||
The opaque tokenizer (`pkg/tokenizer/opaquetokenizer/provider.go`) garbage-collects expired tokens and flushes cached last-observed-at timestamps to the database on a configurable interval:
|
||||
|
||||
```go
|
||||
// pkg/tokenizer/opaquetokenizer/provider.go
|
||||
|
||||
func (provider *provider) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(provider.config.Opaque.GC.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-provider.stopC:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
|
||||
if err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to get orgs data", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
if err := provider.gc(ctx, org); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to garbage collect tokens", "error", err, "org_id", org.ID)
|
||||
}
|
||||
|
||||
if err := provider.flushLastObservedAt(ctx, org); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", "error", err, "org_id", org.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Its `Stop` does a final gc and flush before returning, so no data is lost on shutdown:
|
||||
|
||||
```go
|
||||
// pkg/tokenizer/opaquetokenizer/provider.go
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
close(provider.stopC)
|
||||
|
||||
orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
if err := provider.gc(ctx, org); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to garbage collect tokens", "error", err, "org_id", org.ID)
|
||||
}
|
||||
|
||||
if err := provider.flushLastObservedAt(ctx, org); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", "error", err, "org_id", org.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
The key points:
|
||||
|
||||
- In the loop, `select` on `stopC` and the ticker. Errors in iterations are logged but do not cause the service to return (which would shut down the application).
|
||||
- Only return an error from `Start` if the failure is unrecoverable.
|
||||
- Use `Stop` to flush or drain any in-memory state before the process exits.
|
||||
|
||||
## How to create a service
|
||||
|
||||
There are two cases: a standalone service and a provider that is also a service.
|
||||
|
||||
### Standalone service
|
||||
|
||||
A standalone service only has the `factory.Service` lifecycle i.e it does not serve as a dependency for other packages. The user reconciliation service is an example.
|
||||
|
||||
1. Define the service interface in your package. Embed `factory.Service`:
|
||||
|
||||
```go
|
||||
// pkg/modules/user/service.go
|
||||
package user
|
||||
|
||||
type Service interface {
|
||||
factory.Service
|
||||
}
|
||||
```
|
||||
|
||||
2. Create the implementation in an `impl` sub-package. Use an unexported struct with an exported constructor that returns the interface:
|
||||
|
||||
```go
|
||||
// pkg/modules/user/impluser/service.go
|
||||
package impluser
|
||||
|
||||
type service struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
// ... dependencies ...
|
||||
stopC chan struct{}
|
||||
}
|
||||
|
||||
func NewService(
|
||||
providerSettings factory.ProviderSettings,
|
||||
// ... dependencies ...
|
||||
) user.Service {
|
||||
return &service{
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/pkg/modules/user"),
|
||||
// ... dependencies ...
|
||||
stopC: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Start(ctx context.Context) error { ... }
|
||||
func (s *service) Stop(ctx context.Context) error { ... }
|
||||
```
|
||||
|
||||
### Provider that is also a service
|
||||
|
||||
Many providers need a managed lifecycle: they poll, sync, or garbage-collect in the background. In this case, embed `factory.Service` in the provider interface. The implementation satisfies both the provider methods and `Start`/`Stop`.
|
||||
|
||||
```go
|
||||
// pkg/tokenizer/tokenizer.go
|
||||
package tokenizer
|
||||
|
||||
type Tokenizer interface {
|
||||
factory.Service
|
||||
CreateToken(context.Context, *authtypes.Identity, map[string]string) (*authtypes.Token, error)
|
||||
GetIdentity(context.Context, string) (*authtypes.Identity, error)
|
||||
// ... other methods ...
|
||||
}
|
||||
```
|
||||
|
||||
The implementation (e.g. `pkg/tokenizer/opaquetokenizer/provider.go`) implements `Start`, `Stop`, and all the provider methods on the same struct. See the [provider guide](provider.md) for how to set up the factory, config, and constructor. The `stopC` channel and `Start`/`Stop` methods follow the same patterns described above.
|
||||
|
||||
## How to wire it up
|
||||
|
||||
Wiring happens in `pkg/signoz/signoz.go`.
|
||||
|
||||
### 1. Instantiate the service
|
||||
|
||||
For a standalone service, call the constructor directly:
|
||||
|
||||
```go
|
||||
userService := impluser.NewService(providerSettings, store, module, orgGetter, authz, config.User.Root)
|
||||
```
|
||||
|
||||
For a provider that is also a service, use `factory.NewProviderFromNamedMap` as described in the [provider guide](provider.md). The returned value already implements `factory.Service`.
|
||||
|
||||
### 2. Register in the registry
|
||||
|
||||
Wrap the service with `factory.NewNamedService` and pass it to `factory.NewRegistry`:
|
||||
|
||||
```go
|
||||
registry, err := factory.NewRegistry(
|
||||
instrumentation.Logger(),
|
||||
// ... other services ...
|
||||
factory.NewNamedService(factory.MustNewName("user"), userService),
|
||||
)
|
||||
```
|
||||
|
||||
The name must be unique across all services. The registry handles the rest:
|
||||
|
||||
- **Start**: launches all services concurrently in goroutines.
|
||||
- **Wait**: blocks until a service returns an error, the context is cancelled, or a SIGINT/SIGTERM is received. Any service error triggers application shutdown.
|
||||
- **Stop**: stops all services concurrently, collects errors via `errors.Join`.
|
||||
|
||||
You do not call `Start` or `Stop` on individual services. The registry does it.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- A service has a managed lifecycle: `Start` blocks, `Stop` unblocks it.
|
||||
- Use `stopC chan struct{}` for shutdown coordination. `close(stopC)` in `Stop`, `<-stopC` in `Start`.
|
||||
- Service shapes: idle (block on `stopC`) and scheduled (ticker loop with `select`).
|
||||
- Unexported struct, exported `NewService` constructor returning the interface.
|
||||
- First constructor parameter is `factory.ProviderSettings`. Create scoped settings with `factory.NewScopedProviderSettings`.
|
||||
- Register in `factory.Registry` with `factory.NewNamedService`. The registry starts and stops everything.
|
||||
- Only return an error from `Start` if the failure is unrecoverable. Log and continue for transient errors in polling loops.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Google Guava - ServiceExplained](https://github.com/google/guava/wiki/ServiceExplained) - the service lifecycle pattern takes inspiration from this
|
||||
- [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector) - Worth studying for its approach to building composable components
|
||||
260
docs/contributing/go/testing.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Testing
|
||||
|
||||
This document provides rules for writing tests that catch real bugs and do not become a maintenance burden. It covers both how to write good tests and how to recognize bad ones.
|
||||
|
||||
## Why we write tests
|
||||
|
||||
Tests exist to give confidence that the system behaves correctly. A good test suite lets you change code and know immediately (or in a reasonable time) whether you broke something. A bad test suite lets you change code (and then spend hours figuring out whether the failures are real) and still lets the bugs slip in.
|
||||
|
||||
Every test should be written to answer one question: **if this test fails, does that mean a user-visible behavior is broken?** If the answer is no, reconsider whether the test should exist.
|
||||
|
||||
Not all tests are equal. Different scopes serve different purposes, and the balance matters.
|
||||
|
||||
- **Unit tests**: Fast, focused, test a single function or type in isolation. These form the foundation. They should run in milliseconds, have no I/O, and be fully deterministic.
|
||||
- **Integration tests**: Verify that components work together against real dependencies (ClickHouse, PostgreSQL, etc.). Slower, but catch problems that unit tests cannot: real query behavior, configuration issues, serialization mismatches.
|
||||
- **End-to-end tests**: Validate full system behavior from the outside. Expensive to write and maintain, but necessary for critical user flows.
|
||||
|
||||
When a test can be written at a smaller scope, prefer the smaller scope. But do not force a unit test where an integration test is the natural fit.
|
||||
|
||||
## What to test
|
||||
|
||||
### Test behaviors, not implementations
|
||||
|
||||
A test should verify what the code does, not how it does it (unless the goal of the test is specifically how something happen). If you can refactor the internals of a function e.g, change a query, rename a variable, restructure the logic and no user-visible behavior changes, no test should break.
|
||||
|
||||
```go
|
||||
// Good: tests the behavior "given this input, expect this output."
|
||||
func TestDiscountApplied(t *testing.T) {
|
||||
order := NewOrder(item("widget", 100))
|
||||
order.ApplyDiscount(10)
|
||||
assert.Equal(t, 90, order.Total())
|
||||
}
|
||||
|
||||
// Bad: tests the implementation "did it call the right internal method?"
|
||||
func TestDiscountApplied(t *testing.T) {
|
||||
mockPricer := new(MockPricer)
|
||||
mockPricer.On("CalculateDiscount", 100, 10).Return(90)
|
||||
order := NewOrder(item("widget", 100), WithPricer(mockPricer))
|
||||
order.ApplyDiscount(10)
|
||||
mockPricer.AssertCalled(t, "CalculateDiscount", 100, 10)
|
||||
}
|
||||
```
|
||||
|
||||
The first test survives a refactoring of how discounts are calculated. The second test breaks the moment you rename the method, change its signature, or inline the logic.
|
||||
|
||||
**The refactoring test**: before committing a test, ask if someone refactors the internals tomorrow without changing any behavior, will this test break? If yes, consider updating the test.
|
||||
|
||||
### Output format as behavior
|
||||
|
||||
Some functions exist specifically to produce a formatted output: a query builder generates SQL, a serializer generates JSON, a code generator produces source code. In these cases, the output string *is* the behavior and asserting on it is valid and necessary. The function's contract is the exact output it produces.
|
||||
|
||||
This is different from testing a function that *uses* a query internally. If a function's job is to fetch data from a database, the query it sends is an implementation detail and the returned data is the behavior. If its job is to *build* a query for someone else to execute, the query string is the behavior.
|
||||
|
||||
The distinction: **is the formatted output the function's product, or the function's mechanism?** Test the product, not the mechanism.
|
||||
|
||||
### Test at the public API boundary
|
||||
|
||||
Write tests against the exported functions and methods that consumers actually call. Do not test unexported helpers directly. If an unexported function has complex logic worth testing, that is a signal it should be extracted into its own package with its own public API.
|
||||
|
||||
### Test edge cases and error paths
|
||||
|
||||
The most valuable tests cover the cases that are easy to get wrong:
|
||||
|
||||
- Empty inputs, nil inputs, zero values.
|
||||
- Boundary conditions (off-by-one, first element, last element).
|
||||
- Error conditions (what happens when the dependency fails?).
|
||||
- Concurrent access, if the code is designed for it.
|
||||
|
||||
A test for the happy path of a trivial function adds little value. A test for the error path of a complex function prevents real bugs.
|
||||
|
||||
### The Beyonce Rule
|
||||
|
||||
"If you liked it, then you should have put a test on it." Any behavior you want to preserve such as correctness, performance characteristics, security constraints, error handling should be covered by a test. If it breaks and there is no test, that is not a regression; it is an untested assumption.
|
||||
|
||||
## How to write a test
|
||||
|
||||
### Structure: arrange, act, assert
|
||||
|
||||
Every test should have three clearly separated sections:
|
||||
|
||||
```go
|
||||
func TestTransferInsufficientFunds(t *testing.T) {
|
||||
// Arrange: set up the preconditions.
|
||||
from := NewAccount(50)
|
||||
to := NewAccount(0)
|
||||
|
||||
// Act: perform the operation being tested.
|
||||
err := Transfer(from, to, 100)
|
||||
|
||||
// Assert: verify the outcome.
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, 50, from.Balance())
|
||||
assert.Equal(t, 0, to.Balance())
|
||||
}
|
||||
```
|
||||
|
||||
Do not interleave setup and assertions. Do not put assertions in helper functions that also perform setup. Keep the three sections visually distinct.
|
||||
|
||||
### One behavior per test
|
||||
|
||||
Each test function should verify one behavior. If a test name needs "and" in it, split it into two tests.
|
||||
|
||||
```go
|
||||
// Good: one behavior per test.
|
||||
func TestParseValidInput(t *testing.T) { ... }
|
||||
func TestParseEmptyInput(t *testing.T) { ... }
|
||||
func TestParseMalformedInput(t *testing.T) { ... }
|
||||
|
||||
// Bad: multiple behaviors in one test.
|
||||
func TestParse(t *testing.T) {
|
||||
// test valid input
|
||||
// test empty input
|
||||
// test malformed input
|
||||
}
|
||||
```
|
||||
|
||||
Table-driven tests are fine when the behavior is the same and only the inputs/outputs vary.
|
||||
|
||||
### Name tests after behaviors
|
||||
|
||||
Test names should describe the scenario and the expected outcome, not the function being tested.
|
||||
|
||||
```go
|
||||
// Good: describes the behavior.
|
||||
func TestWithdrawal_InsufficientFunds_ReturnsError(t *testing.T)
|
||||
func TestWithdrawal_ZeroBalance_ReturnsError(t *testing.T)
|
||||
|
||||
// Bad: describes the function.
|
||||
func TestWithdraw(t *testing.T)
|
||||
func TestWithdrawError(t *testing.T)
|
||||
```
|
||||
|
||||
### Eliminate logic in tests
|
||||
|
||||
Tests should be straight-line code. No `if`, no `for`, no `switch`. If you feel the need to add control flow to a test, either split it into multiple tests or restructure the test data.
|
||||
|
||||
A test with logic in it needs its own tests. That is a sign something has gone wrong.
|
||||
|
||||
### Write clear failure messages
|
||||
|
||||
When a test fails, the failure message should tell you what went wrong without reading the test source.
|
||||
|
||||
```go
|
||||
// Good: failure message explains the context.
|
||||
assert.Equal(t, expected, actual, "discount should be applied to order total")
|
||||
|
||||
// Bad: failure message is just the default.
|
||||
assert.Equal(t, expected, actual)
|
||||
```
|
||||
|
||||
Use `require` for preconditions that must hold for the rest of the test to make sense. Use `assert` for the actual verifications. This avoids cascading failures from a single root cause.
|
||||
|
||||
## How to recognize a bad test
|
||||
|
||||
A bad test costs more to maintain than the bugs it prevents. Learning to identify bad tests is as important as learning to write good ones. Always evaluate a test critically before commiting it.
|
||||
|
||||
### Tests that duplicate the implementation
|
||||
|
||||
If a test contains the same logic as the code it tests, it verifies nothing. It will pass when the code is wrong in the same way the test is wrong, and it will break whenever the code changes even if the change is correct.
|
||||
|
||||
A common form: mocking a database, setting up canned rows, calling a function that queries and scans those rows, then asserting that the function returned exactly those rows. The test encodes the query, the row structure, and the scan logic. The same things the production code does. If the function has no branching logic beyond "query and scan," this test is a mirror of the implementation, not a check on it. An integration test against a real database verifies the actual behavior; the mock-based test verifies that the code matches the test author's expectations of the code.
|
||||
|
||||
### Tests for functions with no interesting logic
|
||||
|
||||
Not every function needs a test. A function that prepares a query, sends it, and scans the result has no branching, no edge cases, and no logic that could be wrong independently of the query being correct. Unit-testing it means mocking the database, which means the test does not verify the query works. It only verifies the function calls the mock in the expected way.
|
||||
|
||||
Ask: **what bug would this test catch that would not be caught by the integration test or by the tests of the calling code?** If the answer is nothing, skip the unit test. A missing test is better than a test that provides false confidence.
|
||||
|
||||
### Tests that rebuild the dependency boundary
|
||||
|
||||
When a test creates an in-package mock of an external interface (database driver, HTTP client, file system) and that mock contains non-trivial logic (reflection-based scanning, response simulation, state machines), the test is now testing its own mock as much as the production code. Bugs in the mock produce false passes or false failures, and the mock must be maintained alongside the real dependency.
|
||||
|
||||
If the mock is complex enough to have its own bugs, you have rebuilt the dependency boundary rather than testing against it. Use the real dependency (via integration test) or use a well-maintained fake provided by the dependency's authors.
|
||||
|
||||
### Tests that exist for coverage
|
||||
|
||||
A test that exercises a function without meaningfully verifying its output adds coverage without adding confidence. Calling a type-conversion function with every numeric type and asserting it does not panic covers lines but does not catch regressions. The function would need to be rewritten to fail, and any such rewrite would be caught by the callers' tests.
|
||||
|
||||
Before writing a test, identify the specific failure mode it guards against. If you cannot name one, the test is not worth writing.
|
||||
|
||||
### Tests that test the language
|
||||
|
||||
Do not test that language type system, standard library, or well-known third-party libraries work correctly. Testing that `reflect.Kind` returns the right value for each type, that pointer dereferencing works, or that a type switch dispatches correctly adds maintenance burden without catching any plausible bug in your code.
|
||||
|
||||
## Brittle tests
|
||||
|
||||
A brittle test is one that fails when production code changes without an actual bug being introduced. Brittle tests are expensive: they slow down development, train people to ignore failures, and provide no real safety net. Common sources of brittleness:
|
||||
|
||||
- **Asserting on implementation details**: Verifying which internal methods were called, in what order, or with what intermediate values. If the method is renamed or the order changes but the output is the same, the test breaks for no reason.
|
||||
- **Asserting on serialized representations when the format is not the contract**: Matching exact SQL strings, JSON output, or log messages produced by a function whose job is not to produce that format.
|
||||
- **Over-constrained mocks**: Setting up a mock that expects specific arguments in a specific sequence. Any refactoring of the call pattern breaks the mock setup even if behavior is preserved.
|
||||
- **Shared mutable state**: Tests that depend on data left behind by other tests. A change in execution order or a new test case causes unrelated failures.
|
||||
- **Time-dependence**: Tests that use `time.Now()`, `time.Sleep()`, or real timers. These produce flaky results and break under load.
|
||||
|
||||
When you encounter a brittle test, fix or delete it. Do not work around it.
|
||||
|
||||
## DAMP
|
||||
|
||||
Test code should prioritize clarity (DAMP: Descriptive And Meaningful Phrases).
|
||||
|
||||
```go
|
||||
// DAMP: each test is self-contained and readable.
|
||||
func TestCreateUser(t *testing.T) {
|
||||
user := User{Name: "Alice", Email: "alice@example.com"}
|
||||
err := store.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCreateDuplicateUser(t *testing.T) {
|
||||
user := User{Name: "Alice", Email: "alice@example.com"}
|
||||
_ = store.Create(ctx, user)
|
||||
err := store.Create(ctx, user)
|
||||
assert.ErrorIs(t, err, ErrAlreadyExists)
|
||||
}
|
||||
```
|
||||
|
||||
Shared setup helpers are fine for constructing objects with sensible defaults. But each test should explicitly set the values it depends on rather than relying on hidden defaults in a shared fixture.
|
||||
|
||||
## Flaky tests
|
||||
|
||||
A flaky test is one that sometimes passes and sometimes fails without any code change. Flaky tests erode trust in the entire suite. Once people learn to re-run and ignore failures, real bugs slip through.
|
||||
|
||||
Common causes and fixes:
|
||||
|
||||
- **Timing and sleeps**: Replace `time.Sleep` with channels, condition variables, or polling with a timeout.
|
||||
- **Uncontrolled concurrency**: Use deterministic synchronization rather than relying on goroutine scheduling.
|
||||
- **Shared state between tests**: Each test should set up and tear down its own state.
|
||||
|
||||
If a test is flaky and you cannot fix the root cause quickly, skip or delete it. A skipped test with an explanation is better than a flaky test that trains everyone to ignore red builds.
|
||||
|
||||
## Code coverage
|
||||
|
||||
Code coverage measures which lines were executed, not whether the code is correct. A function that is called but whose output is never checked has 100% coverage and 0% verification.
|
||||
|
||||
Do not use coverage as a target to hit. Use it as a tool to find gaps such as untested error paths, unreachable branches, dead code. A codebase with 60% meaningful coverage is better than one with 95% coverage achieved by testing trivial getters.
|
||||
|
||||
## Tests are code
|
||||
|
||||
Tests must be maintained and they are not second-class citizen. You should apply the same standards for readability, naming, and structure that you apply to production code. We do not tolerate complexity in tests just because they are tests.
|
||||
|
||||
However, tests should be simpler than production code. If a test requires its own helper library, complex setup, or nested control flow, step back and ask whether you are testing the right thing at the right level. This is not a blanket rule but a prompt to pause, assess the situation, and check whether the complexity is justified.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- If refactoring internals breaks your test but no behavior changed, the test is likely bad. Delete it or consider updating it.
|
||||
- Test what the code does, not how it does it. Verify outputs and state, not method calls.
|
||||
- Output format is behavior when the function's job is to produce that format. It is not behavior when the function uses it internally.
|
||||
- Ask what specific bug this test catches. If you cannot name one, do not write it.
|
||||
- Always evaluate whether the test adds confidence, not just lines.
|
||||
- One behavior per test. Name it after the scenario, not the function.
|
||||
- No logic in tests. Straight-line code only.
|
||||
- Flaky tests are not acceptable. Fix the root cause or nuke the test code.
|
||||
- Coverage measures execution, not correctness.
|
||||
|
||||
## Mandatory reading
|
||||
|
||||
- What to look for in a code review: Tests - https://google.github.io/eng-practices/review/reviewer/looking-for.html#tests
|
||||
- Testing Overview - https://abseil.io/resources/swe-book/html/ch11.html
|
||||
- Unit Testing - https://abseil.io/resources/swe-book/html/ch12.html
|
||||
- Test Doubles - https://abseil.io/resources/swe-book/html/ch13.html
|
||||
- Larger Testing - https://abseil.io/resources/swe-book/html/ch14.html
|
||||
@@ -24,7 +24,7 @@ The configuration file is a JSON array containing data source objects. Each obje
|
||||
| `label` | `string` | Display name shown to users (e.g., `"AWS EC2"`) |
|
||||
| `tags` | `string[]` | Array of category tags for grouping (e.g., `["AWS"]`, `["database"]`) |
|
||||
| `module` | `string` | Destination module after onboarding completion |
|
||||
| `imgUrl` | `string` | Path to the logo/icon (e.g., `"/Logos/ec2.svg"`) |
|
||||
| `imgUrl` | `string` | Path to the logo/icon **(SVG required)** (e.g., `"/Logos/ec2.svg"`) |
|
||||
|
||||
### Optional Keys
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package oidccallbackauthn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
@@ -19,25 +20,27 @@ const (
|
||||
redirectPath string = "/api/v1/complete/oidc"
|
||||
)
|
||||
|
||||
var (
|
||||
scopes []string = []string{"email", oidc.ScopeOpenID}
|
||||
)
|
||||
var defaultScopes []string = []string{"email", "profile", oidc.ScopeOpenID}
|
||||
|
||||
var _ authn.CallbackAuthN = (*AuthN)(nil)
|
||||
|
||||
type AuthN struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
store authtypes.AuthNStore
|
||||
licensing licensing.Licensing
|
||||
httpClient *client.Client
|
||||
}
|
||||
|
||||
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings) (*AuthN, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn")
|
||||
|
||||
httpClient, err := client.New(providerSettings.Logger, providerSettings.TracerProvider, providerSettings.MeterProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AuthN{
|
||||
settings: settings,
|
||||
store: store,
|
||||
licensing: licensing,
|
||||
httpClient: httpClient,
|
||||
@@ -126,7 +129,40 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
}
|
||||
}
|
||||
|
||||
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
|
||||
name := ""
|
||||
if nameClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Name; nameClaim != "" {
|
||||
if n, ok := claims[nameClaim].(string); ok {
|
||||
name = n
|
||||
}
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if groupsClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Groups; groupsClaim != "" {
|
||||
if claimValue, exists := claims[groupsClaim]; exists {
|
||||
switch g := claimValue.(type) {
|
||||
case []any:
|
||||
for _, group := range g {
|
||||
if gs, ok := group.(string); ok {
|
||||
groups = append(groups, gs)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
// Some IDPs return a single group as a string instead of an array
|
||||
groups = append(groups, g)
|
||||
default:
|
||||
a.settings.Logger().WarnContext(ctx, "oidc: unsupported groups type", "type", fmt.Sprintf("%T", claimValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
role := ""
|
||||
if roleClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Role; roleClaim != "" {
|
||||
if r, ok := claims[roleClaim].(string); ok {
|
||||
role = r
|
||||
}
|
||||
}
|
||||
|
||||
return authtypes.NewCallbackIdentity(name, email, authDomain.StorableAuthDomain().OrgID, state, groups, role), nil
|
||||
}
|
||||
|
||||
func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDomain) *authtypes.AuthNProviderInfo {
|
||||
@@ -145,6 +181,13 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
scopes := make([]string, len(defaultScopes))
|
||||
copy(scopes, defaultScopes)
|
||||
|
||||
if authDomain.AuthDomainConfig().RoleMapping != nil && len(authDomain.AuthDomainConfig().RoleMapping.GroupMappings) > 0 {
|
||||
scopes = append(scopes, "groups")
|
||||
}
|
||||
|
||||
return oidcProvider, &oauth2.Config{
|
||||
ClientID: authDomain.AuthDomainConfig().OIDC.ClientID,
|
||||
ClientSecret: authDomain.AuthDomainConfig().OIDC.ClientSecret,
|
||||
|
||||
@@ -96,7 +96,26 @@ func (a *AuthN) HandleCallback(ctx context.Context, formValues url.Values) (*aut
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "saml: invalid email").WithAdditional("The nameID assertion is used to retrieve the email address, please check your IDP configuration and try again.")
|
||||
}
|
||||
|
||||
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
|
||||
name := ""
|
||||
if nameAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Name; nameAttribute != "" {
|
||||
if val := assertionInfo.Values.Get(nameAttribute); val != "" {
|
||||
name = val
|
||||
}
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if groupAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Groups; groupAttribute != "" {
|
||||
groups = assertionInfo.Values.GetAll(groupAttribute)
|
||||
}
|
||||
|
||||
role := ""
|
||||
if roleAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Role; roleAttribute != "" {
|
||||
if val := assertionInfo.Values.Get(roleAttribute); val != "" {
|
||||
role = val
|
||||
}
|
||||
}
|
||||
|
||||
return authtypes.NewCallbackIdentity(name, email, authDomain.StorableAuthDomain().OrgID, state, groups, role), nil
|
||||
}
|
||||
|
||||
func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDomain) *authtypes.AuthNProviderInfo {
|
||||
|
||||
@@ -2,12 +2,18 @@ package openfgaauthz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaserver"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/authzstore/sqlauthzstore"
|
||||
pkgopenfgaauthz "github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/roletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
|
||||
@@ -15,84 +21,319 @@ import (
|
||||
|
||||
type provider struct {
|
||||
pkgAuthzService authz.AuthZ
|
||||
openfgaServer *openfgaserver.Server
|
||||
licensing licensing.Licensing
|
||||
store roletypes.Store
|
||||
registry []authz.RegisterTypeable
|
||||
}
|
||||
|
||||
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, licensing licensing.Licensing, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) {
|
||||
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema)
|
||||
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, licensing, registry)
|
||||
})
|
||||
}
|
||||
|
||||
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile) (authz.AuthZ, error) {
|
||||
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, licensing licensing.Licensing, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
|
||||
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema)
|
||||
pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
openfgaServer, err := openfgaserver.NewOpenfgaServer(ctx, pkgAuthzService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &provider{
|
||||
pkgAuthzService: pkgAuthzService,
|
||||
openfgaServer: openfgaServer,
|
||||
licensing: licensing,
|
||||
store: sqlauthzstore.NewSqlAuthzStore(sqlstore),
|
||||
registry: registry,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Start(ctx context.Context) error {
|
||||
return provider.pkgAuthzService.Start(ctx)
|
||||
return provider.openfgaServer.Start(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
return provider.pkgAuthzService.Stop(ctx)
|
||||
return provider.openfgaServer.Stop(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) Check(ctx context.Context, tuple *openfgav1.TupleKey) error {
|
||||
return provider.pkgAuthzService.Check(ctx, tuple)
|
||||
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
|
||||
return provider.openfgaServer.CheckWithTupleCreation(ctx, claims, orgID, relation, typeable, selectors, roleSelectors)
|
||||
}
|
||||
|
||||
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, _ authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error {
|
||||
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tuples, err := typeable.Tuples(subject, relation, selectors, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.BatchCheck(ctx, tuples)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
|
||||
return provider.openfgaServer.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, typeable, selectors, roleSelectors)
|
||||
}
|
||||
|
||||
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, _ authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error {
|
||||
subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tuples, err := typeable.Tuples(subject, relation, selectors, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.BatchCheck(ctx, tuples)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) BatchCheck(ctx context.Context, tuples []*openfgav1.TupleKey) error {
|
||||
return provider.pkgAuthzService.BatchCheck(ctx, tuples)
|
||||
func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*openfgav1.TupleKey) (map[string]*authtypes.TupleKeyAuthorization, error) {
|
||||
return provider.openfgaServer.BatchCheck(ctx, tupleReq)
|
||||
}
|
||||
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
|
||||
return provider.pkgAuthzService.ListObjects(ctx, subject, relation, typeable)
|
||||
return provider.openfgaServer.ListObjects(ctx, subject, relation, typeable)
|
||||
}
|
||||
|
||||
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
|
||||
return provider.pkgAuthzService.Write(ctx, additions, deletions)
|
||||
return provider.openfgaServer.Write(ctx, additions, deletions)
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
|
||||
return provider.pkgAuthzService.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
|
||||
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
|
||||
}
|
||||
|
||||
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
|
||||
return provider.pkgAuthzService.List(ctx, orgID)
|
||||
}
|
||||
|
||||
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
|
||||
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
|
||||
}
|
||||
|
||||
func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, name string, subject string) error {
|
||||
return provider.pkgAuthzService.Grant(ctx, orgID, name, subject)
|
||||
}
|
||||
|
||||
func (provider *provider) ModifyGrant(ctx context.Context, orgID valuer.UUID, existingRoleName string, updatedRoleName string, subject string) error {
|
||||
return provider.pkgAuthzService.ModifyGrant(ctx, orgID, existingRoleName, updatedRoleName, subject)
|
||||
}
|
||||
|
||||
func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, name string, subject string) error {
|
||||
return provider.pkgAuthzService.Revoke(ctx, orgID, name, subject)
|
||||
}
|
||||
|
||||
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*roletypes.Role) error {
|
||||
return provider.pkgAuthzService.CreateManagedRoles(ctx, orgID, managedRoles)
|
||||
}
|
||||
|
||||
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
|
||||
grantTuples, err := provider.getManagedRoleGrantTuples(orgID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tuples = append(tuples, grantTuples...)
|
||||
|
||||
managedRoleTuples, err := provider.getManagedRoleTransactionTuples(orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tuples = append(tuples, managedRoleTuples...)
|
||||
|
||||
return provider.Write(ctx, tuples, nil)
|
||||
}
|
||||
|
||||
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
|
||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
return provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
|
||||
}
|
||||
|
||||
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) {
|
||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
existingRole, err := provider.store.GetByOrgIDAndName(ctx, role.OrgID, role.Name)
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if existingRole != nil {
|
||||
return roletypes.NewRoleFromStorableRole(existingRole), nil
|
||||
}
|
||||
|
||||
err = provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
|
||||
typeables := make([]authtypes.Typeable, 0)
|
||||
for _, register := range provider.registry {
|
||||
typeables = append(typeables, register.MustGetTypeables()...)
|
||||
}
|
||||
|
||||
resources := make([]*authtypes.Resource, 0)
|
||||
for _, typeable := range typeables {
|
||||
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
|
||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
storableRole, err := provider.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objects := make([]*authtypes.Object, 0)
|
||||
for _, resource := range provider.GetResources(ctx) {
|
||||
if slices.Contains(authtypes.TypeableRelations[resource.Type], relation) {
|
||||
resourceObjects, err := provider.
|
||||
ListObjects(
|
||||
ctx,
|
||||
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.Name, orgID, &authtypes.RelationAssignee),
|
||||
relation,
|
||||
authtypes.MustNewTypeableFromType(resource.Type, resource.Name),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objects = append(objects, resourceObjects...)
|
||||
}
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
|
||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
return provider.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
|
||||
}
|
||||
|
||||
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
|
||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.Write(ctx, additionTuples, deletionTuples)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
storableRole, err := provider.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role := roletypes.NewRoleFromStorableRole(storableRole)
|
||||
err = role.ErrIfManaged()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return provider.store.Delete(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
|
||||
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
|
||||
}
|
||||
|
||||
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
tuples := []*openfgav1.TupleKey{}
|
||||
|
||||
// Grant the admin role to the user
|
||||
adminSubject := authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil)
|
||||
adminTuple, err := authtypes.TypeableRole.Tuples(
|
||||
adminSubject,
|
||||
authtypes.RelationAssignee,
|
||||
[]authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
|
||||
},
|
||||
orgID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, adminTuple...)
|
||||
|
||||
// Grant the admin role to the anonymous user
|
||||
anonymousSubject := authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
|
||||
anonymousTuple, err := authtypes.TypeableRole.Tuples(
|
||||
anonymousSubject,
|
||||
authtypes.RelationAssignee,
|
||||
[]authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAnonymousRoleName),
|
||||
},
|
||||
orgID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, anonymousTuple...)
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
transactionsByRole := make(map[string][]*authtypes.Transaction)
|
||||
for _, register := range provider.registry {
|
||||
for roleName, txns := range register.MustGetManagedRoleTransactions() {
|
||||
transactionsByRole[roleName] = append(transactionsByRole[roleName], txns...)
|
||||
}
|
||||
}
|
||||
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for roleName, transactions := range transactionsByRole {
|
||||
for _, txn := range transactions {
|
||||
typeable := authtypes.MustNewTypeableFromType(txn.Object.Resource.Type, txn.Object.Resource.Name)
|
||||
txnTuples, err := typeable.Tuples(
|
||||
authtypes.MustNewSubject(
|
||||
authtypes.TypeableRole,
|
||||
roleName,
|
||||
orgID,
|
||||
&authtypes.RelationAssignee,
|
||||
),
|
||||
txn.Relation,
|
||||
[]authtypes.Selector{txn.Object.Selector},
|
||||
orgID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, txnTuples...)
|
||||
}
|
||||
}
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
103
ee/authz/openfgaserver/server.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package openfgaserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
pkgAuthzService authz.AuthZ
|
||||
}
|
||||
|
||||
func NewOpenfgaServer(ctx context.Context, pkgAuthzService authz.AuthZ) (*Server, error) {
|
||||
|
||||
return &Server{
|
||||
pkgAuthzService: pkgAuthzService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (server *Server) Start(ctx context.Context) error {
|
||||
return server.pkgAuthzService.Start(ctx)
|
||||
}
|
||||
|
||||
func (server *Server) Stop(ctx context.Context) error {
|
||||
return server.pkgAuthzService.Stop(ctx)
|
||||
}
|
||||
|
||||
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
|
||||
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
|
||||
for idx, tuple := range tupleSlice {
|
||||
tuples[strconv.Itoa(idx)] = tuple
|
||||
}
|
||||
|
||||
response, err := server.BatchCheck(ctx, tuples)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resp := range response {
|
||||
if resp.Authorized {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
|
||||
}
|
||||
|
||||
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
|
||||
subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
|
||||
for idx, tuple := range tupleSlice {
|
||||
tuples[strconv.Itoa(idx)] = tuple
|
||||
}
|
||||
|
||||
response, err := server.BatchCheck(ctx, tuples)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resp := range response {
|
||||
if resp.Authorized {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
|
||||
}
|
||||
|
||||
func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openfgav1.TupleKey) (map[string]*authtypes.TupleKeyAuthorization, error) {
|
||||
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
|
||||
}
|
||||
|
||||
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
|
||||
return server.pkgAuthzService.ListObjects(ctx, subject, relation, typeable)
|
||||
}
|
||||
|
||||
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
|
||||
return server.pkgAuthzService.Write(ctx, additions, deletions)
|
||||
}
|
||||
282
ee/gateway/httpgateway/provider.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package httpgateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/http/client"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/gatewaytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
config gateway.Config
|
||||
httpClient *client.Client
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func NewProviderFactory(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, ps factory.ProviderSettings, c gateway.Config) (gateway.Gateway, error) {
|
||||
return New(ctx, ps, c, licensing)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config gateway.Config, licensing licensing.Licensing) (gateway.Gateway, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/gateway/httpgateway")
|
||||
|
||||
httpClient, err := client.New(
|
||||
settings.Logger(),
|
||||
providerSettings.TracerProvider,
|
||||
providerSettings.MeterProvider,
|
||||
client.WithRequestResponseLog(true),
|
||||
client.WithRetryCount(3),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
settings: settings,
|
||||
config: config,
|
||||
httpClient: httpClient,
|
||||
licensing: licensing,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetIngestionKeys(ctx context.Context, orgID valuer.UUID, page, perPage int) (*gatewaytypes.GettableIngestionKeys, error) {
|
||||
qParams := url.Values{}
|
||||
qParams.Add("page", strconv.Itoa(page))
|
||||
qParams.Add("per_page", strconv.Itoa(perPage))
|
||||
|
||||
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys", qParams, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ingestionKeys []gatewaytypes.IngestionKey
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &ingestionKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pagination gatewaytypes.Pagination
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "_pagination").String()), &pagination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gatewaytypes.GettableIngestionKeys{
|
||||
Keys: ingestionKeys,
|
||||
Pagination: pagination,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) SearchIngestionKeysByName(ctx context.Context, orgID valuer.UUID, name string, page, perPage int) (*gatewaytypes.GettableIngestionKeys, error) {
|
||||
qParams := url.Values{}
|
||||
qParams.Add("name", name)
|
||||
qParams.Add("page", strconv.Itoa(page))
|
||||
qParams.Add("per_page", strconv.Itoa(perPage))
|
||||
|
||||
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys/search", qParams, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ingestionKeys []gatewaytypes.IngestionKey
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &ingestionKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pagination gatewaytypes.Pagination
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "_pagination").String()), &pagination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gatewaytypes.GettableIngestionKeys{
|
||||
Keys: ingestionKeys,
|
||||
Pagination: pagination,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) CreateIngestionKey(ctx context.Context, orgID valuer.UUID, name string, tags []string, expiresAt time.Time) (*gatewaytypes.GettableCreatedIngestionKey, error) {
|
||||
requestBody := gatewaytypes.PostableIngestionKey{
|
||||
Name: name,
|
||||
Tags: tags,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
requestBodyBytes, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseBody, err := provider.do(ctx, orgID, http.MethodPost, "/v1/workspaces/me/keys", nil, requestBodyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var createdKeyResponse gatewaytypes.GettableCreatedIngestionKey
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &createdKeyResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &createdKeyResponse, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) UpdateIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string, name string, tags []string, expiresAt time.Time) error {
|
||||
requestBody := gatewaytypes.PostableIngestionKey{
|
||||
Name: name,
|
||||
Tags: tags,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
requestBodyBytes, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = provider.do(ctx, orgID, http.MethodPatch, "/v1/workspaces/me/keys/"+keyID, nil, requestBodyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) DeleteIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string) error {
|
||||
_, err := provider.do(ctx, orgID, http.MethodDelete, "/v1/workspaces/me/keys/"+keyID, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) CreateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, keyID string, signal string, limitConfig gatewaytypes.LimitConfig, tags []string) (*gatewaytypes.GettableCreatedIngestionKeyLimit, error) {
|
||||
requestBody := gatewaytypes.PostableIngestionKeyLimit{
|
||||
Signal: signal,
|
||||
Config: limitConfig,
|
||||
Tags: tags,
|
||||
}
|
||||
requestBodyBytes, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseBody, err := provider.do(ctx, orgID, http.MethodPost, "/v1/workspaces/me/keys/"+keyID+"/limits", nil, requestBodyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var createdIngestionKeyLimitResponse gatewaytypes.GettableCreatedIngestionKeyLimit
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &createdIngestionKeyLimitResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &createdIngestionKeyLimitResponse, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) UpdateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string, limitConfig gatewaytypes.LimitConfig, tags []string) error {
|
||||
requestBody := gatewaytypes.UpdatableIngestionKeyLimit{
|
||||
Config: limitConfig,
|
||||
Tags: tags,
|
||||
}
|
||||
requestBodyBytes, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = provider.do(ctx, orgID, http.MethodPatch, "/v1/workspaces/me/limits/"+limitID, nil, requestBodyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) DeleteIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string) error {
|
||||
_, err := provider.do(ctx, orgID, http.MethodDelete, "/v1/workspaces/me/limits/"+limitID, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) do(ctx context.Context, orgID valuer.UUID, method string, path string, queryParams url.Values, body []byte) ([]byte, error) {
|
||||
license, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "no valid license found").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
// build url
|
||||
requestURL := provider.config.URL.JoinPath(path)
|
||||
|
||||
// add query params to the url
|
||||
if queryParams != nil {
|
||||
requestURL.RawQuery = queryParams.Encode()
|
||||
}
|
||||
|
||||
// build request
|
||||
request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add headers needed to call gateway
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("X-Signoz-Cloud-Api-Key", license.Key)
|
||||
request.Header.Set("X-Consumer-Username", "lid:00000000-0000-0000-0000-000000000000")
|
||||
request.Header.Set("X-Consumer-Groups", "ns:default")
|
||||
|
||||
// execute request
|
||||
response, err := provider.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read response
|
||||
defer response.Body.Close()
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// only 2XX
|
||||
if response.StatusCode/100 == 2 {
|
||||
return responseBody, nil
|
||||
}
|
||||
|
||||
errorMessage := gjson.GetBytes(responseBody, "error").String()
|
||||
if errorMessage == "" {
|
||||
errorMessage = "an unknown error occurred"
|
||||
}
|
||||
|
||||
// return error for non 2XX
|
||||
return nil, provider.errFromStatusCode(response.StatusCode, errorMessage)
|
||||
}
|
||||
|
||||
func (provider *Provider) errFromStatusCode(code int, errorMessage string) error {
|
||||
switch code {
|
||||
case http.StatusBadRequest:
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, errorMessage)
|
||||
case http.StatusUnauthorized:
|
||||
return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, errorMessage)
|
||||
case http.StatusForbidden:
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, errorMessage)
|
||||
case http.StatusNotFound:
|
||||
return errors.New(errors.TypeNotFound, errors.CodeNotFound, errorMessage)
|
||||
case http.StatusConflict:
|
||||
return errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, errorMessage)
|
||||
}
|
||||
|
||||
return errors.New(errors.TypeInternal, errors.CodeInternal, errorMessage)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/role"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -26,12 +25,11 @@ type module struct {
|
||||
pkgDashboardModule dashboard.Module
|
||||
store dashboardtypes.Store
|
||||
settings factory.ScopedProviderSettings
|
||||
role role.Module
|
||||
querier querier.Querier
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
|
||||
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
|
||||
|
||||
@@ -39,7 +37,6 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
|
||||
pkgDashboardModule: pkgDashboardModule,
|
||||
store: store,
|
||||
settings: scopedProviderSettings,
|
||||
role: role,
|
||||
querier: querier,
|
||||
licensing: licensing,
|
||||
}
|
||||
@@ -59,29 +56,6 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.Newf(errors.TypeAlreadyExists, dashboardtypes.ErrCodePublicDashboardAlreadyExists, "dashboard with id %s is already public", storablePublicDashboard.DashboardID)
|
||||
}
|
||||
|
||||
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.role.Assign(ctx, role.ID, orgID, authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.StringValue(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
additionObject := authtypes.MustNewObject(
|
||||
authtypes.Resource{
|
||||
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
|
||||
Type: authtypes.TypeMetaResource,
|
||||
},
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
|
||||
)
|
||||
|
||||
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.CreatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -126,6 +100,7 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
|
||||
|
||||
return []authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, id.StringValue()),
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, authtypes.WildCardSelectorString),
|
||||
}, storableDashboard.OrgID, nil
|
||||
}
|
||||
|
||||
@@ -188,29 +163,6 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
publicDashboard, err := module.GetPublic(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletionObject := authtypes.MustNewObject(
|
||||
authtypes.Resource{
|
||||
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
|
||||
Type: authtypes.TypeMetaResource,
|
||||
},
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
|
||||
)
|
||||
|
||||
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -248,10 +200,6 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, m
|
||||
return module.pkgDashboardModule.GetByMetricNames(ctx, orgID, metricNames)
|
||||
}
|
||||
|
||||
func (module *module) MustGetTypeables() []authtypes.Typeable {
|
||||
return module.pkgDashboardModule.MustGetTypeables()
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.List(ctx, orgID)
|
||||
}
|
||||
@@ -264,34 +212,28 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, role, lock)
|
||||
}
|
||||
|
||||
func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
|
||||
publicDashboard, err := module.store.GetPublic(ctx, dashboardID.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletionObject := authtypes.MustNewObject(
|
||||
authtypes.Resource{
|
||||
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
|
||||
Type: authtypes.TypeMetaResource,
|
||||
},
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
|
||||
)
|
||||
|
||||
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func (module *module) MustGetTypeables() []authtypes.Typeable {
|
||||
return module.pkgDashboardModule.MustGetTypeables()
|
||||
}
|
||||
|
||||
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
|
||||
return map[string][]*authtypes.Transaction{
|
||||
roletypes.SigNozAnonymousRoleName: {
|
||||
{
|
||||
ID: valuer.GenerateUUID(),
|
||||
Relation: authtypes.RelationRead,
|
||||
Object: *authtypes.MustNewObject(
|
||||
authtypes.Resource{
|
||||
Type: authtypes.TypeMetaResource,
|
||||
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
|
||||
},
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, "*"),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (module *module) deletePublic(ctx context.Context, _ valuer.UUID, dashboardID valuer.UUID) error {
|
||||
return module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
}
|
||||
|
||||
178
ee/querier/handler.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package querier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
set factory.ProviderSettings
|
||||
querier querier.Querier
|
||||
community querier.Handler
|
||||
}
|
||||
|
||||
func NewHandler(set factory.ProviderSettings, querier querier.Querier, community querier.Handler) querier.Handler {
|
||||
return &handler{
|
||||
set: set,
|
||||
querier: querier,
|
||||
community: community,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
|
||||
bodyBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to read request body: %v", err))
|
||||
return
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
ctx := req.Context()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var queryRangeRequest qbtypes.QueryRangeRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&queryRangeRequest); err != nil {
|
||||
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to decode request body: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stackTrace := string(debug.Stack())
|
||||
|
||||
queryJSON, _ := json.Marshal(queryRangeRequest)
|
||||
|
||||
h.set.Logger.ErrorContext(ctx, "panic in QueryRange",
|
||||
"error", r,
|
||||
"user", claims.UserID,
|
||||
"payload", string(queryJSON),
|
||||
"stacktrace", stackTrace,
|
||||
)
|
||||
|
||||
render.Error(rw, errors.NewInternalf(
|
||||
errors.CodeInternal,
|
||||
"Something went wrong on our end. It's not you, it's us. Our team is notified about it. Reach out to support if issue persists.",
|
||||
))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if anomalyQuery, ok := queryRangeRequest.IsAnomalyRequest(); ok {
|
||||
anomalies, err := h.handleAnomalyQuery(ctx, orgID, anomalyQuery, queryRangeRequest)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.NewInternalf(errors.CodeInternal, "failed to get anomalies: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
results := []any{}
|
||||
for _, item := range anomalies.Results {
|
||||
results = append(results, item)
|
||||
}
|
||||
|
||||
// Build step intervals from the anomaly query
|
||||
stepIntervals := make(map[string]uint64)
|
||||
if anomalyQuery.StepInterval.Duration > 0 {
|
||||
stepIntervals[anomalyQuery.Name] = uint64(anomalyQuery.StepInterval.Duration.Seconds())
|
||||
}
|
||||
|
||||
finalResp := &qbtypes.QueryRangeResponse{
|
||||
Type: queryRangeRequest.RequestType,
|
||||
Data: qbtypes.QueryData{
|
||||
Results: results,
|
||||
},
|
||||
Meta: qbtypes.ExecStats{
|
||||
StepIntervals: stepIntervals,
|
||||
},
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, finalResp)
|
||||
return
|
||||
}
|
||||
|
||||
// regular query range request, delegate to community handler
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
h.community.QueryRange(rw, req)
|
||||
}
|
||||
|
||||
func (h *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request) {
|
||||
h.community.QueryRawStream(rw, req)
|
||||
}
|
||||
|
||||
func (h *handler) ReplaceVariables(rw http.ResponseWriter, req *http.Request) {
|
||||
h.community.ReplaceVariables(rw, req)
|
||||
}
|
||||
|
||||
func extractSeasonality(anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]) anomalyV2.Seasonality {
|
||||
for _, fn := range anomalyQuery.Functions {
|
||||
if fn.Name == qbtypes.FunctionNameAnomaly {
|
||||
for _, arg := range fn.Args {
|
||||
if arg.Name == "seasonality" {
|
||||
if seasonalityStr, ok := arg.Value.(string); ok {
|
||||
switch seasonalityStr {
|
||||
case "weekly":
|
||||
return anomalyV2.SeasonalityWeekly
|
||||
case "hourly":
|
||||
return anomalyV2.SeasonalityHourly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return anomalyV2.SeasonalityDaily // default
|
||||
}
|
||||
|
||||
func (h *handler) createAnomalyProvider(seasonality anomalyV2.Seasonality) anomalyV2.Provider {
|
||||
switch seasonality {
|
||||
case anomalyV2.SeasonalityWeekly:
|
||||
return anomalyV2.NewWeeklyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.WeeklyProvider](h.querier),
|
||||
anomalyV2.WithLogger[*anomalyV2.WeeklyProvider](h.set.Logger),
|
||||
)
|
||||
case anomalyV2.SeasonalityHourly:
|
||||
return anomalyV2.NewHourlyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.HourlyProvider](h.querier),
|
||||
anomalyV2.WithLogger[*anomalyV2.HourlyProvider](h.set.Logger),
|
||||
)
|
||||
default:
|
||||
return anomalyV2.NewDailyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.DailyProvider](h.querier),
|
||||
anomalyV2.WithLogger[*anomalyV2.DailyProvider](h.set.Logger),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
|
||||
seasonality := extractSeasonality(anomalyQuery)
|
||||
provider := h.createAnomalyProvider(seasonality)
|
||||
|
||||
return provider.GetAnomalies(ctx, orgID, &anomalyV2.AnomaliesRequest{Params: queryRangeRequest})
|
||||
}
|
||||
@@ -50,7 +50,7 @@ type GetAnomaliesResponse struct {
|
||||
//
|
||||
// ^ ^
|
||||
// | |
|
||||
// (rounded value for past peiod) + (seasonal growth)
|
||||
// (rounded value for past period) + (seasonal growth)
|
||||
//
|
||||
// score = abs(value - prediction) / stddev (current_season_query)
|
||||
type anomalyQueryParams struct {
|
||||
@@ -74,12 +74,12 @@ type anomalyQueryParams struct {
|
||||
// : For daily seasonality, this is the query range params for the (now-2d-5m, now-1d)
|
||||
// : For hourly seasonality, this is the query range params for the (now-2h-5m, now-1h)
|
||||
PastSeasonQuery *v3.QueryRangeParamsV3
|
||||
// Past2SeasonQuery is the query range params for past 2 seasonal period to the current season
|
||||
// Past2SeasonQuery is the query range params for past 2 seasonal periods to the current season
|
||||
// Example: For weekly seasonality, this is the query range params for the (now-3w-5m, now-2w)
|
||||
// : For daily seasonality, this is the query range params for the (now-3d-5m, now-2d)
|
||||
// : For hourly seasonality, this is the query range params for the (now-3h-5m, now-2h)
|
||||
Past2SeasonQuery *v3.QueryRangeParamsV3
|
||||
// Past3SeasonQuery is the query range params for past 3 seasonal period to the current season
|
||||
// Past3SeasonQuery is the query range params for past 3 seasonal periods to the current season
|
||||
// Example: For weekly seasonality, this is the query range params for the (now-4w-5m, now-3w)
|
||||
// : For daily seasonality, this is the query range params for the (now-4d-5m, now-3d)
|
||||
// : For hourly seasonality, this is the query range params for the (now-4h-5m, now-3h)
|
||||
|
||||
@@ -2,16 +2,13 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
querierAPI "github.com/SigNoz/signoz/pkg/querier"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
@@ -32,10 +29,10 @@ type APIHandlerOptions struct {
|
||||
IntegrationsController *integrations.Controller
|
||||
CloudIntegrationsController *cloudintegrations.Controller
|
||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||
Gateway *httputil.ReverseProxy
|
||||
GatewayUrl string
|
||||
// Querier Influx Interval
|
||||
FluxInterval time.Duration
|
||||
GlobalConfig global.Config
|
||||
}
|
||||
|
||||
type APIHandler struct {
|
||||
@@ -44,7 +41,7 @@ type APIHandler struct {
|
||||
}
|
||||
|
||||
// NewAPIHandler returns an APIHandler
|
||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
|
||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.Config) (*APIHandler, error) {
|
||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||
Reader: opts.DataConnector,
|
||||
RuleManager: opts.RulesManager,
|
||||
@@ -54,11 +51,9 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
})
|
||||
}, config)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -79,10 +74,6 @@ func (ah *APIHandler) UM() *usage.Manager {
|
||||
return ah.opts.UsageManager
|
||||
}
|
||||
|
||||
func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
|
||||
return ah.opts.Gateway
|
||||
}
|
||||
|
||||
// RegisterRoutes registers routes for this handler on the given router
|
||||
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
// note: add ee override methods first
|
||||
@@ -105,14 +96,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
// v4
|
||||
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
||||
|
||||
// v5
|
||||
router.HandleFunc("/api/v5/query_range", am.ViewAccess(ah.queryRangeV5)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v5/substitute_vars", am.ViewAccess(ah.QuerierAPI.ReplaceVariables)).Methods(http.MethodPost)
|
||||
|
||||
// Gateway
|
||||
router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.EditAccess(ah.ServeGatewayHTTP))
|
||||
|
||||
ah.APIHandler.RegisterRoutes(router, am)
|
||||
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
ingestionUrl, signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
|
||||
signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
|
||||
if apiErr != nil {
|
||||
RespondError(w, basemodel.WrapApiError(
|
||||
apiErr, "couldn't deduce ingestion url and signoz api url",
|
||||
@@ -84,7 +84,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
result.IngestionUrl = ingestionUrl
|
||||
result.IngestionUrl = ah.opts.GlobalConfig.IngestionURL.String()
|
||||
result.SigNozAPIUrl = signozApiUrl
|
||||
|
||||
gatewayUrl := ah.opts.GatewayUrl
|
||||
@@ -186,7 +186,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||
string, string, *basemodel.ApiError,
|
||||
string, *basemodel.ApiError,
|
||||
) {
|
||||
// TODO: remove this struct from here
|
||||
type deploymentResponse struct {
|
||||
@@ -200,7 +200,7 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
|
||||
|
||||
respBytes, err := ah.Signoz.Zeus.GetDeployment(ctx, licenseKey)
|
||||
if err != nil {
|
||||
return "", "", basemodel.InternalError(fmt.Errorf(
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't query for deployment info: error: %w", err,
|
||||
))
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
|
||||
|
||||
err = json.Unmarshal(respBytes, resp)
|
||||
if err != nil {
|
||||
return "", "", basemodel.InternalError(fmt.Errorf(
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't unmarshal deployment info response: error: %w", err,
|
||||
))
|
||||
}
|
||||
@@ -219,16 +219,14 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
|
||||
|
||||
if len(regionDns) < 1 || len(deploymentName) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", "", basemodel.InternalError(fmt.Errorf(
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"deployment info response not in expected shape. couldn't determine region dns and deployment name",
|
||||
))
|
||||
}
|
||||
|
||||
ingestionUrl := fmt.Sprintf("https://ingest.%s", regionDns)
|
||||
|
||||
signozApiUrl := fmt.Sprintf("https://%s.%s", deploymentName, regionDns)
|
||||
|
||||
return ingestionUrl, signozApiUrl, nil
|
||||
return signozApiUrl, nil
|
||||
}
|
||||
|
||||
type ingestionKey struct {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
validPath := false
|
||||
for _, allowedPrefix := range gateway.AllowedPrefix {
|
||||
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
|
||||
validPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validPath {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
//Create headers
|
||||
var licenseKey string
|
||||
if license != nil {
|
||||
licenseKey = license.Key
|
||||
}
|
||||
|
||||
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
|
||||
req.Header.Set("X-Consumer-Username", "lid:00000000-0000-0000-0000-000000000000")
|
||||
req.Header.Set("X-Consumer-Groups", "ns:default")
|
||||
|
||||
ah.Gateway().ServeHTTP(rw, req)
|
||||
}
|
||||
@@ -2,16 +2,11 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
|
||||
@@ -20,8 +15,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"go.uber.org/zap"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -144,140 +137,3 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func extractSeasonality(anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]) anomalyV2.Seasonality {
|
||||
for _, fn := range anomalyQuery.Functions {
|
||||
if fn.Name == qbtypes.FunctionNameAnomaly {
|
||||
for _, arg := range fn.Args {
|
||||
if arg.Name == "seasonality" {
|
||||
if seasonalityStr, ok := arg.Value.(string); ok {
|
||||
switch seasonalityStr {
|
||||
case "weekly":
|
||||
return anomalyV2.SeasonalityWeekly
|
||||
case "hourly":
|
||||
return anomalyV2.SeasonalityHourly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return anomalyV2.SeasonalityDaily // default
|
||||
}
|
||||
|
||||
func createAnomalyProvider(aH *APIHandler, seasonality anomalyV2.Seasonality) anomalyV2.Provider {
|
||||
switch seasonality {
|
||||
case anomalyV2.SeasonalityWeekly:
|
||||
return anomalyV2.NewWeeklyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.WeeklyProvider](aH.Signoz.Querier),
|
||||
anomalyV2.WithLogger[*anomalyV2.WeeklyProvider](aH.Signoz.Instrumentation.Logger()),
|
||||
)
|
||||
case anomalyV2.SeasonalityHourly:
|
||||
return anomalyV2.NewHourlyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.HourlyProvider](aH.Signoz.Querier),
|
||||
anomalyV2.WithLogger[*anomalyV2.HourlyProvider](aH.Signoz.Instrumentation.Logger()),
|
||||
)
|
||||
default:
|
||||
return anomalyV2.NewDailyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.DailyProvider](aH.Signoz.Querier),
|
||||
anomalyV2.WithLogger[*anomalyV2.DailyProvider](aH.Signoz.Instrumentation.Logger()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (aH *APIHandler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
|
||||
seasonality := extractSeasonality(anomalyQuery)
|
||||
provider := createAnomalyProvider(aH, seasonality)
|
||||
|
||||
return provider.GetAnomalies(ctx, orgID, &anomalyV2.AnomaliesRequest{Params: queryRangeRequest})
|
||||
}
|
||||
|
||||
func (aH *APIHandler) queryRangeV5(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
bodyBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to read request body: %v", err))
|
||||
return
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
ctx := req.Context()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var queryRangeRequest qbtypes.QueryRangeRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&queryRangeRequest); err != nil {
|
||||
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to decode request body: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stackTrace := string(debug.Stack())
|
||||
|
||||
queryJSON, _ := json.Marshal(queryRangeRequest)
|
||||
|
||||
aH.Signoz.Instrumentation.Logger().ErrorContext(ctx, "panic in QueryRange",
|
||||
"error", r,
|
||||
"user", claims.UserID,
|
||||
"payload", string(queryJSON),
|
||||
"stacktrace", stackTrace,
|
||||
)
|
||||
|
||||
render.Error(rw, errors.NewInternalf(
|
||||
errors.CodeInternal,
|
||||
"Something went wrong on our end. It's not you, it's us. Our team is notified about it. Reach out to support if issue persists.",
|
||||
))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if anomalyQuery, ok := queryRangeRequest.IsAnomalyRequest(); ok {
|
||||
anomalies, err := aH.handleAnomalyQuery(ctx, orgID, anomalyQuery, queryRangeRequest)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.NewInternalf(errors.CodeInternal, "failed to get anomalies: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
results := []any{}
|
||||
for _, item := range anomalies.Results {
|
||||
results = append(results, item)
|
||||
}
|
||||
|
||||
// Build step intervals from the anomaly query
|
||||
stepIntervals := make(map[string]uint64)
|
||||
if anomalyQuery.StepInterval.Duration > 0 {
|
||||
stepIntervals[anomalyQuery.Name] = uint64(anomalyQuery.StepInterval.Duration.Seconds())
|
||||
}
|
||||
|
||||
finalResp := &qbtypes.QueryRangeResponse{
|
||||
Type: queryRangeRequest.RequestType,
|
||||
Data: qbtypes.QueryData{
|
||||
Results: results,
|
||||
},
|
||||
Meta: qbtypes.ExecStats{
|
||||
StepIntervals: stepIntervals,
|
||||
},
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, finalResp)
|
||||
return
|
||||
} else {
|
||||
// regular query range request, let the querier handle it
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
aH.QuerierAPI.QueryRange(rw, req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/gorilla/handlers"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||
"github.com/SigNoz/signoz/ee/query-service/rules"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
@@ -72,11 +71,6 @@ type Server struct {
|
||||
|
||||
// NewServer creates and initializes Server
|
||||
func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
gatewayProxy, err := gateway.NewProxy(config.Gateway.URL.String(), gateway.RoutePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cacheForTraceDetail, err := memorycache.New(context.TODO(), signoz.Instrumentation.ToProviderSettings(), cache.Config{
|
||||
Provider: "memory",
|
||||
Memory: cache.Memory{
|
||||
@@ -170,11 +164,11 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
FluxInterval: config.Querier.FluxInterval,
|
||||
Gateway: gatewayProxy,
|
||||
GatewayUrl: config.Gateway.URL.String(),
|
||||
GlobalConfig: config.Global,
|
||||
}
|
||||
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts, signoz, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,11 +230,9 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterCloudIntegrationsRoutes(r, am)
|
||||
apiHandler.RegisterFieldsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterInfraMetricsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
apiHandler.RegisterQueryRangeV5Routes(r, am)
|
||||
apiHandler.RegisterWebSocketPaths(r, am)
|
||||
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
||||
apiHandler.RegisterThirdPartyApiRoutes(r, am)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
func NewNoopProxy() (*httputil.ReverseProxy, error) {
|
||||
return &httputil.ReverseProxy{}, nil
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
RoutePrefix string = "/api/gateway"
|
||||
AllowedPrefix []string = []string{"/v1/workspaces/me", "/v2/profiles/me", "/v2/deployments/me"}
|
||||
)
|
||||
|
||||
type proxy struct {
|
||||
url *url.URL
|
||||
stripPath string
|
||||
}
|
||||
|
||||
func NewProxy(u string, stripPath string) (*httputil.ReverseProxy, error) {
|
||||
url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := &proxy{url: url, stripPath: stripPath}
|
||||
|
||||
return &httputil.ReverseProxy{
|
||||
Rewrite: proxy.rewrite,
|
||||
ModifyResponse: proxy.modifyResponse,
|
||||
ErrorHandler: proxy.errorHandler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *proxy) rewrite(pr *httputil.ProxyRequest) {
|
||||
pr.SetURL(p.url)
|
||||
pr.SetXForwarded()
|
||||
pr.Out.URL.Path = cleanPath(strings.ReplaceAll(pr.Out.URL.Path, p.stripPath, ""))
|
||||
}
|
||||
|
||||
func (p *proxy) modifyResponse(res *http.Response) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxy) errorHandler(rw http.ResponseWriter, req *http.Request, err error) {
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
|
||||
func cleanPath(p string) string {
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
if p[0] != '/' {
|
||||
p = "/" + p
|
||||
}
|
||||
np := path.Clean(p)
|
||||
if p[len(p)-1] == '/' && np != "/" {
|
||||
if len(p) == len(np)+1 && strings.HasPrefix(p, np) {
|
||||
np = p
|
||||
} else {
|
||||
np += "/"
|
||||
}
|
||||
}
|
||||
return np
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProxyRewrite(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
url *url.URL
|
||||
stripPath string
|
||||
in *url.URL
|
||||
expected *url.URL
|
||||
}{
|
||||
{
|
||||
name: "SamePathAdded",
|
||||
url: &url.URL{Scheme: "http", Host: "backend", Path: "/path1"},
|
||||
stripPath: "/strip",
|
||||
in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"},
|
||||
expected: &url.URL{Scheme: "http", Host: "backend", Path: "/path1/path1"},
|
||||
},
|
||||
{
|
||||
name: "NoStripPathInput",
|
||||
url: &url.URL{Scheme: "http", Host: "backend"},
|
||||
stripPath: "",
|
||||
in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"},
|
||||
expected: &url.URL{Scheme: "http", Host: "backend", Path: "/strip/path1"},
|
||||
},
|
||||
{
|
||||
name: "NoStripPathPresentInReq",
|
||||
url: &url.URL{Scheme: "http", Host: "backend"},
|
||||
stripPath: "/not-found",
|
||||
in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"},
|
||||
expected: &url.URL{Scheme: "http", Host: "backend", Path: "/strip/path1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
proxy, err := NewProxy(tc.url.String(), tc.stripPath)
|
||||
require.NoError(t, err)
|
||||
inReq, err := http.NewRequest(http.MethodGet, tc.in.String(), nil)
|
||||
require.NoError(t, err)
|
||||
proxyReq := &httputil.ProxyRequest{
|
||||
In: inReq,
|
||||
Out: inReq.Clone(context.Background()),
|
||||
}
|
||||
proxy.Rewrite(proxyReq)
|
||||
|
||||
assert.Equal(t, tc.expected.Host, proxyReq.Out.URL.Host)
|
||||
assert.Equal(t, tc.expected.Scheme, proxyReq.Out.URL.Scheme)
|
||||
assert.Equal(t, tc.expected.Path, proxyReq.Out.URL.Path)
|
||||
assert.Equal(t, tc.expected.Query(), proxyReq.Out.URL.Query())
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2"
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/formatter"
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
|
||||
@@ -63,6 +63,8 @@ type AnomalyRule struct {
|
||||
seasonality anomaly.Seasonality
|
||||
}
|
||||
|
||||
var _ baserules.Rule = (*AnomalyRule)(nil)
|
||||
|
||||
func NewAnomalyRule(
|
||||
id string,
|
||||
orgID valuer.UUID,
|
||||
@@ -234,17 +236,20 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
||||
}
|
||||
}
|
||||
|
||||
hasData := len(queryResult.AnomalyScores) > 0
|
||||
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
|
||||
return ruletypes.Vector{*missingDataAlert}, nil
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
|
||||
|
||||
for _, series := range queryResult.AnomalyScores {
|
||||
if r.Condition() != nil && r.Condition().RequireMinPoints {
|
||||
if len(series.Points) < r.Condition().RequiredNumPoints {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
continue
|
||||
}
|
||||
if !r.Condition().ShouldEval(series) {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
continue
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
@@ -287,6 +292,11 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
|
||||
queryResult := transition.ConvertV5TimeSeriesDataToV4Result(qbResult)
|
||||
|
||||
hasData := len(queryResult.AnomalyScores) > 0
|
||||
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
|
||||
return ruletypes.Vector{*missingDataAlert}, nil
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
@@ -305,11 +315,9 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
|
||||
for _, series := range seriesToProcess {
|
||||
if r.Condition().RequireMinPoints {
|
||||
if len(series.Points) < r.Condition().RequiredNumPoints {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
continue
|
||||
}
|
||||
if !r.Condition().ShouldEval(series) {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
continue
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
@@ -323,11 +331,11 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
return resultVector, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) {
|
||||
func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
|
||||
prevState := r.State()
|
||||
|
||||
valueFormatter := formatter.FromUnit(r.Unit())
|
||||
valueFormatter := units.FormatterFromUnit(r.Unit())
|
||||
|
||||
var res ruletypes.Vector
|
||||
var err error
|
||||
@@ -340,7 +348,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
r.mtx.Lock()
|
||||
@@ -415,7 +423,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
if _, ok := alerts[h]; ok {
|
||||
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", "rule_id", r.ID(), "alert", alerts[h])
|
||||
err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
@@ -484,7 +492,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
continue
|
||||
}
|
||||
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration() {
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration().Duration() {
|
||||
a.State = model.StateFiring
|
||||
a.FiredAt = ts
|
||||
state := model.StateFiring
|
||||
@@ -547,7 +555,7 @@ func (r *AnomalyRule) String() string {
|
||||
ar := ruletypes.PostableRule{
|
||||
AlertName: r.Name(),
|
||||
RuleCondition: r.Condition(),
|
||||
EvalWindow: ruletypes.Duration(r.EvalWindow()),
|
||||
EvalWindow: r.EvalWindow(),
|
||||
Labels: r.Labels().Map(),
|
||||
Annotations: r.Annotations().Map(),
|
||||
PreferredChannels: r.PreferredChannels(),
|
||||
|
||||
268
ee/query-service/rules/anomaly_test.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// mockAnomalyProvider is a mock implementation of anomaly.Provider for testing.
|
||||
// We need this because the anomaly provider makes 6 different queries for various
|
||||
// time periods (current, past period, current season, past season, past 2 seasons,
|
||||
// past 3 seasons), making it cumbersome to create mock data.
|
||||
type mockAnomalyProvider struct {
|
||||
responses []*anomaly.GetAnomaliesResponse
|
||||
callCount int
|
||||
}
|
||||
|
||||
func (m *mockAnomalyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *anomaly.GetAnomaliesRequest) (*anomaly.GetAnomaliesResponse, error) {
|
||||
if m.callCount >= len(m.responses) {
|
||||
return &anomaly.GetAnomaliesResponse{Results: []*v3.Result{}}, nil
|
||||
}
|
||||
resp := m.responses[m.callCount]
|
||||
m.callCount++
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
|
||||
// Test basic AlertOnAbsent functionality (without AbsentFor grace period)
|
||||
|
||||
baseTime := time.Unix(1700000000, 0)
|
||||
evalWindow := valuer.MustParseTextDuration("5m")
|
||||
evalTime := baseTime.Add(5 * time.Minute)
|
||||
|
||||
target := 500.0
|
||||
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "Test anomaly no data",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: RuleTypeAnomaly,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: evalWindow,
|
||||
Frequency: valuer.MustParseTextDuration("1m"),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
Target: &target,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
Expression: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Temporality: v3.Unspecified,
|
||||
},
|
||||
},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
Name: "Test anomaly no data",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseNoData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
description string
|
||||
alertOnAbsent bool
|
||||
expectAlerts int
|
||||
}{
|
||||
{
|
||||
description: "AlertOnAbsent=false",
|
||||
alertOnAbsent: false,
|
||||
expectAlerts: 0,
|
||||
},
|
||||
{
|
||||
description: "AlertOnAbsent=true",
|
||||
alertOnAbsent: true,
|
||||
expectAlerts: 1,
|
||||
},
|
||||
}
|
||||
|
||||
logger := instrumentationtest.New().Logger()
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.description, func(t *testing.T) {
|
||||
postableRule.RuleCondition.AlertOnAbsent = c.alertOnAbsent
|
||||
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||
|
||||
rule, err := NewAnomalyRule(
|
||||
"test-anomaly-rule",
|
||||
valuer.GenerateUUID(),
|
||||
&postableRule,
|
||||
reader,
|
||||
nil,
|
||||
logger,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
rule.provider = &mockAnomalyProvider{
|
||||
responses: []*anomaly.GetAnomaliesResponse{responseNoData},
|
||||
}
|
||||
|
||||
alertsFound, err := rule.Eval(context.Background(), evalTime)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expectAlerts, alertsFound)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
|
||||
// Test missing data alert with AbsentFor grace period
|
||||
// 1. Call Eval with data at time t1, to populate lastTimestampWithDatapoints
|
||||
// 2. Call Eval without data at time t2
|
||||
// 3. Alert fires only if t2 - t1 > AbsentFor
|
||||
|
||||
baseTime := time.Unix(1700000000, 0)
|
||||
evalWindow := valuer.MustParseTextDuration("5m")
|
||||
|
||||
// Set target higher than test data so regular threshold alerts don't fire
|
||||
target := 500.0
|
||||
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "Test anomaly no data with AbsentFor",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: RuleTypeAnomaly,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: evalWindow,
|
||||
Frequency: valuer.MustParseTextDuration("1m"),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
AlertOnAbsent: true,
|
||||
Target: &target,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
Expression: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Temporality: v3.Unspecified,
|
||||
},
|
||||
},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
Name: "Test anomaly no data with AbsentFor",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseNoData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
description string
|
||||
absentFor uint64
|
||||
timeBetweenEvals time.Duration
|
||||
expectAlertOnEval2 int
|
||||
}{
|
||||
{
|
||||
description: "WithinGracePeriod",
|
||||
absentFor: 5,
|
||||
timeBetweenEvals: 4 * time.Minute,
|
||||
expectAlertOnEval2: 0,
|
||||
},
|
||||
{
|
||||
description: "AfterGracePeriod",
|
||||
absentFor: 5,
|
||||
timeBetweenEvals: 6 * time.Minute,
|
||||
expectAlertOnEval2: 1,
|
||||
},
|
||||
}
|
||||
|
||||
logger := instrumentationtest.New().Logger()
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.description, func(t *testing.T) {
|
||||
postableRule.RuleCondition.AbsentFor = c.absentFor
|
||||
|
||||
t1 := baseTime.Add(5 * time.Minute)
|
||||
t2 := t1.Add(c.timeBetweenEvals)
|
||||
|
||||
responseWithData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{
|
||||
{
|
||||
Labels: map[string]string{"test": "label"},
|
||||
Points: []v3.Point{
|
||||
{Timestamp: baseTime.UnixMilli(), Value: 1.0},
|
||||
{Timestamp: baseTime.Add(time.Minute).UnixMilli(), Value: 1.5},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||
|
||||
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
rule.provider = &mockAnomalyProvider{
|
||||
responses: []*anomaly.GetAnomaliesResponse{responseWithData, responseNoData},
|
||||
}
|
||||
|
||||
alertsFound1, err := rule.Eval(context.Background(), t1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, alertsFound1, "First eval with data should not alert")
|
||||
|
||||
alertsFound2, err := rule.Eval(context.Background(), t2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expectAlertOnEval2, alertsFound2)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
@@ -47,8 +47,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
|
||||
rules = append(rules, tr)
|
||||
|
||||
// create ch rule task for evalution
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
// create ch rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
|
||||
@@ -71,8 +71,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
|
||||
rules = append(rules, pr)
|
||||
|
||||
// create promql rule task for evalution
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
// create promql rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
@@ -95,8 +95,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
|
||||
rules = append(rules, ar)
|
||||
|
||||
// create anomaly rule task for evalution
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
// create anomaly rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
@@ -203,22 +203,17 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
// set timestamp to current utc time
|
||||
ts := time.Now().UTC()
|
||||
|
||||
count, err := rule.Eval(ctx, ts)
|
||||
alertsFound, err := rule.Eval(ctx, ts)
|
||||
if err != nil {
|
||||
zap.L().Error("evaluating rule failed", zap.String("rule", rule.Name()), zap.Error(err))
|
||||
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
|
||||
}
|
||||
alertsFound, ok := count.(int)
|
||||
if !ok {
|
||||
return 0, basemodel.InternalError(fmt.Errorf("something went wrong"))
|
||||
}
|
||||
rule.SendAlerts(ctx, ts, 0, time.Duration(1*time.Minute), opts.NotifyFunc)
|
||||
rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc)
|
||||
|
||||
return alertsFound, nil
|
||||
}
|
||||
|
||||
// newTask returns an appropriate group for
|
||||
// rule type
|
||||
// newTask returns an appropriate group for the rule type
|
||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID valuer.UUID) baserules.Task {
|
||||
if taskType == baserules.TaskTypeCh {
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
alertmanagermock "github.com/SigNoz/signoz/pkg/alertmanager/mocks"
|
||||
alertmanagermock "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus/prometheustest"
|
||||
|
||||
@@ -3,6 +3,7 @@ package httpzeus
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/client"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -119,8 +121,13 @@ func (provider *Provider) PutMeters(ctx context.Context, key string, data []byte
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) PutProfile(ctx context.Context, key string, body []byte) error {
|
||||
_, err := provider.do(
|
||||
func (provider *Provider) PutProfile(ctx context.Context, key string, profile *zeustypes.PostableProfile) error {
|
||||
body, err := json.Marshal(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/profiles/me"),
|
||||
http.MethodPut,
|
||||
@@ -131,10 +138,15 @@ func (provider *Provider) PutProfile(ctx context.Context, key string, body []byt
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) PutHost(ctx context.Context, key string, body []byte) error {
|
||||
_, err := provider.do(
|
||||
func (provider *Provider) PutHost(ctx context.Context, key string, host *zeustypes.PostableHost) error {
|
||||
body, err := json.Marshal(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/deployments/me/hosts"),
|
||||
provider.config.URL.JoinPath("/v2/deployments/me/host"),
|
||||
http.MethodPut,
|
||||
key,
|
||||
body,
|
||||
@@ -169,21 +181,28 @@ func (provider *Provider) do(ctx context.Context, url *url.URL, method string, k
|
||||
return body, nil
|
||||
}
|
||||
|
||||
return nil, provider.errFromStatusCode(response.StatusCode)
|
||||
errorMessage := gjson.GetBytes(body, "error").String()
|
||||
if errorMessage == "" {
|
||||
errorMessage = "an unknown error occurred"
|
||||
}
|
||||
|
||||
return nil, provider.errFromStatusCode(response.StatusCode, errorMessage)
|
||||
}
|
||||
|
||||
// This can be taken down to the client package
|
||||
func (provider *Provider) errFromStatusCode(statusCode int) error {
|
||||
func (provider *Provider) errFromStatusCode(statusCode int, errorMessage string) error {
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "bad request")
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, errorMessage)
|
||||
case http.StatusUnauthorized:
|
||||
return errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
|
||||
return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, errorMessage)
|
||||
case http.StatusForbidden:
|
||||
return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "forbidden")
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, errorMessage)
|
||||
case http.StatusNotFound:
|
||||
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "not found")
|
||||
return errors.New(errors.TypeNotFound, errors.CodeNotFound, errorMessage)
|
||||
case http.StatusConflict:
|
||||
return errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, errorMessage)
|
||||
}
|
||||
|
||||
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "internal")
|
||||
return errors.New(errors.TypeInternal, errors.CodeInternal, errorMessage)
|
||||
}
|
||||
|
||||
74
frontend/.cursor/rules/testing-conventions.mdc
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
description: Core testing conventions - imports, rendering, MSW, interactions, queries
|
||||
globs: **/*.test.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Testing Conventions
|
||||
|
||||
## Imports
|
||||
|
||||
Always import from the test harness, never directly from `@testing-library/react`:
|
||||
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
```
|
||||
|
||||
## Router
|
||||
|
||||
Use the built-in router in `render`:
|
||||
|
||||
```ts
|
||||
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
|
||||
```
|
||||
|
||||
Only mock `useLocation` / `useParams` if the test depends on their values.
|
||||
|
||||
## MSW
|
||||
|
||||
Global MSW server runs automatically. Override per-test:
|
||||
|
||||
```ts
|
||||
server.use(
|
||||
rest.get('*/api/v1/foo', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ ok: true })))
|
||||
);
|
||||
```
|
||||
|
||||
Keep large response fixtures in `mocks-server/__mockdata_`.
|
||||
|
||||
## Interactions
|
||||
|
||||
- Prefer `userEvent` for real user interactions (click, type, select, tab).
|
||||
- Use `fireEvent` only for low-level events not covered by `userEvent` (e.g., scroll, resize). Wrap in `act(...)` if needed.
|
||||
- Always `await` interactions:
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
```
|
||||
|
||||
## Timers
|
||||
|
||||
No global fake timers. Per-test only, for debounce/throttle:
|
||||
|
||||
```ts
|
||||
jest.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
|
||||
await user.type(screen.getByRole('textbox'), 'query');
|
||||
jest.advanceTimersByTime(400);
|
||||
jest.useRealTimers();
|
||||
```
|
||||
|
||||
## Queries
|
||||
|
||||
Prefer accessible queries: `getByRole` > `findByRole` > `getByLabelText` > visible text > `data-testid` (last resort).
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Never import from `@testing-library/react` directly
|
||||
- Never use global fake timers
|
||||
- Never wrap `render` in `act(...)`
|
||||
- Never mock infra dependencies locally (router, react-query)
|
||||
- Limit to 3-5 focused tests per file
|
||||
54
frontend/.cursor/rules/testing-mock-strategy.mdc
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: When to use global vs local mocks in tests
|
||||
globs: **/*.test.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Mock Strategy
|
||||
|
||||
## Use Global Mocks For
|
||||
|
||||
High-frequency dependencies (20+ test files):
|
||||
- Core infrastructure: react-router-dom, react-query, antd
|
||||
- Browser APIs: ResizeObserver, matchMedia, localStorage
|
||||
- Utility libraries: date-fns, lodash
|
||||
|
||||
Available global mock files (from jest.config.ts):
|
||||
- `uplot` -> `__mocks__/uplotMock.ts`
|
||||
|
||||
## Use Local Mocks For
|
||||
|
||||
- Business logic dependencies (API endpoints, custom hooks, domain components)
|
||||
- Test-specific behavior (different data per test, error scenarios, loading states)
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
Used in 20+ test files?
|
||||
YES -> Global mock
|
||||
NO -> Business logic or test-specific?
|
||||
YES -> Local mock
|
||||
NO -> Consider global if usage grows
|
||||
```
|
||||
|
||||
## Correct Usage
|
||||
|
||||
```ts
|
||||
// Global mocks are already available - just import
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
// Local mocks for business logic
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => mockTracesData),
|
||||
}));
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
```ts
|
||||
// Never re-mock globally mocked dependencies locally
|
||||
jest.mock('react-router-dom', () => ({ ... }));
|
||||
|
||||
// Never put test-specific data in global mocks
|
||||
jest.mock('../api/tracesService', () => ({ getTraces: jest.fn(() => specificTestData) }));
|
||||
```
|
||||
54
frontend/.cursor/rules/testing-type-safety.mdc
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: TypeScript type safety requirements for Jest tests
|
||||
globs: **/*.test.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# TypeScript Type Safety in Tests
|
||||
|
||||
All Jest tests must be fully type-safe. Never use `any`.
|
||||
|
||||
## Mock Function Typing
|
||||
|
||||
```ts
|
||||
// Use jest.mocked for module mocks
|
||||
import useFoo from 'hooks/useFoo';
|
||||
jest.mock('hooks/useFoo');
|
||||
const mockUseFoo = jest.mocked(useFoo);
|
||||
|
||||
// Use jest.MockedFunction for standalone mocks
|
||||
const mockFetch = jest.fn() as jest.MockedFunction<(id: number) => Promise<User>>;
|
||||
```
|
||||
|
||||
## Mock Data
|
||||
|
||||
Define interfaces for all mock data:
|
||||
|
||||
```ts
|
||||
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
|
||||
|
||||
const mockProps: ComponentProps = {
|
||||
title: 'Test',
|
||||
data: [mockUser],
|
||||
onSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
|
||||
};
|
||||
```
|
||||
|
||||
## Hook Mocking Pattern
|
||||
|
||||
```ts
|
||||
import useFoo from 'hooks/useFoo';
|
||||
jest.mock('hooks/useFoo');
|
||||
const mockUseFoo = jest.mocked(useFoo);
|
||||
mockUseFoo.mockReturnValue(/* minimal shape */);
|
||||
```
|
||||
|
||||
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
|
||||
|
||||
## Checklist
|
||||
|
||||
- All mock functions use `jest.MockedFunction<T>` or `jest.mocked()`
|
||||
- All mock data has proper interfaces
|
||||
- No `any` types in test files
|
||||
- Component props are typed
|
||||
- API response types are defined
|
||||
@@ -1,484 +0,0 @@
|
||||
# Persona
|
||||
You are an expert developer with deep knowledge of Jest, React Testing Library, MSW, and TypeScript, tasked with creating unit tests for this repository.
|
||||
|
||||
# Auto-detect TypeScript Usage
|
||||
Check for TypeScript in the project through tsconfig.json or package.json dependencies.
|
||||
Adjust syntax based on this detection.
|
||||
|
||||
# TypeScript Type Safety for Jest Tests
|
||||
**CRITICAL**: All Jest tests MUST be fully type-safe with proper TypeScript types.
|
||||
|
||||
**Type Safety Requirements:**
|
||||
- Use proper TypeScript interfaces for all mock data
|
||||
- Type all Jest mock functions with `jest.MockedFunction<T>`
|
||||
- Use generic types for React components and hooks
|
||||
- Define proper return types for mock functions
|
||||
- Use `as const` for literal types when needed
|
||||
- Avoid `any` type – use proper typing instead
|
||||
|
||||
# Unit Testing Focus
|
||||
Focus on critical functionality (business logic, utility functions, component behavior)
|
||||
Mock dependencies (API calls, external modules) before imports
|
||||
Test multiple data scenarios (valid inputs, invalid inputs, edge cases)
|
||||
Write maintainable tests with descriptive names grouped in describe blocks
|
||||
|
||||
# Global vs Local Mocks
|
||||
**Use Global Mocks for:**
|
||||
- High-frequency dependencies (20+ test files)
|
||||
- Core infrastructure (react-router-dom, react-query, antd)
|
||||
- Standard implementations across the app
|
||||
- Browser APIs (ResizeObserver, matchMedia, localStorage)
|
||||
- Utility libraries (date-fns, lodash)
|
||||
|
||||
**Use Local Mocks for:**
|
||||
- Business logic dependencies (5-15 test files)
|
||||
- Test-specific behavior (different data per test)
|
||||
- API endpoints with specific responses
|
||||
- Domain-specific components
|
||||
- Error scenarios and edge cases
|
||||
|
||||
**Global Mock Files Available (from jest.config.ts):**
|
||||
- `uplot` → `__mocks__/uplotMock.ts`
|
||||
|
||||
# Repo-specific Testing Conventions
|
||||
|
||||
## Imports
|
||||
Always import from our harness:
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
```
|
||||
For API mocks:
|
||||
```ts
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
```
|
||||
Do not import directly from `@testing-library/react`.
|
||||
|
||||
## Router
|
||||
Use the router built into render:
|
||||
```ts
|
||||
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
|
||||
```
|
||||
Only mock `useLocation` / `useParams` if the test depends on them.
|
||||
|
||||
## Hook Mocks
|
||||
Pattern:
|
||||
```ts
|
||||
import useFoo from 'hooks/useFoo';
|
||||
jest.mock('hooks/useFoo');
|
||||
const mockUseFoo = jest.mocked(useFoo);
|
||||
mockUseFoo.mockReturnValue(/* minimal shape */ as any);
|
||||
```
|
||||
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
|
||||
|
||||
## MSW
|
||||
Global MSW server runs automatically.
|
||||
Override per-test:
|
||||
```ts
|
||||
server.use(
|
||||
rest.get('*/api/v1/foo', (_req, res, ctx) => res(ctx.status(200), ctx.json({ ok: true })))
|
||||
);
|
||||
```
|
||||
Keep large responses in `mocks-server/__mockdata_`.
|
||||
|
||||
## Interactions
|
||||
- Prefer `userEvent` for real user interactions (click, type, select, tab).
|
||||
- Use `fireEvent` only for low-level/programmatic events not covered by `userEvent` (e.g., scroll, resize, setting `element.scrollTop` for virtualization). Wrap in `act(...)` if needed.
|
||||
- Always await interactions:
|
||||
```ts
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
```
|
||||
|
||||
```ts
|
||||
// Example: virtualized list scroll (no userEvent helper)
|
||||
const scroller = container.querySelector('[data-test-id="virtuoso-scroller"]') as HTMLElement;
|
||||
scroller.scrollTop = targetScrollTop;
|
||||
act(() => { fireEvent.scroll(scroller); });
|
||||
```
|
||||
|
||||
## Timers
|
||||
❌ No global fake timers.
|
||||
✅ Per-test only, for debounce/throttle:
|
||||
```ts
|
||||
jest.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
|
||||
await user.type(screen.getByRole('textbox'), 'query');
|
||||
jest.advanceTimersByTime(400);
|
||||
jest.useRealTimers();
|
||||
```
|
||||
|
||||
## Queries
|
||||
Prefer accessible queries (`getByRole`, `findByRole`, `getByLabelText`).
|
||||
Fallback: visible text.
|
||||
Last resort: `data-testid`.
|
||||
|
||||
# Example Test (using only configured global mocks)
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
import MyComponent from '../MyComponent';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
it('renders and interacts', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
|
||||
);
|
||||
|
||||
render(<MyComponent />, undefined, { initialRoute: '/foo' });
|
||||
|
||||
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: /refresh/i }));
|
||||
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
# Anti-patterns
|
||||
❌ Importing RTL directly
|
||||
❌ Using global fake timers
|
||||
❌ Wrapping render in `act(...)`
|
||||
❌ Mocking infra dependencies locally (router, react-query)
|
||||
✅ Use our harness (`tests/test-utils`)
|
||||
✅ Use MSW for API overrides
|
||||
✅ Use userEvent + await
|
||||
✅ Pin time only in tests that assert relative dates
|
||||
|
||||
# Best Practices
|
||||
- **Critical Functionality**: Prioritize testing business logic and utilities
|
||||
- **Dependency Mocking**: Global mocks for infra, local mocks for business logic
|
||||
- **Data Scenarios**: Always test valid, invalid, and edge cases
|
||||
- **Descriptive Names**: Make test intent clear
|
||||
- **Organization**: Group related tests in describe
|
||||
- **Consistency**: Match repo conventions
|
||||
- **Edge Cases**: Test null, undefined, unexpected values
|
||||
- **Limit Scope**: 3–5 focused tests per file
|
||||
- **Use Helpers**: `rqSuccess`, `makeUser`, etc.
|
||||
- **No Any**: Enforce type safety
|
||||
|
||||
# Example Test
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
import MyComponent from '../MyComponent';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
it('renders and interacts', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
|
||||
);
|
||||
|
||||
render(<MyComponent />, undefined, { initialRoute: '/foo' });
|
||||
|
||||
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: /refresh/i }));
|
||||
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
# Anti-patterns
|
||||
❌ Importing RTL directly
|
||||
❌ Using global fake timers
|
||||
❌ Wrapping render in `act(...)`
|
||||
❌ Mocking infra dependencies locally (router, react-query)
|
||||
✅ Use our harness (`tests/test-utils`)
|
||||
✅ Use MSW for API overrides
|
||||
✅ Use userEvent + await
|
||||
✅ Pin time only in tests that assert relative dates
|
||||
|
||||
# TypeScript Type Safety Examples
|
||||
|
||||
## Proper Mock Typing
|
||||
```ts
|
||||
// ✅ GOOD - Properly typed mocks
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T;
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Type the mock functions
|
||||
const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise<ApiResponse<User>>>;
|
||||
const mockUpdateUser = jest.fn() as jest.MockedFunction<(user: User) => Promise<ApiResponse<User>>>;
|
||||
|
||||
// Mock implementation with proper typing
|
||||
mockFetchUser.mockResolvedValue({
|
||||
data: { id: 1, name: 'John Doe', email: 'john@example.com' },
|
||||
status: 200,
|
||||
message: 'Success'
|
||||
});
|
||||
|
||||
// ❌ BAD - Using any type
|
||||
const mockFetchUser = jest.fn() as any; // Don't do this
|
||||
```
|
||||
|
||||
## React Component Testing with Types
|
||||
```ts
|
||||
// ✅ GOOD - Properly typed component testing
|
||||
interface ComponentProps {
|
||||
title: string;
|
||||
data: User[];
|
||||
onUserSelect: (user: User) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TestComponent: React.FC<ComponentProps> = ({ title, data, onUserSelect, isLoading = false }) => {
|
||||
// Component implementation
|
||||
};
|
||||
|
||||
describe('TestComponent', () => {
|
||||
it('should render with proper props', () => {
|
||||
// Arrange - Type the props properly
|
||||
const mockProps: ComponentProps = {
|
||||
title: 'Test Title',
|
||||
data: [{ id: 1, name: 'John', email: 'john@example.com' }],
|
||||
onUserSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
// Act
|
||||
render(<TestComponent {...mockProps} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Hook Testing with Types
|
||||
```ts
|
||||
// ✅ GOOD - Properly typed hook testing
|
||||
interface UseUserDataReturn {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
const useUserData = (id: number): UseUserDataReturn => {
|
||||
// Hook implementation
|
||||
};
|
||||
|
||||
describe('useUserData', () => {
|
||||
it('should return user data with proper typing', () => {
|
||||
// Arrange
|
||||
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
|
||||
mockFetchUser.mockResolvedValue({
|
||||
data: mockUser,
|
||||
status: 200,
|
||||
message: 'Success'
|
||||
});
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useUserData(1));
|
||||
|
||||
// Assert
|
||||
expect(result.current.user).toEqual(mockUser);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Global Mock Type Safety
|
||||
```ts
|
||||
// ✅ GOOD - Type-safe global mocks
|
||||
// In __mocks__/routerMock.ts
|
||||
export const mockUseLocation = (overrides: Partial<Location> = {}): Location => ({
|
||||
pathname: '/traces',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// In test files
|
||||
const location = useLocation(); // Properly typed from global mock
|
||||
expect(location.pathname).toBe('/traces');
|
||||
```
|
||||
|
||||
# TypeScript Configuration for Jest
|
||||
|
||||
## Required Jest Configuration
|
||||
```json
|
||||
// jest.config.ts
|
||||
{
|
||||
"preset": "ts-jest/presets/js-with-ts-esm",
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"useESM": true,
|
||||
"isolatedModules": true,
|
||||
"tsconfig": "<rootDir>/tsconfig.jest.json"
|
||||
}
|
||||
},
|
||||
"extensionsToTreatAsEsm": [".ts", ".tsx"],
|
||||
"moduleFileExtensions": ["ts", "tsx", "js", "json"]
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Jest Configuration
|
||||
```json
|
||||
// tsconfig.jest.json
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "@testing-library/jest-dom"],
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"__mocks__/**/*"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Common Type Safety Patterns
|
||||
|
||||
### Mock Function Typing
|
||||
```ts
|
||||
// ✅ GOOD - Proper mock function typing
|
||||
const mockApiCall = jest.fn() as jest.MockedFunction<typeof apiCall>;
|
||||
const mockEventHandler = jest.fn() as jest.MockedFunction<(event: Event) => void>;
|
||||
|
||||
// ❌ BAD - Using any
|
||||
const mockApiCall = jest.fn() as any;
|
||||
```
|
||||
|
||||
### Generic Mock Typing
|
||||
```ts
|
||||
// ✅ GOOD - Generic mock typing
|
||||
interface MockApiResponse<T> {
|
||||
data: T;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const mockFetchData = jest.fn() as jest.MockedFunction<
|
||||
<T>(endpoint: string) => Promise<MockApiResponse<T>>
|
||||
>;
|
||||
|
||||
// Usage
|
||||
mockFetchData<User>('/users').mockResolvedValue({
|
||||
data: { id: 1, name: 'John' },
|
||||
status: 200
|
||||
});
|
||||
```
|
||||
|
||||
### React Testing Library with Types
|
||||
```ts
|
||||
// ✅ GOOD - Typed testing utilities
|
||||
import { render, screen, RenderResult } from '@testing-library/react';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
type TestComponentProps = ComponentProps<typeof TestComponent>;
|
||||
|
||||
const renderTestComponent = (props: Partial<TestComponentProps> = {}): RenderResult => {
|
||||
const defaultProps: TestComponentProps = {
|
||||
title: 'Test',
|
||||
data: [],
|
||||
onSelect: jest.fn(),
|
||||
...props
|
||||
};
|
||||
|
||||
return render(<TestComponent {...defaultProps} />);
|
||||
};
|
||||
```
|
||||
|
||||
### Error Handling with Types
|
||||
```ts
|
||||
// ✅ GOOD - Typed error handling
|
||||
interface ApiError {
|
||||
message: string;
|
||||
code: number;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const mockApiError: ApiError = {
|
||||
message: 'API Error',
|
||||
code: 500,
|
||||
details: { endpoint: '/users' }
|
||||
};
|
||||
|
||||
mockFetchUser.mockRejectedValue(new Error(JSON.stringify(mockApiError)));
|
||||
```
|
||||
|
||||
## Type Safety Checklist
|
||||
- [ ] All mock functions use `jest.MockedFunction<T>`
|
||||
- [ ] All mock data has proper interfaces
|
||||
- [ ] No `any` types in test files
|
||||
- [ ] Generic types are used where appropriate
|
||||
- [ ] Error types are properly defined
|
||||
- [ ] Component props are typed
|
||||
- [ ] Hook return types are defined
|
||||
- [ ] API response types are defined
|
||||
- [ ] Global mocks are type-safe
|
||||
- [ ] Test utilities are properly typed
|
||||
|
||||
# Mock Decision Tree
|
||||
```
|
||||
Is it used in 20+ test files?
|
||||
├─ YES → Use Global Mock
|
||||
│ ├─ react-router-dom
|
||||
│ ├─ react-query
|
||||
│ ├─ antd components
|
||||
│ └─ browser APIs
|
||||
│
|
||||
└─ NO → Is it business logic?
|
||||
├─ YES → Use Local Mock
|
||||
│ ├─ API endpoints
|
||||
│ ├─ Custom hooks
|
||||
│ └─ Domain components
|
||||
│
|
||||
└─ NO → Is it test-specific?
|
||||
├─ YES → Use Local Mock
|
||||
│ ├─ Error scenarios
|
||||
│ ├─ Loading states
|
||||
│ └─ Specific data
|
||||
│
|
||||
└─ NO → Consider Global Mock
|
||||
└─ If it becomes frequently used
|
||||
```
|
||||
|
||||
# Common Anti-Patterns to Avoid
|
||||
|
||||
❌ **Don't mock global dependencies locally:**
|
||||
```js
|
||||
// BAD - This is already globally mocked
|
||||
jest.mock('react-router-dom', () => ({ ... }));
|
||||
```
|
||||
|
||||
❌ **Don't create global mocks for test-specific data:**
|
||||
```js
|
||||
// BAD - This should be local
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => specificTestData)
|
||||
}));
|
||||
```
|
||||
|
||||
✅ **Do use global mocks for infrastructure:**
|
||||
```js
|
||||
// GOOD - Use global mock
|
||||
import { useLocation } from 'react-router-dom';
|
||||
```
|
||||
|
||||
✅ **Do create local mocks for business logic:**
|
||||
```js
|
||||
// GOOD - Local mock for specific test needs
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => mockTracesData)
|
||||
}));
|
||||
```
|
||||
@@ -2,4 +2,6 @@ node_modules
|
||||
build
|
||||
*.typegen.ts
|
||||
i18-generate-hash.js
|
||||
src/parser/TraceOperatorParser/**
|
||||
src/parser/TraceOperatorParser/**
|
||||
|
||||
orval.config.ts
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* ESLint Configuration for SigNoz Frontend
|
||||
*/
|
||||
module.exports = {
|
||||
ignorePatterns: ['src/parser/*.ts', 'scripts/update-registry.js'],
|
||||
env: {
|
||||
@@ -7,16 +10,12 @@ module.exports = {
|
||||
'jest/globals': true,
|
||||
},
|
||||
extends: [
|
||||
'airbnb',
|
||||
'airbnb-typescript',
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:sonarjs/recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings',
|
||||
'plugin:react/jsx-runtime',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
@@ -25,16 +24,21 @@ module.exports = {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 12,
|
||||
ecmaVersion: 2021,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
'@typescript-eslint',
|
||||
'simple-import-sort',
|
||||
'react-hooks',
|
||||
'prettier',
|
||||
'jest',
|
||||
'react', // React-specific rules
|
||||
'@typescript-eslint', // TypeScript linting
|
||||
'simple-import-sort', // Auto-sort imports
|
||||
'react-hooks', // React Hooks rules
|
||||
'prettier', // Code formatting
|
||||
'jest', // Jest test rules
|
||||
'jsx-a11y', // Accessibility rules
|
||||
'import', // Import/export linting
|
||||
'sonarjs', // Code quality/complexity
|
||||
// TODO: Uncomment after running: yarn add -D eslint-plugin-spellcheck
|
||||
// 'spellcheck', // Correct spellings
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
@@ -48,78 +52,166 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Code quality rules
|
||||
'prefer-const': 'error', // Enforces const for variables never reassigned
|
||||
'no-var': 'error', // Disallows var, enforces let/const
|
||||
'no-else-return': ['error', { allowElseIf: false }], // Reduces nesting by disallowing else after return
|
||||
'no-cond-assign': 'error', // Prevents accidental assignment in conditions (if (x = 1) instead of if (x === 1))
|
||||
'no-debugger': 'error', // Disallows debugger statements in production code
|
||||
curly: 'error', // Requires curly braces for all control statements
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }], // Enforces === and !== (allows == null for null/undefined check)
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
|
||||
// TODO: Change this to error in May 2026
|
||||
'max-params': ['warn', 3], // a function can have max 3 params after which it should become an object
|
||||
|
||||
// TypeScript rules
|
||||
'@typescript-eslint/explicit-function-return-type': 'error', // Requires explicit return types on functions
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
// Disallows unused variables/args
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_', // Allows unused args prefixed with _ (e.g., _unusedParam)
|
||||
varsIgnorePattern: '^_', // Allows unused vars prefixed with _ (e.g., _unusedVar)
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn', // Warns when using 'any' type (consider upgrading to error)
|
||||
// TODO: Change to 'error' after fixing ~80 empty function placeholders in providers/contexts
|
||||
'@typescript-eslint/no-empty-function': 'off', // Disallows empty function bodies
|
||||
'@typescript-eslint/no-var-requires': 'error', // Disallows require() in TypeScript (use import instead)
|
||||
'@typescript-eslint/ban-ts-comment': 'warn', // Allows @ts-ignore comments (sometimes needed for third-party libs)
|
||||
'no-empty-function': 'off', // Disabled in favor of TypeScript version above
|
||||
|
||||
// React rules
|
||||
'react/jsx-filename-extension': [
|
||||
'error',
|
||||
{
|
||||
extensions: ['.tsx', '.js', '.jsx'],
|
||||
extensions: ['.tsx', '.jsx'], // Warns if JSX is used in non-.jsx/.tsx files
|
||||
},
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'error',
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
'react/no-array-index-key': 'error',
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
process.env.platform === 'win32' ? 'windows' : 'unix',
|
||||
],
|
||||
'@typescript-eslint/default-param-last': 'off',
|
||||
'react/prop-types': 'off', // Disabled - using TypeScript instead
|
||||
'react/jsx-props-no-spreading': 'off', // Allows {...props} spreading (common in HOCs, forms, wrappers)
|
||||
'react/no-array-index-key': 'error', // Prevents using array index as key (causes bugs when list changes)
|
||||
|
||||
// simple sort error
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
|
||||
// hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
|
||||
// airbnb
|
||||
'no-underscore-dangle': 'off',
|
||||
'no-console': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
'no-plusplus': 'off',
|
||||
// Accessibility rules
|
||||
'jsx-a11y/label-has-associated-control': [
|
||||
'error',
|
||||
{
|
||||
required: {
|
||||
some: ['nesting', 'id'],
|
||||
some: ['nesting', 'id'], // Labels must either wrap inputs or use htmlFor/id
|
||||
},
|
||||
},
|
||||
],
|
||||
'jsx-a11y/label-has-for': [
|
||||
|
||||
// React Hooks rules
|
||||
'react-hooks/rules-of-hooks': 'error', // Enforces Rules of Hooks (only call at top level)
|
||||
'react-hooks/exhaustive-deps': 'warn', // Warns about missing dependencies in useEffect/useMemo/useCallback
|
||||
|
||||
// Import/export rules
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
js: 'never', // Disallows .js extension in imports
|
||||
jsx: 'never', // Disallows .jsx extension in imports
|
||||
ts: 'never', // Disallows .ts extension in imports
|
||||
tsx: 'never', // Disallows .tsx extension in imports
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }], // Prevents importing packages not in package.json
|
||||
'import/no-cycle': 'warn', // Warns about circular dependencies
|
||||
|
||||
// Import sorting rules
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
required: {
|
||||
some: ['nesting', 'id'],
|
||||
},
|
||||
groups: [
|
||||
['^react', '^@?\\w'], // React first, then external packages
|
||||
['^@/'], // Absolute imports with @ alias
|
||||
['^\\u0000'], // Side effect imports (import './file')
|
||||
['^\\.'], // Relative imports
|
||||
['^.+\\.s?css$'], // Style imports
|
||||
],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'simple-import-sort/exports': 'error', // Auto-sorts exports
|
||||
|
||||
// eslint rules need to remove
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
// https://typescript-eslint.io/rules/consistent-return/ check the warning for details
|
||||
'consistent-return': 'off',
|
||||
// Prettier - code formatting
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{},
|
||||
{
|
||||
usePrettierrc: true,
|
||||
usePrettierrc: true, // Uses .prettierrc.json for formatting rules
|
||||
},
|
||||
],
|
||||
|
||||
// SonarJS - code quality and complexity
|
||||
'sonarjs/no-duplicate-string': 'off', // Disabled - can be noisy (enable periodically to check)
|
||||
|
||||
// State management governance
|
||||
// Approved patterns: Zustand, nuqs (URL state), react-query (server state), useState/useRef/useReducer, localStorage/sessionStorage for simple cases
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'redux',
|
||||
message:
|
||||
'[State mgmt] redux is deprecated. Migrate to Zustand, nuqs, or react-query.',
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
message:
|
||||
'[State mgmt] react-redux is deprecated. Migrate to Zustand, nuqs, or react-query.',
|
||||
},
|
||||
{
|
||||
name: 'xstate',
|
||||
message:
|
||||
'[State mgmt] xstate is deprecated. Migrate to Zustand or react-query.',
|
||||
},
|
||||
{
|
||||
name: '@xstate/react',
|
||||
message:
|
||||
'[State mgmt] @xstate/react is deprecated. Migrate to Zustand or react-query.',
|
||||
},
|
||||
{
|
||||
// Restrict React Context — useState/useRef/useReducer remain allowed
|
||||
name: 'react',
|
||||
importNames: ['createContext', 'useContext'],
|
||||
message:
|
||||
'[State mgmt] React Context is deprecated. Migrate shared state to Zustand.',
|
||||
},
|
||||
{
|
||||
// immer used standalone as a store pattern is deprecated; Zustand bundles it internally
|
||||
name: 'immer',
|
||||
message:
|
||||
'[State mgmt] Direct immer usage is deprecated. Use Zustand (which integrates immer via the immer middleware) instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/*.test.{js,jsx,ts,tsx}',
|
||||
'**/*.spec.{js,jsx,ts,tsx}',
|
||||
'**/__tests__/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
// Tests often have intentional duplication and complexity - disable SonarJS rules
|
||||
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
|
||||
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
|
||||
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/api/generated/**/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'no-nested-ternary': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -4,5 +4,14 @@
|
||||
"tabWidth": 1,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"semi": true
|
||||
"semi": true,
|
||||
"printWidth": 80,
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"quoteProps": "as-needed",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
|
||||
8
frontend/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"prettier.requireConfig": true
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
Embrace the spirit of collaboration and contribute to the success of our open-source project by adhering to these frontend development guidelines with precision and passion.
|
||||
|
||||
### Export Style
|
||||
|
||||
- **React components** (`src/components/`, `src/container/`, `src/pages/`): Prefer **default exports** for the main component in each file
|
||||
- **Utilities, hooks, APIs, types, constants** (`src/utils/`, `src/hooks/`, `src/api/`, `src/lib/`, `src/types/`, `src/constants/`): Prefer **named exports** for better tree-shaking and explicit imports
|
||||
|
||||
### React and Components
|
||||
|
||||
- Strive to create small and modular components, ensuring they are divided into individual pieces for improved maintainability and reusability.
|
||||
@@ -34,12 +39,18 @@ Embrace the spirit of collaboration and contribute to the success of our open-so
|
||||
### Linting and Setup
|
||||
|
||||
- It is crucial to refrain from disabling ESLint and TypeScript errors within the project. If there is a specific rule that needs to be disabled, provide a clear and justified explanation for doing so. Maintaining the integrity of the linting and type-checking processes ensures code quality and consistency throughout the codebase.
|
||||
- In our project, we rely on several essential ESLint plugins, namely:
|
||||
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/)
|
||||
- [airbnb styleguide](https://github.com/airbnb/javascript)
|
||||
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs)
|
||||
- In our project, we rely on several essential ESLint plugins and configurations:
|
||||
|
||||
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
|
||||
- [eslint:recommended](https://eslint.org/docs/latest/rules/) - Core ESLint rules for JavaScript best practices
|
||||
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/) - TypeScript-specific linting rules
|
||||
- [plugin:react](https://github.com/jsx-eslint/eslint-plugin-react) - React best practices and patterns
|
||||
- [plugin:react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) - Rules of Hooks enforcement
|
||||
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) - Code quality and complexity analysis
|
||||
- [plugin:prettier](https://github.com/prettier/eslint-plugin-prettier) - Code formatting via Prettier
|
||||
- [simple-import-sort](https://github.com/lydell/eslint-plugin-simple-import-sort) - Automatic import organization
|
||||
- [plugin:jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - Accessibility rules for JSX elements
|
||||
|
||||
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface MockUPlotInstance {
|
||||
export interface MockUPlotPaths {
|
||||
spline: jest.Mock;
|
||||
bars: jest.Mock;
|
||||
linear: jest.Mock;
|
||||
stepped: jest.Mock;
|
||||
}
|
||||
|
||||
// Create mock instance methods
|
||||
@@ -23,10 +25,23 @@ const createMockUPlotInstance = (): MockUPlotInstance => ({
|
||||
setSeries: jest.fn(),
|
||||
});
|
||||
|
||||
// Create mock paths
|
||||
const mockPaths: MockUPlotPaths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
// Path builder: (self, seriesIdx, idx0, idx1) => paths or null
|
||||
const createMockPathBuilder = (name: string): jest.Mock =>
|
||||
jest.fn(() => ({
|
||||
name, // To test if the correct pathBuilder is used
|
||||
stroke: jest.fn(),
|
||||
fill: jest.fn(),
|
||||
clip: jest.fn(),
|
||||
}));
|
||||
|
||||
// Create mock paths - linear, spline, stepped needed by UPlotSeriesBuilder.getPathBuilder
|
||||
const mockPaths = {
|
||||
spline: jest.fn(() => createMockPathBuilder('spline')),
|
||||
bars: jest.fn(() => createMockPathBuilder('bars')),
|
||||
linear: jest.fn(() => createMockPathBuilder('linear')),
|
||||
stepped: jest.fn((opts?: { align?: number }) =>
|
||||
createMockPathBuilder(`stepped-(${opts?.align ?? 0})`),
|
||||
),
|
||||
};
|
||||
|
||||
// Mock static methods
|
||||
|
||||
@@ -17,6 +17,8 @@ const config: Config.InitialOptions = {
|
||||
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^@signozhq/icons$':
|
||||
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
|
||||
},
|
||||
globals: {
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
|
||||
97
frontend/orval.config.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* When making changes to this file, remove this the file name from .eslintignore and tsconfig.json
|
||||
* The reason this is required because of the moduleResolution being "node". Changing this is a more detailed effort.
|
||||
* So, until then, we will keep this file ignored for eslint and typescript.
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'orval';
|
||||
|
||||
export default defineConfig({
|
||||
signoz: {
|
||||
input: {
|
||||
target: '../docs/api/openapi.yml',
|
||||
},
|
||||
output: {
|
||||
target: './src/api/generated/services',
|
||||
client: 'react-query',
|
||||
httpClient: 'axios',
|
||||
mode: 'tags-split',
|
||||
prettier: true,
|
||||
headers: true,
|
||||
clean: true,
|
||||
override: {
|
||||
query: {
|
||||
useQuery: true,
|
||||
useMutation: true,
|
||||
useInvalidate: true,
|
||||
signal: true,
|
||||
useOperationIdAsQueryKey: false,
|
||||
},
|
||||
useDates: true,
|
||||
useNamedParameters: true,
|
||||
enumGenerationType: 'enum',
|
||||
mutator: {
|
||||
path: './src/api/generatedAPIInstance.ts',
|
||||
name: 'GeneratedAPIInstance',
|
||||
},
|
||||
|
||||
jsDoc: {
|
||||
filter: (schema) => {
|
||||
const allowlist = [
|
||||
'type',
|
||||
'format',
|
||||
'maxLength',
|
||||
'minLength',
|
||||
'description',
|
||||
'minimum',
|
||||
'maximum',
|
||||
'exclusiveMinimum',
|
||||
'exclusiveMaximum',
|
||||
'pattern',
|
||||
'nullable',
|
||||
'enum',
|
||||
];
|
||||
return Object.entries(schema || {})
|
||||
.filter(([key]) => allowlist.includes(key))
|
||||
.map(([key, value]: [string, any]) => ({
|
||||
key,
|
||||
value,
|
||||
}))
|
||||
.sort((a, b) => a.key.length - b.key.length);
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
schemas: {
|
||||
suffix: 'DTO',
|
||||
},
|
||||
responses: {
|
||||
suffix: 'Response',
|
||||
},
|
||||
parameters: {
|
||||
suffix: 'Params',
|
||||
},
|
||||
requestBodies: {
|
||||
suffix: 'Body',
|
||||
},
|
||||
},
|
||||
|
||||
// info is of type InfoObject from openapi spec
|
||||
header: (info: { title: string; version: string }): string[] => [
|
||||
`! Do not edit manually`,
|
||||
`* The file has been auto-generated using Orval for SigNoz`,
|
||||
`* regenerate with 'yarn generate:api'`,
|
||||
...(info.title ? [info.title] : []),
|
||||
...(info.version ? [`OpenAPI spec version: ${info.version}`] : []),
|
||||
],
|
||||
|
||||
// @ts-expect-error
|
||||
// propertySortOrder, urlEncodeParameters, aliasCombinedTypes
|
||||
// are valid options in the document without types
|
||||
propertySortOrder: 'Alphabetical',
|
||||
urlEncodeParameters: true,
|
||||
aliasCombinedTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -18,7 +18,8 @@
|
||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest",
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent"
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent",
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.15.0"
|
||||
@@ -48,12 +49,16 @@
|
||||
"@signozhq/calendar": "0.0.0",
|
||||
"@signozhq/callout": "0.0.2",
|
||||
"@signozhq/checkbox": "0.0.2",
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/radio-group": "0.0.2",
|
||||
"@signozhq/resizable": "0.0.0",
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/switch": "0.0.2",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
@@ -103,6 +108,7 @@
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"i18next-http-backend": "^1.3.2",
|
||||
"immer": "11.1.3",
|
||||
"jest": "^27.5.1",
|
||||
"js-base64": "^3.7.2",
|
||||
"less": "^4.1.2",
|
||||
@@ -111,6 +117,7 @@
|
||||
"lucide-react": "0.498.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"motion": "12.4.13",
|
||||
"nuqs": "2.8.8",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "5.4.1",
|
||||
@@ -124,7 +131,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-drag-listview": "2.0.0",
|
||||
"react-error-boundary": "4.0.11",
|
||||
"react-force-graph": "^1.43.0",
|
||||
"react-force-graph-2d": "^1.29.1",
|
||||
"react-full-screen": "1.1.1",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
@@ -156,7 +163,8 @@
|
||||
"webpack": "5.94.0",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||
"xstate": "^4.31.0"
|
||||
"xstate": "^4.31.0",
|
||||
"zustand": "5.0.11"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -219,16 +227,11 @@
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.4",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jest": "^26.9.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
@@ -242,6 +245,7 @@
|
||||
"lint-staged": "^12.5.0",
|
||||
"msw": "1.3.2",
|
||||
"npm-run-all": "latest",
|
||||
"orval": "7.18.0",
|
||||
"portfinder-sync": "^0.0.2",
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "2.2.1",
|
||||
@@ -285,4 +289,4 @@
|
||||
"on-headers": "^1.1.0",
|
||||
"tmp": "0.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/public/Logos/convex-logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="184" height="188" fill="none" viewBox="0 0 184 188"><path fill="#f3b01c" d="M108.092 130.021c18.166-2.018 35.293-11.698 44.723-27.854-4.466 39.961-48.162 65.218-83.83 49.711-3.286-1.425-6.115-3.796-8.056-6.844-8.016-12.586-10.65-28.601-6.865-43.135 10.817 18.668 32.81 30.111 54.028 28.122"/><path fill="#8d2676" d="M53.401 90.174c-7.364 17.017-7.682 36.94 1.345 53.336-31.77-23.902-31.423-75.052-.388-98.715 2.87-2.187 6.282-3.485 9.86-3.683 14.713-.776 29.662 4.91 40.146 15.507-21.3.212-42.046 13.857-50.963 33.555"/><path fill="#ee342f" d="M114.637 61.855C103.89 46.87 87.069 36.668 68.639 36.358c35.625-16.17 79.446 10.047 84.217 48.807.444 3.598-.139 7.267-1.734 10.512-6.656 13.518-18.998 24.002-33.42 27.882 10.567-19.599 9.263-43.544-3.065-61.704"/></svg>
|
||||
|
After Width: | Height: | Size: 811 B |
3
frontend/public/Logos/google-adk.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
frontend/public/Logos/groq.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Groq</title><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>
|
||||
|
After Width: | Height: | Size: 568 B |
3
frontend/public/Logos/langtrace.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" preserveAspectRatio="xMidYMid meet">
|
||||
<image width="200" height="200" href="data:image/webp;base64,UklGRuINAABXRUJQVlA4WAoAAAAQAAAAxwAAxwAAQUxQSEMFAAABoLz+/6M4+7ogM2lSYJ6SJrj4L3h1+x5SBO+Snph+38I8vfiU9N6jsEdTPG16Ipue7Jree0QYnirID2f5HmxTfr/v7xoREwBc9nwVjaf0/bnzd599NiqVaaxUyqVnty/l9uupeFQDGqs9wzkDbTbO6P0qaQKDQ2NTJjrULGZHBgMkCS47Y6Lj2ZllGi3U2PeFKrq0WvghplAhvMtAlxvbIgSYuy5fQw7W8hvmii1yzERuskxYWEpyvIpcZfmkIqKQ/h45/DndKRptG0NOs21+kcxP/40cL6dDoujYWEHO/73VK4T+Igpwspd/czIoyIyfb8qaVyjMF2sUfnmWPEahPkl4eHUYhbubT91TKOCHvRxKmChkM84b3+8MBc3SPq7Mvo4Cv65ypPcjCr00wI1FDAXPFnFi1ETx6zzwnkASZr2uU68hES+pLvOfQzKeC7pKe4OEfKO5SJtAUk5orgm+QWK+8btEPYfkPKe6wnsJCXrN64YskvS4C3Qk6qjjFiFVzUUOG2BkQdbjKLWEhP0420G+60ja6z7npJG4vzsmzqjDEg7pMZG8ZrczHiKBp5zg2Y0kPuyxL4FEXmyb8pRKjxW7ViOZV9vkf0GnV3PsySChM7b0Iqn7bPBO0qrYYd1WJPZGy0J/U6sy36o0kvtXi/xlev2lWbMNCb7Nkk5GMRayIo0k1y1QPtPsvdJeEomebC9PtfG2woxq1Ug7GST7sTbmMrqZc1vbgIRf11qecvmWIjXK1cKtbEPS72pBMWhnqM1iSPxYsx+o912zAvUKTbQq9arBRsuQ/Msanabf6QYBRj8zUDeIEjhYNywDQ3VZGRirK8rAJACopgyYKkAfSmEPgC4HwwCn5SAHYMiB4dFQEr+KykI0LgvxlCykdFnQ98nC/pws5C7JwvnbsnD3mSw8K8nCp7IsGBVZqMgDyuL0tCxgRRYq8mDIQvmTLJSeycKzu7Jw+7wsXMrJQm6/LOzTZUFPyUIqLgvxqCxEv5IFzWPIgQGQk4PTAMNyoAP0yEEfgGrKgKkCwKQMFAEAxmQgWzckA8N1gzIwWBcw6ccCdXCafnlouIx+yxoFq9Srao2gQL0CNP2Oej80i1Ev1kw1aGcozWAX7bZBi+Ea5WqRViBPuTy0vI5yG1qba9KNzW0NjtEtA21GqlRj4XZgnGp5aDtJtWR7ynuafVbaA51mabAwxCjGOq2AbRTbBpZqf9Gr7LcGfqVXGiyeX6HW3yGrYCO1toLlHUVaTXqtgz5a9YKdGUplwNY5r+j0wm8PrKbTarBZeUylp4pdsJhKCbDdc5hGuz32AUxR6CE4stukj9njDEgw6rA4OPV36qTBsb7rtLnucw7M/kiZkgpO7mF0YQPg7EUmWRaB00epooPzj9MkCy70XqPIJa8bQD1Hj3MquNP/hhpvguBWbYIWExq4V3tDiTcauDl4jg7n/OBu9RIVrqngdm+WBie8wEGdAOYo8HEREx1bBLwcKIntYy/wU70usuuzgae+NBMV+90HnI2bYjITwN/ehyKa6gYu7xbPYeC0J/FELI+XeHgFoKx5IY5XaxTguj8jiswc4H7vpAiK/SBC79a/eVfZ2AGCDKXLPPs7PR8E6t/GeMW2aSDYzvRnHr3XQyBgJZlnfKmOJxUQdTjD+GEei4DQ527I13hQy6+bC+KPbDPcZuwKAxGV2A+Fqluqhe9jKpBSW5ZnzjPPLAsCRQODI9mi6RRzamxoMACUVfv1M4ZdRm64RwUaa9F4St+fu3T7WalcqeB0pWJ8fnb3fG6/nopHv/IAlwEAVlA4IHgIAACwOACdASrIAMgAPm0yk0YkIyGhL7cowIANiWlu4ME0Z2dc36Rf1TwF/nn82/JL256O/rLnEfsT+C/sHnB3g+7b+q9QL8a/on6Tb4Tj/+M9ALu//uu6A9B/rT/mfcA/Tv/W+g/8e8Ab6l/afYA/iX9A/6n+T9RP/H/vvnE/Mv7r/0P8N8CP8e/m3/O/s/an9Bz9JzCMO+Myw4dZdESvjaIP6nMyViAm4nbqrdpXzuF+/VTRlZPwI47fmfvDl5hbzvvZesvGUKjN7ws4T3LThWfoRQ4AoAA8e9lYL1nVnC85qq4mvj9Iph8YxivL/rrIH+nx3GNtE7c2oXhy5gEx47Z8sNva+odGL0UOFre7a/8pcCVRR+VAqOA+CSTUwrA/FB3RytCzvsm/UJ3htyMkuiFBPkIQkgeyZRBek+/i/YVS2uzhmupStTtUD66oGcS5+EVAH8fWxWkDLcyezIzzEhXFpHDKBmiU8U3CGZPeURs5O40NHmZ1yuL/LSebx3inBTn4fScXY5NtGQmTuvJRgK1ZkPKfur71V57K9aGxoHcFJWEWoT+wdJqn/+CEXgxOXAsN7CY9yIhMXtIKmxfysDoOvJcu/r+kfQ+KAF12SAAA/tznX6ah3T+0kRzuFzn2fQ8fFQ3Jwhf+PAnqgJuUaDbeHheuyuveOy2cK7s60D6dQJ/6PJXvNmeGbDhD3I1NjZn9oc4JUzlHaJP8whuvwQH/+01Q425kRC4hJihTqmiFP5nRhpc7iojBlxQWo4EVUGzU3wuFln27vn07cdcSf8WjpT+zeWz4PVcN8pkVGsiKq7cBNzA3mqWriMtC2CSTtTYTdjMvF7Ijau6AcNe7AIgk6dia7G1bv5y7LBzTXH6/6CeEBeH+MU/ERnSMlwi7VF5hFXzLzRHBV/v1swHdmHAOmY60klxbO1sY26jVQdRXsf4BTv4aJ1LVbXI3ZFDBhVXuY/q0r3zY1ve75gXWrzLOdUpmWVI+ohESBQUZ+rU+U6osFlcstDVHqCNBEOiT1jawA4oQoqrxGTonFFIGpeu2Xj8UTnLeFE2Md3o2FwYCri8tnQ5YN/XViP9pfXkt2qOy/yEtKZZNhoBmIHfHbQrYCz5Kh2bdgqFw5O1uy/Hou6pfwPWz3f2JocCmu2hqd32lMlF7X6xR+B7KcNvMTXv9GkELd8Xu/Kfpoax7QPHsq9RNLcdLDme5+5J8r5I31b/bsb2Dr3veZ5TYKOfV6S+zqm3Pm3su3t6C2kn7SOtPaXrXGBgTZapBKzj5rvfobSFdc/F15bMv/SRizlLib5FfiLOiZ6k/j6LGHT37iY4b1+TQFbOGGn+tY5WRq6SisawiHhcRzE9LURYQtib7q0jxkwQBuBCXPTBOJAkaAG7Vp5dU+qxrV88x9ASUpuVyMu1qzoKu7SKuhpoqbqdcdNWiBV+lVb41JHBEJ7oeHuTt/PyKeXXDdZ5ohET/Y75lxzxI6uzQ5r4IGRvLWul1kvJbP1nnNsAoQhO0tt0L60Pp7S5cTCavKf2FyYs/nxKXq6DKJtk8uRIycOOtWfyvZHYkjulFNNFR/IjND9uujdq66p3qRvf1Nm8SZOIwpni9IrNC9uOzRXnq9BhaDPbRNwLK2DHcYuH1nwzm4cUSUBu7gk5JC5UGE1CfP0TLXwWWr+6JUra/cyPfLM1oeynDbzAToAPUuWA2L9545vaFlmshP8Nj7r96uNDW6ngAyjF0RPM4pH5pGHzui7GI9KmamJujUcZHQLh3/w1hSn4IJdpYUoZMQHC3iKNFeOvi94ZueafCvFyASXifsLgpzzwNt9z5juYKX+CJES84eVuQ5+BncnQF6BZqo2ezHyrmL/ctjxvWpRnj33RIWMu1kccgXDdGBgevRfNIHxouiYJs6eE/Gt593jZcGq/L55a1l9OO/jzhuDz4lDbhFfmbpK/sEfDFeY3XHjDMj+TkeVBLAbJ5EVVKCYeWVurm5iroOfGnBuUSzeP2mKC2ENA3HaMy2jTzGV7FMun64t97WY1zOM8R5FG7QyAeZuNQos1KH15jO1Dymt2/DO6bHgFWloeRbpv+sovdYJSx2/gjgvYHEO2VwRFao5Bny2/MA31GzjN4eTyWvA16rhRu0NMu477gNZEaCRgHvcTVDchTfVckI4Cx4gd4YhUAgjKD5wvCFwX8upRTi2Rdy3FeXDhmw3xEntdU9rGdmncE7jFw+s+vAR2v+V2PjV7jAf9YvwKTdX8v/49RppBE21OCxhVheNMphMDyhtGRopafi54pihoCFgfyYzNzbyFH5voOmGEIIE+cSgHxkMFNbiR3/7BbwcP5SBwy6u/vmP02xcKsRdFj1UW7ZLxdk8dzs8oZEioxMnOR2rxrQ+N9z5nc6U7u40DDVEdwbJuyyQHU/LiE4wmO1N3bxC33X10I8gHmjukAh/Axq6s/eRqK1hwiHkR98coaA6CjFu3hAWv3mwnvK2Y+j/MzAuzOaxEiEMKOJp8JB+dsF7YyNwRM9+pFGSmF95KwrPdEM+/ZUBRDawKe5Kh0NkfZPSesQvMnn3ZgX+3P8ebe1SHo/4p00CyCfNiZ+efcxFg/mw1jqPsN7MOOv+lOglbWG3QEqlDs3OqCKIzANydcibObrwLNDGusfzk42/iksBPVgCgkil6O65u7r5O+q6NhFJRIaVVtGEYSUXNrHnsnBz30tuyBsc1bP9wF3KI7iNCPcDEV+4lT5OMJp5vKeg6p4BGfY76Aap/wY5Pdu/JHo3yI47AqoWq3Z5/P+O6mBnP3a6N/BdS62DLwp0WWwLAn/k1aiJr2oMje765btFUIiGn0MJ3o4nk4ANHxLRR9HrZndMvIAcAOBq5offJZ+HpBeAQ5WEqiPOd5kpWNg0wkdJCMpHwJ4AAAAA==" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
3
frontend/public/Logos/openlit.svg
Normal file
|
After Width: | Height: | Size: 14 KiB |
3
frontend/public/Logos/traceloop.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" preserveAspectRatio="xMidYMid meet">
|
||||
<image width="256" height="256" href="data:image/webp;base64,UklGRjoOAABXRUJQVlA4WAoAAAAQAAAA/wAA/wAAQUxQSLsGAAAB8IZtE+q22baNJFsOrKIpqC01lLw1jOWayszMTGFmZo6s0nWVmQzXFeb4KoUjwwVO1BguV3HBDIL5YZqZNda/bIqICYCw/8P+DxvYmulyt2AX/ce+2UkyIm5/v8iHWra4XZnWDt1ciF178NP+Au7/H2pbeHN7did2/dV3cfX4HDV22tuw52AoGHicp8c+1DnH3sqJoaH/OpavUWsnANyEoeKvPRmeQL1vAmthyIBT6ezlihVaMzB0rLCRPYKap2eFEHg12beqZblDiclkv6nmbg4l3qEyUPVmDCW/pOqnGwZDifepLtAt6AklFlBBnWqer0OJu8j2qPb1UyFEw7lkk1V7yqgKHf4G5PGNilUZMClkaEygg+WKTQKI/DlUmACMRrFaP0cCQL/S0OADCwdcekap0n7QekBRKOC0AW+KR6WiAdC2scHf1VU+COwXvK+Pf4MBHUxaW9qFBX58tTtITMmqVKV0bRJ01nHDXV3z7aPOBbkJaXdpeYMDwv4/C6GR+uJYLV/OPF+MdeCjY8W/ekusnJQHxtK/mGp0JiPXh5oGdtxlkeBY5UVz5j8aISFmYSny+nIzOtJnM+p7IJnNNr0RzXtsMN9LNShwc592RnhR49oMph7/RFM3PcBk/whleke0MbAadW66jiVyG5o8cBeL5TOUWj0QAIwS1Pq3OI7laPraJI7xKLfEAFiOen/IMNBvPtzG0L9BEC6H6AbFgol0X6GGY+g2ouSG6NdQ80VkF/pUeJfM/qcofC1PtYNkD6CKZWTXouy8MtVaIqgW64AxVG8KK0Pd46neV2Iw1VJh2juoPtJtjbR63XpQOZVIppohrP6gauVA/YYOTRFUDwo7uEa1T8kG6bAbqHsJWzNKtTvJoEiFF8hgj6xRkK/Yf6x0L2pwpjvdnaLyAUYE9LoF6COOKPAMMG4XFBgBANPU2gCcl1ab7gvg7OuVMw1ab1LqcxsLpNabbEd3Fhh6RsomaHuCT6HgQhswDykxlcsOzJccF+GbAO1fnq3O7pHA32P+X6b5MRX47ZOq+LIvhw4nTMx2e7Qsypt5Ocjs+dDffjzlkV68dd4wkBl1tyv/pIfcnT0xAcL+P9twwsRst4f8ZL7rnighQ+duLvaI//FvjxhCLpuZW+TR0p09MaFjl2cje9Vku4AbfkSz/rWop4DhO1Hb7Ms7MMGHEo9fwmXfiGY+NYzLuiCI+vomtLMJhf4+nCdqG5q7IZXH9gnqvKmNaSjW24/lUzR7bQrLWtR6GgCMCMjBnRxPovmPRzDciGoHRgDko+S76Lr/XwF8ic5aqBfmwygUfYDuOdTwv3S3o+aj1sjCvmTbVcBhZB+ptuagsIepbI06jCX7VbWD9cLmUF2EOrqouqPq9Sh8LdVgJT6n6qOb+JVUVyjxAVW0cmXCxlFdqMQyKmuzamV5wq6ngtM6PEQFP6uW95qs6iiyt1TwxZLNU+216AZRm4B8pArfAHmCX7GGaFguqdFBB/9QIDCIDv6m2HIAo0TQRGBMrDXfCmCMrVSrxACAgdVivrRwwB0Bs+2I5IBrGpSqHgitR3iFfGoH3vuazLWlB/CmV6vkHQFt99ksoeY1C3APPmKipuk24E48oNDmPtDBjFwfU9nCWBBoe/iASbxrHCDQctf2gCq+3AzopJH64lj6B1MsIDX2llfHin9soBWknpfx8lgtX0w1IOz/sxA6briL/LbhhhzrxZl3aXltX0GxV90l/gZHp5LWliKrf++zdhGj3/8DNT251CEiZnYBmrJ0bVJHjA1+5D91M1+fb1HdlsVRbLZJdWha/wajnQFFKHO+hWmMFzU+GMd07lY0ddGANvqVotQVPCMbUOeiGJYe/0KTl/YDgMifUe6jHLEVqPU2C8ff0fQ/RwJMQsF/RDN8gHo/zZCJCk4Co0oSLqG7LKjY6Qi6nzSoMp5C0ZU2spWo+a1kQ1DFp76WhaPJilVzkc3U4WuPsNepzkHVD5J9r4MnKGw5VYpulWSHdQii8LVUg3X7i+y4DtgsbAFVsm6nyX7Wodkt7EmqSL9qu8k+1sGdJSyJCvJVW0D2kg5ZGbKOAfk41QaT9Q6okG4tFPUU3fm1iu0B+s81KLTCTZKORdDBJL2CoxgSGhW4CQCcchquAEbbHrUWAOcL5nMCANhzpLTcCqzRJ5T6yMICS82WY28FdqeMymuB+fzNKi22APM4n6mcdmj35kK+lndjgN3yfIU6B68B/kF7zFN4M3TUmulytzB4t0/pCyLtD3z8X78ajUc2XAMyhy8/WCevxe3KtELY/2H/hw0OAFZQOCBYBwAAcDIAnQEqAAEAAT5tMpJFpCOhmApkyEAGxLE3cLO/I/8A/AD9APeogBDf+wH8n/gH4AfoB/D8D/bUGJv3b8rvGLBD2z8kv6L+13LBGO/UZ+8+1n5jf2T+j+wDzCP0i/03UP8wn6r/83/Ye+Z/jv7B7I/13/ID5AP6p/eP/Z7WHqRft57AH8g/2XpTfsZ/3fkf/a/9xvgd/XX/4ewB6AHVf9AP4B+AH6V0/6X/37XCnfIaNJCv3UeIbyMN5GG7FGGXSbcdew8aH+Bc8jPujAWD5wlI0vGluc15OXPagLZ8UxFfBAlngGwyAgncED6mebGS9plnBjVRDpz0QjHI7NbLJCxCvFKIDQxTIXTAX5W3acocmvQ/OXAfnjdGNjAFiZXJq+/TbBzGEHdg+muQ+hFFPR2JnvUSNpSYiGR57j5nlFet8+EyOPVmbGlR6r47DqM+38DTtDHOHz36jaDYexg9qHpxk3+59nQC1SOzy4m4u34GLkKXdKCpMehdu53FcSHUZwi37E2m0zNsuu+Q0aSFfuo8Q3kYbyMN5GG6AAD8hoAAAAAAAtSatOsx3/xQJLZJVNwvp3oF/sWdRuskPj8s391kh8d5sIy+9WFVihdPhMB/7oNptL8qeszrOKuQiABZ2qtQOEHxZqWJnio26h/5Vo4krKGOO0sEz6Hv5rWo4fY5j+Zkip4N2flcerdMsQa4aobjtm8EeilMPd1RKe3T2L0a/Cv9h3t065fg6SjwXyBvg2hYN5RVMz/+NTMiir38FhGGkGCU+n7zH7a6z+ZsKOKACwNxEF24gJYE5Z8YDT/LDnQYzNPs7fnw0DWqqrx34u1n8kIsG90iig+vnPhkaVDzEnlMOItgwAov/p/WcPmzTS/ESKniY3/MPn06vQ7wic6l2jPoEyPrpPntLEOo7YMH2DlbEeYrf53exjRiZcoP/Ct0vnxkiHQpZk7RAgR6cllYgknBYToYbOnOoJ31iIPn+tjP9Wi1AD4rjN8FcbwzXiiXhq7Gpi/P39FnE7kEHxM3vJ6IfeN8kET6YhP9+bS4Ndq9z76kggowPI6pCeXTbktqMjiZBKP512NVTKWySMPWK2cDgYHLavMlfOsRt+ejI3anQJ8WOB5Sj4jg+PmmRO0YbrVrKJlMyW9DQhQxUYSb68hxwsa0pT95muv26wX/UbDfNNZqiY+FY3E3IrOLBfx/PvkZag+Z1tU15DmS4oWxI1MZPiItkOO5brQrLJ4gEX/aqDZVYH5qQqEYAdzEawMyn+D8EIsnVFuZBNbvILVTzHSwk5p+TPibb4Bij8La/IJ3+b8zP3KyRwEs7cd4bm+Kk65mOeRHpY//wUWtOhkyo+qCpkP9B+BZwjtgg93EV8KAePfDCEvQd6szMwcXg364KeljltsktN3CEwdKtH5sTvqV4jy9/ySVzVng2aUX/rOq5NjhT5vpK5nuKGWPdhY6RWG/JJfzsRE9JFNVJDLuZWWPyOhtK52DWPq8KkvizSdW1HXse/jEbnWBMMY4QwuO7eYDSR462HFk4zWyEZKugT+Uop5KD4KF+4xpfPgWfPcD4doS/+fEI2HjOrl8ctTT0EKp255SHoe3jdEi6QVMzZoAJS4j6vntijLltNjzgznBcSIrdKAgCdZjX9+VDrtqECJvxN3Z0S2oEoVZf2tji5V6DXamlubRVYpvsgB0DK/ACFIRFc4ggMNRpcbv9wYeN/ch3k6B5RkfzyDTkXb+RkS8ZLdbq1+tq/I8eK9g1/wuAZNhQkc5QmjUYgl3QAo86JmxC4zO0au6U/fSE11r16YB3WkeheQcJUKCbRTGS8OOs30DN7fpcxuuyEU3yM8HEqoyjd1rdQAi9K6LdRvQyLh3A9JmvA1EAjN8i2uLOeZi81BhiwAsZle61AOM78/l7yxuHy40ECHE9YostmdueER4HTWcMMGg0X8eJcujrbN1S4qyGxITPLghrKViISKTyPZ5qSo6EwnWhwA+XrAtKvtNVyS5f2iWAFmfHlQxfB+NXNwhxPwQ2akkMg3wgSifNE4lDRelowqblVu+ByzHBWsJkemp5mw2u/1Wl+RmpkyV7U5cIjxTkKyprg12fSsb7L3rIIHxd4ZgDArHDgESQGwDGC+iMlFrZKAfhcSUxuvPko5fkWdmQ/+oI2RMP4/d0h4/foef6PFHier1x+nRlT+0ZjBnE6Z32aVtbLbEPc+Z/EgAGsRF1RpteYADfJzufleSz65a2AZGpwwvDlrHbL9WdGx4Ww776VnxhYV27x6KH0biDB/mTxsIwv4zQQiKkDjcibMmzgp2PvZ3foNNj+SEDx11+lqzPpayPGMBRlQgr9yVhV93j2rzdy+mYvCoHNBVmA6kubmaB2ilYOI/AMpq6CFrzpkL8UJoedOwyLxtwJIWPjP+a++KkMbgYxBuo7dViu8XMjZ9jD1lMMABEIMT5fvOnzyyoKUFfnseMxeoFZjuySXC9vxk95ivyfGaZgXSPDdaiQAGYrj9aBUAAAAAAAA=" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -12,5 +12,6 @@
|
||||
"pipeline": "Pipeline",
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics"
|
||||
"logs_to_metrics": "Logs To Metrics",
|
||||
"roles": "Roles"
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
"pipeline": "Pipeline",
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics"
|
||||
"logs_to_metrics": "Logs To Metrics",
|
||||
"roles": "Roles"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"FORGOT_PASSWORD": "SigNoz | Forgot Password",
|
||||
"HOME": "SigNoz | Home",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
@@ -72,5 +73,6 @@
|
||||
"API_MONITORING": "SigNoz | External APIs",
|
||||
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
|
||||
"METER": "SigNoz | Meter"
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles"
|
||||
}
|
||||
|
||||
1
frontend/public/svgs/barber-pool.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path fill="#fff" d="m9.05 20.616 13.968-.178-.06-9.283-14.417.178z"/><path fill="#d80d1a" d="m10.165 20.736-1.302-2.253.127-5.209 5.133 7.491z"/><path fill="#1d86fa" d="m16.72 20.616-6.416-9.401 4.298.06 6.536 9.372z"/><path fill="#d80d1a" d="m22.958 19.125-5.58-7.88 4.118.12 2.12 2.956z"/><path fill="#a1d2d6" d="m8.75 11.722 14.208-.09-.098-.86-14.14.114zM8.99 21.214l14.237-.18-.329-.864-14.237.177z"/><path fill="#c8c8c8" d="M22.772 21.163c.122.147.882.175.882.175s.444.825 1.41.707c.865-.106 1.2-.829 1.2-.829s1.463-.338 2.259-1.464c.846-1.2.882-2.383.882-4.076 0-1.836-.143-3.088-1.076-4.076-1.073-1.134-2.186-1.218-2.186-1.218s-.39-.654-1.271-.636c-.883.018-1.218.74-1.218.74s-.704.151-.882.282c-.091.067-.067 2.703-.054 5.294.012 2.5-.042 4.988.054 5.1"/><path fill="#858585" d="M28.541 16.538c.353.018.345-1.851.036-3.229-.354-1.57-1.694-2.082-1.8-1.993s-.071 1.675.017 1.764c.09.09.918.494 1.2 1.182.283.69.278 2.262.547 2.276M25.299 21.99l-.056-12.213s.258.04.509.207c.242.162.389.395.389.395l.127 10.828s-.176.325-.35.465c-.381.309-.62.318-.62.318"/><path fill="#e1e0e0" d="m24.692 22.043-.157-12.255s-.323.04-.565.258c-.204.182-.318.44-.318.44l.107 11.01s.164.223.353.354c.23.157.58.193.58.193"/><path fill="#c8c8c8" d="M9.13 21.32c.158-.142.036-10.216-.053-10.392-.089-.175-.829-1.069-2.01-1-1.167.069-1.536.876-1.536.876s-1.169.155-2.065 1.5c-.635.954-.82 2.19-.806 3.74.015 1.712.12 3.319 1.142 4.341.982.982 1.94 1.022 1.94 1.022s.267.87 1.573.87c1.289.002 1.816-.956 1.816-.956"/><path fill="#858585" d="M7.548 22.274s-.373-1.907-.427-6.158.256-6.186.256-6.186.258-.017.504.067c.276.093.458.24.458.24s-.244 3.656-.227 5.879c.018 2.222.37 5.85.37 5.85s-.265.153-.423.206c-.164.051-.51.102-.51.102"/><path fill="#e1e0e0" d="M6.858 22.234s-.48-2.45-.533-6.032.157-6.163.157-6.163-.422.127-.633.322c-.21.196-.318.44-.318.44s-.122 3.359-.14 5.39c-.02 2.454.351 5.216.351 5.216s.085.296.345.496c.367.285.771.331.771.331"/><path fill="#fff" d="M6.1 16.763c.19 0 .3 1.251.38 2.58.045.744.411 2.058-.08 2.058-.49 0-.584-.476-.617-2.09-.034-1.612.064-2.548.317-2.548M3.173 16.667c.12-.042.35.634.903 1.156.39.369.76.49.855.744.096.254.222 1.613-.062 1.631-.396.023-1.022-.262-1.393-.933-.59-1.062-.525-2.518-.303-2.598"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
frontend/public/svgs/tv.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path fill="#455a64" d="m15.255 10.911-4.82-6.048a.204.204 0 0 1 .025-.278.2.2 0 0 1 .284.018l5.127 5.775a.2.2 0 0 1-.018.284l-.307.271a.2.2 0 0 1-.291-.022M16.494 10.878l-.276-.245a.22.22 0 0 1-.017-.313l5.097-5.746a.22.22 0 0 1 .313-.018c.09.078.1.213.027.307l-4.822 5.99a.227.227 0 0 1-.322.025"/><path fill="#455a64" d="M12.389 11.214c0-.775 1.72-1.406 3.842-1.406s3.842.629 3.842 1.406z"/><path fill="#82aec0" d="M28.353 29.337H3.652c-.544 0-.985-.458-.985-1.02V11.8c0-.564.44-1.02.985-1.02h24.703c.542 0 .985.458.985 1.02v16.516c-.003.562-.443 1.02-.987 1.02"/><path fill="#212121" d="M27.637 28.368H4.438a.79.79 0 0 1-.789-.79V12.456a.79.79 0 0 1 .79-.79h23.198c.436 0 .789.354.789.79v15.124a.79.79 0 0 1-.79.789"/><path fill="#b9e4ea" d="m5.2 25.995-1.068 1.677c-.186.293.014.691.36.694h18.414a.536.536 0 0 0 .478-.77l-.667-1.373z"/><path fill="#455a64" d="M27.312 27.662h-2.53a.464.464 0 0 1-.465-.464V21.92c0-.258.209-.464.464-.464h2.531c.258 0 .465.209.465.464v5.278a.464.464 0 0 1-.465.464M26.048 20.226a1.531 1.531 0 1 0 0-3.062 1.531 1.531 0 0 0 0 3.062"/><path fill="#82aec0" d="M24.966 17.798a.254.254 0 0 0-.009.313l.504.678c.158.21.363.382.6.497l.76.37a.254.254 0 0 0 .316-.38l-.505-.678a1.65 1.65 0 0 0-.6-.498l-.76-.369a.26.26 0 0 0-.306.067"/><path fill="#455a64" d="M26.048 16.268a1.531 1.531 0 1 0 0-3.062 1.531 1.531 0 0 0 0 3.062"/><path fill="#82aec0" d="M26.049 13.33c-.118 0-.22.08-.247.194l-.2.822a1.65 1.65 0 0 0 0 .78l.2.822a.254.254 0 0 0 .493 0l.2-.822a1.65 1.65 0 0 0 0-.78l-.2-.822a.256.256 0 0 0-.247-.194"/><path fill="#2f7889" d="M22.992 25.219c-.17.77-.742 1.366-1.475 1.529-1.205.269-3.45.604-7.148.604-3.883 0-6.487-.369-7.89-.642-.775-.151-1.384-.787-1.536-1.607-.348-1.885-.786-5.652.018-10.357.15-.871.814-1.54 1.643-1.653 1.495-.207 4.167-.483 7.765-.483 3.414 0 5.7.25 6.997.45.797.124 1.435.77 1.598 1.608.962 4.957.43 8.733.028 10.55"/><path fill="#212121" fill-rule="evenodd" d="M14.37 13.055c-3.576 0-6.227.274-7.705.478-.623.085-1.147.593-1.265 1.288-.794 4.64-.361 8.353-.02 10.201.121.653.6 1.138 1.184 1.252 1.375.267 3.951.634 7.805.634 3.672 0 5.884-.334 7.051-.594.553-.122 1.002-.576 1.139-1.192.392-1.772.917-5.485-.032-10.369-.13-.67-.633-1.161-1.23-1.254-1.272-.197-3.536-.444-6.928-.444m-7.827-.403c1.514-.209 4.206-.486 7.826-.486 3.436 0 5.746.25 7.064.454h.001c1 .156 1.771.959 1.966 1.964.976 5.028.439 8.867.027 10.73-.206.928-.9 1.665-1.814 1.868-1.242.277-3.52.615-7.244.615-3.91 0-6.544-.372-7.975-.65-.967-.19-1.705-.976-1.887-1.963m2.036-12.532c-1.035.142-1.84.972-2.02 2.02-.815 4.768-.372 8.59-.016 10.512" clip-rule="evenodd"/><path fill="url(#a)" d="M8.686 14.175c.373-.045 1.08-.013 1.329.744.248.758-.298.963-.591 1.187-.843.642-1.26.887-1.767 1.587-.404.56-1.111.384-1.322.015-.169-.298-.293-1.253.064-1.884.771-1.351 1.914-1.605 2.287-1.649"/><defs><linearGradient id="a" x1="8.303" x2="8.07" y1="9.59" y2="18.014" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
44
frontend/scripts/post-types-generation.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "\n\n---\nRenamed tag files to index.ts...\n"
|
||||
# Rename tag files to index.ts in services directories
|
||||
# tags-split creates: services/tagName/tagName.ts -> rename to services/tagName/index.ts
|
||||
find src/api/generated/services -mindepth 1 -maxdepth 1 -type d | while read -r dir; do
|
||||
dirname=$(basename "$dir")
|
||||
tagfile="$dir/$dirname.ts"
|
||||
if [ -f "$tagfile" ]; then
|
||||
mv "$tagfile" "$dir/index.ts"
|
||||
echo "Renamed $tagfile -> $dir/index.ts"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "\n✅ Tag files renamed to index.ts"
|
||||
|
||||
# Format generated files
|
||||
echo "\n\n---\nRunning prettier...\n"
|
||||
if ! prettier --write src/api/generated; then
|
||||
echo "Prettier formatting failed!"
|
||||
exit 1
|
||||
fi
|
||||
echo "\n✅ Prettier formatting successful"
|
||||
|
||||
|
||||
# Fix linting issues
|
||||
echo "\n\n---\nRunning eslint...\n"
|
||||
if ! yarn lint --fix --quiet src/api/generated; then
|
||||
echo "ESLint check failed! Please fix linting errors before proceeding."
|
||||
exit 1
|
||||
fi
|
||||
echo "\n✅ ESLint check successful"
|
||||
|
||||
|
||||
# Check for type errors
|
||||
echo "\n\n---\nChecking for type errors...\n"
|
||||
if ! tsc --noEmit; then
|
||||
echo "Type check failed! Please fix type errors before proceeding."
|
||||
exit 1
|
||||
fi
|
||||
echo "\n✅ Type check successful"
|
||||
|
||||
|
||||
echo "\n\n---\n ✅✅✅ API generation complete!"
|
||||
@@ -1,3 +1,6 @@
|
||||
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import getAll from 'api/v1/user/get';
|
||||
@@ -9,9 +12,6 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
|
||||
@@ -297,7 +297,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
}, [isLoggedInState, pathname, user, isOldRoute, currentRoute, location]);
|
||||
|
||||
// NOTE: disabling this rule as there is no need to have div
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
@@ -30,9 +33,6 @@ import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { extractDomain } from 'utils/app';
|
||||
|
||||
@@ -218,12 +218,8 @@ function App(): JSX.Element {
|
||||
pathname === ROUTES.ONBOARDING ||
|
||||
pathname.startsWith('/public/dashboard/')
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Pylon('hideChatBubble');
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Pylon('showChatBubble');
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
@@ -194,6 +194,10 @@ export const Login = Loadable(
|
||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||
);
|
||||
|
||||
export const ForgotPassword = Loadable(
|
||||
() => import(/* webpackChunkName: "ForgotPassword" */ 'pages/ForgotPassword'),
|
||||
);
|
||||
|
||||
export const UnAuthorized = Loadable(
|
||||
() => import(/* webpackChunkName: "UnAuthorized" */ 'pages/UnAuthorized'),
|
||||
);
|
||||
@@ -304,3 +308,15 @@ export const PublicDashboardPage = Loadable(
|
||||
/* webpackChunkName: "Public Dashboard Page" */ 'pages/PublicDashboard'
|
||||
),
|
||||
);
|
||||
|
||||
export const AlertTypeSelectionPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
|
||||
),
|
||||
);
|
||||
|
||||
export const MeterExplorerPage = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
|
||||
import MessagingQueues from 'pages/MessagingQueues';
|
||||
import MeterExplorer from 'pages/MeterExplorer';
|
||||
import { RouteProps } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import {
|
||||
AlertHistory,
|
||||
AlertOverview,
|
||||
AlertTypeSelectionPage,
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
ApiMonitoring,
|
||||
@@ -17,6 +15,7 @@ import {
|
||||
DashboardWidget,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
ForgotPassword,
|
||||
Home,
|
||||
InfrastructureMonitoring,
|
||||
InstalledIntegrations,
|
||||
@@ -28,6 +27,8 @@ import {
|
||||
LogsExplorer,
|
||||
LogsIndexToFields,
|
||||
LogsSaveViews,
|
||||
MessagingQueuesMainPage,
|
||||
MeterExplorerPage,
|
||||
MetricsExplorer,
|
||||
OldLogsExplorer,
|
||||
Onboarding,
|
||||
@@ -339,6 +340,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: false,
|
||||
key: 'LOGIN',
|
||||
},
|
||||
{
|
||||
path: ROUTES.FORGOT_PASSWORD,
|
||||
exact: true,
|
||||
component: ForgotPassword,
|
||||
isPrivate: false,
|
||||
key: 'FORGOT_PASSWORD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.UN_AUTHORIZED,
|
||||
exact: true,
|
||||
@@ -391,28 +399,28 @@ const routes: AppRoutes[] = [
|
||||
{
|
||||
path: ROUTES.MESSAGING_QUEUES_KAFKA,
|
||||
exact: true,
|
||||
component: MessagingQueues,
|
||||
component: MessagingQueuesMainPage,
|
||||
key: 'MESSAGING_QUEUES_KAFKA',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
|
||||
exact: true,
|
||||
component: MessagingQueues,
|
||||
component: MessagingQueuesMainPage,
|
||||
key: 'MESSAGING_QUEUES_CELERY_TASK',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.MESSAGING_QUEUES_OVERVIEW,
|
||||
exact: true,
|
||||
component: MessagingQueues,
|
||||
component: MessagingQueuesMainPage,
|
||||
key: 'MESSAGING_QUEUES_OVERVIEW',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL,
|
||||
exact: true,
|
||||
component: MessagingQueues,
|
||||
component: MessagingQueuesMainPage,
|
||||
key: 'MESSAGING_QUEUES_KAFKA_DETAIL',
|
||||
isPrivate: true,
|
||||
},
|
||||
@@ -455,21 +463,21 @@ const routes: AppRoutes[] = [
|
||||
{
|
||||
path: ROUTES.METER,
|
||||
exact: true,
|
||||
component: MeterExplorer,
|
||||
component: MeterExplorerPage,
|
||||
key: 'METER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METER_EXPLORER,
|
||||
exact: true,
|
||||
component: MeterExplorer,
|
||||
component: MeterExplorerPage,
|
||||
key: 'METER_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METER_EXPLORER_VIEWS,
|
||||
exact: true,
|
||||
component: MeterExplorer,
|
||||
component: MeterExplorerPage,
|
||||
key: 'METER_EXPLORER_VIEWS',
|
||||
isPrivate: true,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import cacheBursting from '../../i18n-translations-hash.json';
|
||||
|
||||
|
||||
48
frontend/src/api/ErrorResponseHandlerForGeneratedAPIs.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
// Handles errors from generated API hooks (which use RenderErrorResponseDTO)
|
||||
export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
error: AxiosError<RenderErrorResponseDTO>,
|
||||
): never {
|
||||
const { response, request } = error;
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (response) {
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: response.data.error.code,
|
||||
message: response.data.error.message,
|
||||
url: response.data.error.url ?? '',
|
||||
errors: (response.data.error.errors ?? []).map((e) => ({
|
||||
message: e.message ?? '',
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
// The request was made but no response was received
|
||||
if (request) {
|
||||
throw new APIError({
|
||||
httpStatusCode: error.status || 500,
|
||||
error: {
|
||||
code: error.code || error.name,
|
||||
message: error.message,
|
||||
url: '',
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
throw new APIError({
|
||||
httpStatusCode: error.status || 500,
|
||||
error: {
|
||||
code: error.name,
|
||||
message: error.message,
|
||||
url: '',
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
CreateIngestionKeyProps,
|
||||
IngestionKeyProps,
|
||||
} from 'types/api/ingestionKeys/types';
|
||||
|
||||
const createIngestionKey = async (
|
||||
props: CreateIngestionKeyProps,
|
||||
): Promise<SuccessResponse<IngestionKeyProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.post('/workspaces/me/keys', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default createIngestionKey;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types';
|
||||
|
||||
const deleteIngestionKey = async (
|
||||
id: string,
|
||||
): Promise<SuccessResponse<AllIngestionKeyProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.delete(
|
||||
`/workspaces/me/keys/${id}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteIngestionKey;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import {
|
||||
AllIngestionKeyProps,
|
||||
GetIngestionKeyProps,
|
||||
} from 'types/api/ingestionKeys/types';
|
||||
|
||||
export const getAllIngestionKeys = (
|
||||
props: GetIngestionKeyProps,
|
||||
): Promise<AxiosResponse<AllIngestionKeyProps>> => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { search, per_page, page } = props;
|
||||
|
||||
const BASE_URL = '/workspaces/me/keys';
|
||||
const URL_QUERY_PARAMS =
|
||||
search && search.length > 0
|
||||
? `/search?name=${search}&page=1&per_page=100`
|
||||
: `?page=${page}&per_page=${per_page}`;
|
||||
|
||||
return GatewayApiV1Instance.get(`${BASE_URL}${URL_QUERY_PARAMS}`);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-throw-literal */
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
AddLimitProps,
|
||||
LimitSuccessProps,
|
||||
} from 'types/api/ingestionKeys/limits/types';
|
||||
|
||||
interface SuccessResponse<T> {
|
||||
statusCode: number;
|
||||
error: null;
|
||||
message: string;
|
||||
payload: T;
|
||||
}
|
||||
|
||||
interface ErrorResponse {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message: string;
|
||||
payload: null;
|
||||
}
|
||||
|
||||
const createLimitForIngestionKey = async (
|
||||
props: AddLimitProps,
|
||||
): Promise<SuccessResponse<LimitSuccessProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.post(
|
||||
`/workspaces/me/keys/${props.keyID}/limits`,
|
||||
{
|
||||
...props,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
// Axios error
|
||||
const errResponse: ErrorResponse = {
|
||||
statusCode: error.response?.status || 500,
|
||||
error: error.response?.data?.error,
|
||||
message: error.response?.data?.status || 'An error occurred',
|
||||
payload: null,
|
||||
};
|
||||
|
||||
throw errResponse;
|
||||
} else {
|
||||
// Non-Axios error
|
||||
const errResponse: ErrorResponse = {
|
||||
statusCode: 500,
|
||||
error: 'Unknown error',
|
||||
message: 'An unknown error occurred',
|
||||
payload: null,
|
||||
};
|
||||
|
||||
throw errResponse;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default createLimitForIngestionKey;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types';
|
||||
|
||||
const deleteLimitsForIngestionKey = async (
|
||||
id: string,
|
||||
): Promise<SuccessResponse<AllIngestionKeyProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.delete(
|
||||
`/workspaces/me/limits/${id}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteLimitsForIngestionKey;
|
||||
@@ -1,65 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-throw-literal */
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
LimitSuccessProps,
|
||||
UpdateLimitProps,
|
||||
} from 'types/api/ingestionKeys/limits/types';
|
||||
|
||||
interface SuccessResponse<T> {
|
||||
statusCode: number;
|
||||
error: null;
|
||||
message: string;
|
||||
payload: T;
|
||||
}
|
||||
|
||||
interface ErrorResponse {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message: string;
|
||||
payload: null;
|
||||
}
|
||||
|
||||
const updateLimitForIngestionKey = async (
|
||||
props: UpdateLimitProps,
|
||||
): Promise<SuccessResponse<LimitSuccessProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.patch(
|
||||
`/workspaces/me/limits/${props.limitID}`,
|
||||
{
|
||||
config: props.config,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
// Axios error
|
||||
const errResponse: ErrorResponse = {
|
||||
statusCode: error.response?.status || 500,
|
||||
error: error.response?.data?.error,
|
||||
message: error.response?.data?.status || 'An error occurred',
|
||||
payload: null,
|
||||
};
|
||||
|
||||
throw errResponse;
|
||||
} else {
|
||||
// Non-Axios error
|
||||
const errResponse: ErrorResponse = {
|
||||
statusCode: 500,
|
||||
error: 'Unknown error',
|
||||
message: 'An unknown error occurred',
|
||||
payload: null,
|
||||
};
|
||||
|
||||
throw errResponse;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default updateLimitForIngestionKey;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IngestionKeysPayloadProps,
|
||||
UpdateIngestionKeyProps,
|
||||
} from 'types/api/ingestionKeys/types';
|
||||
|
||||
const updateIngestionKey = async (
|
||||
props: UpdateIngestionKeyProps,
|
||||
): Promise<SuccessResponse<IngestionKeysPayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.patch(
|
||||
`/workspaces/me/keys/${props.id}`,
|
||||
{
|
||||
...props.data,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default updateIngestionKey;
|
||||
@@ -4,8 +4,6 @@ export const apiV2 = '/api/v2/';
|
||||
export const apiV3 = '/api/v3/';
|
||||
export const apiV4 = '/api/v4/';
|
||||
export const apiV5 = '/api/v5/';
|
||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||
export const gatewayApiV2 = '/api/gateway/v2/';
|
||||
export const apiAlertManager = '/api/alertmanager/';
|
||||
|
||||
export default apiV1;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { GatewayApiV2Instance as axios } from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { DeploymentsDataProps } from 'types/api/customDomain/types';
|
||||
|
||||
export const getDeploymentsData = (): Promise<
|
||||
AxiosResponse<DeploymentsDataProps>
|
||||
> => axios.get(`/deployments/me`);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { GatewayApiV2Instance as axios } from 'api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
PayloadProps,
|
||||
UpdateCustomDomainProps,
|
||||
} from 'types/api/customDomain/types';
|
||||
|
||||
const updateSubDomainAPI = async (
|
||||
props: UpdateCustomDomainProps,
|
||||
): Promise<SuccessResponse<PayloadProps> | AxiosError> =>
|
||||
axios.put(`/deployments/me/host`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
export default updateSubDomainAPI;
|
||||