Compare commits

..

10 Commits

18 changed files with 68 additions and 78 deletions

9
.claude/rules/contrib.md Normal file
View File

@@ -0,0 +1,9 @@
# Contribution guidelines
- When making Go changes, always ensure they follow the contributing guildelines in [`docs/contributing/go/`](../../docs/contributing/go/).
- If any API contract is modified, generate the OpenAPI specs with `make gen-openapi-specs`.
- Always keep the OpenAPI spec generated in a separate commit, so the whole commit can be dropped in case of conflicts during merge. Do not try to resolve conflict in generated files, instead just generate them again.
- Avoid breaking function calls unncessarily into multilines for couple of arguments.
- Try to keep most computational only logic in types package itself related to a domain type, use modules as the orchestraction layer cordinating different layers and all db queries in store layer. Check the serviceaccount modules for inspiration when confused.
- When defining types, keep the structure of file to have any constants and variables first, then exported types and exported methods and then finally the unexported types and methods.
- Never import types or other modules in migration files, duplicate the required type or method to keep migration free from changes.

View File

@@ -2,6 +2,9 @@
- **Follow the template** (`.github/pull_request_template.md`): fill in its headings (Description / Issues closed by this PR / Screenshots / Additional Information). Don't add sections the template doesn't have.
- **Keep only the headings that apply.** Delete every heading that has nothing under it, along with its `<!--...-->` placeholder comment. The body must never contain an empty heading — if only Description applies, the body has exactly that one heading.
- **Keep the description concise and human-readable.** A few plain bullets saying what changed and why, for a reviewer skimming it — not a wall of text, not a restatement of the diff, not generated boilerplate.
- **Keep the description concise and human-readable.** A few non repeatative bullets saying what changed and why, for a reviewer skimming it — not a wall of text, not a restatement of the diff, not generated boilerplate and not the user agent conversation details.
- **Reference issues with `Closes #issue-number`** under "Issues closed by this PR" so they auto-close on merge. This goes in the PR description only — never in commit messages.
- **Breaking changes can be added in additional information section** if any.
- **AI assistance in commits may optionally be disclosed with an `Assisted-by:` trailer** naming the model (e.g. `Assisted-by: Claude Opus 4.5`) — do NOT use a `Co-authored-by:` trailer for this.
- **Keep the commit body short and human readable** focused on decision made if any. Commit body must not re-iterate the changes done, skip if title is sufficient in conveying the change.
- **Use convensional commit format** for commits and PR title.

1
.gitignore vendored
View File

@@ -232,3 +232,4 @@ pyrightconfig.json
# dev
.dev/
.claude/worktrees/
.claude/settings.local.json

10
.husky/pre-commit Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
staged_go_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.go')
if [ -n "$staged_go_files" ]; then
echo "$staged_go_files" | xargs gofmt -l -w
echo "$staged_go_files" | xargs git add
fi
cd frontend && pnpm lint-staged

View File

