mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-02 12:02:09 +00:00
Compare commits
4 Commits
nv/4102
...
testing-fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
036f362fa6 | ||
|
|
6a9a910eb6 | ||
|
|
4cd7489280 | ||
|
|
ff0736532d |
46
.github/workflows/jsci.yaml
vendored
46
.github/workflows/jsci.yaml
vendored
@@ -71,49 +71,3 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: validate md languages
|
||||
run: bash frontend/scripts/validate-md-languages.sh
|
||||
authz:
|
||||
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'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Install Python dependencies
|
||||
working-directory: ./tests/integration
|
||||
run: |
|
||||
uv sync
|
||||
|
||||
- name: Start test environment
|
||||
run: |
|
||||
make py-test-setup
|
||||
|
||||
- name: Generate permissions.type.ts
|
||||
run: |
|
||||
node frontend/scripts/generate-permissions-type.js
|
||||
|
||||
- name: Teardown test environment
|
||||
if: always()
|
||||
run: |
|
||||
make py-test-teardown
|
||||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
|
||||
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -80,32 +80,9 @@ Do not define an interface before you have at least two concrete implementations
|
||||
|
||||
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.
|
||||
See [Types](types.md#when-a-new-type-is-warranted) for the criteria that justify introducing a new type.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
|
||||
@@ -49,6 +49,43 @@ Follow these rules:
|
||||
|
||||
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
|
||||
@@ -90,9 +127,7 @@ Never introduce circular imports. If package A needs package B and B needs A, ex
|
||||
|
||||
## 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.
|
||||
See [Types](types.md) for full conventions on type placement, naming variants, composition, and constructors.
|
||||
|
||||
## How do I merge or move packages?
|
||||
|
||||
@@ -105,6 +140,10 @@ When two packages are tightly coupled (one imports the other's constants, they c
|
||||
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?
|
||||
|
||||
See [Types](types.md#typed-domain-values-pkgvaluer) for valuer types, when to use them, and the enum pattern using `valuer.String`.
|
||||
|
||||
## 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:
|
||||
@@ -119,6 +158,10 @@ package cache
|
||||
|
||||
- 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. See [Types](types.md#typed-domain-values-pkgvaluer) for details.
|
||||
- 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.
|
||||
|
||||
@@ -8,7 +8,31 @@ 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
|
||||
@@ -20,3 +44,5 @@ We **recommend** (almost enforce) reviewing these guides before contributing to
|
||||
- [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
|
||||
- [Types](types.md) - Type placement, naming variants, composition, and constructors
|
||||
|
||||
260
docs/contributing/go/testing.md
Normal file
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
|
||||
272
docs/contributing/go/types.md
Normal file
272
docs/contributing/go/types.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Types
|
||||
|
||||
This guide covers how types are organised, named, constructed, and composed so you can add new ones consistently.
|
||||
|
||||
## Where do types live?
|
||||
|
||||
Types live in `pkg/types/` and its sub-packages:
|
||||
|
||||
```
|
||||
pkg/types/
|
||||
├── auditable.go # TimeAuditable, UserAuditable
|
||||
├── identity.go # Identifiable (UUID primary key)
|
||||
├── user.go # User, PostableRegisterOrgAndAdmin, UserStore
|
||||
├── alertmanagertypes/ # Alert manager domain types
|
||||
│ ├── channel.go
|
||||
│ ├── receiver.go
|
||||
│ └── config.go
|
||||
├── authtypes/ # Auth domain types
|
||||
└── ruletypes/ # Rule domain types
|
||||
└── maintenance.go
|
||||
```
|
||||
|
||||
Follow these rules:
|
||||
|
||||
1. **Embeddable building blocks** go in `pkg/types/` directly `Identifiable`, `TimeAuditable`, `UserAuditable`.
|
||||
2. **Domain-specific types** go in a sub-package named `pkg/types/<domain>types/` (e.g., `alertmanagertypes`, `ruletypes`, `authtypes`).
|
||||
3. **No domain logic** in type packages. Only data structures, constants, and simple methods. Domain services import from type packages, not the other way around.
|
||||
4. **Domain services import types, not vice versa.** If a type needs a service, the design is likely wrong and you should restructure so the service operates on the type.
|
||||
|
||||
## Type variants
|
||||
|
||||
A domain entity often has multiple representations depending on where it appears in the system. We use naming prefixes to distinguish them:
|
||||
|
||||
| Prefix | Purpose | Example |
|
||||
|---|---|---|
|
||||
| `Postable<Type>` | API request input | `PostableRegisterOrgAndAdmin` |
|
||||
| `Gettable<Type>` | API response output | `GettablePlannedMaintenance` |
|
||||
| `Storable<Type>` | Database model (embeds `bun.BaseModel`) | `StorablePlannedMaintenance` |
|
||||
| Plain `<Type>` | Domain logic type | `User` |
|
||||
|
||||
Not every entity needs all four variants. Start with the plain type and add variants only when the API or database representation genuinely differs.
|
||||
|
||||
Here is a concrete example from `pkg/types/ruletypes/maintenance.go`:
|
||||
|
||||
```go
|
||||
// Database model embeds bun.BaseModel and composition types
|
||||
type StorablePlannedMaintenance struct {
|
||||
bun.BaseModel `bun:"table:planned_maintenance"`
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Description string `bun:"description,type:text"`
|
||||
Schedule *Schedule `bun:"schedule,type:text,notnull"`
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
}
|
||||
|
||||
// API response: flat struct with JSON tags, computed fields like Status
|
||||
type GettablePlannedMaintenance struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Schedule *Schedule `json:"schedule"`
|
||||
RuleIDs []string `json:"alertIds"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Status string `json:"status"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
```
|
||||
|
||||
When the API shape exactly matches the domain type, use a type alias instead of duplicating fields:
|
||||
|
||||
```go
|
||||
// From pkg/types/user.go
|
||||
type GettableUser = User
|
||||
```
|
||||
|
||||
## Composition via embedding
|
||||
|
||||
`pkg/types/` provides small, reusable structs that you embed into your domain types:
|
||||
|
||||
```go
|
||||
// pkg/types/identity.go
|
||||
type Identifiable struct {
|
||||
ID valuer.UUID `json:"id" bun:"id,pk,type:text"`
|
||||
}
|
||||
|
||||
// pkg/types/auditable.go
|
||||
type TimeAuditable struct {
|
||||
CreatedAt time.Time `bun:"created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `bun:"updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
type UserAuditable struct {
|
||||
CreatedBy string `bun:"created_by,type:text" json:"createdBy"`
|
||||
UpdatedBy string `bun:"updated_by,type:text" json:"updatedBy"`
|
||||
}
|
||||
```
|
||||
|
||||
Compose them in a database model:
|
||||
|
||||
```go
|
||||
type StorablePlannedMaintenance struct {
|
||||
bun.BaseModel `bun:"table:planned_maintenance"`
|
||||
types.Identifiable // adds ID (UUID primary key)
|
||||
types.TimeAuditable // adds CreatedAt, UpdatedAt
|
||||
types.UserAuditable // adds CreatedBy, UpdatedBy
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Description string `bun:"description,type:text"`
|
||||
}
|
||||
```
|
||||
|
||||
See [SQL](sql.md) for full database patterns including migrations and queries.
|
||||
|
||||
## Constructors
|
||||
|
||||
Constructors validate inputs and return a ready-to-use value:
|
||||
|
||||
```go
|
||||
// New<Type> validates and returns a pointer + error
|
||||
func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID) (*User, error) {
|
||||
if email.IsZero() {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
||||
}
|
||||
if role == "" {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role is required")
|
||||
}
|
||||
if orgID.IsZero() {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
|
||||
}
|
||||
|
||||
return &User{
|
||||
Identifiable: Identifiable{ID: valuer.GenerateUUID()},
|
||||
DisplayName: displayName,
|
||||
Email: email,
|
||||
Role: role,
|
||||
OrgID: orgID,
|
||||
TimeAuditable: TimeAuditable{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
Follow these conventions:
|
||||
|
||||
- **`New<Type>(args) (*Type, error)`**: validates inputs, returns an error on failure. Use this in production code.
|
||||
- **Validation at construction**: check required fields, format constraints, and invariants in the constructor. Callers should not need to validate after construction.
|
||||
- **Generate IDs internally**: constructors call `valuer.GenerateUUID()` callers do not pass IDs in.
|
||||
- **Set timestamps internally**: constructors set `CreatedAt` and `UpdatedAt` to `time.Now()`.
|
||||
|
||||
## Typed domain values (`pkg/valuer/`)
|
||||
|
||||
The `pkg/valuer` package provides typed wrappers for common domain values. These types carry validation, normalization, and consistent serialization (JSON, SQL, text) that raw Go primitives do not.
|
||||
|
||||
| Type | Wraps | Invariant |
|
||||
|---|---|---|
|
||||
| `valuer.UUID` | `google/uuid.UUID` | Valid UUIDv7, generated via `GenerateUUID()` |
|
||||
| `valuer.Email` | `string` | Valid email format, lowercased and trimmed |
|
||||
| `valuer.String` | `string` | Lowercased and trimmed |
|
||||
| `valuer.TextDuration` | `time.Duration` | Valid duration, text-serializable |
|
||||
|
||||
### When to use a valuer type
|
||||
|
||||
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`.
|
||||
|
||||
### The `Valuer` interface
|
||||
|
||||
Every valuer type implements the `Valuer` interface, which gives you serialization for free:
|
||||
|
||||
```go
|
||||
type Valuer interface {
|
||||
IsZero() bool // check for zero value
|
||||
StringValue() string // raw string representation
|
||||
fmt.Stringer // String() for printing
|
||||
json.Marshaler / json.Unmarshaler // JSON
|
||||
sql.Scanner / driver.Valuer // database
|
||||
encoding.TextMarshaler / TextUnmarshaler // text
|
||||
ginbinding.BindUnmarshaler // HTTP query/path params
|
||||
}
|
||||
```
|
||||
|
||||
Use them in struct fields:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Identifiable
|
||||
Email valuer.Email `bun:"email" json:"email"`
|
||||
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
See [Abstractions](abstractions.md) for the full set of rules on when abstractions are and aren't justified.
|
||||
|
||||
## Store interfaces
|
||||
|
||||
Each domain type package defines a store interface for persistence. The store interface lives alongside the types it operates on:
|
||||
|
||||
```go
|
||||
// From pkg/types/ruletypes/maintenance.go
|
||||
type MaintenanceStore interface {
|
||||
CreatePlannedMaintenance(context.Context, GettablePlannedMaintenance) (valuer.UUID, error)
|
||||
DeletePlannedMaintenance(context.Context, valuer.UUID) error
|
||||
GetPlannedMaintenanceByID(context.Context, valuer.UUID) (*GettablePlannedMaintenance, error)
|
||||
EditPlannedMaintenance(context.Context, GettablePlannedMaintenance, valuer.UUID) error
|
||||
GetAllPlannedMaintenance(context.Context, string) ([]*GettablePlannedMaintenance, error)
|
||||
}
|
||||
```
|
||||
|
||||
Conventions:
|
||||
|
||||
- Name the interface `<Domain>Store` (e.g., `UserStore`, `MaintenanceStore`).
|
||||
- Accept `context.Context` as the first parameter.
|
||||
- Use typed values (`valuer.UUID`, `valuer.Email`) instead of raw strings for identifiers.
|
||||
- Implementations go in separate packages (e.g., `sqlstore/`), see [SQL](sql.md) for details.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Shared types live in `pkg/types/`, domain types in `pkg/types/<domain>types/`.
|
||||
- No domain logic in type packages only data structures, constants, and simple methods.
|
||||
- Use `Storable`, `Gettable`, `Postable` prefixes when API or database representation differs from the domain type.
|
||||
- Embed `Identifiable`, `TimeAuditable`, and `UserAuditable` for standard fields instead of repeating them.
|
||||
- Constructors (`New<Type>`) validate, generate IDs, and set timestamps.
|
||||
- Use `pkg/valuer/` types instead of raw strings for domain identifiers like UUIDs and emails.
|
||||
- Store interfaces live alongside the types they persist and use `context.Context` as the first parameter.
|
||||
@@ -2,11 +2,7 @@
|
||||
* ESLint Configuration for SigNoz Frontend
|
||||
*/
|
||||
module.exports = {
|
||||
ignorePatterns: [
|
||||
'src/parser/*.ts',
|
||||
'scripts/update-registry.js',
|
||||
'scripts/generate-permissions-type.js',
|
||||
],
|
||||
ignorePatterns: ['src/parser/*.ts', 'scripts/update-registry.js'],
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest",
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent",
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
|
||||
"generate:permissions-type": "node scripts/generate-permissions-type.js"
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.15.0"
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const axios = require('axios');
|
||||
|
||||
const PERMISSIONS_TYPE_FILE = path.join(
|
||||
__dirname,
|
||||
'../src/hooks/useAuthZ/permissions.type.ts',
|
||||
);
|
||||
|
||||
const SIGNOZ_INTEGRATION_IMAGE = 'signoz:integration';
|
||||
const LOCAL_BACKEND_URL = 'http://localhost:8080';
|
||||
|
||||
function log(message) {
|
||||
console.log(`[generate-permissions-type] ${message}`);
|
||||
}
|
||||
|
||||
function getBackendUrlFromDocker() {
|
||||
try {
|
||||
const output = execSync(
|
||||
`docker ps --filter "ancestor=${SIGNOZ_INTEGRATION_IMAGE}" --format "{{.Ports}}"`,
|
||||
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
|
||||
).trim();
|
||||
|
||||
if (!output) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const portMatch = output.match(/0\.0\.0\.0:(\d+)->8080\/tcp/);
|
||||
if (portMatch) {
|
||||
return `http://localhost:${portMatch[1]}`;
|
||||
}
|
||||
|
||||
const ipv6Match = output.match(/:::(\d+)->8080\/tcp/);
|
||||
if (ipv6Match) {
|
||||
return `http://localhost:${ipv6Match[1]}`;
|
||||
}
|
||||
} catch (err) {
|
||||
log(`Warning: Could not get port from docker: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function checkBackendHealth(url, maxAttempts = 3, delayMs = 1000) {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
await axios.get(`${url}/api/v1/health`, {
|
||||
timeout: 5000,
|
||||
validateStatus: (status) => status === 200,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise((r) => setTimeout(r, delayMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function discoverBackendUrl() {
|
||||
const dockerUrl = getBackendUrlFromDocker();
|
||||
if (dockerUrl) {
|
||||
log(`Found ${SIGNOZ_INTEGRATION_IMAGE} container, trying ${dockerUrl}...`);
|
||||
if (await checkBackendHealth(dockerUrl)) {
|
||||
log(`Backend found at ${dockerUrl} (from py-test-setup)`);
|
||||
return dockerUrl;
|
||||
}
|
||||
log(`Backend at ${dockerUrl} is not responding`);
|
||||
}
|
||||
|
||||
log(`Trying local backend at ${LOCAL_BACKEND_URL}...`);
|
||||
if (await checkBackendHealth(LOCAL_BACKEND_URL)) {
|
||||
log(`Backend found at ${LOCAL_BACKEND_URL}`);
|
||||
return LOCAL_BACKEND_URL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchResources(backendUrl) {
|
||||
log('Fetching resources from API...');
|
||||
const resourcesUrl = `${backendUrl}/api/v1/authz/resources`;
|
||||
|
||||
const { data: response } = await axios.get(resourcesUrl);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function transformResponse(apiResponse) {
|
||||
if (!apiResponse.data) {
|
||||
throw new Error('Invalid API response: missing data field');
|
||||
}
|
||||
|
||||
const { resources, relations } = apiResponse.data;
|
||||
|
||||
return {
|
||||
status: apiResponse.status || 'success',
|
||||
data: {
|
||||
resources: resources,
|
||||
relations: relations,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function generateTypeScriptFile(data) {
|
||||
const resourcesStr = data.data.resources
|
||||
.map(
|
||||
(r) =>
|
||||
`\t\t\t{\n\t\t\t\tname: '${r.name}',\n\t\t\t\ttype: '${r.type}',\n\t\t\t}`,
|
||||
)
|
||||
.join(',\n');
|
||||
|
||||
const relationsStr = Object.entries(data.data.relations)
|
||||
.map(
|
||||
([type, relations]) =>
|
||||
`\t\t\t${type}: [${relations.map((r) => `'${r}'`).join(', ')}]`,
|
||||
)
|
||||
.join(',\n');
|
||||
|
||||
return `// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY scripts/generate-permissions-type
|
||||
export default {
|
||||
\tstatus: '${data.status}',
|
||||
\tdata: {
|
||||
\t\tresources: [
|
||||
${resourcesStr}
|
||||
\t\t],
|
||||
\t\trelations: {
|
||||
${relationsStr}
|
||||
\t\t},
|
||||
\t},
|
||||
} as const;
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
log('Starting permissions type generation...');
|
||||
|
||||
const backendUrl = await discoverBackendUrl();
|
||||
|
||||
if (!backendUrl) {
|
||||
console.error('\n' + '='.repeat(80));
|
||||
console.error('ERROR: No running SigNoz backend found!');
|
||||
console.error('='.repeat(80));
|
||||
console.error(
|
||||
'\nThe permissions type generator requires a running SigNoz backend.',
|
||||
);
|
||||
console.error('\nFor local development, start the backend with:');
|
||||
console.error(' make go-run-enterprise');
|
||||
console.error(
|
||||
'\nFor CI or integration testing, start the test environment with:',
|
||||
);
|
||||
console.error(' make py-test-setup');
|
||||
console.error(
|
||||
'\nIf running in CI and seeing this error, check that the py-test-setup',
|
||||
);
|
||||
console.error('step completed successfully before this step runs.');
|
||||
console.error('='.repeat(80) + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log('Fetching resources...');
|
||||
const apiResponse = await fetchResources(backendUrl);
|
||||
|
||||
log('Transforming response...');
|
||||
const transformed = transformResponse(apiResponse);
|
||||
|
||||
log('Generating TypeScript file...');
|
||||
const content = generateTypeScriptFile(transformed);
|
||||
|
||||
log(`Writing to ${PERMISSIONS_TYPE_FILE}...`);
|
||||
fs.writeFileSync(PERMISSIONS_TYPE_FILE, content, 'utf8');
|
||||
|
||||
const rootDir = path.join(__dirname, '../..');
|
||||
const relativePath = path.relative(
|
||||
path.join(rootDir, 'frontend'),
|
||||
PERMISSIONS_TYPE_FILE,
|
||||
);
|
||||
log('Linting generated file...');
|
||||
execSync(`cd frontend && yarn eslint --fix ${relativePath}`, {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log('Successfully generated permissions.type.ts');
|
||||
} catch (error) {
|
||||
log(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RouteProps } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import { createGuardedRoute } from '../components/createGuardedRoute/createGuardedRoute';
|
||||
import {
|
||||
AlertHistory,
|
||||
AlertOverview,
|
||||
@@ -85,11 +84,7 @@ const routes: AppRoutes[] = [
|
||||
{
|
||||
path: ROUTES.HOME,
|
||||
exact: true,
|
||||
component: createGuardedRoute(
|
||||
Home,
|
||||
'delete',
|
||||
'dashboard:e6dbc08b-976d-4c41-8572-93c990c3b297',
|
||||
),
|
||||
component: Home,
|
||||
isPrivate: true,
|
||||
key: 'HOME',
|
||||
},
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
import {
|
||||
AuthtypesGettableTransactionDTO,
|
||||
AuthtypesTransactionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { BrandedPermission } from 'hooks/useAuthZ/types';
|
||||
import { buildPermission } from 'hooks/useAuthZ/utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import { GuardAuthZ } from './GuardAuthZ';
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL || '';
|
||||
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
|
||||
|
||||
function authzMockResponse(
|
||||
payload: AuthtypesTransactionDTO[],
|
||||
authorizedByIndex: boolean[],
|
||||
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
|
||||
return {
|
||||
data: payload.map((txn, i) => ({
|
||||
relation: txn.relation,
|
||||
object: txn.object,
|
||||
authorized: authorizedByIndex[i] ?? false,
|
||||
})),
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
describe('GuardAuthZ', () => {
|
||||
const TestChild = (): ReactElement => <div>Protected Content</div>;
|
||||
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
|
||||
const ErrorFallback = (error: Error): ReactElement => (
|
||||
<div>Error occurred: {error.message}</div>
|
||||
);
|
||||
const NoPermissionFallback = (_response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}): ReactElement => <div>Access denied</div>;
|
||||
const NoPermissionFallbackWithSuggestions = (response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}): ReactElement => (
|
||||
<div>
|
||||
Access denied. Required permission: {response.requiredPermissionName}
|
||||
</div>
|
||||
);
|
||||
|
||||
it('should render children when permission is granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render fallbackOnLoading when loading', () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.delay('infinite'),
|
||||
ctx.status(200),
|
||||
ctx.json({ data: [], status: 'success' }),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="read"
|
||||
object="dashboard:*"
|
||||
fallbackOnLoading={<LoadingFallback />}
|
||||
>
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null when loading and no fallbackOnLoading provided', () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.delay('infinite'),
|
||||
ctx.status(200),
|
||||
ctx.json({ data: [], status: 'success' }),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render fallbackOnError when API error occurs', async () => {
|
||||
const errorMessage = 'Internal Server Error';
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: errorMessage }));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="read"
|
||||
object="dashboard:*"
|
||||
fallbackOnError={ErrorFallback}
|
||||
>
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Error occurred:/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should pass error object to fallbackOnError function', async () => {
|
||||
const errorMessage = 'Network request failed';
|
||||
let receivedError: Error | null = null;
|
||||
|
||||
const errorFallbackWithCapture = (error: Error): ReactElement => {
|
||||
receivedError = error;
|
||||
return <div>Captured error: {error.message}</div>;
|
||||
};
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: errorMessage }));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="read"
|
||||
object="dashboard:*"
|
||||
fallbackOnError={errorFallbackWithCapture}
|
||||
>
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedError).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(receivedError).toBeInstanceOf(Error);
|
||||
expect(screen.getByText(/Captured error:/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null when error occurs and no fallbackOnError provided', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render fallbackOnNoPermissions when permission is denied', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="update"
|
||||
object="dashboard:123"
|
||||
fallbackOnNoPermissions={NoPermissionFallback}
|
||||
>
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Access denied')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null when permission is denied and no fallbackOnNoPermissions provided', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
|
||||
}),
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="update" object="dashboard:123">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null when permissions object is null', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json({ data: [], status: 'success' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should pass requiredPermissionName to fallbackOnNoPermissions', async () => {
|
||||
const permission = buildPermission('update', 'dashboard:123');
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="update"
|
||||
object="dashboard:123"
|
||||
fallbackOnNoPermissions={NoPermissionFallbackWithSuggestions}
|
||||
>
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Access denied. Required permission:/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
new RegExp(permission.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle different relation and object combinations', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}),
|
||||
);
|
||||
|
||||
const { rerender } = render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
rerender(
|
||||
<GuardAuthZ relation="delete" object="dashboard:456">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
import {
|
||||
AuthZObject,
|
||||
AuthZRelation,
|
||||
BrandedPermission,
|
||||
} from 'hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { buildPermission } from 'hooks/useAuthZ/utils';
|
||||
|
||||
export type GuardAuthZProps<R extends AuthZRelation> = {
|
||||
children: ReactElement;
|
||||
relation: R;
|
||||
object: AuthZObject<R>;
|
||||
fallbackOnLoading?: JSX.Element;
|
||||
fallbackOnError?: (error: Error) => JSX.Element;
|
||||
fallbackOnNoPermissions?: (response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}) => JSX.Element;
|
||||
};
|
||||
|
||||
export function GuardAuthZ<R extends AuthZRelation>({
|
||||
children,
|
||||
relation,
|
||||
object,
|
||||
fallbackOnLoading,
|
||||
fallbackOnError,
|
||||
fallbackOnNoPermissions,
|
||||
}: GuardAuthZProps<R>): JSX.Element | null {
|
||||
const permission = buildPermission<R>(relation, object);
|
||||
|
||||
const { permissions, isLoading, error } = useAuthZ([permission]);
|
||||
|
||||
if (isLoading) {
|
||||
return fallbackOnLoading ?? null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return fallbackOnError?.(error) ?? null;
|
||||
}
|
||||
|
||||
if (!permissions?.[permission]?.isGranted) {
|
||||
return (
|
||||
fallbackOnNoPermissions?.({
|
||||
requiredPermissionName: permission,
|
||||
}) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
.guard-authz-error-no-authz {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding: 24px;
|
||||
|
||||
.guard-authz-error-no-authz-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
color: var(--l1-foreground);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: var(--l3-foreground);
|
||||
line-height: 18px;
|
||||
|
||||
span {
|
||||
background-color: var(--l3-background);
|
||||
white-space: nowrap;
|
||||
padding: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,472 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
import type { RouteComponentProps } from 'react-router-dom';
|
||||
import {
|
||||
AuthtypesGettableTransactionDTO,
|
||||
AuthtypesTransactionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import { createGuardedRoute } from './createGuardedRoute';
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL || '';
|
||||
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
|
||||
|
||||
function authzMockResponse(
|
||||
payload: AuthtypesTransactionDTO[],
|
||||
authorizedByIndex: boolean[],
|
||||
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
|
||||
return {
|
||||
data: payload.map((txn, i) => ({
|
||||
relation: txn.relation,
|
||||
object: txn.object,
|
||||
authorized: authorizedByIndex[i] ?? false,
|
||||
})),
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
describe('createGuardedRoute', () => {
|
||||
const TestComponent = ({ testProp }: { testProp: string }): ReactElement => (
|
||||
<div>Test Component: {testProp}</div>
|
||||
);
|
||||
|
||||
it('should render component when permission is granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'read',
|
||||
'dashboard:*',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: {},
|
||||
isExact: true,
|
||||
path: '/dashboard',
|
||||
url: '/dashboard',
|
||||
};
|
||||
|
||||
const props = {
|
||||
testProp: 'test-value',
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should substitute route parameters in object string', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'read',
|
||||
'dashboard:{id}',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: { id: '123' },
|
||||
isExact: true,
|
||||
path: '/dashboard/:id',
|
||||
url: '/dashboard/123',
|
||||
};
|
||||
|
||||
const props = {
|
||||
testProp: 'test-value',
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple route parameters', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = (await req.json()) as AuthtypesTransactionDTO[];
|
||||
const txn = payload[0];
|
||||
const responseData: AuthtypesGettableTransactionDTO[] = [
|
||||
{
|
||||
relation: txn.relation,
|
||||
object: {
|
||||
resource: {
|
||||
name: txn.object.resource.name,
|
||||
type: txn.object.resource.type,
|
||||
},
|
||||
selector: '123:456',
|
||||
},
|
||||
authorized: true,
|
||||
},
|
||||
];
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({ data: responseData, status: 'success' }),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'update',
|
||||
'dashboard:{id}:{version}',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: { id: '123', version: '456' },
|
||||
isExact: true,
|
||||
path: '/dashboard/:id/:version',
|
||||
url: '/dashboard/123/456',
|
||||
};
|
||||
|
||||
const props = {
|
||||
testProp: 'test-value',
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep placeholder when route parameter is missing', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'read',
|
||||
'dashboard:{id}',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: {},
|
||||
isExact: true,
|
||||
path: '/dashboard',
|
||||
url: '/dashboard',
|
||||
};
|
||||
|
||||
const props = {
|
||||
testProp: 'test-value',
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render loading fallback when loading', () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.delay('infinite'),
|
||||
ctx.status(200),
|
||||
ctx.json({ data: [], status: 'success' }),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'read',
|
||||
'dashboard:*',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: {},
|
||||
isExact: true,
|
||||
path: '/dashboard',
|
||||
url: '/dashboard',
|
||||
};
|
||||
|
||||
const props = {
|
||||
testProp: 'test-value',
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
expect(screen.getByText('SigNoz')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Test Component: test-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error fallback when API error occurs', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'read',
|
||||
'dashboard:*',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: {},
|
||||
isExact: true,
|
||||
path: '/dashboard',
|
||||
url: '/dashboard',
|
||||
};
|
||||
|
||||
const props = {
|
||||
testProp: 'test-value',
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText('Test Component: test-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render no permissions fallback when permission is denied', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'update',
|
||||
'dashboard:{id}',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: { id: '123' },
|
||||
isExact: true,
|
||||
path: '/dashboard/:id',
|
||||
url: '/dashboard/123',
|
||||
};
|
||||
|
||||
const props = {
|
||||
testProp: 'test-value',
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const heading = document.querySelector('h3');
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading?.textContent).toMatch(/permission to view/i);
|
||||
});
|
||||
|
||||
expect(screen.getByText('update')).toBeInTheDocument();
|
||||
expect(screen.getByText('dashboard:123')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Test Component: test-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should pass all props to wrapped component', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}),
|
||||
);
|
||||
|
||||
const ComponentWithMultipleProps = ({
|
||||
prop1,
|
||||
prop2,
|
||||
prop3,
|
||||
}: {
|
||||
prop1: string;
|
||||
prop2: number;
|
||||
prop3: boolean;
|
||||
}): ReactElement => (
|
||||
<div>
|
||||
{prop1} - {prop2} - {prop3.toString()}
|
||||
</div>
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
ComponentWithMultipleProps,
|
||||
'read',
|
||||
'dashboard:*',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: {},
|
||||
isExact: true,
|
||||
path: '/dashboard',
|
||||
url: '/dashboard',
|
||||
};
|
||||
|
||||
const props = {
|
||||
prop1: 'value1',
|
||||
prop2: 42,
|
||||
prop3: true,
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('value1 - 42 - true')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should memoize resolved object based on route params', async () => {
|
||||
let requestCount = 0;
|
||||
const requestedObjects: string[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
requestCount++;
|
||||
const payload = (await req.json()) as AuthtypesTransactionDTO[];
|
||||
const obj = payload[0]?.object;
|
||||
const name = obj?.resource?.name;
|
||||
const selector = obj?.selector ?? '*';
|
||||
const objectStr =
|
||||
obj?.resource?.type === 'metaresources' ? name : `${name}:${selector}`;
|
||||
requestedObjects.push(objectStr ?? '');
|
||||
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'read',
|
||||
'dashboard:{id}',
|
||||
);
|
||||
|
||||
const mockMatch1 = {
|
||||
params: { id: '123' },
|
||||
isExact: true,
|
||||
path: '/dashboard/:id',
|
||||
url: '/dashboard/123',
|
||||
};
|
||||
|
||||
const props1 = {
|
||||
testProp: 'test-value-1',
|
||||
match: mockMatch1,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
const { unmount } = render(<GuardedComponent {...props1} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Component: test-value-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(requestCount).toBe(1);
|
||||
expect(requestedObjects).toContain('dashboard:123');
|
||||
|
||||
unmount();
|
||||
|
||||
const mockMatch2 = {
|
||||
params: { id: '456' },
|
||||
isExact: true,
|
||||
path: '/dashboard/:id',
|
||||
url: '/dashboard/456',
|
||||
};
|
||||
|
||||
const props2 = {
|
||||
testProp: 'test-value-2',
|
||||
match: mockMatch2,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props2} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Component: test-value-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(requestCount).toBe(2);
|
||||
expect(requestedObjects).toContain('dashboard:456');
|
||||
});
|
||||
|
||||
it('should handle different relation types', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}),
|
||||
);
|
||||
|
||||
const GuardedComponent = createGuardedRoute(
|
||||
TestComponent,
|
||||
'delete',
|
||||
'dashboard:{id}',
|
||||
);
|
||||
|
||||
const mockMatch = {
|
||||
params: { id: '789' },
|
||||
isExact: true,
|
||||
path: '/dashboard/:id',
|
||||
url: '/dashboard/789',
|
||||
};
|
||||
|
||||
const props = {
|
||||
testProp: 'test-value',
|
||||
match: mockMatch,
|
||||
location: ({} as unknown) as RouteComponentProps['location'],
|
||||
history: ({} as unknown) as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { ComponentType, ReactElement, useMemo } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import {
|
||||
AuthZObject,
|
||||
AuthZRelation,
|
||||
BrandedPermission,
|
||||
} from 'hooks/useAuthZ/types';
|
||||
import { parsePermission } from 'hooks/useAuthZ/utils';
|
||||
|
||||
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import AppLoading from '../AppLoading/AppLoading';
|
||||
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
|
||||
|
||||
import './createGuardedRoute.styles.scss';
|
||||
|
||||
const onErrorFallback = (): JSX.Element => <ErrorBoundaryFallback />;
|
||||
|
||||
function OnNoPermissionsFallback(response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}): ReactElement {
|
||||
const { relation, object } = parsePermission(response.requiredPermissionName);
|
||||
|
||||
return (
|
||||
<div className="guard-authz-error-no-authz">
|
||||
<div className="guard-authz-error-no-authz-content">
|
||||
<img src="/Icons/no-data.svg" alt="No permission" />
|
||||
<h3>Uh-oh! You don’t have permission to view this page.</h3>
|
||||
<p>
|
||||
You need the following permission to view this page:
|
||||
<br />
|
||||
Relation: <span>{relation}</span>
|
||||
<br />
|
||||
Object: <span>{object}</span>
|
||||
<br />
|
||||
Ask your SigNoz administrator to grant access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export function createGuardedRoute<P extends object, R extends AuthZRelation>(
|
||||
Component: ComponentType<P>,
|
||||
relation: R,
|
||||
object: AuthZObject<R>,
|
||||
): ComponentType<P & RouteComponentProps<Record<string, string>>> {
|
||||
return function GuardedRouteComponent(
|
||||
props: P & RouteComponentProps<Record<string, string>>,
|
||||
): ReactElement {
|
||||
const resolvedObject = useMemo(() => {
|
||||
const paramPattern = /\{([^}]+)\}/g;
|
||||
return object.replace(paramPattern, (match, paramName) => {
|
||||
const paramValue = props.match?.params?.[paramName];
|
||||
return paramValue !== undefined ? paramValue : match;
|
||||
}) as AuthZObject<R>;
|
||||
}, [props.match?.params]);
|
||||
|
||||
return (
|
||||
<GuardAuthZ
|
||||
relation={relation}
|
||||
object={resolvedObject}
|
||||
fallbackOnLoading={<AppLoading />}
|
||||
fallbackOnError={onErrorFallback}
|
||||
fallbackOnNoPermissions={(response): ReactElement => (
|
||||
<OnNoPermissionsFallback {...response} />
|
||||
)}
|
||||
>
|
||||
<Component {...props} />
|
||||
</GuardAuthZ>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY scripts/generate-permissions-type
|
||||
export default {
|
||||
status: 'success',
|
||||
data: {
|
||||
resources: [
|
||||
{
|
||||
name: 'dashboard',
|
||||
type: 'metaresource',
|
||||
},
|
||||
{
|
||||
name: 'dashboards',
|
||||
type: 'metaresources',
|
||||
},
|
||||
],
|
||||
relations: {
|
||||
create: ['metaresources'],
|
||||
delete: ['user', 'role', 'organization', 'metaresource'],
|
||||
list: ['metaresources'],
|
||||
read: ['user', 'role', 'organization', 'metaresource'],
|
||||
update: ['user', 'role', 'organization', 'metaresource'],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,57 +0,0 @@
|
||||
import permissionsType from './permissions.type';
|
||||
import { ObjectSeparator } from './utils';
|
||||
|
||||
type PermissionsData = typeof permissionsType.data;
|
||||
export type Resource = PermissionsData['resources'][number];
|
||||
export type ResourceName = Resource['name'];
|
||||
export type ResourceType = Resource['type'];
|
||||
|
||||
type RelationsByType = PermissionsData['relations'];
|
||||
|
||||
type ResourceTypeMap = {
|
||||
[K in ResourceName]: Extract<Resource, { name: K }>['type'];
|
||||
};
|
||||
|
||||
type RelationName = keyof RelationsByType;
|
||||
|
||||
type ResourcesForRelation<R extends RelationName> = Extract<
|
||||
Resource,
|
||||
{ type: RelationsByType[R][number] }
|
||||
>['name'];
|
||||
|
||||
type IsPluralResource<
|
||||
R extends ResourceName
|
||||
> = ResourceTypeMap[R] extends 'metaresources' ? true : false;
|
||||
|
||||
type ObjectForResource<R extends ResourceName> = R extends infer U
|
||||
? U extends ResourceName
|
||||
? IsPluralResource<U> extends true
|
||||
? U
|
||||
: `${U}${typeof ObjectSeparator}${string}`
|
||||
: never
|
||||
: never;
|
||||
|
||||
type RelationToObject<R extends RelationName> = ObjectForResource<
|
||||
ResourcesForRelation<R>
|
||||
>;
|
||||
|
||||
type AllRelations = RelationName;
|
||||
|
||||
export type AuthZRelation = AllRelations;
|
||||
export type AuthZResource = ResourceName;
|
||||
export type AuthZObject<R extends AuthZRelation> = RelationToObject<R>;
|
||||
|
||||
export type BrandedPermission = string & { __brandedPermission: true };
|
||||
|
||||
export type AuthZCheckResponse = Record<
|
||||
BrandedPermission,
|
||||
{
|
||||
isGranted: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export type UseAuthZResult = {
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
permissions: AuthZCheckResponse | null;
|
||||
};
|
||||
@@ -1,496 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
AuthtypesGettableTransactionDTO,
|
||||
AuthtypesTransactionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { AllTheProviders } from 'tests/test-utils';
|
||||
|
||||
import { BrandedPermission } from './types';
|
||||
import { useAuthZ } from './useAuthZ';
|
||||
import { buildPermission } from './utils';
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL || '';
|
||||
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
|
||||
|
||||
function authzMockResponse(
|
||||
payload: AuthtypesTransactionDTO[],
|
||||
authorizedByIndex: boolean[],
|
||||
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
|
||||
return {
|
||||
data: payload.map((txn, i) => ({
|
||||
relation: txn.relation,
|
||||
object: txn.object,
|
||||
authorized: authorizedByIndex[i] ?? false,
|
||||
})),
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
const wrapper = ({ children }: { children: ReactElement }): ReactElement => (
|
||||
<AllTheProviders>{children}</AllTheProviders>
|
||||
);
|
||||
|
||||
describe('useAuthZ', () => {
|
||||
it('should fetch and return permissions successfully', async () => {
|
||||
const permission1 = buildPermission('read', 'dashboard:*');
|
||||
const permission2 = buildPermission('update', 'dashboard:123');
|
||||
|
||||
const expectedResponse = {
|
||||
[permission1]: {
|
||||
isGranted: true,
|
||||
},
|
||||
[permission2]: {
|
||||
isGranted: false,
|
||||
},
|
||||
};
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [true, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAuthZ([permission1, permission2]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.permissions).toBeNull();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.permissions).toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const permission = buildPermission('read', 'dashboard:*');
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAuthZ([permission]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).not.toBeNull();
|
||||
expect(result.current.permissions).toBeNull();
|
||||
});
|
||||
|
||||
it('should refetch when permissions array changes', async () => {
|
||||
const permission1 = buildPermission('read', 'dashboard:*');
|
||||
const permission2 = buildPermission('update', 'dashboard:123');
|
||||
const permission3 = buildPermission('delete', 'dashboard:456');
|
||||
|
||||
let requestCount = 0;
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
requestCount++;
|
||||
const payload = await req.json();
|
||||
|
||||
if (payload.length === 1) {
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
}
|
||||
|
||||
const authorized = payload.map(
|
||||
(txn: { relation: string }) =>
|
||||
txn.relation === 'read' || txn.relation === 'delete',
|
||||
);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, authorized)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result, rerender } = renderHook<
|
||||
ReturnType<typeof useAuthZ>,
|
||||
BrandedPermission[]
|
||||
>((permissions) => useAuthZ(permissions), {
|
||||
wrapper,
|
||||
initialProps: [permission1],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(requestCount).toBe(1);
|
||||
expect(result.current.permissions).toEqual({
|
||||
[permission1]: {
|
||||
isGranted: true,
|
||||
},
|
||||
});
|
||||
|
||||
rerender([permission1, permission2, permission3]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(requestCount).toBe(2);
|
||||
expect(result.current.permissions).toEqual({
|
||||
[permission1]: {
|
||||
isGranted: true,
|
||||
},
|
||||
[permission2]: {
|
||||
isGranted: false,
|
||||
},
|
||||
[permission3]: {
|
||||
isGranted: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not refetch when permissions array order changes but content is the same', async () => {
|
||||
const permission1 = buildPermission('read', 'dashboard:*');
|
||||
const permission2 = buildPermission('update', 'dashboard:123');
|
||||
|
||||
let requestCount = 0;
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
requestCount++;
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [true, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result, rerender } = renderHook<
|
||||
ReturnType<typeof useAuthZ>,
|
||||
BrandedPermission[]
|
||||
>((permissions) => useAuthZ(permissions), {
|
||||
wrapper,
|
||||
initialProps: [permission1, permission2],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(requestCount).toBe(1);
|
||||
|
||||
rerender([permission2, permission1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(requestCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle empty permissions array', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json({ data: [], status: 'success' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAuthZ([]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.permissions).toEqual({});
|
||||
});
|
||||
|
||||
it('should send correct payload format to API', async () => {
|
||||
const permission1 = buildPermission('read', 'dashboard:*');
|
||||
const permission2 = buildPermission('update', 'dashboard:123');
|
||||
|
||||
let receivedPayload: any = null;
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
receivedPayload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(receivedPayload, [true, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAuthZ([permission1, permission2]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(receivedPayload).toHaveLength(2);
|
||||
expect(receivedPayload[0]).toMatchObject({
|
||||
relation: 'read',
|
||||
object: {
|
||||
resource: { name: 'dashboard', type: 'metaresource' },
|
||||
selector: '*',
|
||||
},
|
||||
});
|
||||
expect(receivedPayload[1]).toMatchObject({
|
||||
relation: 'update',
|
||||
object: {
|
||||
resource: { name: 'dashboard', type: 'metaresource' },
|
||||
selector: '123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should batch multiple hooks into single flight request', async () => {
|
||||
const permission1 = buildPermission('read', 'dashboard:*');
|
||||
const permission2 = buildPermission('update', 'dashboard:123');
|
||||
const permission3 = buildPermission('delete', 'dashboard:456');
|
||||
|
||||
let requestCount = 0;
|
||||
const receivedPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
requestCount++;
|
||||
const payload = await req.json();
|
||||
receivedPayloads.push(payload);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [true, false, true])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
const { result: result3 } = renderHook(() => useAuthZ([permission3]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result1.current.isLoading).toBe(false);
|
||||
expect(result2.current.isLoading).toBe(false);
|
||||
expect(result3.current.isLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
|
||||
expect(requestCount).toBe(1);
|
||||
expect(receivedPayloads).toHaveLength(1);
|
||||
expect(receivedPayloads[0]).toHaveLength(3);
|
||||
expect(receivedPayloads[0][0]).toMatchObject({
|
||||
relation: 'read',
|
||||
object: {
|
||||
resource: { name: 'dashboard', type: 'metaresource' },
|
||||
selector: '*',
|
||||
},
|
||||
});
|
||||
expect(receivedPayloads[0][1]).toMatchObject({
|
||||
relation: 'update',
|
||||
object: { resource: { name: 'dashboard' }, selector: '123' },
|
||||
});
|
||||
expect(receivedPayloads[0][2]).toMatchObject({
|
||||
relation: 'delete',
|
||||
object: { resource: { name: 'dashboard' }, selector: '456' },
|
||||
});
|
||||
|
||||
expect(result1.current.permissions).toEqual({
|
||||
[permission1]: { isGranted: true },
|
||||
});
|
||||
expect(result2.current.permissions).toEqual({
|
||||
[permission2]: { isGranted: false },
|
||||
});
|
||||
expect(result3.current.permissions).toEqual({
|
||||
[permission3]: { isGranted: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should create separate batches for calls after single flight window', async () => {
|
||||
const permission1 = buildPermission('read', 'dashboard:*');
|
||||
const permission2 = buildPermission('update', 'dashboard:123');
|
||||
const permission3 = buildPermission('delete', 'dashboard:456');
|
||||
|
||||
let requestCount = 0;
|
||||
const receivedPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
requestCount++;
|
||||
const payload = await req.json();
|
||||
receivedPayloads.push(payload);
|
||||
const authorized = payload.length === 1 ? [true] : [false, true];
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, authorized)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result1.current.isLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
|
||||
expect(requestCount).toBe(1);
|
||||
expect(receivedPayloads[0]).toHaveLength(1);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
const { result: result3 } = renderHook(() => useAuthZ([permission3]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result2.current.isLoading).toBe(false);
|
||||
expect(result3.current.isLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
|
||||
expect(requestCount).toBe(2);
|
||||
expect(receivedPayloads).toHaveLength(2);
|
||||
expect(receivedPayloads[1]).toHaveLength(2);
|
||||
expect(receivedPayloads[1][0]).toMatchObject({
|
||||
relation: 'update',
|
||||
object: { resource: { name: 'dashboard' }, selector: '123' },
|
||||
});
|
||||
expect(receivedPayloads[1][1]).toMatchObject({
|
||||
relation: 'delete',
|
||||
object: { resource: { name: 'dashboard' }, selector: '456' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should map permissions correctly when API returns response out of order', async () => {
|
||||
const permission1 = buildPermission('read', 'dashboard:*');
|
||||
const permission2 = buildPermission('update', 'dashboard:123');
|
||||
const permission3 = buildPermission('delete', 'dashboard:456');
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
const reversed = [...payload].reverse();
|
||||
const authorizedByReversed = [true, false, true];
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: reversed.map((txn: any, i: number) => ({
|
||||
relation: txn.relation,
|
||||
object: txn.object,
|
||||
authorized: authorizedByReversed[i],
|
||||
})),
|
||||
status: 'success',
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useAuthZ([permission1, permission2, permission3]),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.permissions).toEqual({
|
||||
[permission1]: { isGranted: true },
|
||||
[permission2]: { isGranted: false },
|
||||
[permission3]: { isGranted: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not leak state between separate batches', async () => {
|
||||
const permission1 = buildPermission('read', 'dashboard:*');
|
||||
const permission2 = buildPermission('update', 'dashboard:123');
|
||||
|
||||
let requestCount = 0;
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
requestCount++;
|
||||
const payload = await req.json();
|
||||
const authorized = payload.map(
|
||||
(txn: { relation: string }) => txn.relation === 'read',
|
||||
);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, authorized)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result1.current.isLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
|
||||
expect(requestCount).toBe(1);
|
||||
expect(result1.current.permissions).toEqual({
|
||||
[permission1]: { isGranted: true },
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result2.current.isLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
|
||||
expect(requestCount).toBe(2);
|
||||
expect(result1.current.permissions).toEqual({
|
||||
[permission1]: { isGranted: true },
|
||||
});
|
||||
expect(result2.current.permissions).toEqual({
|
||||
[permission2]: { isGranted: false },
|
||||
});
|
||||
expect(result1.current.permissions).not.toHaveProperty(permission2);
|
||||
expect(result2.current.permissions).not.toHaveProperty(permission1);
|
||||
});
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { authzCheck } from 'api/generated/services/authz';
|
||||
import type {
|
||||
AuthtypesObjectDTO,
|
||||
AuthtypesTransactionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { AuthZCheckResponse, BrandedPermission, UseAuthZResult } from './types';
|
||||
import {
|
||||
gettableTransactionToPermission,
|
||||
permissionToTransactionDto,
|
||||
} from './utils';
|
||||
|
||||
let ctx: Promise<AuthZCheckResponse> | null;
|
||||
let pendingPermissions: BrandedPermission[] = [];
|
||||
const SINGLE_FLIGHT_WAIT_TIME_MS = 50;
|
||||
const AUTHZ_CACHE_TIME = 20_000;
|
||||
|
||||
function dispatchPermission(
|
||||
permission: BrandedPermission,
|
||||
): Promise<AuthZCheckResponse> {
|
||||
pendingPermissions.push(permission);
|
||||
|
||||
if (!ctx) {
|
||||
let resolve: (v: AuthZCheckResponse) => void, reject: (reason?: any) => void;
|
||||
ctx = new Promise<AuthZCheckResponse>((r, re) => {
|
||||
resolve = r;
|
||||
reject = re;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const copiedPermissions = pendingPermissions.slice();
|
||||
pendingPermissions = [];
|
||||
ctx = null;
|
||||
|
||||
fetchManyPermissions(copiedPermissions).then(resolve).catch(reject);
|
||||
}, SINGLE_FLIGHT_WAIT_TIME_MS);
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
async function fetchManyPermissions(
|
||||
permissions: BrandedPermission[],
|
||||
): Promise<AuthZCheckResponse> {
|
||||
const payload: AuthtypesTransactionDTO[] = permissions.map((permission) => {
|
||||
const dto = permissionToTransactionDto(permission);
|
||||
const object: AuthtypesObjectDTO = {
|
||||
resource: {
|
||||
name: dto.object.resource.name,
|
||||
type: dto.object.resource.type,
|
||||
},
|
||||
selector: dto.object.selector,
|
||||
};
|
||||
return { relation: dto.relation, object };
|
||||
});
|
||||
|
||||
const { data } = await authzCheck(payload);
|
||||
|
||||
const fromApi = (data ?? []).reduce<AuthZCheckResponse>((acc, item) => {
|
||||
const permission = gettableTransactionToPermission(item);
|
||||
acc[permission] = { isGranted: !!item.authorized };
|
||||
return acc;
|
||||
}, {} as AuthZCheckResponse);
|
||||
|
||||
return permissions.reduce<AuthZCheckResponse>((acc, permission) => {
|
||||
acc[permission] = fromApi[permission] ?? { isGranted: false };
|
||||
return acc;
|
||||
}, {} as AuthZCheckResponse);
|
||||
}
|
||||
|
||||
export function useAuthZ(permissions: BrandedPermission[]): UseAuthZResult {
|
||||
const queryResults = useQueries(
|
||||
permissions.map((permission) => {
|
||||
return {
|
||||
queryKey: ['authz', permission],
|
||||
cacheTime: AUTHZ_CACHE_TIME,
|
||||
refetchOnMount: false,
|
||||
refetchIntervalInBackground: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: true,
|
||||
queryFn: async (): Promise<AuthZCheckResponse> => {
|
||||
const response = await dispatchPermission(permission);
|
||||
|
||||
return {
|
||||
[permission]: {
|
||||
isGranted: response[permission].isGranted,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const isLoading = useMemo(() => queryResults.some((q) => q.isLoading), [
|
||||
queryResults,
|
||||
]);
|
||||
const error = useMemo(
|
||||
() =>
|
||||
!isLoading
|
||||
? (queryResults.find((q) => !!q.error)?.error as Error) || null
|
||||
: null,
|
||||
[isLoading, queryResults],
|
||||
);
|
||||
const data = useMemo(() => {
|
||||
if (isLoading || error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return queryResults.reduce((acc, q) => {
|
||||
if (!q.data) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(q.data)) {
|
||||
acc[key as BrandedPermission] = value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as AuthZCheckResponse);
|
||||
}, [isLoading, error, queryResults]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
permissions: data ?? null,
|
||||
};
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { AuthtypesTransactionDTO } from '../../api/generated/services/sigNoz.schemas';
|
||||
import permissionsType from './permissions.type';
|
||||
import {
|
||||
AuthZObject,
|
||||
AuthZRelation,
|
||||
AuthZResource,
|
||||
BrandedPermission,
|
||||
ResourceName,
|
||||
ResourceType,
|
||||
} from './types';
|
||||
|
||||
export const PermissionSeparator = '||__||';
|
||||
export const ObjectSeparator = ':';
|
||||
|
||||
export function buildPermission<R extends AuthZRelation>(
|
||||
relation: R,
|
||||
object: AuthZObject<R>,
|
||||
): BrandedPermission {
|
||||
return `${relation}${PermissionSeparator}${object}` as BrandedPermission;
|
||||
}
|
||||
|
||||
export function buildObjectString(
|
||||
resource: AuthZResource,
|
||||
objectId: string,
|
||||
): `${AuthZResource}${typeof ObjectSeparator}${string}` {
|
||||
return `${resource}${ObjectSeparator}${objectId}` as const;
|
||||
}
|
||||
|
||||
export function parsePermission(
|
||||
permission: BrandedPermission,
|
||||
): {
|
||||
relation: AuthZRelation;
|
||||
object: string;
|
||||
} {
|
||||
const [relation, object] = permission.split(PermissionSeparator);
|
||||
return { relation: relation as AuthZRelation, object };
|
||||
}
|
||||
|
||||
const resourceNameToType = permissionsType.data.resources.reduce((acc, r) => {
|
||||
acc[r.name] = r.type;
|
||||
return acc;
|
||||
}, {} as Record<ResourceName, ResourceType>);
|
||||
|
||||
export function permissionToTransactionDto(
|
||||
permission: BrandedPermission,
|
||||
): AuthtypesTransactionDTO {
|
||||
const { relation, object: objectStr } = parsePermission(permission);
|
||||
const directType = resourceNameToType[objectStr as ResourceName];
|
||||
if (directType === 'metaresources') {
|
||||
return {
|
||||
relation,
|
||||
object: {
|
||||
resource: { name: objectStr, type: directType },
|
||||
selector: '*',
|
||||
},
|
||||
};
|
||||
}
|
||||
const [resourceName, selector] = objectStr.split(ObjectSeparator);
|
||||
const type =
|
||||
resourceNameToType[resourceName as ResourceName] ?? 'metaresource';
|
||||
|
||||
return {
|
||||
relation,
|
||||
object: {
|
||||
resource: { name: resourceName, type },
|
||||
selector: selector || '*',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function gettableTransactionToPermission(
|
||||
item: AuthtypesTransactionDTO,
|
||||
): BrandedPermission {
|
||||
const {
|
||||
relation,
|
||||
object: { resource, selector },
|
||||
} = item;
|
||||
const resourceName = String(resource.name);
|
||||
const selectorStr = typeof selector === 'string' ? selector : '*';
|
||||
const objectStr =
|
||||
resource.type === 'metaresources'
|
||||
? resourceName
|
||||
: `${resourceName}${ObjectSeparator}${selectorStr}`;
|
||||
return `${relation}${PermissionSeparator}${objectStr}` as BrandedPermission;
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
createSetCursorHandler,
|
||||
createSetLegendHandler,
|
||||
createSetSeriesHandler,
|
||||
getPlot,
|
||||
isScrollEventInPlot,
|
||||
updatePlotVisibility,
|
||||
updateWindowSize,
|
||||
@@ -54,7 +53,7 @@ export default function TooltipPlugin({
|
||||
const [viewState, setState] = useState<TooltipViewState>(
|
||||
createInitialViewState,
|
||||
);
|
||||
const { hasPlot, isHovering, isPinned, contents, style } = viewState;
|
||||
const { plot, isHovering, isPinned, contents, style } = viewState;
|
||||
|
||||
/**
|
||||
* Merge a partial view update into the current React state.
|
||||
@@ -73,25 +72,12 @@ export default function TooltipPlugin({
|
||||
layoutRef.current?.observer.disconnect();
|
||||
layoutRef.current = createLayoutObserver(layoutRef);
|
||||
|
||||
/**
|
||||
* Plot lifecycle and GC: viewState uses hasPlot (boolean), not the plot
|
||||
* reference; clearPlotReferences runs in cleanup so
|
||||
* detached canvases can be garbage collected.
|
||||
*/
|
||||
// Controller holds the mutable interaction state for this tooltip
|
||||
// instance. It is intentionally *not* React state so uPlot hooks
|
||||
// and DOM listeners can update it freely without triggering a
|
||||
// render on every mouse move.
|
||||
const controller: TooltipControllerState = createInitialControllerState();
|
||||
|
||||
/**
|
||||
* Clear plot references so detached canvases can be garbage collected.
|
||||
*/
|
||||
const clearPlotReferences = (): void => {
|
||||
controller.plot = null;
|
||||
updateState({ hasPlot: false });
|
||||
};
|
||||
|
||||
const syncTooltipWithDashboard = syncMode === DashboardCursorSync.Tooltip;
|
||||
|
||||
// Enable uPlot's built-in cursor sync when requested so that
|
||||
@@ -124,10 +110,9 @@ export default function TooltipPlugin({
|
||||
// Lock uPlot's internal cursor when the tooltip is pinned so
|
||||
// subsequent mouse moves do not move the crosshair.
|
||||
function updateCursorLock(): void {
|
||||
const plot = getPlot(controller);
|
||||
if (plot) {
|
||||
if (controller.plot) {
|
||||
// @ts-ignore uPlot cursor lock is not working as expected
|
||||
plot.cursor._lock = controller.pinned;
|
||||
controller.plot.cursor._lock = controller.pinned;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +142,8 @@ export default function TooltipPlugin({
|
||||
const isPinnedBeforeDismiss = controller.pinned;
|
||||
controller.pinned = false;
|
||||
controller.hoverActive = false;
|
||||
const plot = getPlot(controller);
|
||||
if (plot) {
|
||||
plot.setCursor({ left: -10, top: -10 });
|
||||
if (controller.plot) {
|
||||
controller.plot.setCursor({ left: -10, top: -10 });
|
||||
}
|
||||
scheduleRender(isPinnedBeforeDismiss);
|
||||
}
|
||||
@@ -167,12 +151,11 @@ export default function TooltipPlugin({
|
||||
// Build the React node to be rendered inside the tooltip by
|
||||
// delegating to the caller-provided `render` function.
|
||||
function createTooltipContents(): React.ReactNode {
|
||||
const plot = getPlot(controller);
|
||||
if (!controller.hoverActive || !plot) {
|
||||
if (!controller.hoverActive || !controller.plot) {
|
||||
return null;
|
||||
}
|
||||
return renderRef.current({
|
||||
uPlotInstance: plot,
|
||||
uPlotInstance: controller.plot,
|
||||
dataIndexes: controller.seriesIndexes,
|
||||
seriesIndex: controller.focusedSeriesIndex,
|
||||
isPinned: controller.pinned,
|
||||
@@ -257,13 +240,9 @@ export default function TooltipPlugin({
|
||||
|
||||
// When pinning is enabled, a click on the plot overlay while
|
||||
// hovering converts the transient tooltip into a pinned one.
|
||||
// Uses getPlot(controller) to avoid closing over u (plot), which
|
||||
// would retain the plot and detached canvases across unmounts.
|
||||
const handleUPlotOverClick = (event: MouseEvent): void => {
|
||||
const plot = getPlot(controller);
|
||||
const handleUPlotOverClick = (u: uPlot, event: MouseEvent): void => {
|
||||
if (
|
||||
plot &&
|
||||
event.target === plot.over &&
|
||||
event.target === u.over &&
|
||||
controller.hoverActive &&
|
||||
!controller.pinned &&
|
||||
controller.focusedSeriesIndex != null
|
||||
@@ -281,9 +260,10 @@ export default function TooltipPlugin({
|
||||
// on the controller and optionally attach the pinning handler.
|
||||
const handleInit = (u: uPlot): void => {
|
||||
controller.plot = u;
|
||||
updateState({ hasPlot: true });
|
||||
updateState({ plot: u });
|
||||
if (canPinTooltip) {
|
||||
overClickHandler = handleUPlotOverClick;
|
||||
overClickHandler = (event: MouseEvent): void =>
|
||||
handleUPlotOverClick(u, event);
|
||||
u.over.addEventListener('click', overClickHandler);
|
||||
}
|
||||
};
|
||||
@@ -319,6 +299,7 @@ export default function TooltipPlugin({
|
||||
const handleSetCursor = createSetCursorHandler(ctx);
|
||||
|
||||
handleWindowResize();
|
||||
|
||||
const removeReadyHook = config.addHook('ready', (): void =>
|
||||
updatePlotVisibility(controller),
|
||||
);
|
||||
@@ -344,20 +325,16 @@ export default function TooltipPlugin({
|
||||
removeSetSeriesHook();
|
||||
removeSetLegendHook();
|
||||
removeSetCursorHook();
|
||||
if (overClickHandler) {
|
||||
const plot = getPlot(controller);
|
||||
if (plot) {
|
||||
plot.over.removeEventListener('click', overClickHandler);
|
||||
}
|
||||
if (controller.plot && overClickHandler) {
|
||||
controller.plot.over.removeEventListener('click', overClickHandler);
|
||||
overClickHandler = null;
|
||||
}
|
||||
clearPlotReferences();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config]);
|
||||
|
||||
useLayoutEffect((): void => {
|
||||
if (!hasPlot || !layoutRef.current) {
|
||||
if (!plot || !layoutRef.current) {
|
||||
return;
|
||||
}
|
||||
const layout = layoutRef.current;
|
||||
@@ -372,9 +349,9 @@ export default function TooltipPlugin({
|
||||
layout.width = 0;
|
||||
layout.height = 0;
|
||||
}
|
||||
}, [isHovering, hasPlot]);
|
||||
}, [isHovering, plot]);
|
||||
|
||||
if (!hasPlot) {
|
||||
if (!plot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,6 @@ import {
|
||||
|
||||
const WINDOW_OFFSET = 16;
|
||||
|
||||
/** Get the plot instance from the controller; returns null if never set or cleared. */
|
||||
export function getPlot(controller: TooltipControllerState): uPlot | null {
|
||||
return controller.plot ?? null;
|
||||
}
|
||||
|
||||
export function createInitialControllerState(): TooltipControllerState {
|
||||
return {
|
||||
plot: null,
|
||||
@@ -51,13 +46,12 @@ export function updateWindowSize(controller: TooltipControllerState): void {
|
||||
* This is used to decide if a synced tooltip should be shown at all.
|
||||
*/
|
||||
export function updatePlotVisibility(controller: TooltipControllerState): void {
|
||||
const plot = getPlot(controller);
|
||||
if (!plot) {
|
||||
if (!controller.plot) {
|
||||
controller.plotWithinViewport = false;
|
||||
return;
|
||||
}
|
||||
controller.plotWithinViewport = isPlotInViewport(
|
||||
plot.rect,
|
||||
controller.plot.rect,
|
||||
controller.windowWidth,
|
||||
controller.windowHeight,
|
||||
);
|
||||
@@ -72,11 +66,10 @@ export function isScrollEventInPlot(
|
||||
event: Event,
|
||||
controller: TooltipControllerState,
|
||||
): boolean {
|
||||
const plot = getPlot(controller);
|
||||
return (
|
||||
event.target instanceof Node &&
|
||||
plot !== null &&
|
||||
event.target.contains(plot.root)
|
||||
controller.plot !== null &&
|
||||
event.target.contains(controller.plot.root)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,12 +165,11 @@ export function createSetLegendHandler(
|
||||
): (u: uPlot) => void {
|
||||
return (u: uPlot): void => {
|
||||
const { controller } = ctx;
|
||||
const plot = getPlot(controller);
|
||||
if (!plot?.cursor?.idxs) {
|
||||
if (!controller.plot?.cursor?.idxs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSeriesIndexes = plot.cursor.idxs.slice();
|
||||
const newSeriesIndexes = controller.plot.cursor.idxs.slice();
|
||||
const isAnySeriesActive = newSeriesIndexes.some((v, i) => i > 0 && v != null);
|
||||
|
||||
const previousCursorDrivenBySync = controller.cursorDrivenBySync;
|
||||
|
||||
@@ -18,8 +18,7 @@ export enum DashboardCursorSync {
|
||||
}
|
||||
|
||||
export interface TooltipViewState {
|
||||
/** Whether a plot instance exists; plot reference is in controller, not state. */
|
||||
hasPlot?: boolean;
|
||||
plot?: uPlot | null;
|
||||
style: Partial<CSSProperties>;
|
||||
isHovering: boolean;
|
||||
isPinned: boolean;
|
||||
|
||||
@@ -123,7 +123,7 @@ export function createInitialViewState(): TooltipViewState {
|
||||
isHovering: false,
|
||||
isPinned: false,
|
||||
contents: null,
|
||||
hasPlot: false,
|
||||
plot: null,
|
||||
dismiss: (): void => {},
|
||||
};
|
||||
}
|
||||
|
||||
354
go.mod
354
go.mod
@@ -1,51 +1,51 @@
|
||||
module github.com/SigNoz/signoz
|
||||
|
||||
go 1.25.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
dario.cat/mergo v1.0.1
|
||||
github.com/AfterShip/clickhouse-sql-parser v0.4.16
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.1
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.2
|
||||
github.com/SigNoz/signoz-otel-collector v0.129.10-rc.9
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/bytedance/sonic v1.14.1
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-co-op/gocron v1.30.1
|
||||
github.com/go-openapi/runtime v0.29.2
|
||||
github.com/go-openapi/strfmt v0.25.0
|
||||
github.com/go-openapi/runtime v0.28.0
|
||||
github.com/go-openapi/strfmt v0.23.0
|
||||
github.com/go-redis/redismock/v9 v9.2.0
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/gojek/heimdall/v7 v7.0.3
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/huandu/go-sqlbuilder v1.35.0
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/knadh/koanf/v2 v2.3.2
|
||||
github.com/mailru/easyjson v0.9.0
|
||||
github.com/open-telemetry/opamp-go v0.22.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0
|
||||
github.com/knadh/koanf/v2 v2.2.0
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/open-telemetry/opamp-go v0.19.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.128.0
|
||||
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67
|
||||
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/alertmanager v0.31.0
|
||||
github.com/prometheus/alertmanager v0.28.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/common v0.67.5
|
||||
github.com/prometheus/prometheus v0.310.0
|
||||
github.com/prometheus/common v0.66.1
|
||||
github.com/prometheus/prometheus v0.304.1
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/redis/go-redis/v9 v9.15.1
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
@@ -54,7 +54,7 @@ require (
|
||||
github.com/sethvargo/go-password v0.2.0
|
||||
github.com/smartystreets/goconvey v1.8.1
|
||||
github.com/soheilhy/cmux v0.1.5
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/srikanthccv/ClickHouse-go-mock v0.13.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggest/jsonschema-go v0.3.78
|
||||
@@ -64,71 +64,43 @@ require (
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
||||
github.com/uptrace/bun/extra/bunotel v1.2.9
|
||||
go.opentelemetry.io/collector/confmap v1.51.0
|
||||
go.opentelemetry.io/collector/otelcol v0.144.0
|
||||
go.opentelemetry.io/collector/pdata v1.51.0
|
||||
go.opentelemetry.io/collector/confmap v1.34.0
|
||||
go.opentelemetry.io/collector/otelcol v0.128.0
|
||||
go.opentelemetry.io/collector/pdata v1.34.0
|
||||
go.opentelemetry.io/contrib/config v0.10.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/metric v1.40.0
|
||||
go.opentelemetry.io/otel/sdk v1.40.0
|
||||
go.opentelemetry.io/otel/trace v1.40.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel/metric v1.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/text v0.33.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
golang.org/x/text v0.32.0
|
||||
google.golang.org/protobuf v1.36.9
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/apimachinery v0.34.0
|
||||
modernc.org/sqlite v1.39.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/hashicorp/go-metrics v0.5.4 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/swaggest/refl v1.4.0 // indirect
|
||||
@@ -136,70 +108,69 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
|
||||
go.opentelemetry.io/collector/client v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configoptional v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configretry v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/exporterhelper v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/xpdata v0.144.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.opentelemetry.io/collector/config/configretry v1.34.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go/auth v0.18.1 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/ClickHouse/ch-go v0.67.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/Yiling-J/theine-go v0.6.2 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/coder/quartz v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
|
||||
github.com/coder/quartz v0.1.2 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dennwc/varint v1.0.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/edsrzf/mmap-go v1.2.0 // indirect
|
||||
github.com/elastic/lunes v0.2.0 // indirect
|
||||
github.com/elastic/lunes v0.1.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/expr-lang/expr v1.17.7
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/expr-lang/expr v1.17.5
|
||||
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/analysis v0.24.2 // indirect
|
||||
github.com/go-openapi/errors v0.22.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/loads v0.23.2 // indirect
|
||||
github.com/go-openapi/spec v0.22.3 // indirect
|
||||
github.com/go-openapi/swag v0.25.4 // indirect
|
||||
github.com/go-openapi/validate v0.25.1 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/loads v0.22.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
@@ -207,22 +178,22 @@ require (
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.1 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.1 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/memberlist v0.5.4 // indirect
|
||||
github.com/hashicorp/memberlist v0.5.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
@@ -230,25 +201,26 @@ require (
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jessevdk/go-flags v1.6.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/leodido/go-syslog/v4 v4.3.0 // indirect
|
||||
github.com/leodido/go-syslog/v4 v4.2.0 // indirect
|
||||
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/mdlayher/vsock v1.2.1 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/miekg/dns v1.1.65 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
@@ -257,27 +229,27 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/natefinch/wrap v0.2.0 // indirect
|
||||
github.com/oklog/run v1.2.0 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1
|
||||
github.com/open-feature/go-sdk v1.17.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.144.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.128.0 // indirect
|
||||
github.com/openfga/openfga v1.10.1
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.23 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pressly/goose/v3 v3.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.15.1 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/sigv4 v0.4.1 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.14.0 // indirect
|
||||
github.com/prometheus/otlptranslator v0.0.0-20250320144820-d800c8b0eb07 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/sigv4 v0.1.2 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
@@ -285,7 +257,7 @@ require (
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/segmentio/backo-go v1.0.1 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
|
||||
@@ -300,92 +272,94 @@ require (
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/swaggest/openapi-go v0.2.60
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/valyala/fastjson v1.6.7 // indirect
|
||||
github.com/trivago/tgo v1.0.7 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vjeantet/grok v1.0.1 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/collector/component v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componentstatus v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componenttest v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configtelemetry v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/provider/envprovider v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/connector v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/connector/connectortest v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/connector/xconnector v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumererror v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumertest v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/exportertest v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/xexporter v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/extension v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/extensioncapabilities v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/extensiontest v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/xextension v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/featuregate v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/fanoutconsumer v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/telemetry v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/testdata v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline/xpipeline v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/processor v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/processorhelper v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/processortest v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/xprocessor v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/receiverhelper v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/receivertest v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/xreceiver v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/semconv v0.128.1-0.20250610090210-188191247685
|
||||
go.opentelemetry.io/collector/service v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/service/hostcapabilities v0.144.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/otelconf v0.18.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.15.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/collector/component v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componentstatus v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componenttest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configtelemetry v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/provider/envprovider v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/xconfmap v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/connector v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/connector/connectortest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/connector/xconnector v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumererror v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumertest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/xconsumer v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/exportertest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/xexporter v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/extension v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/extensioncapabilities v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/extensiontest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/xextension v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/featuregate v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/fanoutconsumer v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/telemetry v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/testdata v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline/xpipeline v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/processor v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/processorhelper v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/processortest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/xprocessor v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/receiverhelper v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/receivertest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/xreceiver v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/semconv v0.128.0
|
||||
go.opentelemetry.io/collector/service v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/service/hostcapabilities v0.128.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/otelconf v0.16.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.58.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
gonum.org/v1/gonum v0.17.0 // indirect
|
||||
google.golang.org/api v0.265.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
google.golang.org/api v0.236.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
gopkg.in/telebot.v3 v3.3.8 // indirect
|
||||
k8s.io/client-go v0.35.0 // indirect
|
||||
k8s.io/client-go v0.34.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/expr-lang/expr => github.com/SigNoz/expr v1.17.7-beta
|
||||
|
||||
@@ -95,7 +95,7 @@ func (d *Dispatcher) Run() {
|
||||
d.ctx, d.cancel = context.WithCancel(context.Background())
|
||||
d.mtx.Unlock()
|
||||
|
||||
d.run(d.alerts.Subscribe(fmt.Sprintf("dispatcher-%s", d.orgID)))
|
||||
d.run(d.alerts.Subscribe())
|
||||
close(d.done)
|
||||
}
|
||||
|
||||
@@ -107,15 +107,14 @@ func (d *Dispatcher) run(it provider.AlertIterator) {
|
||||
|
||||
for {
|
||||
select {
|
||||
case alertWrapper, ok := <-it.Next():
|
||||
if !ok || alertWrapper == nil {
|
||||
case alert, ok := <-it.Next():
|
||||
if !ok {
|
||||
// Iterator exhausted for some reason.
|
||||
if err := it.Err(); err != nil {
|
||||
d.logger.ErrorContext(d.ctx, "Error on alert update", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
alert := alertWrapper.Data
|
||||
|
||||
d.logger.DebugContext(d.ctx, "SigNoz Custom Dispatcher: Received alert", "alert", alert)
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ route:
|
||||
logger := providerSettings.Logger
|
||||
route := dispatch.NewRoute(conf.Route, nil)
|
||||
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, alertmanagertypes.AlertStoreCallback{}, logger, nil, nil)
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -496,7 +496,7 @@ route:
|
||||
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = alerts.Put(ctx, inputAlerts...)
|
||||
err = alerts.Put(inputAlerts...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -638,7 +638,7 @@ route:
|
||||
logger := providerSettings.Logger
|
||||
route := dispatch.NewRoute(conf.Route, nil)
|
||||
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, alertmanagertypes.AlertStoreCallback{}, logger, nil, nil)
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -798,7 +798,7 @@ route:
|
||||
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = alerts.Put(ctx, inputAlerts...)
|
||||
err = alerts.Put(inputAlerts...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -897,7 +897,7 @@ route:
|
||||
logger := providerSettings.Logger
|
||||
route := dispatch.NewRoute(conf.Route, nil)
|
||||
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, alertmanagertypes.AlertStoreCallback{}, logger, nil, nil)
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1028,7 +1028,7 @@ route:
|
||||
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = alerts.Put(ctx, inputAlerts...)
|
||||
err = alerts.Put(inputAlerts...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1159,7 +1159,7 @@ func newAlert(labels model.LabelSet) *alertmanagertypes.Alert {
|
||||
func TestDispatcherRace(t *testing.T) {
|
||||
logger := promslog.NewNopLogger()
|
||||
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, alertmanagertypes.AlertStoreCallback{}, logger, nil, nil)
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1175,7 +1175,6 @@ func TestDispatcherRace(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDispatcherRaceOnFirstAlertNotDeliveredWhenGroupWaitIsZero(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const numAlerts = 5000
|
||||
confData := `receivers:
|
||||
- name: 'slack'
|
||||
@@ -1195,7 +1194,7 @@ route:
|
||||
providerSettings := createTestProviderSettings()
|
||||
logger := providerSettings.Logger
|
||||
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, alertmanagertypes.AlertStoreCallback{}, logger, nil, nil)
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1248,7 +1247,7 @@ route:
|
||||
for i := 0; i < numAlerts; i++ {
|
||||
ruleId := fmt.Sprintf("Alert_%d", i)
|
||||
alert := newAlert(model.LabelSet{"ruleId": model.LabelValue(ruleId)})
|
||||
require.NoError(t, alerts.Put(ctx, alert))
|
||||
require.NoError(t, alerts.Put(alert))
|
||||
}
|
||||
|
||||
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); {
|
||||
@@ -1266,7 +1265,7 @@ func TestDispatcher_DoMaintenance(t *testing.T) {
|
||||
r := prometheus.NewRegistry()
|
||||
marker := alertmanagertypes.NewMarker(r)
|
||||
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, 0, alertmanagertypes.AlertStoreCallback{}, promslog.NewNopLogger(), nil, nil)
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, nil, promslog.NewNopLogger(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1371,7 +1370,7 @@ route:
|
||||
logger := providerSettings.Logger
|
||||
route := dispatch.NewRoute(conf.Route, nil)
|
||||
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, alertmanagertypes.AlertStoreCallback{}, logger, nil, nil)
|
||||
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
|
||||
})
|
||||
}()
|
||||
|
||||
server.alerts, err = mem.NewAlerts(ctx, server.marker, server.srvConfig.Alerts.GCInterval, 0, alertmanagertypes.AlertStoreCallback{}, server.logger, signozRegisterer, nil)
|
||||
server.alerts, err = mem.NewAlerts(ctx, server.marker, server.srvConfig.Alerts.GCInterval, nil, server.logger, signozRegisterer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -203,15 +203,15 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
|
||||
|
||||
func (server *Server) GetAlerts(ctx context.Context, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
|
||||
return alertmanagertypes.NewGettableAlertsFromAlertProvider(server.alerts, server.alertmanagerConfig, server.marker.Status, func(labels model.LabelSet) {
|
||||
server.inhibitor.Mutes(ctx, labels)
|
||||
server.silencer.Mutes(ctx, labels)
|
||||
server.inhibitor.Mutes(labels)
|
||||
server.silencer.Mutes(labels)
|
||||
}, params)
|
||||
}
|
||||
|
||||
func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanagertypes.PostableAlerts) error {
|
||||
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(postableAlerts, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now(), ctx)
|
||||
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(postableAlerts, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
|
||||
// Notification sending alert takes precedence over validation errors.
|
||||
if err := server.alerts.Put(ctx, alerts...); err != nil {
|
||||
if err := server.alerts.Put(alerts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -343,7 +343,6 @@ func (server *Server) TestAlert(ctx context.Context, receiversMap map[*alertmana
|
||||
postableAlerts,
|
||||
time.Duration(server.srvConfig.Global.ResolveTimeout),
|
||||
time.Now(),
|
||||
ctx,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput,
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestServerTestReceiverTypeWebhook(t *testing.T) {
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
URL: config.SecretTemplateURL(webhookURL.String()),
|
||||
URL: &config.SecretURL{URL: webhookURL},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -96,7 +96,7 @@ func TestServerPutAlerts(t *testing.T) {
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
URL: config.SecretTemplateURL("http://localhost/test-receiver"),
|
||||
URL: &config.SecretURL{URL: &url.URL{Host: "localhost", Path: "/test-receiver"}},
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -176,7 +176,7 @@ func TestServerTestAlert(t *testing.T) {
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
URL: config.SecretTemplateURL(webhook1URL.String()),
|
||||
URL: &config.SecretURL{URL: webhook1URL},
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -186,7 +186,7 @@ func TestServerTestAlert(t *testing.T) {
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
URL: config.SecretTemplateURL(webhook2URL.String()),
|
||||
URL: &config.SecretURL{URL: webhook2URL},
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -268,7 +268,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
URL: config.SecretTemplateURL(webhookURL.String()),
|
||||
URL: &config.SecretURL{URL: webhookURL},
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -278,7 +278,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
URL: config.SecretTemplateURL("http://localhost:1/webhook"),
|
||||
URL: &config.SecretURL{URL: &url.URL{Scheme: "http", Host: "localhost:1", Path: "/webhook"}},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -87,24 +87,6 @@ func (client *client) Read(ctx context.Context, query *prompb.Query, sortSeries
|
||||
return remote.FromQueryResult(sortSeries, res), nil
|
||||
}
|
||||
|
||||
func (c *client) ReadMultiple(ctx context.Context, queries []*prompb.Query, sortSeries bool) (storage.SeriesSet, error) {
|
||||
if len(queries) == 0 {
|
||||
return storage.EmptySeriesSet(), nil
|
||||
}
|
||||
if len(queries) == 1 {
|
||||
return c.Read(ctx, queries[0], sortSeries)
|
||||
}
|
||||
sets := make([]storage.SeriesSet, 0, len(queries))
|
||||
for _, q := range queries {
|
||||
ss, err := c.Read(ctx, q, sortSeries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sets = append(sets, ss)
|
||||
}
|
||||
return storage.NewMergeSeriesSet(sets, 0, storage.ChainedSeriesMerge), nil
|
||||
}
|
||||
|
||||
func (client *client) queryToClickhouseQuery(_ context.Context, query *prompb.Query, metricName string, subQuery bool) (string, []any, error) {
|
||||
var clickHouseQuery string
|
||||
var conditions []string
|
||||
|
||||
@@ -2,7 +2,6 @@ package prometheus
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
)
|
||||
|
||||
@@ -20,18 +19,26 @@ func RemoveExtraLabels(res *promql.Result, labelsToRemove ...string) error {
|
||||
case promql.Vector:
|
||||
value := res.Value.(promql.Vector)
|
||||
for i := range value {
|
||||
b := labels.NewBuilder(value[i].Metric)
|
||||
b.Del(labelsToRemove...)
|
||||
newLabels := b.Labels()
|
||||
value[i].Metric = newLabels
|
||||
series := &(value)[i]
|
||||
dst := series.Metric[:0]
|
||||
for _, lbl := range series.Metric {
|
||||
if _, drop := toRemove[lbl.Name]; !drop {
|
||||
dst = append(dst, lbl)
|
||||
}
|
||||
}
|
||||
series.Metric = dst
|
||||
}
|
||||
case promql.Matrix:
|
||||
value := res.Value.(promql.Matrix)
|
||||
for i := range value {
|
||||
b := labels.NewBuilder(value[i].Metric)
|
||||
b.Del(labelsToRemove...)
|
||||
newLabels := b.Labels()
|
||||
value[i].Metric = newLabels
|
||||
series := &(value)[i]
|
||||
dst := series.Metric[:0]
|
||||
for _, lbl := range series.Metric {
|
||||
if _, drop := toRemove[lbl.Name]; !drop {
|
||||
dst = append(dst, lbl)
|
||||
}
|
||||
}
|
||||
series.Metric = dst
|
||||
}
|
||||
case promql.Scalar:
|
||||
return nil
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
)
|
||||
@@ -241,13 +240,14 @@ func (q *promqlQuery) Execute(ctx context.Context) (*qbv5.Result, error) {
|
||||
var series []*qbv5.TimeSeries
|
||||
for _, v := range matrix {
|
||||
var s qbv5.TimeSeries
|
||||
lbls := make([]*qbv5.Label, 0, v.Metric.Len())
|
||||
v.Metric.Range(func(l labels.Label) {
|
||||
lbls := make([]*qbv5.Label, 0, len(v.Metric))
|
||||
for name, value := range v.Metric.Copy().Map() {
|
||||
lbls = append(lbls, &qbv5.Label{
|
||||
Key: telemetrytypes.TelemetryFieldKey{Name: l.Name},
|
||||
Value: l.Value,
|
||||
Key: telemetrytypes.TelemetryFieldKey{Name: name},
|
||||
Value: value,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
s.Labels = lbls
|
||||
|
||||
for idx := range v.Floats {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
@@ -17,9 +18,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
)
|
||||
|
||||
@@ -462,12 +461,12 @@ func toCommonSeries(series promql.Series) v3.Series {
|
||||
Points: make([]v3.Point, 0),
|
||||
}
|
||||
|
||||
series.Metric.Range(func(lbl labels.Label) {
|
||||
for _, lbl := range series.Metric {
|
||||
commonSeries.Labels[lbl.Name] = lbl.Value
|
||||
commonSeries.LabelsArray = append(commonSeries.LabelsArray, map[string]string{
|
||||
lbl.Name: lbl.Value,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
for _, f := range series.Floats {
|
||||
commonSeries.Points = append(commonSeries.Points, v3.Point{
|
||||
|
||||
@@ -35,7 +35,7 @@ func (c *conditionBuilder) conditionFor(
|
||||
return "", err
|
||||
}
|
||||
|
||||
if column.Type.GetType() == schema.ColumnTypeEnumJSON && querybuilder.BodyJSONQueryEnabled {
|
||||
if column.IsJSONColumn() && querybuilder.BodyJSONQueryEnabled {
|
||||
valueType, value := InferDataType(value, operator, key)
|
||||
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
LogsV2TimestampColumn = "timestamp"
|
||||
LogsV2ObservedTimestampColumn = "observed_timestamp"
|
||||
LogsV2BodyColumn = "body"
|
||||
LogsV2BodyJSONColumn = constants.BodyV2Column
|
||||
LogsV2BodyJSONColumn = constants.BodyJSONColumn
|
||||
LogsV2BodyPromotedColumn = constants.BodyPromotedColumn
|
||||
LogsV2TraceIDColumn = "trace_id"
|
||||
LogsV2SpanIDColumn = "span_id"
|
||||
@@ -34,7 +34,7 @@ const (
|
||||
LogsV2ResourcesStringColumn = "resources_string"
|
||||
LogsV2ScopeStringColumn = "scope_string"
|
||||
|
||||
BodyJSONColumnPrefix = constants.BodyV2ColumnPrefix
|
||||
BodyJSONColumnPrefix = constants.BodyJSONColumnPrefix
|
||||
BodyPromotedColumnPrefix = constants.BodyPromotedColumnPrefix
|
||||
)
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type fieldMapper struct{}
|
||||
type fieldMapper struct {}
|
||||
|
||||
func NewFieldMapper() qbtypes.FieldMapper {
|
||||
return &fieldMapper{}
|
||||
|
||||
@@ -253,7 +253,7 @@ func buildListLogsJSONIndexesQuery(cluster string, filters ...string) (string, [
|
||||
sb.Where(sb.Equal("database", telemetrylogs.DBName))
|
||||
sb.Where(sb.Equal("table", telemetrylogs.LogsV2LocalTableName))
|
||||
sb.Where(sb.Or(
|
||||
sb.ILike("expr", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyV2ColumnPrefix))),
|
||||
sb.ILike("expr", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyJSONColumnPrefix))),
|
||||
sb.ILike("expr", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyPromotedColumnPrefix))),
|
||||
))
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestBuildListLogsJSONIndexesQuery(t *testing.T) {
|
||||
expectedArgs: []any{
|
||||
telemetrylogs.DBName,
|
||||
telemetrylogs.LogsV2LocalTableName,
|
||||
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyV2ColumnPrefix)),
|
||||
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyJSONColumnPrefix)),
|
||||
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyPromotedColumnPrefix)),
|
||||
},
|
||||
},
|
||||
@@ -130,7 +130,7 @@ func TestBuildListLogsJSONIndexesQuery(t *testing.T) {
|
||||
expectedArgs: []any{
|
||||
telemetrylogs.DBName,
|
||||
telemetrylogs.LogsV2LocalTableName,
|
||||
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyV2ColumnPrefix)),
|
||||
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyJSONColumnPrefix)),
|
||||
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyPromotedColumnPrefix)),
|
||||
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains("foo")),
|
||||
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains("bar")),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package alertmanagertypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -91,8 +90,8 @@ func NewDeprecatedGettableAlertsFromGettableAlerts(gettableAlerts GettableAlerts
|
||||
}
|
||||
|
||||
// Converts a slice of PostableAlert to a slice of Alert.
|
||||
func NewAlertsFromPostableAlerts(postableAlerts PostableAlerts, resolveTimeout time.Duration, now time.Time, ctx context.Context) ([]*types.Alert, []error) {
|
||||
alerts := v2.OpenAPIAlertsToAlerts(ctx, postableAlerts)
|
||||
func NewAlertsFromPostableAlerts(postableAlerts PostableAlerts, resolveTimeout time.Duration, now time.Time) ([]*types.Alert, []error) {
|
||||
alerts := v2.OpenAPIAlertsToAlerts(postableAlerts)
|
||||
|
||||
for _, alert := range alerts {
|
||||
alert.UpdatedAt = now
|
||||
@@ -197,15 +196,8 @@ func NewGettableAlertsFromAlertProvider(
|
||||
if err = iterator.Err(); err != nil {
|
||||
break
|
||||
}
|
||||
if a == nil {
|
||||
break
|
||||
}
|
||||
alertData := a.Data
|
||||
if alertData == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
routes := dispatch.NewRoute(cfg.alertmanagerConfig.Route, nil).Match(alertData.Labels)
|
||||
routes := dispatch.NewRoute(cfg.alertmanagerConfig.Route, nil).Match(a.Labels)
|
||||
receivers := make([]string, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
receivers = append(receivers, r.RouteOpts.Receiver)
|
||||
@@ -215,11 +207,11 @@ func NewGettableAlertsFromAlertProvider(
|
||||
continue
|
||||
}
|
||||
|
||||
if !alertFilter(alertData, now) {
|
||||
if !alertFilter(a, now) {
|
||||
continue
|
||||
}
|
||||
|
||||
alert := v2.AlertToOpenAPIAlert(alertData, getAlertStatusFunc(alertData.Fingerprint()), receivers, nil)
|
||||
alert := v2.AlertToOpenAPIAlert(a, getAlertStatusFunc(a.Fingerprint()), receivers, nil)
|
||||
|
||||
res = append(res, alert)
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package alertmanagertypes
|
||||
|
||||
import "github.com/prometheus/alertmanager/types"
|
||||
|
||||
// AlertStoreCallback is a no-op implementation of mem.AlertStoreCallback.
|
||||
type AlertStoreCallback struct{}
|
||||
|
||||
func (AlertStoreCallback) PreStore(_ *types.Alert, _ bool) error { return nil }
|
||||
func (AlertStoreCallback) PostStore(_ *types.Alert, _ bool) {}
|
||||
func (AlertStoreCallback) PostDelete(_ *types.Alert) {}
|
||||
@@ -36,8 +36,8 @@ func (i *PromotePath) ValidateAndSetDefaults() error {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "array paths can not be promoted or indexed")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(i.Path, constants.BodyV2ColumnPrefix) || strings.HasPrefix(i.Path, constants.BodyPromotedColumnPrefix) {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "`%s`, `%s` don't add these prefixes to the path", constants.BodyV2ColumnPrefix, constants.BodyPromotedColumnPrefix)
|
||||
if strings.HasPrefix(i.Path, constants.BodyJSONColumnPrefix) || strings.HasPrefix(i.Path, constants.BodyPromotedColumnPrefix) {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "`%s`, `%s` don't add these prefixes to the path", constants.BodyJSONColumnPrefix, constants.BodyPromotedColumnPrefix)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(i.Path, telemetrytypes.BodyJSONStringSearchPrefix) {
|
||||
|
||||
Reference in New Issue
Block a user