Compare commits

..

13 Commits

Author SHA1 Message Date
Abhi kumar
f11153cdec Merge branch 'main' into chore/uplot-builder-restructure 2026-02-28 18:55:41 +05:30
Srikanth Chekuri
95fc905448 chore: move savedview and integration types into their own types package (#10454)
Some checks are pending
build-staging / staging (push) Blocked by required conditions
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-02-28 12:55:26 +00:00
Abhi Kumar
a380c4d6be chore: fixed tsc + test 2026-02-28 18:08:39 +05:30
Abhi Kumar
a0f576e5fb chore: fixed tsc + test 2026-02-28 17:53:44 +05:30
Abhi Kumar
98013ee3b9 chore: fixed tsc + test 2026-02-28 15:51:58 +05:30
Abhi Kumar
97b94fc4f5 chore: updated timezone types 2026-02-28 15:20:24 +05:30
Abhi Kumar
0f3f49b96a chore: updated baseconfigbuilder test 2026-02-28 15:15:21 +05:30
Abhi Kumar
0928a30863 chore: made baseconfigbuilder generic to be used across different charts 2026-02-28 15:10:16 +05:30
Vinicius Lourenço
9f5afaf36f perf(bundle-size): be explicit when including new languages for react-syntax-highlighter (#10228)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-28 14:48:50 +05:30
Srikanth Chekuri
3f69d5cdc2 chore: add docs for service (#10450) 2026-02-28 13:59:42 +05:30
Srikanth Chekuri
98617b5120 chore: add doc for adding new abstractions to codebase (#10444)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: add doc for adding new abstractions to codebase

* chore: reorder

---------

Co-authored-by: Pandey <vibhupandey28@gmail.com>
2026-02-27 20:55:15 +00:00
Vinicius Lourenço
df30852296 perf(bundle-size): move antd-table-saveas-excel out of main bundle (#10229)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-27 17:27:30 +00:00
Srikanth Chekuri
b9beabb425 chore: add guide for packages (#10443) 2026-02-27 13:50:44 +00:00
56 changed files with 1255 additions and 843 deletions

View File

@@ -61,3 +61,13 @@ jobs:
with:
PRIMUS_REF: main
JS_SRC: frontend
md-languages:
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
uses: actions/checkout@v4
- name: validate md languages
run: bash frontend/scripts/validate-md-languages.sh

View File

@@ -0,0 +1,127 @@
# Abstractions
This document provides rules for deciding when a new type, interface, or intermediate representation is warranted in Go code. The goal is to keep the codebase navigable by ensuring every abstraction earns its place.
## The cost of a new abstraction
Every exported type, interface, or wrapper is a permanent commitment. It must be named, documented, tested, and understood by every future contributor. It creates a new concept in the codebase vocabulary. Before introducing one, verify that the cost is justified by a concrete benefit that cannot be achieved with existing mechanisms.
## Before you introduce anything new
Answer these four questions. If writing a PR, include the answers in the description.
1. **What already exists?** Name the specific type, function, interface, or library that covers this ground today.
2. **What does the new abstraction add?** Name the concrete operation, guarantee, or capability. "Cleaner" or "more reusable" are not sufficient; name what the caller can do that it could not do before.
3. **What does the new abstraction drop?** If it wraps or mirrors an existing structure, list what it cannot represent. Every gap must be either justified or handled with an explicit error.
4. **Who consumes it?** List the call sites. If there is only one producer and one consumer in the same call chain, you likely need a function, not a type.
## Rules
### 1. Prefer functions over types
If a piece of logic has one input and one output, write a function. Do not create a struct to hold intermediate state that is built in one place and read in one place. A function is easier to test, easier to inline, and does not expand the vocabulary of the codebase.
```go
// Prefer this:
func ConvertConfig(src ExternalConfig) (InternalConfig, error)
// Over this:
type ConfigAdapter struct { ... }
func NewConfigAdapter(src ExternalConfig) *ConfigAdapter
func (a *ConfigAdapter) ToInternal() (InternalConfig, error)
```
The two-step version is only justified when `ConfigAdapter` has multiple distinct consumers that use it in different ways.
### 2. Do not duplicate structures you do not own
When a library or external package produces a structured output, operate on that output directly. Do not create a parallel type that mirrors a subset of its fields.
A partial copy will:
- **Silently lose data** when the source has fields or variants the copy does not account for.
- **Drift** when the source evolves and the copy is not updated in lockstep.
- **Add a conversion step** that doubles the code surface and the opportunity for bugs.
If you need to shield consumers from a dependency, define a narrow interface over the dependency's type rather than copying its shape into a new struct.
### 3. Never silently discard input
If your code receives structured input and cannot handle part of it, return an error. Do not silently return nil, skip the element, or produce a partial result. Silent data loss is the hardest class of bug to detect because the code appears to work, it just produces wrong results.
```go
// Wrong: silently ignores the unrecognized case.
default:
return nil
// Right: makes the gap visible.
default:
return nil, fmt.Errorf("unsupported %T value: %v", v, v)
```
This applies broadly: type switches, format conversions, data migrations, enum mappings, configuration parsing. Anywhere a `default` or `else` branch can swallow input, it should surface an error instead.
### 4. Do not expose methods that lose information
A method on a structured type should not strip meaning from the structure it belongs to. If a caller needs to iterate over elements for a specific purpose (validation, aggregation, logging), write that logic as a standalone function that operates on the structure with full context, rather than adding a method that returns a reduced view.
```go
// Problematic: callers cannot distinguish how items were related.
func (o *Order) AllLineItems() []LineItem { ... }
// Better: the validation logic operates on the full structure.
func ValidateOrder(o *Order) error { ... }
```
Public methods shape how a type is used. Once a lossy accessor exists, callers will depend on it, and the lost information becomes unrecoverable at those call sites.
### 5. Interfaces should be discovered, not predicted
Do not define an interface before you have at least two concrete implementations that need it. An interface with one implementation is not abstraction; it is indirection that makes it harder to navigate from call site to implementation.
The exception is interfaces required for testing (e.g., for mocking an external dependency). In that case, define the interface in the **consuming** package, not the providing package, following the Go convention of [accepting interfaces and returning structs](https://go.dev/wiki/CodeReviewComments#interfaces).
### 6. Wrappers must add semantics, not just rename
A wrapper type is justified when it adds meaning, validation, or invariants that the underlying type does not carry. It is not justified when it merely renames fields or reorganizes the same data into a different shape.
```go
// Justified: adds validation that the underlying string does not carry.
type OrgID struct{ value string }
func NewOrgID(s string) (OrgID, error) { /* validates format */ }
// Not justified: renames fields with no new invariant or behavior.
type UserInfo struct {
Name string // same as source.Name
Email string // same as source.Email
}
```
Ask: what does the wrapper guarantee that the underlying type does not? If the answer is nothing, use the underlying type directly.
## When a new type IS warranted
A new type earns its place when it meets **at least one** of these criteria:
- **Serialization boundary**: It must be persisted, sent over the wire, or written to config. The source type is unsuitable (unexported fields, function pointers, cycles).
- **Invariant enforcement**: The constructor or methods enforce constraints that raw data does not carry (e.g., non-empty, validated format, bounded range).
- **Multiple distinct consumers**: Three or more call sites use the type in meaningfully different ways. The type is the shared vocabulary between them.
- **Dependency firewall**: The type lives in a lightweight package so that consumers avoid importing a heavy dependency.
## What should I remember?
- A function is almost always simpler than a type. Start with a function; promote to a type only when you have evidence of need.
- Never silently drop data. If you cannot handle it, error.
- If your new type mirrors an existing one, you need a strong reason beyond "nicer to work with".
- If your type has one producer and one consumer, it is indirection, not abstraction.
- Interfaces come from need (multiple implementations), not from prediction.
- When in doubt, do not add it. It is easier to add an abstraction later when the need is clear than to remove one after it has spread through the codebase.
## Further reading
These works and our own lessions shaped the above guidelines
- [The Wrong Abstraction](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction) - Sandi Metz. The wrong abstraction is worse than duplication. If you find yourself passing parameters and adding conditional paths through shared code, inline it back into every caller and let the duplication show you what the right abstraction is.
- [Write code that is easy to delete, not easy to extend](https://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to) - tef. Every abstraction is a bet on the future. Optimize for how cheaply you can remove code when the bet is wrong, not for how easily you can extend it when the bet is right.
- [Goodbye, Clean Code](https://overreacted.io/goodbye-clean-code/) - Dan Abramov. A refactoring that removes duplication can look cleaner while making the code harder to change. Clean-looking and easy-to-change are not the same thing.
- [A Philosophy of Software Design](https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201) - John Ousterhout. Good abstractions are deep: simple interface, complex implementation. A "false abstraction" omits important details while appearing simple, and is worse than no abstraction at all. ([Summary by Pragmatic Engineer](https://blog.pragmaticengineer.com/a-philosophy-of-software-design-review/))
- [Simplicity is Complicated](https://go.dev/talks/2015/simplicity-is-complicated.slide) - Rob Pike. Go-specific. Fewer orthogonal concepts that compose predictably beat many overlapping ones. Features were left out of Go deliberately; the same discipline applies to your own code.

View File

@@ -0,0 +1,126 @@
# Packages
All shared Go code in SigNoz lives under `pkg/`. Each package represents a distinct domain concept and exposes a clear public interface. This guide covers the conventions for creating, naming, and organising packages so the codebase stays consistent as it grows.
## How should I name a package?
Use short, lowercase, single-word names. No underscores or camelCase (`querier`, `cache`, `authz`, not `query_builder` or `dataStore`).
Names must be **domain-specific**. A package name should tell you what problem domain it deals with, not what data structure it wraps. Prefer `alertmanager` over `manager`, `licensing` over `checker`.
Avoid generic names like `util`, `helpers`, `common`, `misc`, or `base`. If you can't name it, the code probably belongs in an existing package.
## When should I create a new package?
Create a new package when:
- The functionality represents a **distinct domain concept** (e.g., `authz`, `licensing`, `cache`).
- Two or more other packages would import it; it serves as shared infrastructure.
- The code has a clear public interface that can stand on its own.
Do **not** create a new package when:
- There is already a package that covers the same domain. Extend the existing package instead.
- The code is only used in one place. Keep it local to the caller.
- You are splitting purely for file size. Use multiple files within the same package instead.
## How should I lay out a package?
A typical package looks like:
```
pkg/cache/
├── cache.go # Public interface + exported types
├── config.go # Configuration types if needed
├── memorycache/ # Implementation sub-package
├── rediscache/ # Another implementation
└── cachetest/ # Test helpers for consumers
```
Follow these rules:
1. **Interface-first file**: The file matching the package name (e.g., `cache.go` in `pkg/cache/`) should define the public interface and core exported types. Keep implementation details out of this file.
2. **One responsibility per file**: Name files after what they contain (`config.go`, `handler.go`, `service.go`), not after the package name. If a package merges two concerns, prefix files to group them (e.g., `memory_store.go`, `redis_store.go` in a storage package).
3. **Sub-packages for implementations**: When a package defines an interface with multiple implementations, put each implementation in its own sub-package (`memorycache/`, `rediscache/`). This keeps the parent package import-free of implementation dependencies.
4. **Test helpers in `{pkg}test/`**: If consumers need test mocks or builders, put them in a `{pkg}test/` sub-package (e.g., `cachetest/`, `sqlstoretest/`). This avoids polluting the main package with test-only code.
5. **Test files stay alongside source**: Unit tests go in `_test.go` files next to the code they test, in the same package.
## How should I name symbols?
### Exported symbols
- **Interfaces**: For single-method interfaces, follow the standard `-er` suffix convention (`Reader`, `Writer`, `Closer`). For multi-method interfaces, use clear nouns (`Cache`, `Store`, `Provider`).
- **Constructors**: `New<Type>(...)` (e.g., `NewMemoryCache()`).
- **Avoid stutter**: Since callers qualify with the package name, don't repeat it. Write `cache.Cache`, not `cache.CacheInterface`. Write `authz.FromRole`, not `authz.AuthzFromRole`.
### Unexported symbols
- Struct receivers: one or two characters (`c`, `f`, `br`).
- Helper functions: descriptive lowercase names (`parseToken`, `buildQuery`).
### Constants
- Use `PascalCase` for exported constants.
- When merging files from different origins into one package, watch out for **name collisions** across files. Prefix to disambiguate when two types share a natural name.
## How should I organise imports?
Group imports in three blocks separated by blank lines:
```go
import (
// 1. Standard library
"fmt"
"net/http"
// 2. External dependencies
"github.com/gorilla/mux"
// 3. Internal
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
)
```
Never introduce circular imports. If package A needs package B and B needs A, extract the shared types into a third package (often under `pkg/types/`).
## Where do shared types go?
Most types belong in `pkg/types/` under a domain-specific sub-package (e.g., `pkg/types/ruletypes`, `pkg/types/authtypes`).
Do not put domain logic in `pkg/types/`. Only data structures, constants, and simple methods.
## How do I merge or move packages?
When two packages are tightly coupled (one imports the other's constants, they cover the same domain), merge them:
1. Pick a domain-specific name for the combined package.
2. Prefix files to preserve origin (e.g., `memory_store.go`, `redis_store.go`).
3. Resolve symbol conflicts explicitly; rename with a prefix rather than silently shadowing.
4. Update all consumers in a single change.
5. Delete the old packages. Do not leave behind re-export shims.
6. Verify with `go build ./...`, `go test ./<new-pkg>/...`, and `go vet ./...`.
## When should I 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 13 sentences:
```go
// Package cache provides a caching interface with pluggable backends
// for in-memory and Redis-based storage.
package cache
```
## What should I remember?
- Package names are domain-specific and lowercase. Never generic names like `util` or `common`.
- The file matching the package name (e.g., `cache.go`) defines the public interface. Implementation details go elsewhere.
- Never introduce circular imports. Extract shared types into `pkg/types/` when needed.
- Watch for symbol name collisions when merging packages, prefix to disambiguate.
- Put test helpers in a `{pkg}test/` sub-package, not in the main package.
- Before submitting, verify with `go build ./...`, `go test ./<your-pkg>/...`, and `go vet ./...`.
- Update all consumers when you rename or move symbols.

View File

@@ -8,4 +8,15 @@ 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. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
- [Abstractions](abstractions.md) - When to introduce new types and intermediate representations
- [Errors](errors.md) - Structured error handling
- [Endpoint](endpoint.md) - HTTP endpoint patterns
- [Flagger](flagger.md) - Feature flag patterns
- [Handler](handler.md) - HTTP handler patterns
- [Integration](integration.md) - Integration testing
- [Provider](provider.md) - Dependency injection and provider patterns
- [Packages](packages.md) - Naming, layout, and conventions for `pkg/` packages
- [Service](service.md) - Managed service lifecycle with `factory.Service`
- [SQL](sql.md) - Database and SQL patterns

View File

@@ -0,0 +1,269 @@
# Service
A service is a component with a managed lifecycle: it starts, runs for the lifetime of the application, and stops gracefully.
Services are distinct from [providers](provider.md). A provider adapts an external dependency behind an interface. A service has a managed lifecycle that is tied to the lifetime of the application.
## When do you need a service?
You need a service when your component needs to do work that outlives a single method call:
- **Periodic work**: polling an external system, garbage-collecting expired data, syncing state on an interval.
- **Graceful shutdown**: holding resources (connections, caches, buffers) that must be flushed or closed before the process exits.
- **Blocking on readiness**: waiting for an external dependency to become available before the application can proceed.
If your component only responds to calls and holds no state that requires cleanup, it is a provider, not a service. If it does both (responds to calls *and* needs a lifecycle), embed `factory.Service` in the provider interface; see [How to create a service](#how-to-create-a-service).
## The interface
The `factory.Service` interface in `pkg/factory/service.go` defines two methods:
```go
type Service interface {
// Starts a service. It should block and should not return until the service is stopped or it fails.
Start(context.Context) error
// Stops a service.
Stop(context.Context) error
}
```
`Start` **must block**. It should not return until the service is stopped (returning `nil`) or something goes wrong (returning an error). If `Start` returns an error, the entire application shuts down.
`Stop` should cause `Start` to unblock and return. It must be safe to call from a different goroutine than the one running `Start`.
## Shutdown coordination
Every service uses a `stopC chan struct{}` to coordinate shutdown:
- **Constructor**: `stopC: make(chan struct{})`
- **Start**: blocks on `<-stopC` (or uses it in a `select` loop)
- **Stop**: `close(stopC)` to unblock `Start`
This is the standard pattern. Do not use `context.WithCancel` or other mechanisms for service-level shutdown coordination. See the examples in the next section.
## Service shapes
Two shapes recur across the codebase (these are not exhaustive, if a new shape is needed, bring it up for discussion before going ahead with the implementation), implemented by convention rather than base classes.
### Idle service
The service does work during startup or shutdown but has nothing to do while running. `Start` blocks on `<-stopC`. `Stop` closes `stopC` and optionally does cleanup.
The JWT tokenizer (`pkg/tokenizer/jwttokenizer/provider.go`) is a good example. It validates and creates tokens on demand via method calls, but has no periodic work to do. It still needs the service lifecycle so the registry can manage its lifetime:
```go
// pkg/tokenizer/jwttokenizer/provider.go
func (provider *provider) Start(ctx context.Context) error {
<-provider.stopC
return nil
}
func (provider *provider) Stop(ctx context.Context) error {
close(provider.stopC)
return nil
}
```
The instrumentation SDK (`pkg/instrumentation/sdk.go`) is idle while running but does real cleanup in `Stop` shutting down its OpenTelemetry tracer and meter providers:
```go
// pkg/instrumentation/sdk.go
func (i *SDK) Start(ctx context.Context) error {
<-i.startCh
return nil
}
func (i *SDK) Stop(ctx context.Context) error {
close(i.startCh)
return errors.Join(
i.sdk.Shutdown(ctx),
i.meterProviderShutdownFunc(ctx),
)
}
```
### Scheduled service
The service runs an operation repeatedly on a fixed interval. `Start` runs a ticker loop with a `select` on `stopC` and the ticker channel.
The opaque tokenizer (`pkg/tokenizer/opaquetokenizer/provider.go`) garbage-collects expired tokens and flushes cached last-observed-at timestamps to the database on a configurable interval:
```go
// pkg/tokenizer/opaquetokenizer/provider.go
func (provider *provider) Start(ctx context.Context) error {
ticker := time.NewTicker(provider.config.Opaque.GC.Interval)
defer ticker.Stop()
for {
select {
case <-provider.stopC:
return nil
case <-ticker.C:
orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to get orgs data", "error", err)
continue
}
for _, org := range orgs {
if err := provider.gc(ctx, org); err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to garbage collect tokens", "error", err, "org_id", org.ID)
}
if err := provider.flushLastObservedAt(ctx, org); err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", "error", err, "org_id", org.ID)
}
}
}
}
}
```
Its `Stop` does a final gc and flush before returning, so no data is lost on shutdown:
```go
// pkg/tokenizer/opaquetokenizer/provider.go
func (provider *provider) Stop(ctx context.Context) error {
close(provider.stopC)
orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
return err
}
for _, org := range orgs {
if err := provider.gc(ctx, org); err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to garbage collect tokens", "error", err, "org_id", org.ID)
}
if err := provider.flushLastObservedAt(ctx, org); err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", "error", err, "org_id", org.ID)
}
}
return nil
}
```
The key points:
- In the loop, `select` on `stopC` and the ticker. Errors in iterations are logged but do not cause the service to return (which would shut down the application).
- Only return an error from `Start` if the failure is unrecoverable.
- Use `Stop` to flush or drain any in-memory state before the process exits.
## How to create a service
There are two cases: a standalone service and a provider that is also a service.
### Standalone service
A standalone service only has the `factory.Service` lifecycle i.e it does not serve as a dependency for other packages. The user reconciliation service is an example.
1. Define the service interface in your package. Embed `factory.Service`:
```go
// pkg/modules/user/service.go
package user
type Service interface {
factory.Service
}
```
2. Create the implementation in an `impl` sub-package. Use an unexported struct with an exported constructor that returns the interface:
```go
// pkg/modules/user/impluser/service.go
package impluser
type service struct {
settings factory.ScopedProviderSettings
// ... dependencies ...
stopC chan struct{}
}
func NewService(
providerSettings factory.ProviderSettings,
// ... dependencies ...
) user.Service {
return &service{
settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/pkg/modules/user"),
// ... dependencies ...
stopC: make(chan struct{}),
}
}
func (s *service) Start(ctx context.Context) error { ... }
func (s *service) Stop(ctx context.Context) error { ... }
```
### Provider that is also a service
Many providers need a managed lifecycle: they poll, sync, or garbage-collect in the background. In this case, embed `factory.Service` in the provider interface. The implementation satisfies both the provider methods and `Start`/`Stop`.
```go
// pkg/tokenizer/tokenizer.go
package tokenizer
type Tokenizer interface {
factory.Service
CreateToken(context.Context, *authtypes.Identity, map[string]string) (*authtypes.Token, error)
GetIdentity(context.Context, string) (*authtypes.Identity, error)
// ... other methods ...
}
```
The implementation (e.g. `pkg/tokenizer/opaquetokenizer/provider.go`) implements `Start`, `Stop`, and all the provider methods on the same struct. See the [provider guide](provider.md) for how to set up the factory, config, and constructor. The `stopC` channel and `Start`/`Stop` methods follow the same patterns described above.
## How to wire it up
Wiring happens in `pkg/signoz/signoz.go`.
### 1. Instantiate the service
For a standalone service, call the constructor directly:
```go
userService := impluser.NewService(providerSettings, store, module, orgGetter, authz, config.User.Root)
```
For a provider that is also a service, use `factory.NewProviderFromNamedMap` as described in the [provider guide](provider.md). The returned value already implements `factory.Service`.
### 2. Register in the registry
Wrap the service with `factory.NewNamedService` and pass it to `factory.NewRegistry`:
```go
registry, err := factory.NewRegistry(
instrumentation.Logger(),
// ... other services ...
factory.NewNamedService(factory.MustNewName("user"), userService),
)
```
The name must be unique across all services. The registry handles the rest:
- **Start**: launches all services concurrently in goroutines.
- **Wait**: blocks until a service returns an error, the context is cancelled, or a SIGINT/SIGTERM is received. Any service error triggers application shutdown.
- **Stop**: stops all services concurrently, collects errors via `errors.Join`.
You do not call `Start` or `Stop` on individual services. The registry does it.
## What should I remember?
- A service has a managed lifecycle: `Start` blocks, `Stop` unblocks it.
- Use `stopC chan struct{}` for shutdown coordination. `close(stopC)` in `Stop`, `<-stopC` in `Start`.
- Service shapes: idle (block on `stopC`) and scheduled (ticker loop with `select`).
- Unexported struct, exported `NewService` constructor returning the interface.
- First constructor parameter is `factory.ProviderSettings`. Create scoped settings with `factory.NewScopedProviderSettings`.
- Register in `factory.Registry` with `factory.NewNamedService`. The registry starts and stops everything.
- Only return an error from `Start` if the failure is unrecoverable. Log and continue for transient errors in polling loops.
## Further reading
- [Google Guava - ServiceExplained](https://github.com/google/guava/wiki/ServiceExplained) - the service lifecycle pattern takes inspiration from this
- [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector) - Worth studying for its approach to building composable components

View File

@@ -170,7 +170,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive)
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}

View File

@@ -19,6 +19,8 @@ const config: Config.InitialOptions = {
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^@signozhq/icons$':
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
},
globals: {
extensionsToTreatAsEsm: ['.ts'],

View File

@@ -214,7 +214,7 @@
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
"@types/react-syntax-highlighter": "15.5.7",
"@types/react-syntax-highlighter": "15.5.13",
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Extracts unique fenced code block language identifiers from all .md files under frontend/src/
# Usage: bash frontend/scripts/extract-md-languages.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_DIR="$SCRIPT_DIR/../src"
grep -roh '```[a-zA-Z0-9_+-]*' "$SRC_DIR" --include='*.md' \
| sed 's/^```//' \
| grep -v '^$' \
| sort -u

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Validates that all fenced code block languages used in .md files are registered
# in the syntax highlighter.
# Usage: bash frontend/scripts/validate-md-languages.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SYNTAX_HIGHLIGHTER="$SCRIPT_DIR/../src/components/MarkdownRenderer/syntaxHighlighter.ts"
# Get all languages used in .md files
md_languages=$("$SCRIPT_DIR/extract-md-languages.sh")
# Get all registered languages from syntaxHighlighter.ts
registered_languages=$(grep -oP "registerLanguage\('\K[^']+" "$SYNTAX_HIGHLIGHTER" | sort -u)
missing_languages=()
for lang in $md_languages; do
if ! echo "$registered_languages" | grep -qx "$lang"; then
missing_languages+=("$lang")
fi
done
if [ ${#missing_languages[@]} -gt 0 ]; then
echo "Error: The following languages are used in .md files but not registered in syntaxHighlighter.ts:"
for lang in "${missing_languages[@]}"; do
echo " - $lang"
done
echo ""
echo "Please add them to: frontend/src/components/MarkdownRenderer/syntaxHighlighter.ts"
exit 1
fi
echo "All markdown code block languages are registered in syntaxHighlighter.ts"

View File

@@ -2,13 +2,12 @@
import ReactMarkdown from 'react-markdown';
import { CodeProps } from 'react-markdown/lib/ast-to-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import logEvent from 'api/common/logEvent';
import { isEmpty } from 'lodash-es';
import rehypeRaw from 'rehype-raw';
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
import SyntaxHighlighter, { a11yDark } from './syntaxHighlighter';
interface LinkProps {
href: string;

View File

@@ -0,0 +1,34 @@
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
import docker from 'react-syntax-highlighter/dist/esm/languages/prism/docker';
import elixir from 'react-syntax-highlighter/dist/esm/languages/prism/elixir';
import go from 'react-syntax-highlighter/dist/esm/languages/prism/go';
import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
import rust from 'react-syntax-highlighter/dist/esm/languages/prism/rust';
import swift from 'react-syntax-highlighter/dist/esm/languages/prism/swift';
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml';
import a11yDark from 'react-syntax-highlighter/dist/esm/styles/prism/a11y-dark';
SyntaxHighlighter.registerLanguage('bash', bash);
SyntaxHighlighter.registerLanguage('docker', docker);
SyntaxHighlighter.registerLanguage('dockerfile', docker);
SyntaxHighlighter.registerLanguage('elixir', elixir);
SyntaxHighlighter.registerLanguage('go', go);
SyntaxHighlighter.registerLanguage('javascript', javascript);
SyntaxHighlighter.registerLanguage('js', javascript);
SyntaxHighlighter.registerLanguage('json', json);
SyntaxHighlighter.registerLanguage('jsx', jsx);
SyntaxHighlighter.registerLanguage('rust', rust);
SyntaxHighlighter.registerLanguage('swift', swift);
SyntaxHighlighter.registerLanguage('ts', typescript);
SyntaxHighlighter.registerLanguage('tsx', tsx);
SyntaxHighlighter.registerLanguage('typescript', typescript);
SyntaxHighlighter.registerLanguage('yaml', yaml);
SyntaxHighlighter.registerLanguage('yml', yaml);
export default SyntaxHighlighter;
export { a11yDark };

View File

@@ -1,3 +1,4 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PrecisionOption } from 'components/Graph/types';
import { LegendConfig, TooltipRenderArgs } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
@@ -8,7 +9,7 @@ interface BaseChartProps {
height: number;
showTooltip?: boolean;
showLegend?: boolean;
timezone: string;
timezone?: Timezone;
canPinTooltip?: boolean;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;

View File

@@ -129,12 +129,12 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
onDestroy={onPlotDestroy}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}
layoutChildren={layoutChildren}
isStackedBarChart={widget.stackedBarChart ?? false}
timezone={timezone}
>
<ContextMenu
coordinates={coordinates}

View File

@@ -5,12 +5,7 @@ import { getInitialStackedBands } from 'container/DashboardContainer/visualizati
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import {
DrawStyle,
LineInterpolation,
LineStyle,
VisibilityMode,
} from 'lib/uPlotV2/config/types';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { get } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -63,7 +58,12 @@ export function prepareBarPanelConfig({
const minStepInterval = Math.min(...Object.values(stepIntervals));
const builder = buildBaseConfig({
widget,
widgetId: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
isDarkMode,
onClick,
onDragSelect,
@@ -98,14 +98,8 @@ export function prepareBarPanelConfig({
builder.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.BAR,
label: label,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
lineStyle: LineStyle.Solid,
lineInterpolation: LineInterpolation.Spline,
showPoints: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
stepInterval: currentStepInterval,
});

View File

@@ -100,7 +100,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
syncMode={DashboardCursorSync.Crosshair}
timezone={timezone.value}
timezone={timezone}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}

View File

@@ -154,7 +154,12 @@ export function prepareHistogramPanelConfig({
isDarkMode: boolean;
}): UPlotConfigBuilder {
const builder = buildBaseConfig({
widget,
widgetId: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
isDarkMode,
apiResponse,
panelMode,
@@ -191,10 +196,8 @@ export function prepareHistogramPanelConfig({
builder.addSeries({
label: '',
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.HISTOGRAM,
drawStyle: DrawStyle.Histogram,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
barWidthFactor: 1,
pointSize: 5,
lineColor: '#3f5ecc',
@@ -216,10 +219,8 @@ export function prepareHistogramPanelConfig({
builder.addSeries({
label: label,
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.HISTOGRAM,
drawStyle: DrawStyle.Histogram,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
barWidthFactor: 1,
pointSize: 5,
isDarkMode,

View File

@@ -118,7 +118,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
}}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
timezone={timezone.value}
timezone={timezone}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}

View File

@@ -82,7 +82,12 @@ export const prepareUPlotConfig = ({
const minStepInterval = Math.min(...Object.values(stepIntervals));
const builder = buildBaseConfig({
widget,
widgetId: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
isDarkMode,
onClick,
onDragSelect,
@@ -120,7 +125,6 @@ export const prepareUPlotConfig = ({
: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
panelType: PANEL_TYPES.TIME_SERIES,
});
});

View File

@@ -1,11 +1,11 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { STEP_INTERVAL_MULTIPLIER } from 'lib/uPlotV2/constants';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { PanelMode } from '../../types';
import { buildBaseConfig } from '../baseConfigBuilder';
import { BaseConfigBuilderProps, buildBaseConfig } from '../baseConfigBuilder';
jest.mock(
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
@@ -27,16 +27,17 @@ jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => ({
default: jest.fn().mockReturnValue({ name: 'onClickPlugin' }),
}));
const createWidget = (overrides: Partial<Widgets> = {}): Widgets =>
({
id: 'widget-1',
yAxisUnit: 'ms',
isLogScale: false,
softMin: undefined,
softMax: undefined,
thresholds: [],
...overrides,
} as Widgets);
const createBaseConfigBuilderProps = (
overrides: Partial<BaseConfigBuilderProps> = {},
): Partial<BaseConfigBuilderProps> => ({
widgetId: 'widget-1',
yAxisUnit: 'ms',
isLogScale: false,
softMin: undefined,
softMax: undefined,
thresholds: [],
...overrides,
});
const createApiResponse = (
overrides: Partial<MetricRangePayloadProps> = {},
@@ -47,7 +48,7 @@ const createApiResponse = (
} as MetricRangePayloadProps);
const baseProps = {
widget: createWidget(),
...createBaseConfigBuilderProps(),
apiResponse: createApiResponse(),
isDarkMode: true,
panelMode: PanelMode.DASHBOARD_VIEW,
@@ -67,7 +68,7 @@ describe('buildBaseConfig', () => {
const builder = buildBaseConfig({
...baseProps,
panelMode: PanelMode.DASHBOARD_VIEW,
widget: createWidget({ id: 'my-widget' }),
...createBaseConfigBuilderProps({ widgetId: 'my-widget' }),
});
expect(builder.getWidgetId()).toBe('my-widget');
@@ -127,7 +128,7 @@ describe('buildBaseConfig', () => {
it('configures log scale on y axis when widget.isLogScale is true', () => {
const builder = buildBaseConfig({
...baseProps,
widget: createWidget({ isLogScale: true }),
...createBaseConfigBuilderProps({ isLogScale: true }),
});
const config = builder.getConfig();
@@ -171,7 +172,7 @@ describe('buildBaseConfig', () => {
it('adds thresholds from widget', () => {
const builder = buildBaseConfig({
...baseProps,
widget: createWidget({
...createBaseConfigBuilderProps({
thresholds: [
{
thresholdValue: 80,
@@ -179,7 +180,7 @@ describe('buildBaseConfig', () => {
thresholdUnit: 'ms',
thresholdLabel: 'High',
},
] as Widgets['thresholds'],
] as ThresholdProps[],
}),
});

View File

@@ -1,5 +1,6 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import onClickPlugin, {
OnClickPluginOpts,
} from 'lib/uPlotLib/plugins/onClickPlugin';
@@ -9,28 +10,32 @@ import {
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { PanelMode } from '../types';
export interface BaseConfigBuilderProps {
widget: Widgets;
widgetId?: string;
thresholds?: ThresholdProps[];
apiResponse: MetricRangePayloadProps;
isDarkMode: boolean;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect?: (startTime: number, endTime: number) => void;
timezone?: Timezone;
panelMode: PanelMode;
panelMode?: PanelMode;
panelType: PANEL_TYPES;
minTimeScale?: number;
maxTimeScale?: number;
stepInterval?: number;
isLogScale?: boolean;
yAxisUnit?: string;
softMin?: number;
softMax?: number;
}
export function buildBaseConfig({
widget,
widgetId,
isDarkMode,
onClick,
onDragSelect,
@@ -38,9 +43,14 @@ export function buildBaseConfig({
timezone,
panelMode,
panelType,
thresholds,
minTimeScale,
maxTimeScale,
stepInterval,
isLogScale,
yAxisUnit,
softMin,
softMax,
}: BaseConfigBuilderProps): UPlotConfigBuilder {
const tzDate = timezone
? (timestamp: number): Date =>
@@ -49,27 +59,26 @@ export function buildBaseConfig({
const builder = new UPlotConfigBuilder({
onDragSelect,
widgetId: widget.id,
widgetId: widgetId,
tzDate,
shouldSaveSelectionPreference: panelMode === PanelMode.DASHBOARD_VIEW,
selectionPreferencesSource: [
PanelMode.DASHBOARD_VIEW,
PanelMode.STANDALONE_VIEW,
].includes(panelMode)
? SelectionPreferencesSource.LOCAL_STORAGE
selectionPreferencesSource: panelMode
? [PanelMode.DASHBOARD_VIEW, PanelMode.STANDALONE_VIEW].includes(panelMode)
? SelectionPreferencesSource.LOCAL_STORAGE
: SelectionPreferencesSource.IN_MEMORY
: SelectionPreferencesSource.IN_MEMORY,
stepInterval,
});
const thresholdOptions: ThresholdsDrawHookOptions = {
scaleKey: 'y',
thresholds: (widget.thresholds || []).map((threshold) => ({
thresholds: (thresholds || []).map((threshold) => ({
thresholdValue: threshold.thresholdValue ?? 0,
thresholdColor: threshold.thresholdColor,
thresholdUnit: threshold.thresholdUnit,
thresholdLabel: threshold.thresholdLabel,
})),
yAxisUnit: widget.yAxisUnit,
yAxisUnit: yAxisUnit,
};
builder.addThresholds(thresholdOptions);
@@ -79,8 +88,8 @@ export function buildBaseConfig({
time: true,
min: minTimeScale,
max: maxTimeScale,
logBase: widget.isLogScale ? 10 : undefined,
distribution: widget.isLogScale
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
@@ -91,11 +100,11 @@ export function buildBaseConfig({
time: false,
min: undefined,
max: undefined,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
softMin: softMin,
softMax: softMax,
thresholds: thresholdOptions,
logBase: widget.isLogScale ? 10 : undefined,
distribution: widget.isLogScale
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
@@ -114,7 +123,7 @@ export function buildBaseConfig({
show: true,
side: 2,
isDarkMode,
isLogScale: widget.isLogScale,
isLogScale: isLogScale,
panelType,
});
@@ -123,8 +132,8 @@ export function buildBaseConfig({
show: true,
side: 3,
isDarkMode,
isLogScale: widget.isLogScale,
yAxisUnit: widget.yAxisUnit,
isLogScale: isLogScale,
yAxisUnit: yAxisUnit,
panelType,
});

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { CloudDownloadOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { unparse } from 'papaparse';
import { DownloadProps } from './Download.types';
@@ -8,25 +8,36 @@ import { DownloadProps } from './Download.types';
import './Download.styles.scss';
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
const downloadExcelFile = (): void => {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excel = new Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
const [isDownloading, setIsDownloading] = useState(false);
const downloadExcelFile = async (): Promise<void> => {
setIsDownloading(true);
try {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excelLib = await import('antd-table-saveas-excel');
const excel = new excelLib.Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
} finally {
setIsDownloading(false);
}
};
const downloadCsvFile = (): void => {
@@ -59,7 +70,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
<Dropdown menu={menu} trigger={['click']}>
<Button
className="download-button"
loading={isLoading}
loading={isLoading || isDownloading}
size="small"
type="link"
>

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Button, Popover, Typography } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { FileDigit, FileDown, Sheet } from 'lucide-react';
import { unparse } from 'papaparse';
@@ -8,25 +8,34 @@ import { DownloadProps } from './DownloadV2.types';
import './DownloadV2.styles.scss';
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
const downloadExcelFile = (): void => {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excel = new Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
const [isDownloading, setIsDownloading] = useState(false);
const downloadExcelFile = async (): Promise<void> => {
setIsDownloading(true);
try {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excelLib = await import('antd-table-saveas-excel');
const excel = new excelLib.Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
} finally {
setIsDownloading(false);
}
};
const downloadCsvFile = (): void => {
@@ -54,6 +63,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
type="text"
onClick={downloadExcelFile}
className="action-btns"
loading={isDownloading}
>
Excel (.xlsx)
</Button>

View File

@@ -4,6 +4,7 @@ import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTimezone } from 'providers/Timezone';
import { TooltipProps } from '../types';
@@ -22,6 +23,14 @@ export default function Tooltip({
const isDarkMode = useIsDarkMode();
const [listHeight, setListHeight] = useState(0);
const tooltipContent = content ?? [];
const { timezone: userTimezone } = useTimezone();
const resolvedTimezone = useMemo(() => {
if (!timezone) {
return userTimezone.value;
}
return timezone.value;
}, [timezone, userTimezone]);
const headerTitle = useMemo(() => {
if (!showTooltipHeader) {
@@ -33,10 +42,10 @@ export default function Tooltip({
return null;
}
return dayjs(data[0][cursorIdx] * 1000)
.tz(timezone)
.tz(resolvedTimezone)
.format(DATE_TIME_FORMATS.MONTH_DATETIME_SECONDS);
}, [
timezone,
resolvedTimezone,
uPlotInstance.data,
uPlotInstance.cursor.idx,
showTooltipHeader,

View File

@@ -83,7 +83,7 @@ function createUPlotInstance(cursorIdx: number | null): uPlot {
function renderTooltip(props: Partial<TooltipTestProps> = {}): RenderResult {
const defaultProps: TooltipTestProps = {
uPlotInstance: createUPlotInstance(null),
timezone: 'UTC',
timezone: { value: 'UTC', name: 'UTC', offset: '0', searchIndex: '0' },
content: [],
showTooltipHeader: true,
// TooltipRenderArgs (not used directly in component but required by type)

View File

@@ -1,4 +1,5 @@
import { ReactNode } from 'react';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PrecisionOption } from 'components/Graph/types';
import uPlot from 'uplot';
@@ -61,7 +62,7 @@ export interface TooltipRenderArgs {
export interface BaseTooltipProps {
showTooltipHeader?: boolean;
timezone: string;
timezone?: Timezone;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
content?: TooltipContentItem[];

View File

@@ -1,4 +1,3 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { calculateWidthBasedOnStepInterval } from 'lib/uPlotV2/utils';
@@ -23,6 +22,9 @@ import {
* Path builders are static and shared across all instances of UPlotSeriesBuilder
*/
let builders: PathBuilders | null = null;
const DEFAULT_LINE_WIDTH = 2;
export const POINT_SIZE_FACTOR = 2.5;
export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
constructor(props: SeriesProps) {
super(props);
@@ -53,7 +55,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
const { lineWidth, lineStyle, lineCap, fillColor } = this.props;
const lineConfig: Partial<Series> = {
stroke: resolvedLineColor,
width: lineWidth ?? 2,
width: lineWidth ?? DEFAULT_LINE_WIDTH,
};
if (lineStyle === LineStyle.Dashed) {
@@ -66,9 +68,9 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
if (fillColor) {
lineConfig.fill = fillColor;
} else if (this.props.panelType === PANEL_TYPES.BAR) {
} else if (this.props.drawStyle === DrawStyle.Bar) {
lineConfig.fill = resolvedLineColor;
} else if (this.props.panelType === PANEL_TYPES.HISTOGRAM) {
} else if (this.props.drawStyle === DrawStyle.Histogram) {
lineConfig.fill = `${resolvedLineColor}40`;
}
@@ -137,10 +139,19 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
drawStyle,
showPoints,
} = this.props;
/**
* If pointSize is not provided, use the lineWidth * POINT_SIZE_FACTOR
* to determine the point size.
* POINT_SIZE_FACTOR is 2, so the point size will be 2x the line width.
*/
const resolvedPointSize =
pointSize ?? (lineWidth ?? DEFAULT_LINE_WIDTH) * POINT_SIZE_FACTOR;
const pointsConfig: Partial<Series.Points> = {
stroke: resolvedLineColor,
fill: resolvedLineColor,
size: !pointSize || pointSize < (lineWidth ?? 2) ? undefined : pointSize,
size: resolvedPointSize,
filter: pointsFilter || undefined,
};
@@ -231,7 +242,7 @@ function getPathBuilder({
throw new Error('Required uPlot path builders are not available');
}
if (drawStyle === DrawStyle.Bar) {
if (drawStyle === DrawStyle.Bar || drawStyle === DrawStyle.Histogram) {
const pathBuilders = uPlot.paths;
return getBarPathBuilder({
pathBuilders,

View File

@@ -1,4 +1,3 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import uPlot from 'uplot';
import {
@@ -43,7 +42,6 @@ describe('UPlotConfigBuilder', () => {
label: 'Requests',
colorMapping: {},
drawStyle: DrawStyle.Line,
panelType: PANEL_TYPES.TIME_SERIES,
...overrides,
});

View File

@@ -1,4 +1,3 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import uPlot from 'uplot';
@@ -9,7 +8,7 @@ import {
LineStyle,
VisibilityMode,
} from '../types';
import { UPlotSeriesBuilder } from '../UPlotSeriesBuilder';
import { POINT_SIZE_FACTOR, UPlotSeriesBuilder } from '../UPlotSeriesBuilder';
const createBaseProps = (
overrides: Partial<SeriesProps> = {},
@@ -19,7 +18,6 @@ const createBaseProps = (
colorMapping: {},
drawStyle: DrawStyle.Line,
isDarkMode: false,
panelType: PANEL_TYPES.TIME_SERIES,
...overrides,
});
@@ -137,7 +135,6 @@ describe('UPlotSeriesBuilder', () => {
const smallPointsBuilder = new UPlotSeriesBuilder(
createBaseProps({
lineWidth: 4,
pointSize: 2,
}),
);
const largePointsBuilder = new UPlotSeriesBuilder(
@@ -150,7 +147,7 @@ describe('UPlotSeriesBuilder', () => {
const smallConfig = smallPointsBuilder.getConfig();
const largeConfig = largePointsBuilder.getConfig();
expect(smallConfig.points?.size).toBeUndefined();
expect(smallConfig.points?.size).toBe(4 * POINT_SIZE_FACTOR); // should be lineWidth * POINT_SIZE_FACTOR, when pointSize is not provided
expect(largeConfig.points?.size).toBe(4);
});

View File

@@ -112,6 +112,7 @@ export enum DrawStyle {
Line = 'line',
Points = 'points',
Bar = 'bar',
Histogram = 'histogram',
}
export enum LineInterpolation {
@@ -168,7 +169,6 @@ export interface PointsConfig {
export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
scaleKey: string;
label?: string;
panelType: PANEL_TYPES;
colorMapping: Record<string, string>;
drawStyle: DrawStyle;
pathBuilder?: Series.PathBuilder;

View File

@@ -6205,10 +6205,10 @@
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-syntax-highlighter@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.7.tgz#bd29020ccb118543d88779848f99059b64b02d0f"
integrity sha512-bo5fEO5toQeyCp0zVHBeggclqf5SQ/Z5blfFmjwO5dkMVGPgmiwZsJh9nu/Bo5L7IHTuGWrja6LxJVE2uB5ZrQ==
"@types/react-syntax-highlighter@15.5.13":
version "15.5.13"
resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz#c5baf62a3219b3bf28d39cfea55d0a49a263d1f2"
integrity sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==
dependencies:
"@types/react" "*"

View File

@@ -17,7 +17,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint creates an invite for a user",
Request: new(types.PostableInvite),
RequestContentType: "application/json",
Response: new(types.User),
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
@@ -43,73 +43,73 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
// if err := router.Handle("/api/v1/invite/{token}", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetInvite), handler.OpenAPIDef{
// ID: "GetInvite",
// Tags: []string{"users"},
// Summary: "Get invite",
// Description: "This endpoint gets an invite by token",
// Request: nil,
// RequestContentType: "",
// Response: new(types.Invite),
// ResponseContentType: "application/json",
// SuccessStatusCode: http.StatusOK,
// ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
// Deprecated: false,
// SecuritySchemes: []handler.OpenAPISecurityScheme{},
// })).Methods(http.MethodGet).GetError(); err != nil {
// return err
// }
if err := router.Handle("/api/v1/invite/{token}", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetInvite), handler.OpenAPIDef{
ID: "GetInvite",
Tags: []string{"users"},
Summary: "Get invite",
Description: "This endpoint gets an invite by token",
Request: nil,
RequestContentType: "",
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
// if err := router.Handle("/api/v1/invite/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteInvite), handler.OpenAPIDef{
// ID: "DeleteInvite",
// Tags: []string{"users"},
// Summary: "Delete invite",
// Description: "This endpoint deletes an invite by id",
// Request: nil,
// RequestContentType: "",
// Response: nil,
// ResponseContentType: "",
// SuccessStatusCode: http.StatusNoContent,
// ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
// Deprecated: false,
// SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
// })).Methods(http.MethodDelete).GetError(); err != nil {
// return err
// }
if err := router.Handle("/api/v1/invite/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteInvite), handler.OpenAPIDef{
ID: "DeleteInvite",
Tags: []string{"users"},
Summary: "Delete invite",
Description: "This endpoint deletes an invite by id",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
// if err := router.Handle("/api/v1/invite", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListInvite), handler.OpenAPIDef{
// ID: "ListInvite",
// Tags: []string{"users"},
// Summary: "List invites",
// Description: "This endpoint lists all invites",
// Request: nil,
// RequestContentType: "",
// Response: make([]*types.Invite, 0),
// ResponseContentType: "application/json",
// SuccessStatusCode: http.StatusOK,
// ErrorStatusCodes: []int{},
// Deprecated: false,
// SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
// })).Methods(http.MethodGet).GetError(); err != nil {
// return err
// }
if err := router.Handle("/api/v1/invite", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListInvite), handler.OpenAPIDef{
ID: "ListInvite",
Tags: []string{"users"},
Summary: "List invites",
Description: "This endpoint lists all invites",
Request: nil,
RequestContentType: "",
Response: make([]*types.Invite, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
// if err := router.Handle("/api/v1/invite/accept", handler.New(provider.authZ.OpenAccess(provider.userHandler.AcceptInvite), handler.OpenAPIDef{
// ID: "AcceptInvite",
// Tags: []string{"users"},
// Summary: "Accept invite",
// Description: "This endpoint accepts an invite by token",
// Request: new(types.PostableAcceptInvite),
// RequestContentType: "application/json",
// Response: new(types.User),
// ResponseContentType: "application/json",
// SuccessStatusCode: http.StatusCreated,
// ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
// Deprecated: false,
// SecuritySchemes: []handler.OpenAPISecurityScheme{},
// })).Methods(http.MethodPost).GetError(); err != nil {
// return err
// }
if err := router.Handle("/api/v1/invite/accept", handler.New(provider.authZ.OpenAccess(provider.userHandler.AcceptInvite), handler.OpenAPIDef{
ID: "AcceptInvite",
Tags: []string{"users"},
Summary: "Accept invite",
Description: "This endpoint accepts an invite by token",
Request: new(types.PostableAcceptInvite),
RequestContentType: "application/json",
Response: new(types.User),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateAPIKey), handler.OpenAPIDef{
ID: "CreateAPIKey",

View File

@@ -3,10 +3,9 @@ package flagger
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
var (
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
FeatureSoftDeleteUsers = featuretypes.MustNewName("soft_delete_users")
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
)
func MustNewRegistry() featuretypes.Registry {
@@ -35,14 +34,6 @@ func MustNewRegistry() featuretypes.Registry {
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
&featuretypes.Feature{
Name: FeatureSoftDeleteUsers,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageStable,
Description: "Controls whether users are soft deleted or not",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
)
if err != nil {
panic(err)

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -24,7 +25,7 @@ func NewModule(sqlstore sqlstore.SQLStore) savedview.Module {
}
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error) {
var views []types.SavedView
var views []savedviewtypes.SavedView
var err error
if len(category) == 0 {
err = module.sqlstore.BunDB().NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND name LIKE ?", orgID, sourcePage, "%"+name+"%").Scan(ctx)
@@ -76,7 +77,7 @@ func (module *module) CreateView(ctx context.Context, orgID string, view v3.Save
createBy := claims.Email
updatedBy := claims.Email
dbView := types.SavedView{
dbView := savedviewtypes.SavedView{
TimeAuditable: types.TimeAuditable{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -105,7 +106,7 @@ func (module *module) CreateView(ctx context.Context, orgID string, view v3.Save
}
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*v3.SavedView, error) {
var view types.SavedView
var view savedviewtypes.SavedView
err := module.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, uuid.StringValue()).Scan(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved view")
@@ -146,7 +147,7 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
updatedBy := claims.Email
_, err = module.sqlstore.BunDB().NewUpdate().
Model(&types.SavedView{}).
Model(&savedviewtypes.SavedView{}).
Set("updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ?",
updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData).
Where("id = ?", uuid.StringValue()).
@@ -160,7 +161,7 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error {
_, err := module.sqlstore.BunDB().NewDelete().
Model(&types.SavedView{}).
Model(&savedviewtypes.SavedView{}).
Where("id = ?", uuid.StringValue()).
Where("org_id = ?", orgID).
Exec(ctx)
@@ -171,7 +172,7 @@ func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.
}
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
savedViews := []*types.SavedView{}
savedViews := []*savedviewtypes.SavedView{}
err := module.
sqlstore.
@@ -184,5 +185,5 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return nil, err
}
return types.NewStatsFromSavedViews(savedViews), nil
return savedviewtypes.NewStatsFromSavedViews(savedViews), nil
}

View File

@@ -141,7 +141,7 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
roleMapping := authDomain.AuthDomainConfig().RoleMapping
role := roleMapping.NewRoleFromCallbackIdentity(callbackIdentity)
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID, types.UserStatusActive)
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID)
if err != nil {
return "", err
}

View File

@@ -13,6 +13,7 @@ import (
root "github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -26,24 +27,24 @@ func NewHandler(module root.Module, getter root.Getter) root.Handler {
return &handler{module: module, getter: getter}
}
// func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
// defer cancel()
func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// req := new(types.PostableAcceptInvite)
// if err := binding.JSON.BindBody(r.Body, req); err != nil {
// render.Error(w, err)
// return
// }
req := new(types.PostableAcceptInvite)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(w, err)
return
}
// user, err := h.module.AcceptInvite(ctx, req.InviteToken, req.Password)
// if err != nil {
// render.Error(w, err)
// return
// }
user, err := h.module.AcceptInvite(ctx, req.InviteToken, req.Password)
if err != nil {
render.Error(w, err)
return
}
// render.Success(w, http.StatusCreated, user)
// }
render.Success(w, http.StatusCreated, user)
}
func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
@@ -61,7 +62,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
return
}
invitedUsers, err := h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{
invites, err := h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{
Invites: []types.PostableInvite{req},
})
if err != nil {
@@ -69,7 +70,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
return
}
render.Success(rw, http.StatusCreated, invitedUsers[0])
render.Success(rw, http.StatusCreated, invites[0])
}
func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
@@ -103,63 +104,63 @@ func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusCreated, nil)
}
// func (h *handler) GetInvite(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
// defer cancel()
func (h *handler) GetInvite(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// token := mux.Vars(r)["token"]
// invite, err := h.module.GetInviteByToken(ctx, token)
// if err != nil {
// render.Error(w, err)
// return
// }
token := mux.Vars(r)["token"]
invite, err := h.module.GetInviteByToken(ctx, token)
if err != nil {
render.Error(w, err)
return
}
// render.Success(w, http.StatusOK, invite)
// }
render.Success(w, http.StatusOK, invite)
}
// func (h *handler) ListInvite(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
// defer cancel()
func (h *handler) ListInvite(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// claims, err := authtypes.ClaimsFromContext(ctx)
// if err != nil {
// render.Error(w, err)
// return
// }
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
// invites, err := h.module.ListInvite(ctx, claims.OrgID)
// if err != nil {
// render.Error(w, err)
// return
// }
invites, err := h.module.ListInvite(ctx, claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
// render.Success(w, http.StatusOK, invites)
// }
render.Success(w, http.StatusOK, invites)
}
// func (h *handler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
// defer cancel()
func (h *handler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// id := mux.Vars(r)["id"]
id := mux.Vars(r)["id"]
// claims, err := authtypes.ClaimsFromContext(ctx)
// if err != nil {
// render.Error(w, err)
// return
// }
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
// uuid, err := valuer.NewUUID(id)
// if err != nil {
// render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
// return
// }
uuid, err := valuer.NewUUID(id)
if err != nil {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
// if err := h.module.DeleteInvite(ctx, claims.OrgID, uuid); err != nil {
// render.Error(w, err)
// return
// }
// render.Success(w, http.StatusNoContent, nil)
// }
if err := h.module.DeleteInvite(ctx, claims.OrgID, uuid); err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
@@ -462,7 +463,7 @@ func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) {
return
}
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email.String())) {
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
return
}
@@ -507,7 +508,7 @@ func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
return
}
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email.String())) {
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
return
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
root "github.com/SigNoz/signoz/pkg/modules/user"
@@ -20,7 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/dustin/go-humanize"
@@ -35,11 +34,10 @@ type Module struct {
authz authz.AuthZ
analytics analytics.Analytics
config user.Config
flagger flagger.Flagger
}
// This module is a WIP, don't take inspiration from this.
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config, flagger flagger.Flagger) root.Module {
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config) root.Module {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
return &Module{
store: store,
@@ -50,59 +48,57 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
analytics: analytics,
authz: authz,
config: config,
flagger: flagger,
}
}
// func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
// invite, err := m.store.GetInviteByToken(ctx, token)
// if err != nil {
// return nil, err
// }
func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
invite, err := m.store.GetInviteByToken(ctx, token)
if err != nil {
return nil, err
}
// user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
// if err != nil {
// return nil, err
// }
user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
if err != nil {
return nil, err
}
// factorPassword, err := types.NewFactorPassword(password, user.ID.StringValue())
// if err != nil {
// return nil, err
// }
factorPassword, err := types.NewFactorPassword(password, user.ID.StringValue())
if err != nil {
return nil, err
}
// err = m.CreateUser(ctx, user, root.WithFactorPassword(factorPassword))
// if err != nil {
// return nil, err
// }
err = m.CreateUser(ctx, user, root.WithFactorPassword(factorPassword))
if err != nil {
return nil, err
}
// if err := m.DeleteInvite(ctx, invite.OrgID.String(), invite.ID); err != nil {
// return nil, err
// }
if err := m.DeleteInvite(ctx, invite.OrgID.String(), invite.ID); err != nil {
return nil, err
}
// return user, nil
// }
return user, nil
}
// func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
// invite, err := m.store.GetInviteByToken(ctx, token)
// if err != nil {
// return nil, err
// }
func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
invite, err := m.store.GetInviteByToken(ctx, token)
if err != nil {
return nil, err
}
// return invite, nil
// }
return invite, nil
}
// CreateBulk implements invite.Module.
func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.User, error) {
//! TODO this is an incomplete implementation - will fix this
func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) {
creator, err := m.store.GetUser(ctx, userID)
if err != nil {
return nil, err
}
invitedUsers := make([]*types.User, 0, len(bulkInvites.Invites))
invites := make([]*types.Invite, 0, len(bulkInvites.Invites))
for _, invite := range bulkInvites.Invites {
// check and active user already exists with this email
// check if user exists
existingUser, err := m.store.GetUserByEmailAndOrgID(ctx, invite.Email, orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
@@ -110,91 +106,70 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
if existingUser != nil {
if err := existingUser.ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "Cannot send invite to root user")
return nil, errors.WithAdditionalf(err, "cannot send invite to root user")
}
}
// check if a pending invite already exists
if existingUser.Status == types.UserStatusPendingInvite {
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email")
}
// if user is in soft deleted state, we reinitiate that
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
if existingUser != nil {
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with the same email")
}
// Check if an invite already exists
existingInvite, err := m.store.GetInviteByEmailAndOrgID(ctx, invite.Email, orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if existingInvite != nil {
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email")
}
role, err := types.NewRole(invite.Role.String())
if err != nil {
return nil, err
}
// create a new user with pending invite status
newUser, err := types.NewUser(invite.Name, invite.Email, role, orgID, types.UserStatusPendingInvite)
newInvite, err := types.NewInvite(invite.Name, role, orgID, invite.Email)
if err != nil {
return nil, err
}
// generate a temp password
password, err := types.GenerateFactorPassword(newUser.ID.StringValue())
if err != nil {
return nil, err
}
// store the user and password in db
err = m.createUserWithoutGrant(ctx, newUser, root.WithFactorPassword(password))
if err != nil {
return nil, err
}
invitedUsers = append(invitedUsers, newUser)
newInvite.InviteLink = fmt.Sprintf("%s/signup?token=%s", invite.FrontendBaseUrl, newInvite.Token)
invites = append(invites, newInvite)
}
// send password reset emails to all the invited users
for i, invitedUser := range invitedUsers {
m.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{
"invitee_email": invitedUser.Email,
"invitee_role": invitedUser.Role,
})
err = m.store.CreateBulkInvite(ctx, invites)
if err != nil {
return nil, err
}
frontendBaseUrl := bulkInvites.Invites[i].FrontendBaseUrl
if frontendBaseUrl == "" {
m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", "invitee_email", invitedUser.Email)
for i := 0; i < len(invites); i++ {
m.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{"invitee_email": invites[i].Email, "invitee_role": invites[i].Role})
// if the frontend base url is not provided, we don't send the email
if bulkInvites.Invites[i].FrontendBaseUrl == "" {
m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", "invitee_email", invites[i].Email)
continue
}
// generate reset password token
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, invitedUser.ID)
if err != nil {
m.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", "error", err)
continue
}
resetLink := m.resetLink(frontendBaseUrl, resetPasswordToken.Token)
tokenLifetime := m.config.Password.Reset.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
// TODO! improve invitation email text and add expiry details too
if err := m.emailing.SendHTML(ctx, invitedUser.Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
if err := m.emailing.SendHTML(ctx, invites[i].Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
"inviter_email": creator.Email,
"link": resetLink,
"Expiry": humanizedTokenLifetime,
"link": fmt.Sprintf("%s/signup?token=%s", bulkInvites.Invites[i].FrontendBaseUrl, invites[i].Token),
}); err != nil {
m.settings.Logger().ErrorContext(ctx, "failed to send invite email", "error", err)
m.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
}
}
return invitedUsers, nil
return invites, nil
}
// func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.User, error) {
// return m.store.ListPendingInviteUsers(ctx, orgID)
// }
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
return m.store.ListInvite(ctx, orgID)
}
// func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
// return m.store.DeleteInvite(ctx, orgID, id)
// }
func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
return m.store.DeleteInvite(ctx, orgID, id)
}
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)
@@ -305,7 +280,7 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return errors.WithAdditionalf(err, "cannot delete root user")
}
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(user.Email.String())) {
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(user.Email.String())) {
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
}
@@ -325,23 +300,12 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return err
}
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
softDeleteUsers := module.flagger.BooleanOrEmpty(ctx, flagger.FeatureSoftDeleteUsers, evalCtx)
if softDeleteUsers {
user.UpdateStatus(types.UserStatusDeleted)
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
return err
}
} else {
if err := module.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil {
return err
}
if err := module.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil {
return err
}
module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Deleted", map[string]any{
"deleted_by": deletedBy,
"is_soft_delete": softDeleteUsers,
"deleted_by": deletedBy,
})
return nil
@@ -429,7 +393,7 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema
return err
}
resetLink := module.resetLink(frontendBaseURL, token.Token)
resetLink := fmt.Sprintf("%s/password-reset?token=%s", frontendBaseURL, token.Token)
tokenLifetime := module.config.Password.Reset.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
@@ -479,25 +443,6 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
return err
}
// update the status of user if this a newly invited user and also grant authz
if user.Status == types.UserStatusPendingInvite {
err = module.authz.Grant(
ctx,
user.OrgID,
roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role),
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
)
if err != nil {
return err
}
user.UpdateStatus(types.UserStatusActive)
err = module.store.UpdateUser(ctx, user.OrgID, user)
if err != nil {
return err
}
}
return module.store.UpdatePassword(ctx, password)
}
@@ -653,7 +598,3 @@ func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.U
return nil
}
func (module *Module) resetLink(frontendBaseUrl string, token string) string {
return fmt.Sprintf("%s/password-reset?token=%s", frontendBaseUrl, token)
}

View File

@@ -26,75 +26,75 @@ func NewStore(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings) typ
}
// CreateBulkInvite implements types.InviteStore.
// func (store *store) CreateBulkInvite(ctx context.Context, invites []*types.Invite) error {
// _, err := store.sqlstore.BunDB().NewInsert().
// Model(&invites).
// Exec(ctx)
func (store *store) CreateBulkInvite(ctx context.Context, invites []*types.Invite) error {
_, err := store.sqlstore.BunDB().NewInsert().
Model(&invites).
Exec(ctx)
// if err != nil {
// return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrInviteAlreadyExists, "invite with email: %s already exists in org: %s", invites[0].Email, invites[0].OrgID)
// }
// return nil
// }
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrInviteAlreadyExists, "invite with email: %s already exists in org: %s", invites[0].Email, invites[0].OrgID)
}
return nil
}
// Delete implements types.InviteStore.
// func (store *store) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
// _, err := store.sqlstore.BunDB().NewDelete().
// Model(&types.Invite{}).
// Where("org_id = ?", orgID).
// Where("id = ?", id).
// Exec(ctx)
// if err != nil {
// return store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with id: %s does not exist in org: %s", id.StringValue(), orgID)
// }
// return nil
// }
func (store *store) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
_, err := store.sqlstore.BunDB().NewDelete().
Model(&types.Invite{}).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with id: %s does not exist in org: %s", id.StringValue(), orgID)
}
return nil
}
// func (store *store) GetInviteByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.Invite, error) {
// invite := new(types.Invite)
func (store *store) GetInviteByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.Invite, error) {
invite := new(types.Invite)
// err := store.
// sqlstore.
// BunDBCtx(ctx).NewSelect().
// Model(invite).
// Where("email = ?", email).
// Where("org_id = ?", orgID).
// Scan(ctx)
// if err != nil {
// return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with email %s does not exist in org %s", email, orgID)
// }
err := store.
sqlstore.
BunDBCtx(ctx).NewSelect().
Model(invite).
Where("email = ?", email).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with email %s does not exist in org %s", email, orgID)
}
// return invite, nil
// }
return invite, nil
}
// func (store *store) GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error) {
// invite := new(types.Invite)
func (store *store) GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error) {
invite := new(types.Invite)
// err := store.
// sqlstore.
// BunDBCtx(ctx).
// NewSelect().
// Model(invite).
// Where("token = ?", token).
// Scan(ctx)
// if err != nil {
// return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite does not exist", token)
// }
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(invite).
Where("token = ?", token).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite does not exist", token)
}
// return invite, nil
// }
return invite, nil
}
// func (store *store) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
// invites := new([]*types.Invite)
// err := store.sqlstore.BunDB().NewSelect().
// Model(invites).
// Where("org_id = ?", orgID).
// Scan(ctx)
// if err != nil {
// return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with org id: %s does not exist", orgID)
// }
// return *invites, nil
// }
func (store *store) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
invites := new([]*types.Invite)
err := store.sqlstore.BunDB().NewSelect().
Model(invites).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with org id: %s does not exist", orgID)
}
return *invites, nil
}
func (store *store) CreatePassword(ctx context.Context, password *types.FactorPassword) error {
_, err := store.
@@ -202,7 +202,6 @@ func (store *store) GetUsersByRoleAndOrgID(ctx context.Context, role types.Role,
Model(&users).
Where("org_id = ?", orgID).
Where("role = ?", role).
Where("status = ?", types.UserStatusActive.StringValue()).
Scan(ctx)
if err != nil {
return nil, err
@@ -222,7 +221,6 @@ func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *typ
Column("role").
Column("is_root").
Column("updated_at").
Column("status").
Where("org_id = ?", orgID).
Where("id = ?", user.ID).
Exec(ctx)
@@ -241,7 +239,6 @@ func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]
NewSelect().
Model(&users).
Where("org_id = ?", orgID).
Where("status = ?", types.UserStatusActive.StringValue()).
Scan(ctx)
if err != nil {
return nil, err
@@ -577,7 +574,6 @@ func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64,
NewSelect().
Model(user).
Where("org_id = ?", orgID).
Where("status = ?", types.UserStatusActive.StringValue()).
Count(ctx)
if err != nil {
return 0, err
@@ -642,21 +638,3 @@ func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.
return users, nil
}
func (store *store) ListPendingInviteUsers(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
users := []*types.User{}
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&users).
Where("org_id = ?", orgID).
Where("status = ?", types.UserStatusPendingInvite.StringValue()).
Scan(ctx)
if err != nil {
return nil, err
}
return users, nil
}

View File

@@ -40,11 +40,11 @@ type Module interface {
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
// invite
CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.User, error)
// ListInvite(ctx context.Context, orgID string) ([]*types.User, error)
// DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
// AcceptInvite(ctx context.Context, token string, password string) (*types.User, error)
// GetInviteByToken(ctx context.Context, token string) (*types.Invite, error)
CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error)
DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
AcceptInvite(ctx context.Context, token string, password string) (*types.User, error)
GetInviteByToken(ctx context.Context, token string) (*types.Invite, error)
// API KEY
CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error
@@ -85,10 +85,10 @@ type Getter interface {
type Handler interface {
// invite
CreateInvite(http.ResponseWriter, *http.Request)
// AcceptInvite(http.ResponseWriter, *http.Request)
// GetInvite(http.ResponseWriter, *http.Request) // public function
// ListInvite(http.ResponseWriter, *http.Request)
// DeleteInvite(http.ResponseWriter, *http.Request)
AcceptInvite(http.ResponseWriter, *http.Request)
GetInvite(http.ResponseWriter, *http.Request) // public function
ListInvite(http.ResponseWriter, *http.Request)
DeleteInvite(http.ResponseWriter, *http.Request)
CreateBulkInvite(http.ResponseWriter, *http.Request)
ListUsers(http.ResponseWriter, *http.Request)

View File

@@ -10,15 +10,16 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type cloudProviderAccountsRepository interface {
listConnected(ctx context.Context, orgId string, provider string) ([]types.CloudIntegration, *model.ApiError)
listConnected(ctx context.Context, orgId string, provider string) ([]integrationtypes.CloudIntegration, *model.ApiError)
get(ctx context.Context, orgId string, provider string, id string) (*types.CloudIntegration, *model.ApiError)
get(ctx context.Context, orgId string, provider string, id string) (*integrationtypes.CloudIntegration, *model.ApiError)
getConnectedCloudAccount(ctx context.Context, orgId string, provider string, accountID string) (*types.CloudIntegration, *model.ApiError)
getConnectedCloudAccount(ctx context.Context, orgId string, provider string, accountID string) (*integrationtypes.CloudIntegration, *model.ApiError)
// Insert an account or update it by (cloudProvider, id)
// for specified non-empty fields
@@ -27,11 +28,11 @@ type cloudProviderAccountsRepository interface {
orgId string,
provider string,
id *string,
config *types.AccountConfig,
config *integrationtypes.AccountConfig,
accountId *string,
agentReport *types.AgentReport,
agentReport *integrationtypes.AgentReport,
removedAt *time.Time,
) (*types.CloudIntegration, *model.ApiError)
) (*integrationtypes.CloudIntegration, *model.ApiError)
}
func newCloudProviderAccountsRepository(store sqlstore.SQLStore) (
@@ -48,8 +49,8 @@ type cloudProviderAccountsSQLRepository struct {
func (r *cloudProviderAccountsSQLRepository) listConnected(
ctx context.Context, orgId string, cloudProvider string,
) ([]types.CloudIntegration, *model.ApiError) {
accounts := []types.CloudIntegration{}
) ([]integrationtypes.CloudIntegration, *model.ApiError) {
accounts := []integrationtypes.CloudIntegration{}
err := r.store.BunDB().NewSelect().
Model(&accounts).
@@ -72,8 +73,8 @@ func (r *cloudProviderAccountsSQLRepository) listConnected(
func (r *cloudProviderAccountsSQLRepository) get(
ctx context.Context, orgId string, provider string, id string,
) (*types.CloudIntegration, *model.ApiError) {
var result types.CloudIntegration
) (*integrationtypes.CloudIntegration, *model.ApiError) {
var result integrationtypes.CloudIntegration
err := r.store.BunDB().NewSelect().
Model(&result).
@@ -97,8 +98,8 @@ func (r *cloudProviderAccountsSQLRepository) get(
func (r *cloudProviderAccountsSQLRepository) getConnectedCloudAccount(
ctx context.Context, orgId string, provider string, accountId string,
) (*types.CloudIntegration, *model.ApiError) {
var result types.CloudIntegration
) (*integrationtypes.CloudIntegration, *model.ApiError) {
var result integrationtypes.CloudIntegration
err := r.store.BunDB().NewSelect().
Model(&result).
@@ -127,11 +128,11 @@ func (r *cloudProviderAccountsSQLRepository) upsert(
orgId string,
provider string,
id *string,
config *types.AccountConfig,
config *integrationtypes.AccountConfig,
accountId *string,
agentReport *types.AgentReport,
agentReport *integrationtypes.AgentReport,
removedAt *time.Time,
) (*types.CloudIntegration, *model.ApiError) {
) (*integrationtypes.CloudIntegration, *model.ApiError) {
// Insert
if id == nil {
temp := valuer.GenerateUUID().StringValue()
@@ -181,7 +182,7 @@ func (r *cloudProviderAccountsSQLRepository) upsert(
)
}
integration := types.CloudIntegration{
integration := integrationtypes.CloudIntegration{
OrgID: orgId,
Provider: provider,
Identifiable: types.Identifiable{ID: valuer.MustNewUUID(*id)},

View File

@@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"golang.org/x/exp/maps"
)
@@ -52,7 +53,7 @@ func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
}
type ConnectedAccountsListResponse struct {
Accounts []types.Account `json:"accounts"`
Accounts []integrationtypes.Account `json:"accounts"`
}
func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cloudProvider string) (
@@ -67,7 +68,7 @@ func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cl
return nil, model.WrapApiError(apiErr, "couldn't list cloud accounts")
}
connectedAccounts := []types.Account{}
connectedAccounts := []integrationtypes.Account{}
for _, a := range accountRecords {
connectedAccounts = append(connectedAccounts, a.Account())
}
@@ -81,7 +82,7 @@ type GenerateConnectionUrlRequest struct {
// Optional. To be specified for updates.
AccountId *string `json:"account_id,omitempty"`
AccountConfig types.AccountConfig `json:"account_config"`
AccountConfig integrationtypes.AccountConfig `json:"account_config"`
AgentConfig SigNozAgentConfig `json:"agent_config"`
}
@@ -149,9 +150,9 @@ func (c *Controller) GenerateConnectionUrl(ctx context.Context, orgId string, cl
}
type AccountStatusResponse struct {
Id string `json:"id"`
CloudAccountId *string `json:"cloud_account_id,omitempty"`
Status types.AccountStatus `json:"status"`
Id string `json:"id"`
CloudAccountId *string `json:"cloud_account_id,omitempty"`
Status integrationtypes.AccountStatus `json:"status"`
}
func (c *Controller) GetAccountStatus(ctx context.Context, orgId string, cloudProvider string, accountId string) (
@@ -217,7 +218,7 @@ func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProv
))
}
agentReport := types.AgentReport{
agentReport := integrationtypes.AgentReport{
TimestampMillis: time.Now().UnixMilli(),
Data: req.Data,
}
@@ -286,10 +287,10 @@ func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProv
}
type UpdateAccountConfigRequest struct {
Config types.AccountConfig `json:"config"`
Config integrationtypes.AccountConfig `json:"config"`
}
func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*types.Account, *model.ApiError) {
func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*integrationtypes.Account, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
@@ -306,7 +307,7 @@ func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, clou
return &account, nil
}
func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*types.CloudIntegration, *model.ApiError) {
func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*integrationtypes.CloudIntegration, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
@@ -346,7 +347,7 @@ func (c *Controller) ListServices(
return nil, model.WrapApiError(apiErr, "couldn't list cloud services")
}
svcConfigs := map[string]*types.CloudServiceConfig{}
svcConfigs := map[string]*integrationtypes.CloudServiceConfig{}
if cloudAccountId != nil {
activeAccount, apiErr := c.accountsRepo.getConnectedCloudAccount(
ctx, orgID, cloudProvider, *cloudAccountId,
@@ -441,8 +442,8 @@ func (c *Controller) GetServiceDetails(
}
type UpdateServiceConfigRequest struct {
CloudAccountId string `json:"cloud_account_id"`
Config types.CloudServiceConfig `json:"config"`
CloudAccountId string `json:"cloud_account_id"`
Config integrationtypes.CloudServiceConfig `json:"config"`
}
func (u *UpdateServiceConfigRequest) Validate(def *services.Definition) error {
@@ -460,8 +461,8 @@ func (u *UpdateServiceConfigRequest) Validate(def *services.Definition) error {
}
type UpdateServiceConfigResponse struct {
Id string `json:"id"`
Config types.CloudServiceConfig `json:"config"`
Id string `json:"id"`
Config integrationtypes.CloudServiceConfig `json:"config"`
}
func (c *Controller) UpdateServiceConfig(

View File

@@ -3,20 +3,20 @@ package cloudintegrations
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
)
type ServiceSummary struct {
services.Metadata
Config *types.CloudServiceConfig `json:"config"`
Config *integrationtypes.CloudServiceConfig `json:"config"`
}
type ServiceDetails struct {
services.Definition
Config *types.CloudServiceConfig `json:"config"`
ConnectionStatus *ServiceConnectionStatus `json:"status,omitempty"`
Config *integrationtypes.CloudServiceConfig `json:"config"`
ConnectionStatus *ServiceConnectionStatus `json:"status,omitempty"`
}
type AccountStatus struct {
@@ -61,7 +61,7 @@ func NewCompiledCollectionStrategy(provider string) (*CompiledCollectionStrategy
// Helper for accumulating strategies for enabled services.
func AddServiceStrategy(serviceType string, cs *CompiledCollectionStrategy,
definitionStrat *services.CollectionStrategy, config *types.CloudServiceConfig) error {
definitionStrat *services.CollectionStrategy, config *integrationtypes.CloudServiceConfig) error {
if definitionStrat.Provider != cs.Provider {
return errors.NewInternalf(CodeMismatchCloudProvider, "can't add %s service strategy to compiled strategy for %s",
definitionStrat.Provider, cs.Provider)

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -18,7 +19,7 @@ type ServiceConfigDatabase interface {
orgID string,
cloudAccountId string,
serviceType string,
) (*types.CloudServiceConfig, *model.ApiError)
) (*integrationtypes.CloudServiceConfig, *model.ApiError)
upsert(
ctx context.Context,
@@ -26,15 +27,15 @@ type ServiceConfigDatabase interface {
cloudProvider string,
cloudAccountId string,
serviceId string,
config types.CloudServiceConfig,
) (*types.CloudServiceConfig, *model.ApiError)
config integrationtypes.CloudServiceConfig,
) (*integrationtypes.CloudServiceConfig, *model.ApiError)
getAllForAccount(
ctx context.Context,
orgID string,
cloudAccountId string,
) (
configsBySvcId map[string]*types.CloudServiceConfig,
configsBySvcId map[string]*integrationtypes.CloudServiceConfig,
apiErr *model.ApiError,
)
}
@@ -56,9 +57,9 @@ func (r *serviceConfigSQLRepository) get(
orgID string,
cloudAccountId string,
serviceType string,
) (*types.CloudServiceConfig, *model.ApiError) {
) (*integrationtypes.CloudServiceConfig, *model.ApiError) {
var result types.CloudIntegrationService
var result integrationtypes.CloudIntegrationService
err := r.store.BunDB().NewSelect().
Model(&result).
@@ -89,14 +90,14 @@ func (r *serviceConfigSQLRepository) upsert(
cloudProvider string,
cloudAccountId string,
serviceId string,
config types.CloudServiceConfig,
) (*types.CloudServiceConfig, *model.ApiError) {
config integrationtypes.CloudServiceConfig,
) (*integrationtypes.CloudServiceConfig, *model.ApiError) {
// get cloud integration id from account id
// if the account is not connected, we don't need to upsert the config
var cloudIntegrationId string
err := r.store.BunDB().NewSelect().
Model((*types.CloudIntegration)(nil)).
Model((*integrationtypes.CloudIntegration)(nil)).
Column("id").
Where("provider = ?", cloudProvider).
Where("account_id = ?", cloudAccountId).
@@ -111,7 +112,7 @@ func (r *serviceConfigSQLRepository) upsert(
))
}
serviceConfig := types.CloudIntegrationService{
serviceConfig := integrationtypes.CloudIntegrationService{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
@@ -139,8 +140,8 @@ func (r *serviceConfigSQLRepository) getAllForAccount(
ctx context.Context,
orgID string,
cloudAccountId string,
) (map[string]*types.CloudServiceConfig, *model.ApiError) {
serviceConfigs := []types.CloudIntegrationService{}
) (map[string]*integrationtypes.CloudServiceConfig, *model.ApiError) {
serviceConfigs := []integrationtypes.CloudIntegrationService{}
err := r.store.BunDB().NewSelect().
Model(&serviceConfigs).
@@ -154,7 +155,7 @@ func (r *serviceConfigSQLRepository) getAllForAccount(
))
}
result := map[string]*types.CloudServiceConfig{}
result := map[string]*integrationtypes.CloudServiceConfig{}
for _, r := range serviceConfigs {
result[r.Type] = &r.Config

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -107,7 +108,7 @@ type IntegrationsListItem struct {
type Integration struct {
IntegrationDetails
Installation *types.InstalledIntegration `json:"installation"`
Installation *integrationtypes.InstalledIntegration `json:"installation"`
}
type Manager struct {
@@ -223,7 +224,7 @@ func (m *Manager) InstallIntegration(
ctx context.Context,
orgId string,
integrationId string,
config types.InstalledIntegrationConfig,
config integrationtypes.InstalledIntegrationConfig,
) (*IntegrationsListItem, *model.ApiError) {
integrationDetails, apiErr := m.getIntegrationDetails(ctx, integrationId)
if apiErr != nil {
@@ -429,7 +430,7 @@ func (m *Manager) getInstalledIntegration(
ctx context.Context,
orgId string,
integrationId string,
) (*types.InstalledIntegration, *model.ApiError) {
) (*integrationtypes.InstalledIntegration, *model.ApiError) {
iis, apiErr := m.installedIntegrationsRepo.get(
ctx, orgId, []string{integrationId},
)
@@ -457,7 +458,7 @@ func (m *Manager) getInstalledIntegrations(
return nil, apiErr
}
installedTypes := utils.MapSlice(installations, func(i types.InstalledIntegration) string {
installedTypes := utils.MapSlice(installations, func(i integrationtypes.InstalledIntegration) string {
return i.Type
})
integrationDetails, apiErr := m.availableIntegrationsRepo.get(ctx, installedTypes)

View File

@@ -4,22 +4,22 @@ import (
"context"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
)
type InstalledIntegrationsRepo interface {
list(ctx context.Context, orgId string) ([]types.InstalledIntegration, *model.ApiError)
list(ctx context.Context, orgId string) ([]integrationtypes.InstalledIntegration, *model.ApiError)
get(
ctx context.Context, orgId string, integrationTypes []string,
) (map[string]types.InstalledIntegration, *model.ApiError)
) (map[string]integrationtypes.InstalledIntegration, *model.ApiError)
upsert(
ctx context.Context,
orgId string,
integrationType string,
config types.InstalledIntegrationConfig,
) (*types.InstalledIntegration, *model.ApiError)
config integrationtypes.InstalledIntegrationConfig,
) (*integrationtypes.InstalledIntegration, *model.ApiError)
delete(ctx context.Context, orgId string, integrationType string) *model.ApiError
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -26,8 +27,8 @@ func NewInstalledIntegrationsSqliteRepo(store sqlstore.SQLStore) (
func (r *InstalledIntegrationsSqliteRepo) list(
ctx context.Context,
orgId string,
) ([]types.InstalledIntegration, *model.ApiError) {
integrations := []types.InstalledIntegration{}
) ([]integrationtypes.InstalledIntegration, *model.ApiError) {
integrations := []integrationtypes.InstalledIntegration{}
err := r.store.BunDB().NewSelect().
Model(&integrations).
@@ -44,8 +45,8 @@ func (r *InstalledIntegrationsSqliteRepo) list(
func (r *InstalledIntegrationsSqliteRepo) get(
ctx context.Context, orgId string, integrationTypes []string,
) (map[string]types.InstalledIntegration, *model.ApiError) {
integrations := []types.InstalledIntegration{}
) (map[string]integrationtypes.InstalledIntegration, *model.ApiError) {
integrations := []integrationtypes.InstalledIntegration{}
typeValues := []interface{}{}
for _, integrationType := range integrationTypes {
@@ -62,7 +63,7 @@ func (r *InstalledIntegrationsSqliteRepo) get(
))
}
result := map[string]types.InstalledIntegration{}
result := map[string]integrationtypes.InstalledIntegration{}
for _, ii := range integrations {
result[ii.Type] = ii
}
@@ -74,10 +75,10 @@ func (r *InstalledIntegrationsSqliteRepo) upsert(
ctx context.Context,
orgId string,
integrationType string,
config types.InstalledIntegrationConfig,
) (*types.InstalledIntegration, *model.ApiError) {
config integrationtypes.InstalledIntegrationConfig,
) (*integrationtypes.InstalledIntegration, *model.ApiError) {
integration := types.InstalledIntegration{
integration := integrationtypes.InstalledIntegration{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -114,7 +115,7 @@ func (r *InstalledIntegrationsSqliteRepo) delete(
ctx context.Context, orgId string, integrationType string,
) *model.ApiError {
_, dbErr := r.store.BunDB().NewDelete().
Model(&types.InstalledIntegration{}).
Model(&integrationtypes.InstalledIntegration{}).
Where("type = ?", integrationType).
Where("org_id = ?", orgId).
Exec(ctx)

View File

@@ -50,7 +50,7 @@ func TestNewHandlers(t *testing.T) {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)
querierHandler := querier.NewHandler(providerSettings, nil, nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil)

View File

@@ -8,7 +8,6 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
@@ -87,11 +86,10 @@ func NewModules(
config Config,
dashboard dashboard.Module,
userGetter user.Getter,
flagger flagger.Flagger,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User, flagger)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User)
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
return Modules{

View File

@@ -49,7 +49,7 @@ func TestNewModules(t *testing.T) {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -388,7 +388,7 @@ func New(
}
// Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter)
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)

View File

@@ -1,88 +0,0 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addStatusUser struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func AddStatusUserFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("add_status_user"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addStatusUser{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
},
)
}
func (migration *addStatusUser) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addStatusUser) Up(ctx context.Context, db *bun.DB) error {
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("users"))
if err != nil {
return err
}
column := &sqlschema.Column{
Name: sqlschema.ColumnName("status"),
DataType: sqlschema.DataTypeText,
Nullable: false,
}
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, false)
indexSqls := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "users", ColumnNames: []sqlschema.ColumnName{"email", "org_id"}})
sqls = append(sqls, indexSqls...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, true); err != nil {
return err
}
return nil
}
func (migration *addStatusUser) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -1,70 +0,0 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type deprecateUserInvite struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewDeprecateUserInviteFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("deprecate_user_invite"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &deprecateUserInvite{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
},
)
}
func (migration *deprecateUserInvite) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *deprecateUserInvite) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("user_invite"))
if err != nil {
return err
}
dropTableSqls := migration.sqlschema.Operator().DropTable(table)
for _, sql := range dropTableSqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *deprecateUserInvite) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -1,4 +1,4 @@
package types
package integrationtypes
import (
"database/sql/driver"
@@ -6,6 +6,7 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/uptrace/bun"
)
@@ -26,17 +27,17 @@ var AllIntegrationUserEmails = []IntegrationUserEmail{
type InstalledIntegration struct {
bun.BaseModel `bun:"table:installed_integration"`
Identifiable
types.Identifiable
Type string `json:"type" bun:"type,type:text,unique:org_id_type"`
Config InstalledIntegrationConfig `json:"config" bun:"config,type:text"`
InstalledAt time.Time `json:"installed_at" bun:"installed_at,default:current_timestamp"`
OrgID string `json:"org_id" bun:"org_id,type:text,unique:org_id_type,references:organizations(id),on_delete:cascade"`
}
type InstalledIntegrationConfig map[string]interface{}
type InstalledIntegrationConfig map[string]any
// For serializing from db
func (c *InstalledIntegrationConfig) Scan(src interface{}) error {
func (c *InstalledIntegrationConfig) Scan(src any) error {
var data []byte
switch v := src.(type) {
case []byte:
@@ -67,8 +68,8 @@ func (c *InstalledIntegrationConfig) Value() (driver.Value, error) {
type CloudIntegration struct {
bun.BaseModel `bun:"table:cloud_integration"`
Identifiable
TimeAuditable
types.Identifiable
types.TimeAuditable
Provider string `json:"provider" bun:"provider,type:text,unique:provider_id"`
Config *AccountConfig `json:"config" bun:"config,type:text"`
AccountID *string `json:"account_id" bun:"account_id,type:text"`
@@ -194,8 +195,8 @@ func (r *AgentReport) Value() (driver.Value, error) {
type CloudIntegrationService struct {
bun.BaseModel `bun:"table:cloud_integration_service,alias:cis"`
Identifiable
TimeAuditable
types.Identifiable
types.TimeAuditable
Type string `bun:"type,type:text,notnull,unique:cloud_integration_id_type"`
Config CloudServiceConfig `bun:"config,type:text"`
CloudIntegrationID string `bun:"cloud_integration_id,type:text,notnull,unique:cloud_integration_id_type,references:cloud_integrations(id),on_delete:cascade"`

View File

@@ -1,12 +1,12 @@
package types
import (
// "encoding/json"
// "time"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
// "github.com/uptrace/bun"
"github.com/uptrace/bun"
)
var (
@@ -14,38 +14,37 @@ var (
ErrInviteNotFound = errors.MustNewCode("invite_not_found")
)
// TODO - remove this commented lines
// type GettableInvite = Invite
type GettableInvite = Invite
// type Invite struct {
// bun.BaseModel `bun:"table:user_invite"`
type Invite struct {
bun.BaseModel `bun:"table:user_invite"`
// Identifiable
// TimeAuditable
// Name string `bun:"name,type:text" json:"name"`
// Email valuer.Email `bun:"email,type:text" json:"email"`
// Token string `bun:"token,type:text" json:"token"`
// Role Role `bun:"role,type:text" json:"role"`
// OrgID valuer.UUID `bun:"org_id,type:text" json:"orgId"`
Identifiable
TimeAuditable
Name string `bun:"name,type:text" json:"name"`
Email valuer.Email `bun:"email,type:text" json:"email"`
Token string `bun:"token,type:text" json:"token"`
Role Role `bun:"role,type:text" json:"role"`
OrgID valuer.UUID `bun:"org_id,type:text" json:"orgId"`
// InviteLink string `bun:"-" json:"inviteLink"`
// }
InviteLink string `bun:"-" json:"inviteLink"`
}
// type InviteEmailData struct {
// CustomerName string
// InviterName string
// InviterEmail string
// Link string
// }
type InviteEmailData struct {
CustomerName string
InviterName string
InviterEmail string
Link string
}
// type PostableAcceptInvite struct {
// DisplayName string `json:"displayName"`
// InviteToken string `json:"token"`
// Password string `json:"password"`
type PostableAcceptInvite struct {
DisplayName string `json:"displayName"`
InviteToken string `json:"token"`
Password string `json:"password"`
// // reference URL to track where the register request is coming from
// SourceURL string `json:"sourceUrl"`
// }
// reference URL to track where the register request is coming from
SourceURL string `json:"sourceUrl"`
}
type PostableInvite struct {
Name string `json:"name"`
@@ -58,45 +57,45 @@ type PostableBulkInviteRequest struct {
Invites []PostableInvite `json:"invites"`
}
// type GettableCreateInviteResponse struct {
// InviteToken string `json:"token"`
// }
type GettableCreateInviteResponse struct {
InviteToken string `json:"token"`
}
// func NewInvite(name string, role Role, orgID valuer.UUID, email valuer.Email) (*Invite, error) {
// invite := &Invite{
// Identifiable: Identifiable{
// ID: valuer.GenerateUUID(),
// },
// Name: name,
// Email: email,
// Token: valuer.GenerateUUID().String(),
// Role: role,
// OrgID: orgID,
// TimeAuditable: TimeAuditable{
// CreatedAt: time.Now(),
// UpdatedAt: time.Now(),
// },
// }
func NewInvite(name string, role Role, orgID valuer.UUID, email valuer.Email) (*Invite, error) {
invite := &Invite{
Identifiable: Identifiable{
ID: valuer.GenerateUUID(),
},
Name: name,
Email: email,
Token: valuer.GenerateUUID().String(),
Role: role,
OrgID: orgID,
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
// return invite, nil
// }
return invite, nil
}
// func (request *PostableAcceptInvite) UnmarshalJSON(data []byte) error {
// type Alias PostableAcceptInvite
func (request *PostableAcceptInvite) UnmarshalJSON(data []byte) error {
type Alias PostableAcceptInvite
// var temp Alias
// if err := json.Unmarshal(data, &temp); err != nil {
// return err
// }
var temp Alias
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
// if temp.InviteToken == "" {
// return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required")
// }
if temp.InviteToken == "" {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required")
}
// if !IsPasswordValid(temp.Password) {
// return ErrInvalidPassword
// }
if !IsPasswordValid(temp.Password) {
return ErrInvalidPassword
}
// *request = PostableAcceptInvite(temp)
// return nil
// }
*request = PostableAcceptInvite(temp)
return nil
}

View File

@@ -1,17 +1,18 @@
package types
package savedviewtypes
import (
"strings"
"github.com/SigNoz/signoz/pkg/types"
"github.com/uptrace/bun"
)
type SavedView struct {
bun.BaseModel `bun:"table:saved_views"`
Identifiable
TimeAuditable
UserAuditable
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID string `json:"orgId" bun:"org_id,notnull"`
Name string `json:"name" bun:"name,type:text,notnull"`
Category string `json:"category" bun:"category,type:text,notnull"`

View File

@@ -23,25 +23,17 @@ var (
ErrCodeRootUserOperationUnsupported = errors.MustNewCode("root_user_operation_unsupported")
)
var (
UserStatusPendingInvite = valuer.NewString("pending_invite")
UserStatusActive = valuer.NewString("active")
UserStatusDeleted = valuer.NewString("deleted")
ValidUserStatus = []valuer.String{UserStatusPendingInvite, UserStatusActive, UserStatusDeleted}
)
type GettableUser = User
type User struct {
bun.BaseModel `bun:"table:users"`
Identifiable
DisplayName string `bun:"display_name" json:"displayName"`
Email valuer.Email `bun:"email" json:"email"`
Role Role `bun:"role" json:"role"`
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
IsRoot bool `bun:"is_root" json:"isRoot"`
Status valuer.String `bun:"status" json:"status"`
DisplayName string `bun:"display_name" json:"displayName"`
Email valuer.Email `bun:"email" json:"email"`
Role Role `bun:"role" json:"role"`
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
IsRoot bool `bun:"is_root" json:"isRoot"`
TimeAuditable
}
@@ -53,7 +45,7 @@ type PostableRegisterOrgAndAdmin struct {
OrgName string `json:"orgName"`
}
func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID, status valuer.String) (*User, 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")
}
@@ -75,7 +67,6 @@ func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUI
Role: role,
OrgID: orgID,
IsRoot: false,
Status: status,
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@@ -101,7 +92,6 @@ func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*Us
Role: RoleAdmin,
OrgID: orgID,
IsRoot: true,
Status: UserStatusActive,
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@@ -121,11 +111,6 @@ func (u *User) Update(displayName string, role Role) {
u.UpdatedAt = time.Now()
}
func (u *User) UpdateStatus(status valuer.String) {
u.Status = status
u.UpdatedAt = time.Now()
}
// PromoteToRoot promotes the user to a root user with admin role.
func (u *User) PromoteToRoot() {
u.IsRoot = true
@@ -154,7 +139,6 @@ func NewTraitsFromUser(user *User) map[string]any {
"role": user.Role,
"email": user.Email.String(),
"display_name": user.DisplayName,
"status": user.Status,
"created_at": user.CreatedAt,
}
}
@@ -177,15 +161,15 @@ func (request *PostableRegisterOrgAndAdmin) UnmarshalJSON(data []byte) error {
type UserStore interface {
// invite
// CreateBulkInvite(ctx context.Context, invites []*Invite) error
// ListInvite(ctx context.Context, orgID string) ([]*Invite, error)
// DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
CreateBulkInvite(ctx context.Context, invites []*Invite) error
ListInvite(ctx context.Context, orgID string) ([]*Invite, error)
DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
// Get invite by token.
// GetInviteByToken(ctx context.Context, token string) (*Invite, error)
GetInviteByToken(ctx context.Context, token string) (*Invite, error)
// Get invite by email and org.
// GetInviteByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*Invite, error)
GetInviteByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*Invite, error)
// Creates a user.
CreateUser(ctx context.Context, user *User) error
@@ -211,9 +195,6 @@ type UserStore interface {
// List users by email and org ids.
ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*User, error)
// List users in pending invite status
ListPendingInviteUsers(ctx context.Context, orgID valuer.UUID) ([]*User, error)
UpdateUser(ctx context.Context, orgID valuer.UUID, user *User) error
DeleteUser(ctx context.Context, orgID string, id string) error