@@ -81,16 +81,20 @@ devenv-clickhouse-clean: ## Clean all ClickHouse data from filesystem
##############################################################
# go commands
##############################################################
SQLITE_PATH ?= signoz.db
SIGNOZ_APISERVER_ADDRESS ?= 0.0.0.0:8080
.PHONY: go-run-enterprise
go-run-enterprise: ## Runs the enterprise go backend server
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
SIGNOZ_SQLSTORE_SQLITE_PATH=$(SQLITE_PATH) \
SIGNOZ_WEB_ENABLED=false \
SIGNOZ_TOKENIZER_JWT_SECRET=secret \
SIGNOZ_ALERTMANAGER_PROVIDER=signoz \
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER=cluster \
SIGNOZ_APISERVER_ADDRESS=$(SIGNOZ_APISERVER_ADDRESS) \
go run -race \
$(GO_BUILD_CONTEXT_ENTERPRISE)/*.go server
@@ -101,16 +105,29 @@ go-test: ## Runs go unit tests
.PHONY: go-run-community
go-run-community: ## Runs the community go backend server
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
SIGNOZ_SQLSTORE_SQLITE_PATH=$(SQLITE_PATH) \
SIGNOZ_WEB_ENABLED=false \
SIGNOZ_TOKENIZER_JWT_SECRET=secret \
SIGNOZ_ALERTMANAGER_PROVIDER=signoz \
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER=cluster \
SIGNOZ_APISERVER_ADDRESS=$(SIGNOZ_APISERVER_ADDRESS) \
go run -race \
$(GO_BUILD_CONTEXT_COMMUNITY)/*.go server
.PHONY: go-stop
go-stop: ## Stops the go backend server listening on SIGNOZ_APISERVER_ADDRESS
@PORT=$(lastword $(subst :, ,$(SIGNOZ_APISERVER_ADDRESS))); \
PIDS=$$(lsof -ti tcp:$$PORT); \
if [ -n "$$PIDS" ]; then \
kill $$PIDS; \
echo "Stopped signoz server on port $$PORT (pid $$PIDS)"; \
else \
echo "No signoz server running on port $$PORT."; \
echo "If it's running on a different port, rerun as: make go-stop SIGNOZ_APISERVER_ADDRESS=host:port"; \
fi
.PHONY: go-build-community $(GO_BUILD_ARCHS_COMMUNITY)
go-build-community: ## Builds the go backend server for community
go-build-community: $(GO_BUILD_ARCHS_COMMUNITY)
@@ -241,3 +258,8 @@ semconv-generate: ## Regenerate semantic-convention families for Go and TypeScri
gen-mocks:
@echo ">> Generating mocks"
@mockery --config .mockery.yml
.PHONY: gen-openapi-specs
gen-openapi-specs:
@go run cmd/enterprise/*.go generate openapi
cd frontend && pnpm generate:api && cd -

View File

@@ -138,6 +138,8 @@ sqlstore:
##################### APIServer #####################
apiserver:
# The TCP address the API server listens on, in the form "host:port".
address: 0.0.0.0:8080
timeout:
# Default request timeout.
default: 60s

View File

@@ -23421,12 +23421,6 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23773,12 +23767,6 @@ paths:
responses:
"200":
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23831,12 +23819,6 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23924,12 +23906,6 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:

View File

@@ -130,7 +130,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
s := &Server{
config: config,
signoz: signoz,
httpHostPort: baseconst.HTTPHostPort,
httpHostPort: config.APIServer.Address,
unavailableChannel: make(chan healthcheck.Status),
usageManager: usageManager,
}
@@ -235,7 +235,7 @@ func (s *Server) initListeners() error {
var err error
publicHostPort := s.httpHostPort
if publicHostPort == "" {
return fmt.Errorf("baseconst.HTTPHostPort is required")
return fmt.Errorf("apiserver.address is required")
}
s.httpConn, err = net.Listen("tcp", publicHostPort)

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd frontend && pnpm lint-staged

View File

@@ -20,7 +20,7 @@
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "pnpm i18n:generate-hash && (is-ci || pnpm husky:configure) && node scripts/update-registry.cjs",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"husky:configure": "cd .. && husky install .husky && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",

View File

@@ -8,6 +8,8 @@ import (
// Config holds the configuration for config.
type Config struct {
// Address is the TCP address the API server listens on, in the form "host:port".
Address string `mapstructure:"address"`
Timeout Timeout `mapstructure:"timeout"`
Logging Logging `mapstructure:"logging"`
}
@@ -32,6 +34,7 @@ func NewConfigFactory() factory.ConfigFactory {
func newConfig() factory.Config {
return &Config{
Address: "0.0.0.0:8080",
Timeout: Timeout{
Default: 60 * time.Second,
Max: 600 * time.Second,

View File

@@ -13,6 +13,7 @@ import (
)
func TestNewWithEnvProvider(t *testing.T) {
t.Setenv("SIGNOZ_APISERVER_ADDRESS", "0.0.0.0:9090")
t.Setenv("SIGNOZ_APISERVER_TIMEOUT_DEFAULT", "70s")
t.Setenv("SIGNOZ_APISERVER_TIMEOUT_MAX", "700s")
t.Setenv("SIGNOZ_APISERVER_TIMEOUT_EXCLUDED__ROUTES", "/excluded1,/excluded2")
@@ -38,6 +39,7 @@ func TestNewWithEnvProvider(t *testing.T) {
require.NoError(t, err)
expected := &Config{
Address: "0.0.0.0:9090",
Timeout: Timeout{
Default: 70 * time.Second,
Max: 700 * time.Second,

View File

@@ -122,7 +122,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
})).Methods(http.MethodPut).GetError(); err != nil {
@@ -173,7 +173,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
@@ -343,7 +343,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
@@ -360,7 +360,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {

View File

@@ -95,7 +95,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
s := &Server{
config: config,
signoz: signoz,
httpHostPort: constants.HTTPHostPort,
httpHostPort: config.APIServer.Address,
unavailableChannel: make(chan healthcheck.Status),
}
@@ -218,7 +218,7 @@ func (s *Server) initListeners() error {
var err error
publicHostPort := s.httpHostPort
if publicHostPort == "" {
return fmt.Errorf("constants.HTTPHostPort is required")
return fmt.Errorf("apiserver.address is required")
}
s.httpConn, err = net.Listen("tcp", publicHostPort)

View File

@@ -11,11 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
)
const (
HTTPHostPort = "0.0.0.0:8080" // Address to serve http (query service)
PrivateHostPort = "0.0.0.0:8085" // Address to server internal services like alert manager
OpAmpWsEndpoint = "0.0.0.0:4320" // address for opamp websocket
)
const OpAmpWsEndpoint = "0.0.0.0:4320" // address for opamp websocket
const MaxAllowedPointsInTimeSeries = 300

View File

@@ -170,7 +170,7 @@ func (u *User) UpdateEmail(email valuer.Email) {
// enrich the error with the specific operation using errors.WithAdditionalf.
func (u *User) ErrIfRoot() error {
if u.IsRoot {
return errors.New(errors.TypeInvalidInput, ErrCodeRootUserOperationUnsupported, "this operation is not supported for the root user")
return errors.New(errors.TypeUnsupported, ErrCodeRootUserOperationUnsupported, "this operation is not supported for the root user")
}
return nil
}
@@ -179,7 +179,7 @@ func (u *User) ErrIfRoot() error {
// This error can be enriched with specific operation by the called using errors.WithAdditionalf.
func (u *User) ErrIfDeleted() error {
if u.Status == UserStatusDeleted {
return errors.New(errors.TypeInvalidInput, ErrCodeUserStatusDeleted, "unsupported operation for deleted user")
return errors.New(errors.TypeUnsupported, ErrCodeUserStatusDeleted, "unsupported operation for deleted user")
}
return nil
}
@@ -188,7 +188,7 @@ func (u *User) ErrIfDeleted() error {
// This error can be enriched with specific operation by the called using errors.WithAdditionalf.
func (u *User) ErrIfPending() error {
if u.Status == UserStatusPendingInvite {
return errors.New(errors.TypeInvalidInput, ErrCodeUserStatusPendingInvite, "unsupported operation for pending user")
return errors.New(errors.TypeUnsupported, ErrCodeUserStatusPendingInvite, "unsupported operation for pending user")
}
return nil
}

View File

@@ -55,33 +55,3 @@ def test_impersonated_user_is_admin(signoz: types.SigNoz) -> None:
)
assert root_detail.status_code == HTTPStatus.OK
assert_user_has_role(root_detail.json()["data"], "signoz-admin")
def test_mutating_root_user_is_rejected(signoz: types.SigNoz) -> None:
"""
The root user is permanently protected, so mutating it is rejected with
400 and a machine readable code. ErrIfRoot runs before the self-delete
guard, so this holds even when the caller is the impersonated root
identity itself.
"""
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/users"),
timeout=2,
)
assert response.status_code == HTTPStatus.OK
root_user = next(
(u for u in response.json()["data"] if u.get("isRoot") is True),
None,
)
assert root_user is not None
response = requests.delete(
signoz.self.host_configs["8080"].get(f"/api/v2/users/{root_user['id']}"),
timeout=2,
)
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
error = response.json()["error"]
assert error["type"] == "invalid-input"
assert error["code"] == "root_user_operation_unsupported"