Compare commits

..

9 Commits

Author SHA1 Message Date
swapnil-signoz
7d4a9b183e Merge branch 'feat/cloud-integrations-apis' of https://github.com/SigNoz/signoz into feat/cloud-integrations-apis 2026-02-27 23:57:06 +05:30
swapnil-signoz
c5fdc4c3d1 Merge branch 'main' into feat/cloud-integrations-apis 2026-02-27 23:31:31 +05:30
Nityananda Gohain
afcf2c6053 Merge branch 'main' into feat/cloud-integrations-apis 2026-02-25 19:53:25 +05:30
swapnil-signoz
ec59fecdda Feat/azure integration (#10289)
* refactor: updating azure cloud integrations api

* feat: extending cloud integration apis

* refactor: updating cloud integration apis

* refactor: updating cloud-integration controller

* refactor: updating cloud provider type

* refactor: updating service details api

* refactor: code beautification

* refactor: sorting services list for consistency

* refactor: cloud integration API cleanup

* refactor: reverting

* ci: fixing lint ci issues

* feat: adding logs

* fix: aws connection url generation

* refactor: removing comment

* chore: wip

* refactor: wip

* refactor:  updating response for conn status

* refactor: removing wrong deps

* feat: adding comments

* feat: adding default values for azure service config

* feat: updating service definitions

* refactor: updating azure telemetry collections strat

* refactor: aws cloud integration provider impl

* refactor: improving aws cloud integration apis structure

* feat: adding logger in opts

* feat: updating cloud integration aws api

* feat: adding azure cloud integration apis

* test: testing generics

* feat: adding more generic struct

* refactor: removing dummy dashboards

* feat: implement service connection status retrieval for Azure integration

* feat: add ingestion status check for metrics and logs in Azure integration

* chore: merge back

* feat: adding base provider

* feat: moving methods to generic base provider

* refactor: updating agent version

* refactor: update generic type parameters in cloud provider and integration types

* refactor: cloud integration type comments and cleanup

* chore: adding more comments

---------

Co-authored-by: Piyush Singariya <piyushsingariya@gmail.com>
2026-02-25 12:56:34 +05:30
swapnil-signoz
5a762c678e Merge branch 'main' into feat/cloud-integrations-apis 2026-02-23 20:11:36 +05:30
swapnil-signoz
55b1311f78 Merge branch 'main' into feat/cloud-integrations-apis 2026-02-23 10:45:09 +05:30
swapnil-signoz
59668698a2 refactor: using QBv5 for connection status (#10288)
* refactor: updating azure cloud integrations api

* feat: extending cloud integration apis

* refactor: updating cloud integration apis

* refactor: updating cloud-integration controller

* refactor: updating cloud provider type

* refactor: updating service details api

* refactor: code beautification

* refactor: sorting services list for consistency

* refactor: cloud integration API cleanup

* refactor: reverting

* ci: fixing lint ci issues

* feat: adding logs

* fix: aws connection url generation

* refactor: removing comment

* chore: wip

* refactor: wip

* refactor:  updating response for conn status

* refactor: removing wrong deps

* feat: adding comments

* feat: adding default values for azure service config

* feat: updating service definitions

* refactor: updating azure telemetry collections strat

* refactor: aws cloud integration provider impl

* refactor: improving aws cloud integration apis structure

* feat: adding logger in opts

* feat: updating cloud integration aws api

* refactor: using QBv5 for connection status

* refactor: status check

* fix: handle unexpected query response types for service metrics and logs

* refactor: implement centralized panic recovery

* refactor: simplify AWS cloud provider initialization by removing unused reader parameter
2026-02-19 16:19:38 +05:30
swapnil-signoz
22ed687d44 Refactor/aws cloudintegration (#10339)
* refactor: updating azure cloud integrations api

* feat: extending cloud integration apis

* refactor: updating cloud integration apis

* refactor: updating cloud-integration controller

* refactor: updating cloud provider type

* refactor: updating service details api

* refactor: code beautification

* refactor: sorting services list for consistency

* refactor: cloud integration API cleanup

* refactor: reverting

* ci: fixing lint ci issues

* feat: adding logs

* fix: aws connection url generation

* refactor: removing comment

* chore: wip

* refactor: wip

* refactor:  updating response for conn status

* refactor: removing wrong deps

* feat: adding comments

* feat: adding default values for azure service config

* feat: updating service definitions

* refactor: updating azure telemetry collections strat

* refactor: aws cloud integration provider impl

* refactor: improving aws cloud integration apis structure

* feat: adding logger in opts

* feat: updating cloud integration aws api

* feat: using generics

* fix: update error handling in cloud integration for deployment info response

* refactor: optimize dashboard retrieval logic in AWS provider

* refactor: update GetDashboardsFromAssets to include orgID parameter
2026-02-18 12:09:35 +05:30
swapnil-signoz
1da016cf1a Feat: improving cloud integrations APIs (#10280)
* refactor: updating azure cloud integrations api

* feat: extending cloud integration apis

* refactor: updating cloud integration apis

* refactor: updating cloud-integration controller

* refactor: updating cloud provider type

* refactor: updating service details api

* refactor: code beautification

* refactor: sorting services list for consistency

* refactor: cloud integration API cleanup

* refactor: reverting

* ci: fixing lint ci issues

* feat: adding logs

* fix: aws connection url generation

* refactor: removing comment

* chore: wip

* refactor: wip

* refactor:  updating response for conn status

* refactor: removing wrong deps

* feat: adding comments

* feat: adding default values for azure service config

* feat: updating service definitions

* refactor: updating azure telemetry collections strat

* refactor: aws cloud integration provider impl

* refactor: improving aws cloud integration apis structure

* feat: adding logger in opts

* feat: updating cloud integration aws api

* feat: using generics
2026-02-18 11:43:26 +05:30
111 changed files with 6045 additions and 11702 deletions

View File

@@ -61,59 +61,3 @@ 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
authz:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: "22"
- name: Install frontend dependencies
working-directory: ./frontend
run: |
yarn install
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install Python dependencies
working-directory: ./tests/integration
run: |
uv sync
- name: Start test environment
run: |
make py-test-setup
- name: Generate permissions.type.ts
run: |
node frontend/scripts/generate-permissions-type.js
- name: Teardown test environment
if: always()
run: |
make py-test-teardown
- name: Check for changes
run: |
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
exit 1
fi

View File

@@ -1763,134 +1763,6 @@ components:
- type
- orgId
type: object
ServiceaccounttypesFactorAPIKey:
properties:
createdAt:
format: date-time
type: string
expires_at:
minimum: 0
type: integer
id:
type: string
key:
type: string
last_used:
format: date-time
type: string
name:
type: string
service_account_id:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- key
- expires_at
- last_used
- service_account_id
type: object
ServiceaccounttypesGettableFactorAPIKeyWithKey:
properties:
id:
type: string
key:
type: string
required:
- id
- key
type: object
ServiceaccounttypesPostableFactorAPIKey:
properties:
expires_at:
minimum: 0
type: integer
name:
type: string
required:
- name
- expires_at
type: object
ServiceaccounttypesPostableServiceAccount:
properties:
email:
type: string
name:
type: string
roles:
items:
type: string
type: array
required:
- name
- email
- roles
type: object
ServiceaccounttypesServiceAccount:
properties:
createdAt:
format: date-time
type: string
email:
type: string
id:
type: string
name:
type: string
orgID:
type: string
roles:
items:
type: string
type: array
status:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- email
- roles
- status
- orgID
type: object
ServiceaccounttypesUpdatableFactorAPIKey:
properties:
expires_at:
minimum: 0
type: integer
name:
type: string
required:
- name
- expires_at
type: object
ServiceaccounttypesUpdatableServiceAccount:
properties:
email:
type: string
name:
type: string
roles:
items:
type: string
type: array
required:
- name
- email
- roles
type: object
ServiceaccounttypesUpdatableServiceAccountStatus:
properties:
status:
type: string
required:
- status
type: object
TelemetrytypesFieldContext:
enum:
- metric
@@ -4665,586 +4537,6 @@ paths:
summary: Patch objects for a role by relation
tags:
- role
/api/v1/service_accounts:
get:
deprecated: false
description: This endpoint lists the service accounts for an organisation
operationId: ListServiceAccounts
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/ServiceaccounttypesServiceAccount'
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: List service accounts
tags:
- serviceaccount
post:
deprecated: false
description: This endpoint creates a service account
operationId: CreateServiceAccount
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccount'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create service account
tags:
- serviceaccount
/api/v1/service_accounts/{id}:
delete:
deprecated: false
description: This endpoint deletes an existing service account
operationId: DeleteServiceAccount
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Deletes a service account
tags:
- serviceaccount
get:
deprecated: false
description: This endpoint gets an existing service account
operationId: GetServiceAccount
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/ServiceaccounttypesServiceAccount'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Gets a service account
tags:
- serviceaccount
put:
deprecated: false
description: This endpoint updates an existing service account
operationId: UpdateServiceAccount
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesUpdatableServiceAccount'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Updates a service account
tags:
- serviceaccount
/api/v1/service_accounts/{id}/keys:
get:
deprecated: false
description: This endpoint lists the service account keys
operationId: ListServiceAccountKeys
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/ServiceaccounttypesFactorAPIKey'
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: List service account keys
tags:
- serviceaccount
post:
deprecated: false
description: This endpoint creates a service account key
operationId: CreateServiceAccountKey
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesPostableFactorAPIKey'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/ServiceaccounttypesGettableFactorAPIKeyWithKey'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create a service account key
tags:
- serviceaccount
/api/v1/service_accounts/{id}/keys/{fid}:
delete:
deprecated: false
description: This endpoint revokes an existing service account key
operationId: RevokeServiceAccountKey
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: fid
required: true
schema:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Revoke a service account key
tags:
- serviceaccount
put:
deprecated: false
description: This endpoint updates an existing service account key
operationId: UpdateServiceAccountKey
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: fid
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesUpdatableFactorAPIKey'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Updates a service account key
tags:
- serviceaccount
/api/v1/service_accounts/{id}/status:
put:
deprecated: false
description: This endpoint updates an existing service account status
operationId: UpdateServiceAccountStatus
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesUpdatableServiceAccountStatus'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Updates a service account status
tags:
- serviceaccount
/api/v1/user:
get:
deprecated: false

View File

@@ -1,127 +0,0 @@
# 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

@@ -10,13 +10,11 @@ We adhere to three primary style guides as our foundation:
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
- [Packages](packages.md) — Naming, layout, and conventions for `pkg/` packages
- [Errors](errors.md) Structured error handling
- [Handler](handler.md) — Writing HTTP handlers and OpenAPI integration
- [Endpoint](endpoint.md) — Endpoint conventions
- [SQL](sql.md) — Database query patterns
- [Provider](provider.md) — Provider pattern
- [Integration](integration.md) — Integration conventions
- [Flagger](flagger.md) — Feature flag conventions

View File

@@ -1,269 +0,0 @@
# 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

@@ -98,20 +98,16 @@ func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndIDs(ctx, orgID, ids)
func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, name string, subject string) error {
return provider.pkgAuthzService.Grant(ctx, orgID, name, subject)
}
func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, names []string, subject string) error {
return provider.pkgAuthzService.Grant(ctx, orgID, names, subject)
func (provider *provider) ModifyGrant(ctx context.Context, orgID valuer.UUID, existingRoleName string, updatedRoleName string, subject string) error {
return provider.pkgAuthzService.ModifyGrant(ctx, orgID, existingRoleName, updatedRoleName, subject)
}
func (provider *provider) ModifyGrant(ctx context.Context, orgID valuer.UUID, existingRoleNames []string, updatedRoleNames []string, subject string) error {
return provider.pkgAuthzService.ModifyGrant(ctx, orgID, existingRoleNames, updatedRoleNames, subject)
}
func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names []string, subject string) error {
return provider.pkgAuthzService.Revoke(ctx, orgID, names, subject)
func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, name string, subject string) error {
return provider.pkgAuthzService.Revoke(ctx, orgID, name, subject)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*roletypes.Role) error {

View File

@@ -1,6 +1,7 @@
package api
import (
"log/slog"
"net/http"
"time"
@@ -10,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
@@ -27,12 +27,12 @@ type APIHandlerOptions struct {
RulesManager *rules.Manager
UsageManager *usage.Manager
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
GatewayUrl string
// Querier Influx Interval
FluxInterval time.Duration
GlobalConfig global.Config
Logger *slog.Logger // this is present in Signoz.Instrumentation but adding for quick access
}
type APIHandler struct {
@@ -46,13 +46,13 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
Reader: opts.DataConnector,
RuleManager: opts.RulesManager,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
Logger: opts.Logger,
}, config)
if err != nil {
@@ -101,14 +101,12 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
}
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
router.HandleFunc(
"/api/v1/cloud-integrations/{cloudProvider}/accounts/generate-connection-params",
am.EditAccess(ah.CloudIntegrationsGenerateConnectionParams),
).Methods(http.MethodGet)
}
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"time"
@@ -13,20 +14,14 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"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"
"go.uber.org/zap"
)
type CloudIntegrationConnectionParamsResponse struct {
IngestionUrl string `json:"ingestion_url,omitempty"`
IngestionKey string `json:"ingestion_key,omitempty"`
SigNozAPIUrl string `json:"signoz_api_url,omitempty"`
SigNozAPIKey string `json:"signoz_api_key,omitempty"`
}
// TODO: move this file with other cloud integration related code
func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
@@ -41,23 +36,21 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
cloudProvider := mux.Vars(r)["cloudProvider"]
if cloudProvider != "aws" {
RespondError(w, basemodel.BadRequest(fmt.Errorf(
"cloud provider not supported: %s", cloudProvider,
)), nil)
cloudProviderString := mux.Vars(r)["cloudProvider"]
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't provision PAT for cloud integration:",
), nil)
apiKey, err := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
if err != nil {
render.Error(w, err)
return
}
result := CloudIntegrationConnectionParamsResponse{
result := integrationtypes.GettableCloudIntegrationConnectionParams{
SigNozAPIKey: apiKey,
}
@@ -71,16 +64,17 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
// Return the API Key (PAT) even if the rest of the params can not be deduced.
// Params not returned from here will be requested from the user via form inputs.
// This enables gracefully degraded but working experience even for non-cloud deployments.
zap.L().Info("ingestion params and signoz api url can not be deduced since no license was found")
ah.Respond(w, result)
ah.opts.Logger.InfoContext(
r.Context(),
"ingestion params and signoz api url can not be deduced since no license was found",
)
render.Success(w, http.StatusOK, result)
return
}
signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't deduce ingestion url and signoz api url",
), nil)
signozApiUrl, err := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
if err != nil {
render.Error(w, err)
return
}
@@ -89,48 +83,41 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
gatewayUrl := ah.opts.GatewayUrl
if len(gatewayUrl) > 0 {
ingestionKey, apiErr := getOrCreateCloudProviderIngestionKey(
ingestionKeyString, err := ah.getOrCreateCloudProviderIngestionKey(
r.Context(), gatewayUrl, license.Key, cloudProvider,
)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't get or create ingestion key",
), nil)
if err != nil {
render.Error(w, err)
return
}
result.IngestionKey = ingestionKey
result.IngestionKey = ingestionKeyString
} else {
zap.L().Info("ingestion key can't be deduced since no gateway url has been configured")
ah.opts.Logger.InfoContext(
r.Context(),
"ingestion key can't be deduced since no gateway url has been configured",
)
}
ah.Respond(w, result)
render.Success(w, http.StatusOK, result)
}
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) (
string, *basemodel.ApiError,
) {
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider valuer.String) (string, error) {
integrationPATName := fmt.Sprintf("%s integration", cloudProvider)
integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
if apiErr != nil {
return "", apiErr
integrationUser, err := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
if err != nil {
return "", err
}
orgIdUUID, err := valuer.NewUUID(orgId)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't parse orgId: %w", err,
))
return "", err
}
allPats, err := ah.Signoz.Modules.User.ListAPIKeys(ctx, orgIdUUID)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't list PATs: %w", err,
))
return "", err
}
for _, p := range allPats {
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
@@ -138,9 +125,10 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
}
}
zap.L().Info(
ah.opts.Logger.InfoContext(
ctx,
"no PAT found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
slog.String("cloudProvider", cloudProvider.String()),
)
newPAT, err := types.NewStorableAPIKey(
@@ -150,68 +138,48 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
0,
)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
))
return "", err
}
err = ah.Signoz.Modules.User.CreateAPIKey(ctx, newPAT)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
))
return "", err
}
return newPAT.Token, nil
}
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
ctx context.Context, orgId string, cloudProvider string,
) (*types.User, *basemodel.ApiError) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
// TODO: move this function out of handler and use proper module structure
func (ah *APIHandler) getOrCreateCloudIntegrationUser(ctx context.Context, orgId string, cloudProvider valuer.String) (*types.User, error) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider.String())
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
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))
return nil, err
}
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err))
return nil, err
}
return cloudIntegrationUser, nil
}
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
string, *basemodel.ApiError,
) {
// TODO: remove this struct from here
type deploymentResponse struct {
Name string `json:"name"`
ClusterInfo struct {
Region struct {
DNS string `json:"dns"`
} `json:"region"`
} `json:"cluster"`
}
// TODO: move this function out of handler and use proper module structure
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (string, error) {
respBytes, err := ah.Signoz.Zeus.GetDeployment(ctx, licenseKey)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't query for deployment info: error: %w", err,
))
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't query for deployment info: error")
}
resp := new(deploymentResponse)
resp := new(integrationtypes.GettableDeployment)
err = json.Unmarshal(respBytes, resp)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't unmarshal deployment info response: error: %w", err,
))
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't unmarshal deployment info response")
}
regionDns := resp.ClusterInfo.Region.DNS
@@ -219,9 +187,10 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
if len(regionDns) < 1 || len(deploymentName) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", basemodel.InternalError(fmt.Errorf(
return "", errors.NewInternalf(
errors.CodeInternal,
"deployment info response not in expected shape. couldn't determine region dns and deployment name",
))
)
}
signozApiUrl := fmt.Sprintf("https://%s.%s", deploymentName, regionDns)
@@ -229,102 +198,85 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
return signozApiUrl, nil
}
type ingestionKey struct {
Name string `json:"name"`
Value string `json:"value"`
// other attributes from gateway response not included here since they are not being used.
}
type ingestionKeysSearchResponse struct {
Status string `json:"status"`
Data []ingestionKey `json:"data"`
Error string `json:"error"`
}
type createIngestionKeyResponse struct {
Status string `json:"status"`
Data ingestionKey `json:"data"`
Error string `json:"error"`
}
func getOrCreateCloudProviderIngestionKey(
ctx context.Context, gatewayUrl string, licenseKey string, cloudProvider string,
) (string, *basemodel.ApiError) {
func (ah *APIHandler) getOrCreateCloudProviderIngestionKey(
ctx context.Context, gatewayUrl string, licenseKey string, cloudProvider valuer.String,
) (string, error) {
cloudProviderKeyName := fmt.Sprintf("%s-integration", cloudProvider)
// see if the key already exists
searchResult, apiErr := requestGateway[ingestionKeysSearchResponse](
searchResult, err := requestGateway[integrationtypes.GettableIngestionKeysSearch](
ctx,
gatewayUrl,
licenseKey,
fmt.Sprintf("/v1/workspaces/me/keys/search?name=%s", cloudProviderKeyName),
nil,
ah.opts.Logger,
)
if apiErr != nil {
return "", basemodel.WrapApiError(
apiErr, "couldn't search for cloudprovider ingestion key",
)
if err != nil {
return "", err
}
if searchResult.Status != "success" {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't search for cloudprovider ingestion key: status: %s, error: %s",
return "", errors.NewInternalf(
errors.CodeInternal,
"couldn't search for cloud provider ingestion key: status: %s, error: %s",
searchResult.Status, searchResult.Error,
))
}
for _, k := range searchResult.Data {
if k.Name == cloudProviderKeyName {
if len(k.Value) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", basemodel.InternalError(fmt.Errorf(
"ingestion keys search response not as expected",
))
}
return k.Value, nil
}
}
zap.L().Info(
"no existing ingestion key found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
)
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
map[string]any{
"name": cloudProviderKeyName,
"tags": []string{"integration", cloudProvider},
},
)
if apiErr != nil {
return "", basemodel.WrapApiError(
apiErr, "couldn't create cloudprovider ingestion key",
)
}
for _, k := range searchResult.Data {
if k.Name != cloudProviderKeyName {
continue
}
if len(k.Value) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", errors.NewInternalf(errors.CodeInternal, "ingestion keys search response not as expected")
}
return k.Value, nil
}
ah.opts.Logger.InfoContext(
ctx,
"no existing ingestion key found for cloud integration, creating a new one",
slog.String("cloudProvider", cloudProvider.String()),
)
createKeyResult, err := requestGateway[integrationtypes.GettableCreateIngestionKey](
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
map[string]any{
"name": cloudProviderKeyName,
"tags": []string{"integration", cloudProvider.String()},
},
ah.opts.Logger,
)
if err != nil {
return "", err
}
if createKeyResult.Status != "success" {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloudprovider ingestion key: status: %s, error: %s",
return "", errors.NewInternalf(
errors.CodeInternal,
"couldn't create cloud provider ingestion key: status: %s, error: %s",
createKeyResult.Status, createKeyResult.Error,
))
)
}
ingestionKey := createKeyResult.Data.Value
if len(ingestionKey) < 1 {
ingestionKeyString := createKeyResult.Data.Value
if len(ingestionKeyString) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", basemodel.InternalError(fmt.Errorf(
return "", errors.NewInternalf(errors.CodeInternal,
"ingestion key creation response not as expected",
))
)
}
return ingestionKey, nil
return ingestionKeyString, nil
}
func requestGateway[ResponseType any](
ctx context.Context, gatewayUrl string, licenseKey string, path string, payload any,
) (*ResponseType, *basemodel.ApiError) {
ctx context.Context, gatewayUrl, licenseKey, path string, payload any, logger *slog.Logger,
) (*ResponseType, error) {
baseUrl := strings.TrimSuffix(gatewayUrl, "/")
reqUrl := fmt.Sprintf("%s%s", baseUrl, path)
@@ -335,13 +287,12 @@ func requestGateway[ResponseType any](
"X-Consumer-Groups": "ns:default",
}
return requestAndParseResponse[ResponseType](ctx, reqUrl, headers, payload)
return requestAndParseResponse[ResponseType](ctx, reqUrl, headers, payload, logger)
}
func requestAndParseResponse[ResponseType any](
ctx context.Context, url string, headers map[string]string, payload any,
) (*ResponseType, *basemodel.ApiError) {
ctx context.Context, url string, headers map[string]string, payload any, logger *slog.Logger,
) (*ResponseType, error) {
reqMethod := http.MethodGet
var reqBody io.Reader
if payload != nil {
@@ -349,18 +300,14 @@ func requestAndParseResponse[ResponseType any](
bodyJson, err := json.Marshal(payload)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't serialize request payload to JSON: %w", err,
))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't marshal payload")
}
reqBody = bytes.NewBuffer([]byte(bodyJson))
reqBody = bytes.NewBuffer(bodyJson)
}
req, err := http.NewRequestWithContext(ctx, reqMethod, url, reqBody)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't prepare request: %w", err,
))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't create req")
}
for k, v := range headers {
@@ -373,23 +320,26 @@ func requestAndParseResponse[ResponseType any](
response, err := client.Do(req)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't make request: %w", err))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't make req")
}
defer response.Body.Close()
defer func() {
err = response.Body.Close()
if err != nil {
logger.ErrorContext(ctx, "couldn't close response body", "error", err)
}
}()
respBody, err := io.ReadAll(response.Body)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't read response: %w", err))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read response body")
}
var resp ResponseType
err = json.Unmarshal(respBody, &resp)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't unmarshal gateway response into %T", resp,
))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't unmarshal response body")
}
return &resp, nil

View File

@@ -37,7 +37,6 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
@@ -121,13 +120,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
)
}
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
)
}
// ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
signoz.SQLStore,
@@ -161,11 +153,11 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
RulesManager: rm,
UsageManager: usageManager,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
GatewayUrl: config.Gateway.URL.String(),
GlobalConfig: config.Global,
Logger: signoz.Instrumentation.Logger(),
}
apiHandler, err := api.NewAPIHandler(apiOpts, signoz, config)

View File

@@ -2,11 +2,7 @@
* ESLint Configuration for SigNoz Frontend
*/
module.exports = {
ignorePatterns: [
'src/parser/*.ts',
'scripts/update-registry.js',
'scripts/generate-permissions-type.js',
],
ignorePatterns: ['src/parser/*.ts', 'scripts/update-registry.js'],
env: {
browser: true,
es2021: true,

View File

@@ -19,8 +19,6 @@ 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

@@ -19,8 +19,7 @@
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.js"
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
},
"engines": {
"node": ">=16.15.0"
@@ -215,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.13",
"@types/react-syntax-highlighter": "15.5.7",
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",

View File

@@ -1,13 +0,0 @@
#!/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

@@ -1,199 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const axios = require('axios');
const PERMISSIONS_TYPE_FILE = path.join(
__dirname,
'../src/hooks/useAuthZ/permissions.type.ts',
);
const SIGNOZ_INTEGRATION_IMAGE = 'signoz:integration';
const LOCAL_BACKEND_URL = 'http://localhost:8080';
function log(message) {
console.log(`[generate-permissions-type] ${message}`);
}
function getBackendUrlFromDocker() {
try {
const output = execSync(
`docker ps --filter "ancestor=${SIGNOZ_INTEGRATION_IMAGE}" --format "{{.Ports}}"`,
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
).trim();
if (!output) {
return null;
}
const portMatch = output.match(/0\.0\.0\.0:(\d+)->8080\/tcp/);
if (portMatch) {
return `http://localhost:${portMatch[1]}`;
}
const ipv6Match = output.match(/:::(\d+)->8080\/tcp/);
if (ipv6Match) {
return `http://localhost:${ipv6Match[1]}`;
}
} catch (err) {
log(`Warning: Could not get port from docker: ${err.message}`);
}
return null;
}
async function checkBackendHealth(url, maxAttempts = 3, delayMs = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await axios.get(`${url}/api/v1/health`, {
timeout: 5000,
validateStatus: (status) => status === 200,
});
return true;
} catch (err) {
if (attempt < maxAttempts) {
await new Promise((r) => setTimeout(r, delayMs));
}
}
}
return false;
}
async function discoverBackendUrl() {
const dockerUrl = getBackendUrlFromDocker();
if (dockerUrl) {
log(`Found ${SIGNOZ_INTEGRATION_IMAGE} container, trying ${dockerUrl}...`);
if (await checkBackendHealth(dockerUrl)) {
log(`Backend found at ${dockerUrl} (from py-test-setup)`);
return dockerUrl;
}
log(`Backend at ${dockerUrl} is not responding`);
}
log(`Trying local backend at ${LOCAL_BACKEND_URL}...`);
if (await checkBackendHealth(LOCAL_BACKEND_URL)) {
log(`Backend found at ${LOCAL_BACKEND_URL}`);
return LOCAL_BACKEND_URL;
}
return null;
}
async function fetchResources(backendUrl) {
log('Fetching resources from API...');
const resourcesUrl = `${backendUrl}/api/v1/authz/resources`;
const { data: response } = await axios.get(resourcesUrl);
return response;
}
function transformResponse(apiResponse) {
if (!apiResponse.data) {
throw new Error('Invalid API response: missing data field');
}
const { resources, relations } = apiResponse.data;
return {
status: apiResponse.status || 'success',
data: {
resources: resources,
relations: relations,
},
};
}
function generateTypeScriptFile(data) {
const resourcesStr = data.data.resources
.map(
(r) =>
`\t\t\t{\n\t\t\t\tname: '${r.name}',\n\t\t\t\ttype: '${r.type}',\n\t\t\t}`,
)
.join(',\n');
const relationsStr = Object.entries(data.data.relations)
.map(
([type, relations]) =>
`\t\t\t${type}: [${relations.map((r) => `'${r}'`).join(', ')}]`,
)
.join(',\n');
return `// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY scripts/generate-permissions-type
export default {
\tstatus: '${data.status}',
\tdata: {
\t\tresources: [
${resourcesStr}
\t\t],
\t\trelations: {
${relationsStr}
\t\t},
\t},
} as const;
`;
}
async function main() {
try {
log('Starting permissions type generation...');
const backendUrl = await discoverBackendUrl();
if (!backendUrl) {
console.error('\n' + '='.repeat(80));
console.error('ERROR: No running SigNoz backend found!');
console.error('='.repeat(80));
console.error(
'\nThe permissions type generator requires a running SigNoz backend.',
);
console.error('\nFor local development, start the backend with:');
console.error(' make go-run-enterprise');
console.error(
'\nFor CI or integration testing, start the test environment with:',
);
console.error(' make py-test-setup');
console.error(
'\nIf running in CI and seeing this error, check that the py-test-setup',
);
console.error('step completed successfully before this step runs.');
console.error('='.repeat(80) + '\n');
process.exit(1);
}
log('Fetching resources...');
const apiResponse = await fetchResources(backendUrl);
log('Transforming response...');
const transformed = transformResponse(apiResponse);
log('Generating TypeScript file...');
const content = generateTypeScriptFile(transformed);
log(`Writing to ${PERMISSIONS_TYPE_FILE}...`);
fs.writeFileSync(PERMISSIONS_TYPE_FILE, content, 'utf8');
const rootDir = path.join(__dirname, '../..');
const relativePath = path.relative(
path.join(rootDir, 'frontend'),
PERMISSIONS_TYPE_FILE,
);
log('Linting generated file...');
execSync(`cd frontend && yarn eslint --fix ${relativePath}`, {
cwd: rootDir,
stdio: 'inherit',
});
log('Successfully generated permissions.type.ts');
} catch (error) {
log(`Error: ${error.message}`);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = { main };

View File

@@ -1,35 +0,0 @@
#!/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

@@ -1,973 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
CreateServiceAccount201,
CreateServiceAccountKey201,
CreateServiceAccountKeyPathParameters,
DeleteServiceAccountPathParameters,
GetServiceAccount200,
GetServiceAccountPathParameters,
ListServiceAccountKeys200,
ListServiceAccountKeysPathParameters,
ListServiceAccounts200,
RenderErrorResponseDTO,
RevokeServiceAccountKeyPathParameters,
ServiceaccounttypesPostableFactorAPIKeyDTO,
ServiceaccounttypesPostableServiceAccountDTO,
ServiceaccounttypesUpdatableFactorAPIKeyDTO,
ServiceaccounttypesUpdatableServiceAccountDTO,
ServiceaccounttypesUpdatableServiceAccountStatusDTO,
UpdateServiceAccountKeyPathParameters,
UpdateServiceAccountPathParameters,
UpdateServiceAccountStatusPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists the service accounts for an organisation
* @summary List service accounts
*/
export const listServiceAccounts = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListServiceAccounts200>({
url: `/api/v1/service_accounts`,
method: 'GET',
signal,
});
};
export const getListServiceAccountsQueryKey = () => {
return [`/api/v1/service_accounts`] as const;
};
export const getListServiceAccountsQueryOptions = <
TData = Awaited<ReturnType<typeof listServiceAccounts>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServiceAccounts>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListServiceAccountsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listServiceAccounts>>
> = ({ signal }) => listServiceAccounts(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listServiceAccounts>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListServiceAccountsQueryResult = NonNullable<
Awaited<ReturnType<typeof listServiceAccounts>>
>;
export type ListServiceAccountsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List service accounts
*/
export function useListServiceAccounts<
TData = Awaited<ReturnType<typeof listServiceAccounts>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServiceAccounts>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListServiceAccountsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List service accounts
*/
export const invalidateListServiceAccounts = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListServiceAccountsQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a service account
* @summary Create service account
*/
export const createServiceAccount = (
serviceaccounttypesPostableServiceAccountDTO: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccount201>({
url: `/api/v1/service_accounts`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesPostableServiceAccountDTO,
signal,
});
};
export const getCreateServiceAccountMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccount>>,
TError,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccount>>,
TError,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
TContext
> => {
const mutationKey = ['createServiceAccount'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createServiceAccount>>,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> }
> = (props) => {
const { data } = props ?? {};
return createServiceAccount(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateServiceAccountMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccount>>
>;
export type CreateServiceAccountMutationBody = BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
export type CreateServiceAccountMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create service account
*/
export const useCreateServiceAccount = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccount>>,
TError,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createServiceAccount>>,
TError,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
TContext
> => {
const mutationOptions = getCreateServiceAccountMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes an existing service account
* @summary Deletes a service account
*/
export const deleteServiceAccount = ({
id,
}: DeleteServiceAccountPathParameters) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}`,
method: 'DELETE',
});
};
export const getDeleteServiceAccountMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccount>>,
TError,
{ pathParams: DeleteServiceAccountPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccount>>,
TError,
{ pathParams: DeleteServiceAccountPathParameters },
TContext
> => {
const mutationKey = ['deleteServiceAccount'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteServiceAccount>>,
{ pathParams: DeleteServiceAccountPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteServiceAccount(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteServiceAccountMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccount>>
>;
export type DeleteServiceAccountMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Deletes a service account
*/
export const useDeleteServiceAccount = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccount>>,
TError,
{ pathParams: DeleteServiceAccountPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteServiceAccount>>,
TError,
{ pathParams: DeleteServiceAccountPathParameters },
TContext
> => {
const mutationOptions = getDeleteServiceAccountMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint gets an existing service account
* @summary Gets a service account
*/
export const getServiceAccount = (
{ id }: GetServiceAccountPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetServiceAccount200>({
url: `/api/v1/service_accounts/${id}`,
method: 'GET',
signal,
});
};
export const getGetServiceAccountQueryKey = ({
id,
}: GetServiceAccountPathParameters) => {
return [`/api/v1/service_accounts/${id}`] as const;
};
export const getGetServiceAccountQueryOptions = <
TData = Awaited<ReturnType<typeof getServiceAccount>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetServiceAccountPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccount>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetServiceAccountQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getServiceAccount>>
> = ({ signal }) => getServiceAccount({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccount>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetServiceAccountQueryResult = NonNullable<
Awaited<ReturnType<typeof getServiceAccount>>
>;
export type GetServiceAccountQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Gets a service account
*/
export function useGetServiceAccount<
TData = Awaited<ReturnType<typeof getServiceAccount>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetServiceAccountPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccount>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceAccountQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Gets a service account
*/
export const invalidateGetServiceAccount = async (
queryClient: QueryClient,
{ id }: GetServiceAccountPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceAccountQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates an existing service account
* @summary Updates a service account
*/
export const updateServiceAccount = (
{ id }: UpdateServiceAccountPathParameters,
serviceaccounttypesUpdatableServiceAccountDTO: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesUpdatableServiceAccountDTO,
});
};
export const getUpdateServiceAccountMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccount>>,
TError,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccount>>,
TError,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
},
TContext
> => {
const mutationKey = ['updateServiceAccount'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateServiceAccount>>,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateServiceAccount(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateServiceAccountMutationResult = NonNullable<
Awaited<ReturnType<typeof updateServiceAccount>>
>;
export type UpdateServiceAccountMutationBody = BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
export type UpdateServiceAccountMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Updates a service account
*/
export const useUpdateServiceAccount = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccount>>,
TError,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateServiceAccount>>,
TError,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
},
TContext
> => {
const mutationOptions = getUpdateServiceAccountMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint lists the service account keys
* @summary List service account keys
*/
export const listServiceAccountKeys = (
{ id }: ListServiceAccountKeysPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListServiceAccountKeys200>({
url: `/api/v1/service_accounts/${id}/keys`,
method: 'GET',
signal,
});
};
export const getListServiceAccountKeysQueryKey = ({
id,
}: ListServiceAccountKeysPathParameters) => {
return [`/api/v1/service_accounts/${id}/keys`] as const;
};
export const getListServiceAccountKeysQueryOptions = <
TData = Awaited<ReturnType<typeof listServiceAccountKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: ListServiceAccountKeysPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServiceAccountKeys>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListServiceAccountKeysQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listServiceAccountKeys>>
> = ({ signal }) => listServiceAccountKeys({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listServiceAccountKeys>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListServiceAccountKeysQueryResult = NonNullable<
Awaited<ReturnType<typeof listServiceAccountKeys>>
>;
export type ListServiceAccountKeysQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List service account keys
*/
export function useListServiceAccountKeys<
TData = Awaited<ReturnType<typeof listServiceAccountKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: ListServiceAccountKeysPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServiceAccountKeys>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListServiceAccountKeysQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List service account keys
*/
export const invalidateListServiceAccountKeys = async (
queryClient: QueryClient,
{ id }: ListServiceAccountKeysPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListServiceAccountKeysQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint creates a service account key
* @summary Create a service account key
*/
export const createServiceAccountKey = (
{ id }: CreateServiceAccountKeyPathParameters,
serviceaccounttypesPostableFactorAPIKeyDTO: BodyType<ServiceaccounttypesPostableFactorAPIKeyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccountKey201>({
url: `/api/v1/service_accounts/${id}/keys`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesPostableFactorAPIKeyDTO,
signal,
});
};
export const getCreateServiceAccountKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountKey>>,
TError,
{
pathParams: CreateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesPostableFactorAPIKeyDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountKey>>,
TError,
{
pathParams: CreateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesPostableFactorAPIKeyDTO>;
},
TContext
> => {
const mutationKey = ['createServiceAccountKey'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createServiceAccountKey>>,
{
pathParams: CreateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesPostableFactorAPIKeyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return createServiceAccountKey(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateServiceAccountKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountKey>>
>;
export type CreateServiceAccountKeyMutationBody = BodyType<ServiceaccounttypesPostableFactorAPIKeyDTO>;
export type CreateServiceAccountKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create a service account key
*/
export const useCreateServiceAccountKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountKey>>,
TError,
{
pathParams: CreateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesPostableFactorAPIKeyDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createServiceAccountKey>>,
TError,
{
pathParams: CreateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesPostableFactorAPIKeyDTO>;
},
TContext
> => {
const mutationOptions = getCreateServiceAccountKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint revokes an existing service account key
* @summary Revoke a service account key
*/
export const revokeServiceAccountKey = ({
id,
fid,
}: RevokeServiceAccountKeyPathParameters) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}/keys/${fid}`,
method: 'DELETE',
});
};
export const getRevokeServiceAccountKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof revokeServiceAccountKey>>,
TError,
{ pathParams: RevokeServiceAccountKeyPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof revokeServiceAccountKey>>,
TError,
{ pathParams: RevokeServiceAccountKeyPathParameters },
TContext
> => {
const mutationKey = ['revokeServiceAccountKey'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof revokeServiceAccountKey>>,
{ pathParams: RevokeServiceAccountKeyPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return revokeServiceAccountKey(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type RevokeServiceAccountKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof revokeServiceAccountKey>>
>;
export type RevokeServiceAccountKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Revoke a service account key
*/
export const useRevokeServiceAccountKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof revokeServiceAccountKey>>,
TError,
{ pathParams: RevokeServiceAccountKeyPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof revokeServiceAccountKey>>,
TError,
{ pathParams: RevokeServiceAccountKeyPathParameters },
TContext
> => {
const mutationOptions = getRevokeServiceAccountKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint updates an existing service account key
* @summary Updates a service account key
*/
export const updateServiceAccountKey = (
{ id, fid }: UpdateServiceAccountKeyPathParameters,
serviceaccounttypesUpdatableFactorAPIKeyDTO: BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}/keys/${fid}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesUpdatableFactorAPIKeyDTO,
});
};
export const getUpdateServiceAccountKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountKey>>,
TError,
{
pathParams: UpdateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountKey>>,
TError,
{
pathParams: UpdateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>;
},
TContext
> => {
const mutationKey = ['updateServiceAccountKey'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateServiceAccountKey>>,
{
pathParams: UpdateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateServiceAccountKey(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateServiceAccountKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof updateServiceAccountKey>>
>;
export type UpdateServiceAccountKeyMutationBody = BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>;
export type UpdateServiceAccountKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Updates a service account key
*/
export const useUpdateServiceAccountKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountKey>>,
TError,
{
pathParams: UpdateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateServiceAccountKey>>,
TError,
{
pathParams: UpdateServiceAccountKeyPathParameters;
data: BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>;
},
TContext
> => {
const mutationOptions = getUpdateServiceAccountKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint updates an existing service account status
* @summary Updates a service account status
*/
export const updateServiceAccountStatus = (
{ id }: UpdateServiceAccountStatusPathParameters,
serviceaccounttypesUpdatableServiceAccountStatusDTO: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}/status`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesUpdatableServiceAccountStatusDTO,
});
};
export const getUpdateServiceAccountStatusMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
TError,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
TError,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
},
TContext
> => {
const mutationKey = ['updateServiceAccountStatus'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateServiceAccountStatus(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateServiceAccountStatusMutationResult = NonNullable<
Awaited<ReturnType<typeof updateServiceAccountStatus>>
>;
export type UpdateServiceAccountStatusMutationBody = BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
export type UpdateServiceAccountStatusMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Updates a service account status
*/
export const useUpdateServiceAccountStatus = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
TError,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
TError,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
},
TContext
> => {
const mutationOptions = getUpdateServiceAccountStatusMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -2090,154 +2090,6 @@ export interface RoletypesRoleDTO {
updatedAt?: Date;
}
export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type integer
* @minimum 0
*/
expires_at: number;
/**
* @type string
*/
id: string;
/**
* @type string
*/
key: string;
/**
* @type string
* @format date-time
*/
last_used: Date;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
service_account_id: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO {
/**
* @type string
*/
id: string;
/**
* @type string
*/
key: string;
}
export interface ServiceaccounttypesPostableFactorAPIKeyDTO {
/**
* @type integer
* @minimum 0
*/
expires_at: number;
/**
* @type string
*/
name: string;
}
export interface ServiceaccounttypesPostableServiceAccountDTO {
/**
* @type string
*/
email: string;
/**
* @type string
*/
name: string;
/**
* @type array
*/
roles: string[];
}
export interface ServiceaccounttypesServiceAccountDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
email: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgID: string;
/**
* @type array
*/
roles: string[];
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
/**
* @type integer
* @minimum 0
*/
expires_at: number;
/**
* @type string
*/
name: string;
}
export interface ServiceaccounttypesUpdatableServiceAccountDTO {
/**
* @type string
*/
email: string;
/**
* @type string
*/
name: string;
/**
* @type array
*/
roles: string[];
}
export interface ServiceaccounttypesUpdatableServiceAccountStatusDTO {
/**
* @type string
*/
status: string;
}
export enum TelemetrytypesFieldContextDTO {
metric = 'metric',
log = 'log',
@@ -3198,78 +3050,6 @@ export type PatchObjectsPathParameters = {
id: string;
relation: string;
};
export type ListServiceAccounts200 = {
/**
* @type array
*/
data: ServiceaccounttypesServiceAccountDTO[];
/**
* @type string
*/
status: string;
};
export type CreateServiceAccount201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteServiceAccountPathParameters = {
id: string;
};
export type GetServiceAccountPathParameters = {
id: string;
};
export type GetServiceAccount200 = {
data: ServiceaccounttypesServiceAccountDTO;
/**
* @type string
*/
status: string;
};
export type UpdateServiceAccountPathParameters = {
id: string;
};
export type ListServiceAccountKeysPathParameters = {
id: string;
};
export type ListServiceAccountKeys200 = {
/**
* @type array
*/
data: ServiceaccounttypesFactorAPIKeyDTO[];
/**
* @type string
*/
status: string;
};
export type CreateServiceAccountKeyPathParameters = {
id: string;
};
export type CreateServiceAccountKey201 = {
data: ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO;
/**
* @type string
*/
status: string;
};
export type RevokeServiceAccountKeyPathParameters = {
id: string;
fid: string;
};
export type UpdateServiceAccountKeyPathParameters = {
id: string;
fid: string;
};
export type UpdateServiceAccountStatusPathParameters = {
id: string;
};
export type ListUsers200 = {
/**
* @type array

View File

@@ -1,321 +0,0 @@
import { ReactElement } from 'react';
import {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ENVIRONMENT } from 'constants/env';
import { BrandedPermission } from 'hooks/useAuthZ/types';
import { buildPermission } from 'hooks/useAuthZ/utils';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import { GuardAuthZ } from './GuardAuthZ';
const BASE_URL = ENVIRONMENT.baseURL || '';
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
function authzMockResponse(
payload: AuthtypesTransactionDTO[],
authorizedByIndex: boolean[],
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
return {
data: payload.map((txn, i) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByIndex[i] ?? false,
})),
status: 'success',
};
}
describe('GuardAuthZ', () => {
const TestChild = (): ReactElement => <div>Protected Content</div>;
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
const ErrorFallback = (error: Error): ReactElement => (
<div>Error occurred: {error.message}</div>
);
const NoPermissionFallback = (_response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => <div>Access denied</div>;
const NoPermissionFallbackWithSuggestions = (response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => (
<div>
Access denied. Required permission: {response.requiredPermissionName}
</div>
);
it('should render children when permission is granted', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
it('should render fallbackOnLoading when loading', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
fallbackOnLoading={<LoadingFallback />}
>
<TestChild />
</GuardAuthZ>,
);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when loading and no fallbackOnLoading provided', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
expect(container.firstChild).toBeNull();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnError when API error occurs', async () => {
const errorMessage = 'Internal Server Error';
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: errorMessage }));
}),
);
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
fallbackOnError={ErrorFallback}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText(/Error occurred:/)).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should pass error object to fallbackOnError function', async () => {
const errorMessage = 'Network request failed';
let receivedError: Error | null = null;
const errorFallbackWithCapture = (error: Error): ReactElement => {
receivedError = error;
return <div>Captured error: {error.message}</div>;
};
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: errorMessage }));
}),
);
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
fallbackOnError={errorFallbackWithCapture}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(receivedError).not.toBeNull();
});
expect(receivedError).toBeInstanceOf(Error);
expect(screen.getByText(/Captured error:/)).toBeInTheDocument();
});
it('should render null when error occurs and no fallbackOnError provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnNoPermissions when permission is denied', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
render(
<GuardAuthZ
relation="update"
object="dashboard:123"
fallbackOnNoPermissions={NoPermissionFallback}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Access denied')).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when permission is denied and no fallbackOnNoPermissions provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
const { container } = render(
<GuardAuthZ relation="update" object="dashboard:123">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when permissions object is null', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({ data: [], status: 'success' }));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should pass requiredPermissionName to fallbackOnNoPermissions', async () => {
const permission = buildPermission('update', 'dashboard:123');
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
render(
<GuardAuthZ
relation="update"
object="dashboard:123"
fallbackOnNoPermissions={NoPermissionFallbackWithSuggestions}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(
screen.getByText(/Access denied. Required permission:/),
).toBeInTheDocument();
});
expect(
screen.getAllByText(
new RegExp(permission.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
).length,
).toBeGreaterThan(0);
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should handle different relation and object combinations', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const { rerender } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
rerender(
<GuardAuthZ relation="delete" object="dashboard:456">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
});

View File

@@ -1,50 +0,0 @@
import { ReactElement } from 'react';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
} from 'hooks/useAuthZ/types';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { buildPermission } from 'hooks/useAuthZ/utils';
export type GuardAuthZProps<R extends AuthZRelation> = {
children: ReactElement;
relation: R;
object: AuthZObject<R>;
fallbackOnLoading?: JSX.Element;
fallbackOnError?: (error: Error) => JSX.Element;
fallbackOnNoPermissions?: (response: {
requiredPermissionName: BrandedPermission;
}) => JSX.Element;
};
export function GuardAuthZ<R extends AuthZRelation>({
children,
relation,
object,
fallbackOnLoading,
fallbackOnError,
fallbackOnNoPermissions,
}: GuardAuthZProps<R>): JSX.Element | null {
const permission = buildPermission<R>(relation, object);
const { permissions, isLoading, error } = useAuthZ([permission]);
if (isLoading) {
return fallbackOnLoading ?? null;
}
if (error) {
return fallbackOnError?.(error) ?? null;
}
if (!permissions?.[permission]?.isGranted) {
return (
fallbackOnNoPermissions?.({
requiredPermissionName: permission,
}) ?? null
);
}
return children;
}

View File

@@ -2,12 +2,13 @@
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

@@ -1,34 +0,0 @@
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,41 +0,0 @@
.guard-authz-error-no-authz {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 24px;
.guard-authz-error-no-authz-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 8px;
max-width: 500px;
}
img {
width: 32px;
height: 32px;
}
h3 {
font-size: 18px;
color: var(--l1-foreground);
line-height: 18px;
}
p {
font-size: 14px;
color: var(--l3-foreground);
line-height: 18px;
span {
background-color: var(--l3-background);
white-space: nowrap;
padding: 0 2px;
}
}
}

View File

@@ -1,472 +0,0 @@
import { ReactElement } from 'react';
import type { RouteComponentProps } from 'react-router-dom';
import {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import { createGuardedRoute } from './createGuardedRoute';
const BASE_URL = ENVIRONMENT.baseURL || '';
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
function authzMockResponse(
payload: AuthtypesTransactionDTO[],
authorizedByIndex: boolean[],
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
return {
data: payload.map((txn, i) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByIndex[i] ?? false,
})),
status: 'success',
};
}
describe('createGuardedRoute', () => {
const TestComponent = ({ testProp }: { testProp: string }): ReactElement => (
<div>Test Component: {testProp}</div>
);
it('should render component when permission is granted', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should substitute route parameters in object string', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:{id}',
);
const mockMatch = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should handle multiple route parameters', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
const txn = payload[0];
const responseData: AuthtypesGettableTransactionDTO[] = [
{
relation: txn.relation,
object: {
resource: {
name: txn.object.resource.name,
type: txn.object.resource.type,
},
selector: '123:456',
},
authorized: true,
},
];
return res(
ctx.status(200),
ctx.json({ data: responseData, status: 'success' }),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'dashboard:{id}:{version}',
);
const mockMatch = {
params: { id: '123', version: '456' },
isExact: true,
path: '/dashboard/:id/:version',
url: '/dashboard/123/456',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should keep placeholder when route parameter is missing', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:{id}',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should render loading fallback when loading', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
expect(screen.getByText('SigNoz')).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render error fallback when API error occurs', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
});
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render no permissions fallback when permission is denied', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'dashboard:{id}',
);
const mockMatch = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
const heading = document.querySelector('h3');
expect(heading).toBeInTheDocument();
expect(heading?.textContent).toMatch(/permission to view/i);
});
expect(screen.getByText('update')).toBeInTheDocument();
expect(screen.getByText('dashboard:123')).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should pass all props to wrapped component', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const ComponentWithMultipleProps = ({
prop1,
prop2,
prop3,
}: {
prop1: string;
prop2: number;
prop3: boolean;
}): ReactElement => (
<div>
{prop1} - {prop2} - {prop3.toString()}
</div>
);
const GuardedComponent = createGuardedRoute(
ComponentWithMultipleProps,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
prop1: 'value1',
prop2: 42,
prop3: true,
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('value1 - 42 - true')).toBeInTheDocument();
});
});
it('should memoize resolved object based on route params', async () => {
let requestCount = 0;
const requestedObjects: string[] = [];
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
requestCount++;
const payload = (await req.json()) as AuthtypesTransactionDTO[];
const obj = payload[0]?.object;
const name = obj?.resource?.name;
const selector = obj?.selector ?? '*';
const objectStr =
obj?.resource?.type === 'metaresources' ? name : `${name}:${selector}`;
requestedObjects.push(objectStr ?? '');
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:{id}',
);
const mockMatch1 = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props1 = {
testProp: 'test-value-1',
match: mockMatch1,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
const { unmount } = render(<GuardedComponent {...props1} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value-1')).toBeInTheDocument();
});
expect(requestCount).toBe(1);
expect(requestedObjects).toContain('dashboard:123');
unmount();
const mockMatch2 = {
params: { id: '456' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/456',
};
const props2 = {
testProp: 'test-value-2',
match: mockMatch2,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props2} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value-2')).toBeInTheDocument();
});
expect(requestCount).toBe(2);
expect(requestedObjects).toContain('dashboard:456');
});
it('should handle different relation types', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'delete',
'dashboard:{id}',
);
const mockMatch = {
params: { id: '789' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/789',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
});

View File

@@ -1,73 +0,0 @@
import { ComponentType, ReactElement, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
} from 'hooks/useAuthZ/types';
import { parsePermission } from 'hooks/useAuthZ/utils';
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import AppLoading from '../AppLoading/AppLoading';
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
import './createGuardedRoute.styles.scss';
const onErrorFallback = (): JSX.Element => <ErrorBoundaryFallback />;
function OnNoPermissionsFallback(response: {
requiredPermissionName: BrandedPermission;
}): ReactElement {
const { relation, object } = parsePermission(response.requiredPermissionName);
return (
<div className="guard-authz-error-no-authz">
<div className="guard-authz-error-no-authz-content">
<img src="/Icons/no-data.svg" alt="No permission" />
<h3>Uh-oh! You dont have permission to view this page.</h3>
<p>
You need the following permission to view this page:
<br />
Relation: <span>{relation}</span>
<br />
Object: <span>{object}</span>
<br />
Ask your SigNoz administrator to grant access.
</p>
</div>
</div>
);
}
// eslint-disable-next-line @typescript-eslint/ban-types
export function createGuardedRoute<P extends object, R extends AuthZRelation>(
Component: ComponentType<P>,
relation: R,
object: AuthZObject<R>,
): ComponentType<P & RouteComponentProps<Record<string, string>>> {
return function GuardedRouteComponent(
props: P & RouteComponentProps<Record<string, string>>,
): ReactElement {
const resolvedObject = useMemo(() => {
const paramPattern = /\{([^}]+)\}/g;
return object.replace(paramPattern, (match, paramName) => {
const paramValue = props.match?.params?.[paramName];
return paramValue !== undefined ? paramValue : match;
}) as AuthZObject<R>;
}, [props.match?.params]);
return (
<GuardAuthZ
relation={relation}
object={resolvedObject}
fallbackOnLoading={<AppLoading />}
fallbackOnError={onErrorFallback}
fallbackOnNoPermissions={(response): ReactElement => (
<OnNoPermissionsFallback {...response} />
)}
>
<Component {...props} />
</GuardAuthZ>
);
};
}

View File

@@ -1,23 +0,0 @@
// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY scripts/generate-permissions-type
export default {
status: 'success',
data: {
resources: [
{
name: 'dashboard',
type: 'metaresource',
},
{
name: 'dashboards',
type: 'metaresources',
},
],
relations: {
create: ['metaresources'],
delete: ['user', 'role', 'organization', 'metaresource'],
list: ['metaresources'],
read: ['user', 'role', 'organization', 'metaresource'],
update: ['user', 'role', 'organization', 'metaresource'],
},
},
} as const;

View File

@@ -1,57 +0,0 @@
import permissionsType from './permissions.type';
import { ObjectSeparator } from './utils';
type PermissionsData = typeof permissionsType.data;
export type Resource = PermissionsData['resources'][number];
export type ResourceName = Resource['name'];
export type ResourceType = Resource['type'];
type RelationsByType = PermissionsData['relations'];
type ResourceTypeMap = {
[K in ResourceName]: Extract<Resource, { name: K }>['type'];
};
type RelationName = keyof RelationsByType;
type ResourcesForRelation<R extends RelationName> = Extract<
Resource,
{ type: RelationsByType[R][number] }
>['name'];
type IsPluralResource<
R extends ResourceName
> = ResourceTypeMap[R] extends 'metaresources' ? true : false;
type ObjectForResource<R extends ResourceName> = R extends infer U
? U extends ResourceName
? IsPluralResource<U> extends true
? U
: `${U}${typeof ObjectSeparator}${string}`
: never
: never;
type RelationToObject<R extends RelationName> = ObjectForResource<
ResourcesForRelation<R>
>;
type AllRelations = RelationName;
export type AuthZRelation = AllRelations;
export type AuthZResource = ResourceName;
export type AuthZObject<R extends AuthZRelation> = RelationToObject<R>;
export type BrandedPermission = string & { __brandedPermission: true };
export type AuthZCheckResponse = Record<
BrandedPermission,
{
isGranted: boolean;
}
>;
export type UseAuthZResult = {
isLoading: boolean;
error: Error | null;
permissions: AuthZCheckResponse | null;
};

View File

@@ -1,496 +0,0 @@
import { ReactElement } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { AllTheProviders } from 'tests/test-utils';
import { BrandedPermission } from './types';
import { useAuthZ } from './useAuthZ';
import { buildPermission } from './utils';
const BASE_URL = ENVIRONMENT.baseURL || '';
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
function authzMockResponse(
payload: AuthtypesTransactionDTO[],
authorizedByIndex: boolean[],
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
return {
data: payload.map((txn, i) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByIndex[i] ?? false,
})),
status: 'success',
};
}
const wrapper = ({ children }: { children: ReactElement }): ReactElement => (
<AllTheProviders>{children}</AllTheProviders>
);
describe('useAuthZ', () => {
it('should fetch and return permissions successfully', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const expectedResponse = {
[permission1]: {
isGranted: true,
},
[permission2]: {
isGranted: false,
},
};
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, [true, false])),
);
}),
);
const { result } = renderHook(() => useAuthZ([permission1, permission2]), {
wrapper,
});
expect(result.current.isLoading).toBe(true);
expect(result.current.permissions).toBeNull();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.error).toBeNull();
expect(result.current.permissions).toEqual(expectedResponse);
});
it('should handle API errors', async () => {
const permission = buildPermission('read', 'dashboard:*');
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const { result } = renderHook(() => useAuthZ([permission]), {
wrapper,
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.error).not.toBeNull();
expect(result.current.permissions).toBeNull();
});
it('should refetch when permissions array changes', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
requestCount++;
const payload = await req.json();
if (payload.length === 1) {
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}
const authorized = payload.map(
(txn: { relation: string }) =>
txn.relation === 'read' || txn.relation === 'delete',
);
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, authorized)),
);
}),
);
const { result, rerender } = renderHook<
ReturnType<typeof useAuthZ>,
BrandedPermission[]
>((permissions) => useAuthZ(permissions), {
wrapper,
initialProps: [permission1],
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(requestCount).toBe(1);
expect(result.current.permissions).toEqual({
[permission1]: {
isGranted: true,
},
});
rerender([permission1, permission2, permission3]);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(requestCount).toBe(2);
expect(result.current.permissions).toEqual({
[permission1]: {
isGranted: true,
},
[permission2]: {
isGranted: false,
},
[permission3]: {
isGranted: true,
},
});
});
it('should not refetch when permissions array order changes but content is the same', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
let requestCount = 0;
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
requestCount++;
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, [true, false])),
);
}),
);
const { result, rerender } = renderHook<
ReturnType<typeof useAuthZ>,
BrandedPermission[]
>((permissions) => useAuthZ(permissions), {
wrapper,
initialProps: [permission1, permission2],
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(requestCount).toBe(1);
rerender([permission2, permission1]);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(requestCount).toBe(1);
});
it('should handle empty permissions array', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({ data: [], status: 'success' }));
}),
);
const { result } = renderHook(() => useAuthZ([]), {
wrapper,
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.permissions).toEqual({});
});
it('should send correct payload format to API', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
let receivedPayload: any = null;
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
receivedPayload = await req.json();
return res(
ctx.status(200),
ctx.json(authzMockResponse(receivedPayload, [true, false])),
);
}),
);
const { result } = renderHook(() => useAuthZ([permission1, permission2]), {
wrapper,
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(receivedPayload).toHaveLength(2);
expect(receivedPayload[0]).toMatchObject({
relation: 'read',
object: {
resource: { name: 'dashboard', type: 'metaresource' },
selector: '*',
},
});
expect(receivedPayload[1]).toMatchObject({
relation: 'update',
object: {
resource: { name: 'dashboard', type: 'metaresource' },
selector: '123',
},
});
});
it('should batch multiple hooks into single flight request', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
const receivedPayloads: any[] = [];
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
requestCount++;
const payload = await req.json();
receivedPayloads.push(payload);
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, [true, false, true])),
);
}),
);
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
wrapper,
});
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
wrapper,
});
const { result: result3 } = renderHook(() => useAuthZ([permission3]), {
wrapper,
});
await waitFor(
() => {
expect(result1.current.isLoading).toBe(false);
expect(result2.current.isLoading).toBe(false);
expect(result3.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(1);
expect(receivedPayloads).toHaveLength(1);
expect(receivedPayloads[0]).toHaveLength(3);
expect(receivedPayloads[0][0]).toMatchObject({
relation: 'read',
object: {
resource: { name: 'dashboard', type: 'metaresource' },
selector: '*',
},
});
expect(receivedPayloads[0][1]).toMatchObject({
relation: 'update',
object: { resource: { name: 'dashboard' }, selector: '123' },
});
expect(receivedPayloads[0][2]).toMatchObject({
relation: 'delete',
object: { resource: { name: 'dashboard' }, selector: '456' },
});
expect(result1.current.permissions).toEqual({
[permission1]: { isGranted: true },
});
expect(result2.current.permissions).toEqual({
[permission2]: { isGranted: false },
});
expect(result3.current.permissions).toEqual({
[permission3]: { isGranted: true },
});
});
it('should create separate batches for calls after single flight window', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
const receivedPayloads: any[] = [];
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
requestCount++;
const payload = await req.json();
receivedPayloads.push(payload);
const authorized = payload.length === 1 ? [true] : [false, true];
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, authorized)),
);
}),
);
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
wrapper,
});
await waitFor(
() => {
expect(result1.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(1);
expect(receivedPayloads[0]).toHaveLength(1);
await new Promise((resolve) => setTimeout(resolve, 100));
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
wrapper,
});
const { result: result3 } = renderHook(() => useAuthZ([permission3]), {
wrapper,
});
await waitFor(
() => {
expect(result2.current.isLoading).toBe(false);
expect(result3.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(2);
expect(receivedPayloads).toHaveLength(2);
expect(receivedPayloads[1]).toHaveLength(2);
expect(receivedPayloads[1][0]).toMatchObject({
relation: 'update',
object: { resource: { name: 'dashboard' }, selector: '123' },
});
expect(receivedPayloads[1][1]).toMatchObject({
relation: 'delete',
object: { resource: { name: 'dashboard' }, selector: '456' },
});
});
it('should map permissions correctly when API returns response out of order', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
const reversed = [...payload].reverse();
const authorizedByReversed = [true, false, true];
return res(
ctx.status(200),
ctx.json({
data: reversed.map((txn: any, i: number) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByReversed[i],
})),
status: 'success',
}),
);
}),
);
const { result } = renderHook(
() => useAuthZ([permission1, permission2, permission3]),
{ wrapper },
);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.permissions).toEqual({
[permission1]: { isGranted: true },
[permission2]: { isGranted: false },
[permission3]: { isGranted: true },
});
});
it('should not leak state between separate batches', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
let requestCount = 0;
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
requestCount++;
const payload = await req.json();
const authorized = payload.map(
(txn: { relation: string }) => txn.relation === 'read',
);
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, authorized)),
);
}),
);
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
wrapper,
});
await waitFor(
() => {
expect(result1.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(1);
expect(result1.current.permissions).toEqual({
[permission1]: { isGranted: true },
});
await new Promise((resolve) => setTimeout(resolve, 100));
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
wrapper,
});
await waitFor(
() => {
expect(result2.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(2);
expect(result1.current.permissions).toEqual({
[permission1]: { isGranted: true },
});
expect(result2.current.permissions).toEqual({
[permission2]: { isGranted: false },
});
expect(result1.current.permissions).not.toHaveProperty(permission2);
expect(result2.current.permissions).not.toHaveProperty(permission1);
});
});

View File

@@ -1,129 +0,0 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { authzCheck } from 'api/generated/services/authz';
import type {
AuthtypesObjectDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AuthZCheckResponse, BrandedPermission, UseAuthZResult } from './types';
import {
gettableTransactionToPermission,
permissionToTransactionDto,
} from './utils';
let ctx: Promise<AuthZCheckResponse> | null;
let pendingPermissions: BrandedPermission[] = [];
const SINGLE_FLIGHT_WAIT_TIME_MS = 50;
const AUTHZ_CACHE_TIME = 20_000;
function dispatchPermission(
permission: BrandedPermission,
): Promise<AuthZCheckResponse> {
pendingPermissions.push(permission);
if (!ctx) {
let resolve: (v: AuthZCheckResponse) => void, reject: (reason?: any) => void;
ctx = new Promise<AuthZCheckResponse>((r, re) => {
resolve = r;
reject = re;
});
setTimeout(() => {
const copiedPermissions = pendingPermissions.slice();
pendingPermissions = [];
ctx = null;
fetchManyPermissions(copiedPermissions).then(resolve).catch(reject);
}, SINGLE_FLIGHT_WAIT_TIME_MS);
}
return ctx;
}
async function fetchManyPermissions(
permissions: BrandedPermission[],
): Promise<AuthZCheckResponse> {
const payload: AuthtypesTransactionDTO[] = permissions.map((permission) => {
const dto = permissionToTransactionDto(permission);
const object: AuthtypesObjectDTO = {
resource: {
name: dto.object.resource.name,
type: dto.object.resource.type,
},
selector: dto.object.selector,
};
return { relation: dto.relation, object };
});
const { data } = await authzCheck(payload);
const fromApi = (data ?? []).reduce<AuthZCheckResponse>((acc, item) => {
const permission = gettableTransactionToPermission(item);
acc[permission] = { isGranted: !!item.authorized };
return acc;
}, {} as AuthZCheckResponse);
return permissions.reduce<AuthZCheckResponse>((acc, permission) => {
acc[permission] = fromApi[permission] ?? { isGranted: false };
return acc;
}, {} as AuthZCheckResponse);
}
export function useAuthZ(permissions: BrandedPermission[]): UseAuthZResult {
const queryResults = useQueries(
permissions.map((permission) => {
return {
queryKey: ['authz', permission],
cacheTime: AUTHZ_CACHE_TIME,
refetchOnMount: false,
refetchIntervalInBackground: false,
refetchOnWindowFocus: false,
refetchOnReconnect: true,
queryFn: async (): Promise<AuthZCheckResponse> => {
const response = await dispatchPermission(permission);
return {
[permission]: {
isGranted: response[permission].isGranted,
},
};
},
};
}),
);
const isLoading = useMemo(() => queryResults.some((q) => q.isLoading), [
queryResults,
]);
const error = useMemo(
() =>
!isLoading
? (queryResults.find((q) => !!q.error)?.error as Error) || null
: null,
[isLoading, queryResults],
);
const data = useMemo(() => {
if (isLoading || error) {
return null;
}
return queryResults.reduce((acc, q) => {
if (!q.data) {
return acc;
}
for (const [key, value] of Object.entries(q.data)) {
acc[key as BrandedPermission] = value;
}
return acc;
}, {} as AuthZCheckResponse);
}, [isLoading, error, queryResults]);
return {
isLoading,
error,
permissions: data ?? null,
};
}

View File

@@ -1,85 +0,0 @@
import { AuthtypesTransactionDTO } from '../../api/generated/services/sigNoz.schemas';
import permissionsType from './permissions.type';
import {
AuthZObject,
AuthZRelation,
AuthZResource,
BrandedPermission,
ResourceName,
ResourceType,
} from './types';
export const PermissionSeparator = '||__||';
export const ObjectSeparator = ':';
export function buildPermission<R extends AuthZRelation>(
relation: R,
object: AuthZObject<R>,
): BrandedPermission {
return `${relation}${PermissionSeparator}${object}` as BrandedPermission;
}
export function buildObjectString(
resource: AuthZResource,
objectId: string,
): `${AuthZResource}${typeof ObjectSeparator}${string}` {
return `${resource}${ObjectSeparator}${objectId}` as const;
}
export function parsePermission(
permission: BrandedPermission,
): {
relation: AuthZRelation;
object: string;
} {
const [relation, object] = permission.split(PermissionSeparator);
return { relation: relation as AuthZRelation, object };
}
const resourceNameToType = permissionsType.data.resources.reduce((acc, r) => {
acc[r.name] = r.type;
return acc;
}, {} as Record<ResourceName, ResourceType>);
export function permissionToTransactionDto(
permission: BrandedPermission,
): AuthtypesTransactionDTO {
const { relation, object: objectStr } = parsePermission(permission);
const directType = resourceNameToType[objectStr as ResourceName];
if (directType === 'metaresources') {
return {
relation,
object: {
resource: { name: objectStr, type: directType },
selector: '*',
},
};
}
const [resourceName, selector] = objectStr.split(ObjectSeparator);
const type =
resourceNameToType[resourceName as ResourceName] ?? 'metaresource';
return {
relation,
object: {
resource: { name: resourceName, type },
selector: selector || '*',
},
};
}
export function gettableTransactionToPermission(
item: AuthtypesTransactionDTO,
): BrandedPermission {
const {
relation,
object: { resource, selector },
} = item;
const resourceName = String(resource.name);
const selectorStr = typeof selector === 'string' ? selector : '*';
const objectStr =
resource.type === 'metaresources'
? resourceName
: `${resourceName}${ObjectSeparator}${selectorStr}`;
return `${relation}${PermissionSeparator}${objectStr}` as BrandedPermission;
}

View File

@@ -8,7 +8,6 @@ import {
createSetCursorHandler,
createSetLegendHandler,
createSetSeriesHandler,
getPlot,
isScrollEventInPlot,
updatePlotVisibility,
updateWindowSize,
@@ -54,7 +53,7 @@ export default function TooltipPlugin({
const [viewState, setState] = useState<TooltipViewState>(
createInitialViewState,
);
const { hasPlot, isHovering, isPinned, contents, style } = viewState;
const { plot, isHovering, isPinned, contents, style } = viewState;
/**
* Merge a partial view update into the current React state.
@@ -73,25 +72,12 @@ export default function TooltipPlugin({
layoutRef.current?.observer.disconnect();
layoutRef.current = createLayoutObserver(layoutRef);
/**
* Plot lifecycle and GC: viewState uses hasPlot (boolean), not the plot
* reference; clearPlotReferences runs in cleanup so
* detached canvases can be garbage collected.
*/
// Controller holds the mutable interaction state for this tooltip
// instance. It is intentionally *not* React state so uPlot hooks
// and DOM listeners can update it freely without triggering a
// render on every mouse move.
const controller: TooltipControllerState = createInitialControllerState();
/**
* Clear plot references so detached canvases can be garbage collected.
*/
const clearPlotReferences = (): void => {
controller.plot = null;
updateState({ hasPlot: false });
};
const syncTooltipWithDashboard = syncMode === DashboardCursorSync.Tooltip;
// Enable uPlot's built-in cursor sync when requested so that
@@ -124,10 +110,9 @@ export default function TooltipPlugin({
// Lock uPlot's internal cursor when the tooltip is pinned so
// subsequent mouse moves do not move the crosshair.
function updateCursorLock(): void {
const plot = getPlot(controller);
if (plot) {
if (controller.plot) {
// @ts-ignore uPlot cursor lock is not working as expected
plot.cursor._lock = controller.pinned;
controller.plot.cursor._lock = controller.pinned;
}
}
@@ -157,9 +142,8 @@ export default function TooltipPlugin({
const isPinnedBeforeDismiss = controller.pinned;
controller.pinned = false;
controller.hoverActive = false;
const plot = getPlot(controller);
if (plot) {
plot.setCursor({ left: -10, top: -10 });
if (controller.plot) {
controller.plot.setCursor({ left: -10, top: -10 });
}
scheduleRender(isPinnedBeforeDismiss);
}
@@ -167,12 +151,11 @@ export default function TooltipPlugin({
// Build the React node to be rendered inside the tooltip by
// delegating to the caller-provided `render` function.
function createTooltipContents(): React.ReactNode {
const plot = getPlot(controller);
if (!controller.hoverActive || !plot) {
if (!controller.hoverActive || !controller.plot) {
return null;
}
return renderRef.current({
uPlotInstance: plot,
uPlotInstance: controller.plot,
dataIndexes: controller.seriesIndexes,
seriesIndex: controller.focusedSeriesIndex,
isPinned: controller.pinned,
@@ -257,13 +240,9 @@ export default function TooltipPlugin({
// When pinning is enabled, a click on the plot overlay while
// hovering converts the transient tooltip into a pinned one.
// Uses getPlot(controller) to avoid closing over u (plot), which
// would retain the plot and detached canvases across unmounts.
const handleUPlotOverClick = (event: MouseEvent): void => {
const plot = getPlot(controller);
const handleUPlotOverClick = (u: uPlot, event: MouseEvent): void => {
if (
plot &&
event.target === plot.over &&
event.target === u.over &&
controller.hoverActive &&
!controller.pinned &&
controller.focusedSeriesIndex != null
@@ -281,9 +260,10 @@ export default function TooltipPlugin({
// on the controller and optionally attach the pinning handler.
const handleInit = (u: uPlot): void => {
controller.plot = u;
updateState({ hasPlot: true });
updateState({ plot: u });
if (canPinTooltip) {
overClickHandler = handleUPlotOverClick;
overClickHandler = (event: MouseEvent): void =>
handleUPlotOverClick(u, event);
u.over.addEventListener('click', overClickHandler);
}
};
@@ -319,6 +299,7 @@ export default function TooltipPlugin({
const handleSetCursor = createSetCursorHandler(ctx);
handleWindowResize();
const removeReadyHook = config.addHook('ready', (): void =>
updatePlotVisibility(controller),
);
@@ -344,20 +325,16 @@ export default function TooltipPlugin({
removeSetSeriesHook();
removeSetLegendHook();
removeSetCursorHook();
if (overClickHandler) {
const plot = getPlot(controller);
if (plot) {
plot.over.removeEventListener('click', overClickHandler);
}
if (controller.plot && overClickHandler) {
controller.plot.over.removeEventListener('click', overClickHandler);
overClickHandler = null;
}
clearPlotReferences();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
useLayoutEffect((): void => {
if (!hasPlot || !layoutRef.current) {
if (!plot || !layoutRef.current) {
return;
}
const layout = layoutRef.current;
@@ -372,9 +349,9 @@ export default function TooltipPlugin({
layout.width = 0;
layout.height = 0;
}
}, [isHovering, hasPlot]);
}, [isHovering, plot]);
if (!hasPlot) {
if (!plot) {
return null;
}

View File

@@ -10,11 +10,6 @@ import {
const WINDOW_OFFSET = 16;
/** Get the plot instance from the controller; returns null if never set or cleared. */
export function getPlot(controller: TooltipControllerState): uPlot | null {
return controller.plot ?? null;
}
export function createInitialControllerState(): TooltipControllerState {
return {
plot: null,
@@ -51,13 +46,12 @@ export function updateWindowSize(controller: TooltipControllerState): void {
* This is used to decide if a synced tooltip should be shown at all.
*/
export function updatePlotVisibility(controller: TooltipControllerState): void {
const plot = getPlot(controller);
if (!plot) {
if (!controller.plot) {
controller.plotWithinViewport = false;
return;
}
controller.plotWithinViewport = isPlotInViewport(
plot.rect,
controller.plot.rect,
controller.windowWidth,
controller.windowHeight,
);
@@ -72,11 +66,10 @@ export function isScrollEventInPlot(
event: Event,
controller: TooltipControllerState,
): boolean {
const plot = getPlot(controller);
return (
event.target instanceof Node &&
plot !== null &&
event.target.contains(plot.root)
controller.plot !== null &&
event.target.contains(controller.plot.root)
);
}
@@ -172,12 +165,11 @@ export function createSetLegendHandler(
): (u: uPlot) => void {
return (u: uPlot): void => {
const { controller } = ctx;
const plot = getPlot(controller);
if (!plot?.cursor?.idxs) {
if (!controller.plot?.cursor?.idxs) {
return;
}
const newSeriesIndexes = plot.cursor.idxs.slice();
const newSeriesIndexes = controller.plot.cursor.idxs.slice();
const isAnySeriesActive = newSeriesIndexes.some((v, i) => i > 0 && v != null);
const previousCursorDrivenBySync = controller.cursorDrivenBySync;

View File

@@ -18,8 +18,7 @@ export enum DashboardCursorSync {
}
export interface TooltipViewState {
/** Whether a plot instance exists; plot reference is in controller, not state. */
hasPlot?: boolean;
plot?: uPlot | null;
style: Partial<CSSProperties>;
isHovering: boolean;
isPinned: boolean;

View File

@@ -123,7 +123,7 @@ export function createInitialViewState(): TooltipViewState {
isHovering: false,
isPinned: false,
contents: null,
hasPlot: false,
plot: null,
dismiss: (): void => {},
};
}

View File

@@ -6205,10 +6205,10 @@
"@types/history" "^4.7.11"
"@types/react" "*"
"@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==
"@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==
dependencies:
"@types/react" "*"

View File

@@ -18,7 +18,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -49,7 +48,6 @@ type provider struct {
authzHandler authz.Handler
zeusHandler zeus.Handler
querierHandler querier.Handler
serviceAccountHandler serviceaccount.Handler
}
func NewFactory(
@@ -71,7 +69,6 @@ func NewFactory(
authzHandler authz.Handler,
zeusHandler zeus.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -96,7 +93,6 @@ func NewFactory(
authzHandler,
zeusHandler,
querierHandler,
serviceAccountHandler,
)
})
}
@@ -123,7 +119,6 @@ func newProvider(
authzHandler authz.Handler,
zeusHandler zeus.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -148,7 +143,6 @@ func newProvider(
authzHandler: authzHandler,
zeusHandler: zeusHandler,
querierHandler: querierHandler,
serviceAccountHandler: serviceAccountHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -229,10 +223,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addServiceAccountRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -1,184 +0,0 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/gorilla/mux"
)
func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Create), handler.OpenAPIDef{
ID: "CreateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Create service account",
Description: "This endpoint creates a service account",
Request: new(serviceaccounttypes.PostableServiceAccount),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.List), handler.OpenAPIDef{
ID: "ListServiceAccounts",
Tags: []string{"serviceaccount"},
Summary: "List service accounts",
Description: "This endpoint lists the service accounts for an organisation",
Request: nil,
RequestContentType: "",
Response: make([]*serviceaccounttypes.ServiceAccount, 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/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Get), handler.OpenAPIDef{
ID: "GetServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Gets a service account",
Description: "This endpoint gets an existing service account",
Request: nil,
RequestContentType: "",
Response: new(serviceaccounttypes.ServiceAccount),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Update), handler.OpenAPIDef{
ID: "UpdateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account",
Description: "This endpoint updates an existing service account",
Request: new(serviceaccounttypes.UpdatableServiceAccount),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/status", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.UpdateStatus), handler.OpenAPIDef{
ID: "UpdateServiceAccountStatus",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account status",
Description: "This endpoint updates an existing service account status",
Request: new(serviceaccounttypes.UpdatableServiceAccountStatus),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Delete), handler.OpenAPIDef{
ID: "DeleteServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Deletes a service account",
Description: "This endpoint deletes an existing service account",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.CreateFactorAPIKey), handler.OpenAPIDef{
ID: "CreateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Create a service account key",
Description: "This endpoint creates a service account key",
Request: new(serviceaccounttypes.PostableFactorAPIKey),
RequestContentType: "",
Response: new(serviceaccounttypes.GettableFactorAPIKeyWithKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.ListFactorAPIKey), handler.OpenAPIDef{
ID: "ListServiceAccountKeys",
Tags: []string{"serviceaccount"},
Summary: "List service account keys",
Description: "This endpoint lists the service account keys",
Request: nil,
RequestContentType: "",
Response: make([]*serviceaccounttypes.FactorAPIKey, 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/service_accounts/{id}/keys/{fid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.UpdateFactorAPIKey), handler.OpenAPIDef{
ID: "UpdateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account key",
Description: "This endpoint updates an existing service account key",
Request: new(serviceaccounttypes.UpdatableFactorAPIKey),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.RevokeFactorAPIKey), handler.OpenAPIDef{
ID: "RevokeServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Revoke a service account key",
Description: "This endpoint revokes an existing service account key",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -62,17 +62,14 @@ type AuthZ interface {
// Lists all the roles for the organization filtered by name
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*roletypes.Role, error)
// Lists all the roles for the organization filtered by ids
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*roletypes.Role, error)
// Grants a role to the subject based on role name.
Grant(context.Context, valuer.UUID, []string, string) error
Grant(context.Context, valuer.UUID, string, string) error
// Revokes a granted role from the subject based on role name.
Revoke(context.Context, valuer.UUID, []string, string) error
Revoke(context.Context, valuer.UUID, string, string) error
// Changes the granted role for the subject based on role name.
ModifyGrant(context.Context, valuer.UUID, []string, []string, string) error
ModifyGrant(context.Context, valuer.UUID, string, string, string) error
// Bootstrap the managed roles.
CreateManagedRoles(context.Context, valuer.UUID, []*roletypes.Role) error

View File

@@ -96,39 +96,6 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID,
return nil, err
}
if len(roles) != len(names) {
return nil, store.sqlstore.WrapNotFoundErrf(
nil,
roletypes.ErrCodeRoleNotFound,
"not all roles found for the provided names: %v", names,
)
}
return roles, nil
}
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&roles).
Where("org_id = ?", orgID).
Where("id IN (?)", bun.In(ids)).
Scan(ctx)
if err != nil {
return nil, err
}
if len(roles) != len(ids) {
return nil, store.sqlstore.WrapNotFoundErrf(
nil,
roletypes.ErrCodeRoleNotFound,
"not all roles found for the provided ids: %v", ids,
)
}
return roles, nil
}

View File

@@ -114,46 +114,28 @@ func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.
return roles, nil
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
storableRoles, err := provider.store.ListByOrgIDAndIDs(ctx, orgID, ids)
if err != nil {
return nil, err
}
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storable := range storableRoles {
roles[idx] = roletypes.NewRoleFromStorableRole(storable)
}
return roles, nil
}
func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, names []string, subject string) error {
selectors := make([]authtypes.Selector, len(names))
for idx, name := range names {
selectors[idx] = authtypes.MustNewSelector(authtypes.TypeRole, name)
}
func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, name string, subject string) error {
tuples, err := authtypes.TypeableRole.Tuples(
subject,
authtypes.RelationAssignee,
selectors,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, name),
},
orgID,
)
if err != nil {
return err
}
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) ModifyGrant(ctx context.Context, orgID valuer.UUID, existingRoleNames []string, updatedRoleNames []string, subject string) error {
err := provider.Revoke(ctx, orgID, existingRoleNames, subject)
func (provider *provider) ModifyGrant(ctx context.Context, orgID valuer.UUID, existingRoleName string, updatedRoleName string, subject string) error {
err := provider.Revoke(ctx, orgID, existingRoleName, subject)
if err != nil {
return err
}
err = provider.Grant(ctx, orgID, updatedRoleNames, subject)
err = provider.Grant(ctx, orgID, updatedRoleName, subject)
if err != nil {
return err
}
@@ -161,16 +143,13 @@ func (provider *provider) ModifyGrant(ctx context.Context, orgID valuer.UUID, ex
return nil
}
func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names []string, subject string) error {
selectors := make([]authtypes.Selector, len(names))
for idx, name := range names {
selectors[idx] = authtypes.MustNewSelector(authtypes.TypeRole, name)
}
func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, name string, subject string) error {
tuples, err := authtypes.TypeableRole.Tuples(
subject,
authtypes.RelationAssignee,
selectors,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, name),
},
orgID,
)
if err != nil {
@@ -199,7 +178,7 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID,
}
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
return provider.Grant(ctx, orgID, []string{roletypes.SigNozAdminRoleName}, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
return provider.Grant(ctx, orgID, roletypes.SigNozAdminRoleName, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
}
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {

View File

@@ -0,0 +1,44 @@
package middleware
import (
"log/slog"
"net/http"
"runtime/debug"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
)
// Recovery is a middleware that recovers from panics, logs the panic,
// and returns a 500 Internal Server Error.
type Recovery struct {
logger *slog.Logger
}
// NewRecovery creates a new Recovery middleware.
func NewRecovery(logger *slog.Logger) Wrapper {
return &Recovery{
logger: logger.With("pkg", "http-middleware-recovery"),
}
}
// Wrap is the middleware handler.
func (m *Recovery) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
m.logger.ErrorContext(
r.Context(),
"panic recovered",
"err", err, "stack", string(debug.Stack()),
)
render.Error(w, errors.NewInternalf(
errors.CodeInternal, "internal server error",
))
}
}()
next.ServeHTTP(w, r)
})
}

View File

@@ -1,62 +0,0 @@
package cloudintegration
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
GetName() cloudintegrationtypes.CloudProviderType
// AgentCheckIn is called by agent to heartbeat and get latest config in response.
AgentCheckIn(ctx context.Context, req *cloudintegrationtypes.PostableAgentCheckInPayload) (any, error)
GenerateConnectionParams(ctx context.Context) (*cloudintegrationtypes.GettableCloudIntegrationConnectionParams, error)
// GenerateConnectionArtifact generates cloud provider specific connection information, client side handles how this information is shown
GenerateConnectionArtifact(ctx context.Context, req *cloudintegrationtypes.PostableConnectionArtifact) (any, error)
// GetAccountStatus returns agent connection status for a cloud integration account
GetAccountStatus(ctx context.Context, orgID, accountID string) (*cloudintegrationtypes.GettableAccountStatus, error)
// ListConnectedAccounts lists accounts where agent is connected
ListConnectedAccounts(ctx context.Context, orgID string) (*cloudintegrationtypes.GettableConnectedAccountsList, error)
// LIstServices return list of services for a cloud provider attached with the accountID. This just returns a summary
ListServices(ctx context.Context, orgID string, accountID *string) (any, error) // returns either GettableAWSServices or GettableAzureServices
// GetServiceDetails returns service definition details for a serviceId. This returns config and other details required to show in service details page on client.
GetServiceDetails(ctx context.Context, req *cloudintegrationtypes.GetServiceDetailsReq) (any, error)
// GetDashboard returns dashboard json for a give cloud integration service dashboard.
// this only returns the dashboard when account is connected and service is enabled
GetDashboard(ctx context.Context, id string, orgID valuer.UUID) (*dashboardtypes.Dashboard, error)
// GetAvailableDashboards returns list of available dashboards across all connected cloud integration accounts in the org.
// this list gets added to dashboard list page
GetAvailableDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
// UpdateAccountConfig updates cloud integration account config
UpdateAccountConfig(ctx context.Context, orgId valuer.UUID, accountId string, config []byte) (any, error)
// UpdateServiceConfig updates cloud integration service config
UpdateServiceConfig(ctx context.Context, serviceId string, orgID valuer.UUID, config []byte) (any, error)
// DisconnectAccount soft deletes/removes a cloud integration account.
DisconnectAccount(ctx context.Context, orgID, accountID string) (*cloudintegrationtypes.CloudIntegration, error)
}
type Handler interface {
AgentCheckIn(http.ResponseWriter, *http.Request)
GenerateConnectionParams(http.ResponseWriter, *http.Request)
GenerateConnectionArtifact(http.ResponseWriter, *http.Request)
ListConnectedAccounts(http.ResponseWriter, *http.Request)
GetAccountStatus(http.ResponseWriter, *http.Request)
ListServices(http.ResponseWriter, *http.Request)
GetServiceDetails(http.ResponseWriter, *http.Request)
UpdateAccountConfig(http.ResponseWriter, *http.Request)
UpdateServiceConfig(http.ResponseWriter, *http.Request)
DisconnectAccount(http.ResponseWriter, *http.Request)
}

View File

@@ -12,7 +12,6 @@ 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"
)
@@ -25,7 +24,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 []savedviewtypes.SavedView
var views []types.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)
@@ -77,7 +76,7 @@ func (module *module) CreateView(ctx context.Context, orgID string, view v3.Save
createBy := claims.Email
updatedBy := claims.Email
dbView := savedviewtypes.SavedView{
dbView := types.SavedView{
TimeAuditable: types.TimeAuditable{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -106,7 +105,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 savedviewtypes.SavedView
var view types.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")
@@ -147,7 +146,7 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
updatedBy := claims.Email
_, err = module.sqlstore.BunDB().NewUpdate().
Model(&savedviewtypes.SavedView{}).
Model(&types.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()).
@@ -161,7 +160,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(&savedviewtypes.SavedView{}).
Model(&types.SavedView{}).
Where("id = ?", uuid.StringValue()).
Where("org_id = ?", orgID).
Exec(ctx)
@@ -172,7 +171,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 := []*savedviewtypes.SavedView{}
savedViews := []*types.SavedView{}
err := module.
sqlstore.
@@ -185,5 +184,5 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return nil, err
}
return savedviewtypes.NewStatsFromSavedViews(savedViews), nil
return types.NewStatsFromSavedViews(savedViews), nil
}

View File

@@ -1,335 +0,0 @@
package implserviceaccount
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
type handler struct {
module serviceaccount.Module
}
func NewHandler(module serviceaccount.Module) serviceaccount.Handler {
return &handler{module: module}
}
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
req := new(serviceaccounttypes.PostableServiceAccount)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
serviceAccount := serviceaccounttypes.NewServiceAccount(req.Name, req.Email, req.Roles, serviceaccounttypes.StatusActive, valuer.MustNewUUID(claims.OrgID))
err = handler.module.Create(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, types.Identifiable{ID: serviceAccount.ID})
}
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
serviceAccount, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, serviceAccount)
}
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
serviceAccounts, err := handler.module.List(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, serviceAccounts)
}
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
req := new(serviceaccounttypes.UpdatableServiceAccount)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
serviceAccount, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
serviceAccount.Update(req.Name, req.Email, req.Roles)
err = handler.module.Update(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) UpdateStatus(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
req := new(serviceaccounttypes.UpdatableServiceAccountStatus)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
serviceAccount, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
serviceAccount.UpdateStatus(req.Status)
err = handler.module.UpdateStatus(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.Delete(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) CreateFactorAPIKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
req := new(serviceaccounttypes.PostableFactorAPIKey)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
// this takes care of checking the existence of service account and the org constraint.
serviceAccount, err := handler.module.GetWithoutRoles(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
factorAPIKey, err := serviceAccount.NewFactorAPIKey(req.Name, req.ExpiresAt)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.CreateFactorAPIKey(ctx, factorAPIKey)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, serviceaccounttypes.NewGettableFactorAPIKeyWithKey(factorAPIKey.ID, factorAPIKey.Key))
}
func (handler *handler) ListFactorAPIKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
serviceAccount, err := handler.module.GetWithoutRoles(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
factorAPIKeys, err := handler.module.ListFactorAPIKey(ctx, serviceAccount.ID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, serviceaccounttypes.NewGettableFactorAPIKeys(factorAPIKeys))
}
func (handler *handler) UpdateFactorAPIKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
factorAPIKeyID, err := valuer.NewUUID(mux.Vars(r)["fid"])
if err != nil {
render.Error(rw, err)
return
}
req := new(serviceaccounttypes.UpdatableFactorAPIKey)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
serviceAccount, err := handler.module.GetWithoutRoles(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
factorAPIKey, err := handler.module.GetFactorAPIKey(ctx, serviceAccount.ID, factorAPIKeyID)
if err != nil {
render.Error(rw, err)
return
}
factorAPIKey.Update(req.Name, req.ExpiresAt)
err = handler.module.UpdateFactorAPIKey(ctx, serviceAccount.ID, factorAPIKey)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) RevokeFactorAPIKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
factorAPIKeyID, err := valuer.NewUUID(mux.Vars(r)["fid"])
if err != nil {
render.Error(rw, err)
return
}
serviceAccount, err := handler.module.GetWithoutRoles(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.RevokeFactorAPIKey(ctx, serviceAccount.ID, factorAPIKeyID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@@ -1,351 +0,0 @@
package implserviceaccount
import (
"context"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store serviceaccounttypes.Store
authz authz.AuthZ
emailing emailing.Emailing
settings factory.ScopedProviderSettings
}
func NewModule(store serviceaccounttypes.Store, authz authz.AuthZ, emailing emailing.Emailing, providerSettings factory.ProviderSettings) serviceaccount.Module {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount")
return &module{store: store, authz: authz, emailing: emailing, settings: settings}
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, serviceAccount *serviceaccounttypes.ServiceAccount) error {
// validates the presence of all roles passed in the create request
roles, err := module.authz.ListByOrgIDAndNames(ctx, orgID, serviceAccount.Roles)
if err != nil {
return err
}
// authz actions cannot run in sql transactions
err = module.authz.Grant(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
if err != nil {
return err
}
storableServiceAccount := serviceaccounttypes.NewStorableServiceAccount(serviceAccount)
storableServiceAccountRoles := serviceaccounttypes.NewStorableServiceAccountRoles(serviceAccount.ID, roles)
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
err := module.store.Create(ctx, storableServiceAccount)
if err != nil {
return err
}
err = module.store.CreateServiceAccountRoles(ctx, storableServiceAccountRoles)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*serviceaccounttypes.ServiceAccount, error) {
storableServiceAccount, err := module.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
// did the orchestration on application layer instead of DB as the ORM also does it anyways for many to many tables.
storableServiceAccountRoles, err := module.store.GetServiceAccountRoles(ctx, id)
if err != nil {
return nil, err
}
roleIDs := make([]valuer.UUID, len(storableServiceAccountRoles))
for idx, sar := range storableServiceAccountRoles {
roleIDs[idx] = valuer.MustNewUUID(sar.RoleID)
}
roles, err := module.authz.ListByOrgIDAndIDs(ctx, orgID, roleIDs)
if err != nil {
return nil, err
}
rolesNames, err := serviceaccounttypes.NewRolesFromStorableServiceAccountRoles(storableServiceAccountRoles, roles)
if err != nil {
return nil, err
}
serviceAccount := serviceaccounttypes.NewServiceAccountFromStorables(storableServiceAccount, rolesNames)
return serviceAccount, nil
}
func (module *module) GetWithoutRoles(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*serviceaccounttypes.ServiceAccount, error) {
storableServiceAccount, err := module.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
// passing []string{} (not nil to prevent panics) roles as the function isn't supposed to put roles.
serviceAccount := serviceaccounttypes.NewServiceAccountFromStorables(storableServiceAccount, []string{})
return serviceAccount, nil
}
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*serviceaccounttypes.ServiceAccount, error) {
storableServiceAccounts, err := module.store.List(ctx, orgID)
if err != nil {
return nil, err
}
storableServiceAccountRoles, err := module.store.ListServiceAccountRolesByOrgID(ctx, orgID)
if err != nil {
return nil, err
}
// convert the service account roles to structured data
saIDToRoleIDs, roleIDs := serviceaccounttypes.GetUniqueRolesAndServiceAccountMapping(storableServiceAccountRoles)
roles, err := module.authz.ListByOrgIDAndIDs(ctx, orgID, roleIDs)
if err != nil {
return nil, err
}
// fill in the role fetched data back to service account
serviceAccounts := serviceaccounttypes.NewServiceAccountsFromRoles(storableServiceAccounts, roles, saIDToRoleIDs)
return serviceAccounts, nil
}
func (module *module) Update(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
serviceAccount, err := module.Get(ctx, orgID, input.ID)
if err != nil {
return err
}
roles, err := module.authz.ListByOrgIDAndNames(ctx, orgID, input.Roles)
if err != nil {
return err
}
// gets the role diff if any to modify grants.
grants, revokes := serviceAccount.PatchRoles(input)
err = module.authz.ModifyGrant(ctx, orgID, revokes, grants, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
if err != nil {
return err
}
storableServiceAccountRoles := serviceaccounttypes.NewStorableServiceAccountRoles(serviceAccount.ID, roles)
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
err := module.store.Update(ctx, orgID, serviceaccounttypes.NewStorableServiceAccount(input))
if err != nil {
return err
}
// delete all the service account roles and create new rather than diff here.
err = module.store.DeleteServiceAccountRoles(ctx, input.ID)
if err != nil {
return err
}
err = module.store.CreateServiceAccountRoles(ctx, storableServiceAccountRoles)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
func (module *module) UpdateStatus(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
serviceAccount, err := module.Get(ctx, orgID, input.ID)
if err != nil {
return err
}
if input.Status == serviceAccount.Status {
return nil
}
switch input.Status {
case serviceaccounttypes.StatusActive:
err := module.activateServiceAccount(ctx, orgID, input)
if err != nil {
return err
}
case serviceaccounttypes.StatusDisabled:
err := module.disableServiceAccount(ctx, orgID, input)
if err != nil {
return err
}
}
return nil
}
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
serviceAccount, err := module.Get(ctx, orgID, id)
if err != nil {
return err
}
// revoke from authz first as this cannot run in sql transaction
err = module.authz.Revoke(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
if err != nil {
return err
}
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
err := module.store.DeleteServiceAccountRoles(ctx, serviceAccount.ID)
if err != nil {
return err
}
err = module.store.RevokeAllFactorAPIKeys(ctx, serviceAccount.ID)
if err != nil {
return err
}
err = module.store.Delete(ctx, serviceAccount.OrgID, serviceAccount.ID)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
func (module *module) CreateFactorAPIKey(ctx context.Context, factorAPIKey *serviceaccounttypes.FactorAPIKey) error {
storableFactorAPIKey := serviceaccounttypes.NewStorableFactorAPIKey(factorAPIKey)
err := module.store.CreateFactorAPIKey(ctx, storableFactorAPIKey)
if err != nil {
return err
}
serviceAccount, err := module.store.GetByID(ctx, factorAPIKey.ServiceAccountID)
if err != nil {
return err
}
if err := module.emailing.SendHTML(ctx, serviceAccount.Email, "New API Key created for your SigNoz account", emailtypes.TemplateNameAPIKeyEvent, map[string]any{
"Name": serviceAccount.Name,
"KeyName": factorAPIKey.Name,
"KeyID": factorAPIKey.ID.String(),
"KeyCreatedAt": factorAPIKey.CreatedAt.String(),
}); err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
}
return nil
}
func (module *module) GetFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, id valuer.UUID) (*serviceaccounttypes.FactorAPIKey, error) {
storableFactorAPIKey, err := module.store.GetFactorAPIKey(ctx, serviceAccountID, id)
if err != nil {
return nil, err
}
return serviceaccounttypes.NewFactorAPIKeyFromStorable(storableFactorAPIKey), nil
}
func (module *module) ListFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID) ([]*serviceaccounttypes.FactorAPIKey, error) {
storables, err := module.store.ListFactorAPIKey(ctx, serviceAccountID)
if err != nil {
return nil, err
}
return serviceaccounttypes.NewFactorAPIKeyFromStorables(storables), nil
}
func (module *module) UpdateFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, factorAPIKey *serviceaccounttypes.FactorAPIKey) error {
return module.store.UpdateFactorAPIKey(ctx, serviceAccountID, serviceaccounttypes.NewStorableFactorAPIKey(factorAPIKey))
}
func (module *module) RevokeFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, id valuer.UUID) error {
factorAPIKey, err := module.GetFactorAPIKey(ctx, serviceAccountID, id)
if err != nil {
return err
}
err = module.store.RevokeFactorAPIKey(ctx, serviceAccountID, id)
if err != nil {
return err
}
serviceAccount, err := module.store.GetByID(ctx, serviceAccountID)
if err != nil {
return err
}
if err := module.emailing.SendHTML(ctx, serviceAccount.Email, "API Key revoked for your SigNoz account", emailtypes.TemplateNameAPIKeyEvent, map[string]any{
"Name": serviceAccount.Name,
"KeyName": factorAPIKey.Name,
"KeyID": factorAPIKey.ID.String(),
"KeyCreatedAt": factorAPIKey.CreatedAt.String(),
}); err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
}
return nil
}
func (module *module) disableServiceAccount(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
err := module.authz.Revoke(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.String(), orgID, nil))
if err != nil {
return err
}
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
// revoke all the API keys on disable
err := module.store.RevokeAllFactorAPIKeys(ctx, input.ID)
if err != nil {
return err
}
// update the status but do not delete the role mappings as we will reuse them on activation.
err = module.Update(ctx, orgID, input)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
func (module *module) activateServiceAccount(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
err := module.authz.Grant(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.String(), orgID, nil))
if err != nil {
return err
}
err = module.Update(ctx, orgID, input)
if err != nil {
return err
}
return nil
}

View File

@@ -1,282 +0,0 @@
package implserviceaccount
import (
"context"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) serviceaccounttypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, storable *serviceaccounttypes.StorableServiceAccount) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(storable).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, serviceaccounttypes.ErrCodeServiceAccountAlreadyExists, "service account with id: %s already exists", storable.ID)
}
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*serviceaccounttypes.StorableServiceAccount, error) {
storable := new(serviceaccounttypes.StorableServiceAccount)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(storable).
Where("id = ?", id).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeServiceAccountNotFound, "service account with id: %s doesn't exist in org: %s", id, orgID)
}
return storable, nil
}
func (store *store) GetByID(ctx context.Context, id valuer.UUID) (*serviceaccounttypes.StorableServiceAccount, error) {
storable := new(serviceaccounttypes.StorableServiceAccount)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(storable).
Where("id = ?", id).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeServiceAccountNotFound, "service account with id: %s doesn't exist", id)
}
return storable, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*serviceaccounttypes.StorableServiceAccount, error) {
storables := make([]*serviceaccounttypes.StorableServiceAccount, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&storables).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, err
}
return storables, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, storable *serviceaccounttypes.StorableServiceAccount) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(storable).
WherePK().
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(serviceaccounttypes.StorableServiceAccount)).
Where("id = ?", id).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (store *store) CreateServiceAccountRoles(ctx context.Context, storables []*serviceaccounttypes.StorableServiceAccountRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(&storables).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, serviceaccounttypes.ErrCodeServiceAccountRoleAlreadyExists, "duplicate role assignments for service account")
}
return nil
}
func (store *store) GetServiceAccountRoles(ctx context.Context, id valuer.UUID) ([]*serviceaccounttypes.StorableServiceAccountRole, error) {
storables := make([]*serviceaccounttypes.StorableServiceAccountRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&storables).
Where("service_account_id = ?", id).
Scan(ctx)
if err != nil {
// no need to wrap not found here as this is many to many table
return nil, err
}
return storables, nil
}
func (store *store) ListServiceAccountRolesByOrgID(ctx context.Context, orgID valuer.UUID) ([]*serviceaccounttypes.StorableServiceAccountRole, error) {
storables := make([]*serviceaccounttypes.StorableServiceAccountRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&storables).
Join("JOIN service_account").
JoinOn("service_account.id = service_account_role.service_account_id").
Where("service_account.org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, err
}
return storables, nil
}
func (store *store) DeleteServiceAccountRoles(ctx context.Context, id valuer.UUID) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(serviceaccounttypes.StorableServiceAccountRole)).
Where("service_account_id = ?", id).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (store *store) CreateFactorAPIKey(ctx context.Context, storable *serviceaccounttypes.StorableFactorAPIKey) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(storable).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, serviceaccounttypes.ErrCodeServiceAccountFactorAPIKeyAlreadyExists, "api key with name: %s already exists for service account: %s", storable.Name, storable.ServiceAccountID)
}
return nil
}
func (store *store) GetFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, id valuer.UUID) (*serviceaccounttypes.StorableFactorAPIKey, error) {
storable := new(serviceaccounttypes.StorableFactorAPIKey)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(storable).
Where("id = ?", id).
Where("service_account_id = ?", serviceAccountID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeServiceAccounFactorAPIKeytNotFound, "api key with id: %s doesn't exist for service account: %s", id, serviceAccountID)
}
return storable, nil
}
func (store *store) ListFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID) ([]*serviceaccounttypes.StorableFactorAPIKey, error) {
storables := make([]*serviceaccounttypes.StorableFactorAPIKey, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&storables).
Where("service_account_id = ?", serviceAccountID).
Scan(ctx)
if err != nil {
return nil, err
}
return storables, nil
}
func (store *store) UpdateFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, storable *serviceaccounttypes.StorableFactorAPIKey) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(storable).
Where("service_account_id = ?", serviceAccountID).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (store *store) RevokeFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, id valuer.UUID) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(serviceaccounttypes.StorableFactorAPIKey)).
Where("service_account_id = ?", serviceAccountID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (store *store) RevokeAllFactorAPIKeys(ctx context.Context, serviceAccountID valuer.UUID) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(serviceaccounttypes.StorableFactorAPIKey)).
Where("service_account_id = ?", serviceAccountID).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (store *store) RunInTx(ctx context.Context, cb func(context.Context) error) error {
return store.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
return cb(ctx)
})
}

View File

@@ -1,69 +0,0 @@
package serviceaccount
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
// Creates a new service account for an organization.
Create(context.Context, valuer.UUID, *serviceaccounttypes.ServiceAccount) error
// Gets a service account by id.
Get(context.Context, valuer.UUID, valuer.UUID) (*serviceaccounttypes.ServiceAccount, error)
// Gets a service account by id without fetching roles.
GetWithoutRoles(context.Context, valuer.UUID, valuer.UUID) (*serviceaccounttypes.ServiceAccount, error)
// List all service accounts for an organization.
List(context.Context, valuer.UUID) ([]*serviceaccounttypes.ServiceAccount, error)
// Updates an existing service account
Update(context.Context, valuer.UUID, *serviceaccounttypes.ServiceAccount) error
// Updates an existing service account status
UpdateStatus(context.Context, valuer.UUID, *serviceaccounttypes.ServiceAccount) error
// Deletes an existing service account by id
Delete(context.Context, valuer.UUID, valuer.UUID) error
// Creates a new API key for a service account
CreateFactorAPIKey(context.Context, *serviceaccounttypes.FactorAPIKey) error
// Gets a factor API key by id
GetFactorAPIKey(context.Context, valuer.UUID, valuer.UUID) (*serviceaccounttypes.FactorAPIKey, error)
// Lists all the API keys for a service account
ListFactorAPIKey(context.Context, valuer.UUID) ([]*serviceaccounttypes.FactorAPIKey, error)
// Updates an existing API key for a service account
UpdateFactorAPIKey(context.Context, valuer.UUID, *serviceaccounttypes.FactorAPIKey) error
// Revokes an existing API key for a service account
RevokeFactorAPIKey(context.Context, valuer.UUID, valuer.UUID) error
}
type Handler interface {
Create(http.ResponseWriter, *http.Request)
Get(http.ResponseWriter, *http.Request)
List(http.ResponseWriter, *http.Request)
Update(http.ResponseWriter, *http.Request)
UpdateStatus(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
CreateFactorAPIKey(http.ResponseWriter, *http.Request)
ListFactorAPIKey(http.ResponseWriter, *http.Request)
UpdateFactorAPIKey(http.ResponseWriter, *http.Request)
RevokeFactorAPIKey(http.ResponseWriter, *http.Request)
}

View File

@@ -463,7 +463,7 @@ func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) {
return
}
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) {
if slices.Contains(integrationtypes.CloudIntegrationUserEmails, createdByUser.Email) {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
return
}
@@ -508,7 +508,7 @@ func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
return
}
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) {
if slices.Contains(integrationtypes.CloudIntegrationUserEmails, createdByUser.Email) {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
return
}

View File

@@ -174,8 +174,8 @@ func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID)
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)
// since assign is idempotant multiple calls to assign won't cause issues in case of retries.
err := module.authz.Grant(ctx, input.OrgID, []string{roletypes.MustGetSigNozManagedRoleFromExistingRole(input.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
// since assign is idempotent multiple calls to assign won't cause issues in case of retries.
err := module.authz.Grant(ctx, input.OrgID, roletypes.MustGetSigNozManagedRoleFromExistingRole(input.Role), authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
if err != nil {
return err
}
@@ -237,8 +237,8 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
if user.Role != "" && user.Role != existingUser.Role {
err = m.authz.ModifyGrant(ctx,
orgID,
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
roletypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role),
roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role),
authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil),
)
if err != nil {
@@ -280,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(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(user.Email.String())) {
if slices.Contains(integrationtypes.CloudIntegrationUserEmails, user.Email) {
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
}
@@ -294,8 +294,8 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
}
// since revoke is idempotant multiple calls to revoke won't cause issues in case of retries
err = module.authz.Revoke(ctx, orgID, []string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
// since revoke is idempotent multiple calls to revoke won't cause issues in case of retries
err = module.authz.Revoke(ctx, orgID, roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role), authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
if err != nil {
return err
}

View File

@@ -159,8 +159,8 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
if oldRole != types.RoleAdmin {
if err := s.authz.ModifyGrant(ctx,
orgID,
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(oldRole)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin)},
roletypes.MustGetSigNozManagedRoleFromExistingRole(oldRole),
roletypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin),
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
); err != nil {
return err

View File

@@ -1,218 +0,0 @@
package cloudintegrations
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"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) ([]integrationtypes.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) (*integrationtypes.CloudIntegration, *model.ApiError)
// Insert an account or update it by (cloudProvider, id)
// for specified non-empty fields
upsert(
ctx context.Context,
orgId string,
provider string,
id *string,
config *integrationtypes.AccountConfig,
accountId *string,
agentReport *integrationtypes.AgentReport,
removedAt *time.Time,
) (*integrationtypes.CloudIntegration, *model.ApiError)
}
func newCloudProviderAccountsRepository(store sqlstore.SQLStore) (
*cloudProviderAccountsSQLRepository, error,
) {
return &cloudProviderAccountsSQLRepository{
store: store,
}, nil
}
type cloudProviderAccountsSQLRepository struct {
store sqlstore.SQLStore
}
func (r *cloudProviderAccountsSQLRepository) listConnected(
ctx context.Context, orgId string, cloudProvider string,
) ([]integrationtypes.CloudIntegration, *model.ApiError) {
accounts := []integrationtypes.CloudIntegration{}
err := r.store.BunDB().NewSelect().
Model(&accounts).
Where("org_id = ?", orgId).
Where("provider = ?", cloudProvider).
Where("removed_at is NULL").
Where("account_id is not NULL").
Where("last_agent_report is not NULL").
Order("created_at").
Scan(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"could not query connected cloud accounts: %w", err,
))
}
return accounts, nil
}
func (r *cloudProviderAccountsSQLRepository) get(
ctx context.Context, orgId string, provider string, id string,
) (*integrationtypes.CloudIntegration, *model.ApiError) {
var result integrationtypes.CloudIntegration
err := r.store.BunDB().NewSelect().
Model(&result).
Where("org_id = ?", orgId).
Where("provider = ?", provider).
Where("id = ?", id).
Scan(ctx)
if err == sql.ErrNoRows {
return nil, model.NotFoundError(fmt.Errorf(
"couldn't find account with Id %s", id,
))
} else if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't query cloud provider accounts: %w", err,
))
}
return &result, nil
}
func (r *cloudProviderAccountsSQLRepository) getConnectedCloudAccount(
ctx context.Context, orgId string, provider string, accountId string,
) (*integrationtypes.CloudIntegration, *model.ApiError) {
var result integrationtypes.CloudIntegration
err := r.store.BunDB().NewSelect().
Model(&result).
Where("org_id = ?", orgId).
Where("provider = ?", provider).
Where("account_id = ?", accountId).
Where("last_agent_report is not NULL").
Where("removed_at is NULL").
Scan(ctx)
if err == sql.ErrNoRows {
return nil, model.NotFoundError(fmt.Errorf(
"couldn't find connected cloud account %s", accountId,
))
} else if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't query cloud provider accounts: %w", err,
))
}
return &result, nil
}
func (r *cloudProviderAccountsSQLRepository) upsert(
ctx context.Context,
orgId string,
provider string,
id *string,
config *integrationtypes.AccountConfig,
accountId *string,
agentReport *integrationtypes.AgentReport,
removedAt *time.Time,
) (*integrationtypes.CloudIntegration, *model.ApiError) {
// Insert
if id == nil {
temp := valuer.GenerateUUID().StringValue()
id = &temp
}
// Prepare clause for setting values in `on conflict do update`
onConflictSetStmts := []string{}
setColStatement := func(col string) string {
return fmt.Sprintf("%s=excluded.%s", col, col)
}
if config != nil {
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("config"),
)
}
if accountId != nil {
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("account_id"),
)
}
if agentReport != nil {
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("last_agent_report"),
)
}
if removedAt != nil {
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("removed_at"),
)
}
// set updated_at to current timestamp if it's an upsert
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("updated_at"),
)
onConflictClause := ""
if len(onConflictSetStmts) > 0 {
onConflictClause = fmt.Sprintf(
"conflict(id, provider, org_id) do update SET\n%s",
strings.Join(onConflictSetStmts, ",\n"),
)
}
integration := integrationtypes.CloudIntegration{
OrgID: orgId,
Provider: provider,
Identifiable: types.Identifiable{ID: valuer.MustNewUUID(*id)},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Config: config,
AccountID: accountId,
LastAgentReport: agentReport,
RemovedAt: removedAt,
}
_, dbErr := r.store.BunDB().NewInsert().
Model(&integration).
On(onConflictClause).
Exec(ctx)
if dbErr != nil {
return nil, model.InternalError(fmt.Errorf(
"could not upsert cloud account record: %w", dbErr,
))
}
upsertedAccount, apiErr := r.get(ctx, orgId, provider, *id)
if apiErr != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't fetch upserted account by id: %w", apiErr.ToError(),
))
}
return upsertedAccount, nil
}

View File

@@ -0,0 +1,571 @@
package baseprovider
import (
"context"
"fmt"
"log/slog"
"sort"
"sync"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/store"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
CodeDashboardNotFound = errors.MustNewCode("dashboard_not_found")
)
// hasValidTimeSeriesData checks if a query response contains valid time series data
// with at least one aggregation, series, and value
func hasValidTimeSeriesData(queryResponse *qbtypes.TimeSeriesData) bool {
return queryResponse != nil &&
len(queryResponse.Aggregations) > 0 &&
len(queryResponse.Aggregations[0].Series) > 0 &&
len(queryResponse.Aggregations[0].Series[0].Values) > 0
}
type BaseCloudProvider[def integrationtypes.Definition, conf integrationtypes.ServiceConfigTyped[def]] struct {
Logger *slog.Logger
Querier querier.Querier
AccountsRepo store.CloudProviderAccountsRepository
ServiceConfigRepo store.ServiceConfigDatabase
ServiceDefinitions *services.ServicesProvider[def]
ProviderType integrationtypes.CloudProviderType
}
func (b *BaseCloudProvider[def, conf]) GetName() integrationtypes.CloudProviderType {
return b.ProviderType
}
// AgentCheckIn is a helper function that handles common agent check-in logic.
// The getAgentConfigFunc should return the provider-specific agent configuration.
func AgentCheckIn[def integrationtypes.Definition, conf integrationtypes.ServiceConfigTyped[def], AgentConfigT any](
b *BaseCloudProvider[def, conf],
ctx context.Context,
req *integrationtypes.PostableAgentCheckInPayload,
getAgentConfigFunc func(context.Context, *integrationtypes.CloudIntegration) (*AgentConfigT, error),
) (*integrationtypes.GettableAgentCheckInRes[AgentConfigT], error) {
// agent can't check in unless the account is already created
existingAccount, err := b.AccountsRepo.Get(ctx, req.OrgID, b.GetName().String(), req.ID)
if err != nil {
return nil, err
}
if existingAccount != nil && existingAccount.AccountID != nil && *existingAccount.AccountID != req.AccountID {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput,
"can't check in with new %s account id %s for account %s with existing %s id %s",
b.GetName().String(), req.AccountID, existingAccount.ID.StringValue(), b.GetName().String(),
*existingAccount.AccountID,
)
}
existingAccount, err = b.AccountsRepo.GetConnectedCloudAccount(ctx, req.OrgID, b.GetName().String(), req.AccountID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if existingAccount != nil && existingAccount.ID.StringValue() != req.ID {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput,
"can't check in to %s account %s with id %s. already connected with id %s",
b.GetName().String(), req.AccountID, req.ID, existingAccount.ID.StringValue(),
)
}
agentReport := integrationtypes.AgentReport{
TimestampMillis: time.Now().UnixMilli(),
Data: req.Data,
}
account, err := b.AccountsRepo.Upsert(
ctx, req.OrgID, b.GetName().String(), &req.ID, nil, &req.AccountID, &agentReport, nil,
)
if err != nil {
return nil, err
}
agentConfig, err := getAgentConfigFunc(ctx, account)
if err != nil {
return nil, err
}
return &integrationtypes.GettableAgentCheckInRes[AgentConfigT]{
AccountId: account.ID.StringValue(),
CloudAccountId: *account.AccountID,
RemovedAt: account.RemovedAt,
IntegrationConfig: *agentConfig,
}, nil
}
func (b *BaseCloudProvider[def, conf]) GetAccountStatus(ctx context.Context, orgID, accountID string) (*integrationtypes.GettableAccountStatus, error) {
accountRecord, err := b.AccountsRepo.Get(ctx, orgID, b.ProviderType.String(), accountID)
if err != nil {
return nil, err
}
return &integrationtypes.GettableAccountStatus{
Id: accountRecord.ID.String(),
CloudAccountId: accountRecord.AccountID,
Status: accountRecord.Status(),
}, nil
}
func (b *BaseCloudProvider[def, conf]) ListConnectedAccounts(ctx context.Context, orgID string) (*integrationtypes.GettableConnectedAccountsList, error) {
accountRecords, err := b.AccountsRepo.ListConnected(ctx, orgID, b.ProviderType.String())
if err != nil {
return nil, err
}
connectedAccounts := make([]*integrationtypes.Account, 0, len(accountRecords))
for _, r := range accountRecords {
connectedAccounts = append(connectedAccounts, r.Account(b.ProviderType))
}
return &integrationtypes.GettableConnectedAccountsList{
Accounts: connectedAccounts,
}, nil
}
func (b *BaseCloudProvider[def, conf]) DisconnectAccount(ctx context.Context, orgID, accountID string) (*integrationtypes.CloudIntegration, error) {
account, err := b.AccountsRepo.Get(ctx, orgID, b.ProviderType.String(), accountID)
if err != nil {
return nil, err
}
tsNow := time.Now()
account, err = b.AccountsRepo.Upsert(
ctx, orgID, b.ProviderType.String(), &accountID, nil, nil, nil, &tsNow,
)
if err != nil {
return nil, err
}
return account, nil
}
func (b *BaseCloudProvider[def, conf]) GetDashboard(ctx context.Context, id string, orgID valuer.UUID) (*dashboardtypes.Dashboard, error) {
allDashboards, err := b.GetAvailableDashboards(ctx, orgID)
if err != nil {
return nil, err
}
for _, d := range allDashboards {
if d.ID == id {
return d, nil
}
}
return nil, errors.NewNotFoundf(CodeDashboardNotFound, "dashboard with id %s not found", id)
}
func (b *BaseCloudProvider[def, conf]) GetServiceConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID valuer.UUID,
definition def,
isMetricsEnabled bool,
isLogsEnabled bool,
) (*integrationtypes.ServiceConnectionStatus, error) {
ingestionStatusCheck := definition.GetIngestionStatusCheck()
if ingestionStatusCheck == nil {
return nil, nil
}
resp := new(integrationtypes.ServiceConnectionStatus)
wg := sync.WaitGroup{}
if len(ingestionStatusCheck.Metrics) > 0 && isMetricsEnabled {
wg.Add(1)
go func() {
defer utils.RecoverPanic(func(err interface{}, stack []byte) {
b.Logger.ErrorContext(
ctx, "panic while getting service metrics connection status",
"service", definition.GetId(),
"error", err,
"stack", string(stack),
)
})
defer wg.Done()
status, _ := b.getServiceMetricsConnectionStatus(ctx, cloudAccountID, orgID, definition)
resp.Metrics = status
}()
}
if len(ingestionStatusCheck.Logs) > 0 && isLogsEnabled {
wg.Add(1)
go func() {
defer utils.RecoverPanic(func(err interface{}, stack []byte) {
b.Logger.ErrorContext(
ctx, "panic while getting service logs connection status",
"service", definition.GetId(),
"error", err,
"stack", string(stack),
)
})
defer wg.Done()
status, _ := b.getServiceLogsConnectionStatus(ctx, cloudAccountID, orgID, definition)
resp.Logs = status
}()
}
wg.Wait()
return resp, nil
}
func (b *BaseCloudProvider[def, conf]) getServiceMetricsConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID valuer.UUID,
definition def,
) ([]*integrationtypes.SignalConnectionStatus, error) {
ingestionStatusCheck := definition.GetIngestionStatusCheck()
if ingestionStatusCheck == nil || len(ingestionStatusCheck.Metrics) < 1 {
return nil, nil
}
statusResp := make([]*integrationtypes.SignalConnectionStatus, 0)
for _, metric := range ingestionStatusCheck.Metrics {
statusResp = append(statusResp, &integrationtypes.SignalConnectionStatus{
CategoryID: metric.Category,
CategoryDisplayName: metric.DisplayName,
})
}
for index, category := range ingestionStatusCheck.Metrics {
queries := make([]qbtypes.QueryEnvelope, 0)
for _, check := range category.Checks {
// TODO: make sure all the cloud providers send these two attributes
// or create map of provider specific filter expression
filterExpression := fmt.Sprintf(`cloud.provider="%s" AND cloud.account.id="%s"`, b.ProviderType.String(), cloudAccountID)
f := ""
for _, attribute := range check.Attributes {
f = fmt.Sprintf("%s %s", attribute.Name, attribute.Operator)
if attribute.Value != "" {
f = fmt.Sprintf("%s '%s'", f, attribute.Value)
}
filterExpression = fmt.Sprintf("%s AND %s", filterExpression, f)
}
queries = append(queries, qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: valuer.GenerateUUID().String(),
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{{
MetricName: check.Key,
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationAvg,
}},
Filter: &qbtypes.Filter{
Expression: filterExpression,
},
},
})
}
resp, err := b.Querier.QueryRange(ctx, orgID, &qbtypes.QueryRangeRequest{
SchemaVersion: "v5",
Start: uint64(time.Now().Add(-time.Hour).UnixMilli()),
End: uint64(time.Now().UnixMilli()),
RequestType: qbtypes.RequestTypeScalar,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
})
if err != nil {
b.Logger.DebugContext(ctx,
"error querying for service metrics connection status",
"error", err,
"service", definition.GetId(),
)
continue
}
if resp != nil && len(resp.Data.Results) < 1 {
continue
}
queryResponse, ok := resp.Data.Results[0].(*qbtypes.TimeSeriesData)
if !ok {
b.Logger.ErrorContext(ctx, "unexpected query response type for service metrics connection status",
"service", definition.GetId(),
)
continue
}
if !hasValidTimeSeriesData(queryResponse) {
continue
}
statusResp[index] = &integrationtypes.SignalConnectionStatus{
CategoryID: category.Category,
CategoryDisplayName: category.DisplayName,
LastReceivedTsMillis: queryResponse.Aggregations[0].Series[0].Values[0].Timestamp,
LastReceivedFrom: fmt.Sprintf("signoz-%s-integration", b.ProviderType.String()),
}
}
return statusResp, nil
}
func (b *BaseCloudProvider[def, conf]) getServiceLogsConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID valuer.UUID,
definition def,
) ([]*integrationtypes.SignalConnectionStatus, error) {
ingestionStatusCheck := definition.GetIngestionStatusCheck()
if ingestionStatusCheck == nil || len(ingestionStatusCheck.Logs) < 1 {
return nil, nil
}
statusResp := make([]*integrationtypes.SignalConnectionStatus, 0)
for _, log := range ingestionStatusCheck.Logs {
statusResp = append(statusResp, &integrationtypes.SignalConnectionStatus{
CategoryID: log.Category,
CategoryDisplayName: log.DisplayName,
})
}
for index, category := range ingestionStatusCheck.Logs {
queries := make([]qbtypes.QueryEnvelope, 0)
for _, check := range category.Checks {
// TODO: make sure all the cloud providers provide required attributes for logs
// or create map of provider specific filter expression
filterExpression := fmt.Sprintf(`cloud.account.id="%s"`, cloudAccountID)
f := ""
for _, attribute := range check.Attributes {
f = fmt.Sprintf("%s %s", attribute.Name, attribute.Operator)
if attribute.Value != "" {
f = fmt.Sprintf("%s '%s'", f, attribute.Value)
}
filterExpression = fmt.Sprintf("%s AND %s", filterExpression, f)
}
queries = append(queries, qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Name: valuer.GenerateUUID().String(),
Signal: telemetrytypes.SignalLogs,
Aggregations: []qbtypes.LogAggregation{{
Expression: "count()",
}},
Filter: &qbtypes.Filter{
Expression: filterExpression,
},
Limit: 10,
Offset: 0,
},
})
}
resp, err := b.Querier.QueryRange(ctx, orgID, &qbtypes.QueryRangeRequest{
SchemaVersion: "v1",
Start: uint64(time.Now().Add(-time.Hour * 1).UnixMilli()),
End: uint64(time.Now().UnixMilli()),
RequestType: qbtypes.RequestTypeTimeSeries,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
})
if err != nil {
b.Logger.DebugContext(ctx,
"error querying for service logs connection status",
"error", err,
"service", definition.GetId(),
)
continue
}
if resp != nil && len(resp.Data.Results) < 1 {
continue
}
queryResponse, ok := resp.Data.Results[0].(*qbtypes.TimeSeriesData)
if !ok {
b.Logger.ErrorContext(ctx, "unexpected query response type for service logs connection status",
"service", definition.GetId(),
)
continue
}
if !hasValidTimeSeriesData(queryResponse) {
continue
}
statusResp[index] = &integrationtypes.SignalConnectionStatus{
CategoryID: category.Category,
CategoryDisplayName: category.DisplayName,
LastReceivedTsMillis: queryResponse.Aggregations[0].Series[0].Values[0].Timestamp,
LastReceivedFrom: fmt.Sprintf("signoz-%s-integration", b.ProviderType.String()),
}
}
return statusResp, nil
}
func (b *BaseCloudProvider[def, conf]) GetAvailableDashboards(
ctx context.Context,
orgID valuer.UUID,
) ([]*dashboardtypes.Dashboard, error) {
accountRecords, err := b.AccountsRepo.ListConnected(ctx, orgID.StringValue(), b.ProviderType.String())
if err != nil {
return nil, err
}
servicesWithAvailableMetrics := map[string]*time.Time{}
for _, ar := range accountRecords {
if ar.AccountID != nil {
configsBySvcId, err := b.ServiceConfigRepo.GetAllForAccount(ctx, orgID.StringValue(), ar.ID.StringValue())
if err != nil {
return nil, err
}
for svcId, config := range configsBySvcId {
var serviceConfig conf
err = integrationtypes.UnmarshalJSON(config, &serviceConfig)
if err != nil {
return nil, err
}
if serviceConfig.IsMetricsEnabled() {
servicesWithAvailableMetrics[svcId] = &ar.CreatedAt
}
}
}
}
svcDashboards := make([]*dashboardtypes.Dashboard, 0)
allServices, err := b.ServiceDefinitions.ListServiceDefinitions(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to list %s service definitions", b.ProviderType.String())
}
// accumulate definitions in a fixed order to ensure same order of dashboards across runs
svcIds := make([]string, 0, len(allServices))
for id := range allServices {
svcIds = append(svcIds, id)
}
sort.Strings(svcIds)
for _, svcId := range svcIds {
svc := allServices[svcId]
serviceDashboardsCreatedAt, ok := servicesWithAvailableMetrics[svcId]
if ok && serviceDashboardsCreatedAt != nil {
svcDashboards = append(
svcDashboards,
integrationtypes.GetDashboardsFromAssets(svc.GetId(), orgID, b.ProviderType, serviceDashboardsCreatedAt, svc.GetAssets())...,
)
servicesWithAvailableMetrics[svcId] = nil
}
}
return svcDashboards, nil
}
func (b *BaseCloudProvider[def, conf]) GetServiceConfig(
ctx context.Context,
definition def,
orgID valuer.UUID,
serviceId,
cloudAccountId string,
) (conf, error) {
var zero conf
activeAccount, err := b.AccountsRepo.GetConnectedCloudAccount(ctx, orgID.String(), b.ProviderType.String(), cloudAccountId)
if err != nil {
return zero, err
}
config, err := b.ServiceConfigRepo.Get(ctx, orgID.String(), activeAccount.ID.StringValue(), serviceId)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return zero, nil
}
return zero, err
}
var serviceConfig conf
err = integrationtypes.UnmarshalJSON(config, &serviceConfig)
if err != nil {
return zero, err
}
if config != nil && serviceConfig.IsMetricsEnabled() {
definition.PopulateDashboardURLs(b.ProviderType, serviceId)
}
return serviceConfig, nil
}
func (b *BaseCloudProvider[def, conf]) UpdateServiceConfig(ctx context.Context, serviceId string, orgID valuer.UUID, config []byte) (any, error) {
definition, err := b.ServiceDefinitions.GetServiceDefinition(ctx, serviceId)
if err != nil {
return nil, err
}
var updateReq integrationtypes.UpdatableServiceConfig[conf]
err = integrationtypes.UnmarshalJSON(config, &updateReq)
if err != nil {
return nil, err
}
// Check if config is provided (use any type assertion for nil check with generics)
if any(updateReq.Config) == nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "config is required")
}
if err = updateReq.Config.Validate(definition); err != nil {
return nil, err
}
// can only update config for a connected cloud account id
_, err = b.AccountsRepo.GetConnectedCloudAccount(
ctx, orgID.String(), b.GetName().String(), updateReq.CloudAccountId,
)
if err != nil {
return nil, err
}
serviceConfigBytes, err := integrationtypes.MarshalJSON(&updateReq.Config)
if err != nil {
return nil, err
}
updatedConfigBytes, err := b.ServiceConfigRepo.Upsert(
ctx, orgID.String(), b.GetName().String(), updateReq.CloudAccountId, serviceId, serviceConfigBytes,
)
if err != nil {
return nil, err
}
var updatedConfig conf
err = integrationtypes.UnmarshalJSON(updatedConfigBytes, &updatedConfig)
if err != nil {
return nil, err
}
return &integrationtypes.UpdatableServiceConfigRes{
ServiceId: serviceId,
Config: updatedConfig,
}, nil
}

View File

@@ -1,43 +0,0 @@
package cloudintegrations
import (
"github.com/SigNoz/signoz/pkg/errors"
)
var (
CodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
CodeMismatchCloudProvider = errors.MustNewCode("cloud_provider_mismatch")
)
// List of all valid cloud regions on Amazon Web Services
var ValidAWSRegions = map[string]bool{
"af-south-1": true, // Africa (Cape Town).
"ap-east-1": true, // Asia Pacific (Hong Kong).
"ap-northeast-1": true, // Asia Pacific (Tokyo).
"ap-northeast-2": true, // Asia Pacific (Seoul).
"ap-northeast-3": true, // Asia Pacific (Osaka).
"ap-south-1": true, // Asia Pacific (Mumbai).
"ap-south-2": true, // Asia Pacific (Hyderabad).
"ap-southeast-1": true, // Asia Pacific (Singapore).
"ap-southeast-2": true, // Asia Pacific (Sydney).
"ap-southeast-3": true, // Asia Pacific (Jakarta).
"ap-southeast-4": true, // Asia Pacific (Melbourne).
"ca-central-1": true, // Canada (Central).
"ca-west-1": true, // Canada West (Calgary).
"eu-central-1": true, // Europe (Frankfurt).
"eu-central-2": true, // Europe (Zurich).
"eu-north-1": true, // Europe (Stockholm).
"eu-south-1": true, // Europe (Milan).
"eu-south-2": true, // Europe (Spain).
"eu-west-1": true, // Europe (Ireland).
"eu-west-2": true, // Europe (London).
"eu-west-3": true, // Europe (Paris).
"il-central-1": true, // Israel (Tel Aviv).
"me-central-1": true, // Middle East (UAE).
"me-south-1": true, // Middle East (Bahrain).
"sa-east-1": true, // South America (Sao Paulo).
"us-east-1": true, // US East (N. Virginia).
"us-east-2": true, // US East (Ohio).
"us-west-1": true, // US West (N. California).
"us-west-2": true, // US West (Oregon).
}

View File

@@ -1,625 +0,0 @@
package cloudintegrations
import (
"context"
"fmt"
"net/url"
"slices"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"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/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"golang.org/x/exp/maps"
)
var SupportedCloudProviders = []string{
"aws",
}
func validateCloudProviderName(name string) *model.ApiError {
if !slices.Contains(SupportedCloudProviders, name) {
return model.BadRequest(fmt.Errorf("invalid cloud provider: %s", name))
}
return nil
}
type Controller struct {
accountsRepo cloudProviderAccountsRepository
serviceConfigRepo ServiceConfigDatabase
}
func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
accountsRepo, err := newCloudProviderAccountsRepository(sqlStore)
if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
}
serviceConfigRepo, err := newServiceConfigRepository(sqlStore)
if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider service config repo: %w", err)
}
return &Controller{
accountsRepo: accountsRepo,
serviceConfigRepo: serviceConfigRepo,
}, nil
}
type ConnectedAccountsListResponse struct {
Accounts []integrationtypes.Account `json:"accounts"`
}
func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cloudProvider string) (
*ConnectedAccountsListResponse, *model.ApiError,
) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgId, cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list cloud accounts")
}
connectedAccounts := []integrationtypes.Account{}
for _, a := range accountRecords {
connectedAccounts = append(connectedAccounts, a.Account())
}
return &ConnectedAccountsListResponse{
Accounts: connectedAccounts,
}, nil
}
type GenerateConnectionUrlRequest struct {
// Optional. To be specified for updates.
AccountId *string `json:"account_id,omitempty"`
AccountConfig integrationtypes.AccountConfig `json:"account_config"`
AgentConfig SigNozAgentConfig `json:"agent_config"`
}
type SigNozAgentConfig struct {
// The region in which SigNoz agent should be installed.
Region string `json:"region"`
IngestionUrl string `json:"ingestion_url"`
IngestionKey string `json:"ingestion_key"`
SigNozAPIUrl string `json:"signoz_api_url"`
SigNozAPIKey string `json:"signoz_api_key"`
Version string `json:"version,omitempty"`
}
type GenerateConnectionUrlResponse struct {
AccountId string `json:"account_id"`
ConnectionUrl string `json:"connection_url"`
}
func (c *Controller) GenerateConnectionUrl(ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest) (*GenerateConnectionUrlResponse, *model.ApiError) {
// Account connection with a simple connection URL may not be available for all providers.
if cloudProvider != "aws" {
return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider))
}
account, apiErr := c.accountsRepo.upsert(
ctx, orgId, cloudProvider, req.AccountId, &req.AccountConfig, nil, nil, nil,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
}
agentVersion := "v0.0.8"
if req.AgentConfig.Version != "" {
agentVersion = req.AgentConfig.Version
}
connectionUrl := fmt.Sprintf(
"https://%s.console.aws.amazon.com/cloudformation/home?region=%s#/stacks/quickcreate?",
req.AgentConfig.Region, req.AgentConfig.Region,
)
for qp, value := range map[string]string{
"param_SigNozIntegrationAgentVersion": agentVersion,
"param_SigNozApiUrl": req.AgentConfig.SigNozAPIUrl,
"param_SigNozApiKey": req.AgentConfig.SigNozAPIKey,
"param_SigNozAccountId": account.ID.StringValue(),
"param_IngestionUrl": req.AgentConfig.IngestionUrl,
"param_IngestionKey": req.AgentConfig.IngestionKey,
"stackName": "signoz-integration",
"templateURL": fmt.Sprintf(
"https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json",
agentVersion,
),
} {
connectionUrl += fmt.Sprintf("&%s=%s", qp, url.QueryEscape(value))
}
return &GenerateConnectionUrlResponse{
AccountId: account.ID.StringValue(),
ConnectionUrl: connectionUrl,
}, nil
}
type AccountStatusResponse struct {
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) (
*AccountStatusResponse, *model.ApiError,
) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
account, apiErr := c.accountsRepo.get(ctx, orgId, cloudProvider, accountId)
if apiErr != nil {
return nil, apiErr
}
resp := AccountStatusResponse{
Id: account.ID.StringValue(),
CloudAccountId: account.AccountID,
Status: account.Status(),
}
return &resp, nil
}
type AgentCheckInRequest struct {
ID string `json:"account_id"`
AccountID string `json:"cloud_account_id"`
// Arbitrary cloud specific Agent data
Data map[string]any `json:"data,omitempty"`
}
type AgentCheckInResponse struct {
AccountId string `json:"account_id"`
CloudAccountId string `json:"cloud_account_id"`
RemovedAt *time.Time `json:"removed_at"`
IntegrationConfig IntegrationConfigForAgent `json:"integration_config"`
}
type IntegrationConfigForAgent struct {
EnabledRegions []string `json:"enabled_regions"`
TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"`
}
func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest) (*AgentCheckInResponse, error) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
existingAccount, apiErr := c.accountsRepo.get(ctx, orgId, cloudProvider, req.ID)
if existingAccount != nil && existingAccount.AccountID != nil && *existingAccount.AccountID != req.AccountID {
return nil, model.BadRequest(fmt.Errorf(
"can't check in with new %s account id %s for account %s with existing %s id %s",
cloudProvider, req.AccountID, existingAccount.ID.StringValue(), cloudProvider, *existingAccount.AccountID,
))
}
existingAccount, apiErr = c.accountsRepo.getConnectedCloudAccount(ctx, orgId, cloudProvider, req.AccountID)
if existingAccount != nil && existingAccount.ID.StringValue() != req.ID {
return nil, model.BadRequest(fmt.Errorf(
"can't check in to %s account %s with id %s. already connected with id %s",
cloudProvider, req.AccountID, req.ID, existingAccount.ID.StringValue(),
))
}
agentReport := integrationtypes.AgentReport{
TimestampMillis: time.Now().UnixMilli(),
Data: req.Data,
}
account, apiErr := c.accountsRepo.upsert(
ctx, orgId, cloudProvider, &req.ID, nil, &req.AccountID, &agentReport, nil,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
}
// prepare and return integration config to be consumed by agent
compiledStrategy, err := NewCompiledCollectionStrategy(cloudProvider)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't init telemetry collection strategy: %w", err,
))
}
agentConfig := IntegrationConfigForAgent{
EnabledRegions: []string{},
TelemetryCollectionStrategy: compiledStrategy,
}
if account.Config != nil && account.Config.EnabledRegions != nil {
agentConfig.EnabledRegions = account.Config.EnabledRegions
}
services, err := services.Map(cloudProvider)
if err != nil {
return nil, err
}
svcConfigs, apiErr := c.serviceConfigRepo.getAllForAccount(
ctx, orgId, account.ID.StringValue(),
)
if apiErr != nil {
return nil, model.WrapApiError(
apiErr, "couldn't get service configs for cloud account",
)
}
// accumulate config in a fixed order to ensure same config generated across runs
configuredServices := maps.Keys(svcConfigs)
slices.Sort(configuredServices)
for _, svcType := range configuredServices {
definition, ok := services[svcType]
if !ok {
continue
}
config := svcConfigs[svcType]
err := AddServiceStrategy(svcType, compiledStrategy, definition.Strategy, config)
if err != nil {
return nil, err
}
}
return &AgentCheckInResponse{
AccountId: account.ID.StringValue(),
CloudAccountId: *account.AccountID,
RemovedAt: account.RemovedAt,
IntegrationConfig: agentConfig,
}, nil
}
type UpdateAccountConfigRequest struct {
Config integrationtypes.AccountConfig `json:"config"`
}
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
}
accountRecord, apiErr := c.accountsRepo.upsert(
ctx, orgId, cloudProvider, &accountId, &req.Config, nil, nil, nil,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
}
account := accountRecord.Account()
return &account, nil
}
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
}
account, apiErr := c.accountsRepo.get(ctx, orgId, cloudProvider, accountId)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't disconnect account")
}
tsNow := time.Now()
account, apiErr = c.accountsRepo.upsert(
ctx, orgId, cloudProvider, &accountId, nil, nil, nil, &tsNow,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't disconnect account")
}
return account, nil
}
type ListServicesResponse struct {
Services []ServiceSummary `json:"services"`
}
func (c *Controller) ListServices(
ctx context.Context,
orgID string,
cloudProvider string,
cloudAccountId *string,
) (*ListServicesResponse, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
definitions, apiErr := services.List(cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list cloud services")
}
svcConfigs := map[string]*integrationtypes.CloudServiceConfig{}
if cloudAccountId != nil {
activeAccount, apiErr := c.accountsRepo.getConnectedCloudAccount(
ctx, orgID, cloudProvider, *cloudAccountId,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't get active account")
}
svcConfigs, apiErr = c.serviceConfigRepo.getAllForAccount(
ctx, orgID, activeAccount.ID.StringValue(),
)
if apiErr != nil {
return nil, model.WrapApiError(
apiErr, "couldn't get service configs for cloud account",
)
}
}
summaries := []ServiceSummary{}
for _, def := range definitions {
summary := ServiceSummary{
Metadata: def.Metadata,
}
summary.Config = svcConfigs[summary.Id]
summaries = append(summaries, summary)
}
return &ListServicesResponse{
Services: summaries,
}, nil
}
func (c *Controller) GetServiceDetails(
ctx context.Context,
orgID string,
cloudProvider string,
serviceId string,
cloudAccountId *string,
) (*ServiceDetails, error) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
definition, err := services.GetServiceDefinition(cloudProvider, serviceId)
if err != nil {
return nil, err
}
details := ServiceDetails{
Definition: *definition,
}
if cloudAccountId != nil {
activeAccount, apiErr := c.accountsRepo.getConnectedCloudAccount(
ctx, orgID, cloudProvider, *cloudAccountId,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't get active account")
}
config, apiErr := c.serviceConfigRepo.get(
ctx, orgID, activeAccount.ID.StringValue(), serviceId,
)
if apiErr != nil && apiErr.Type() != model.ErrorNotFound {
return nil, model.WrapApiError(apiErr, "couldn't fetch service config")
}
if config != nil {
details.Config = config
enabled := false
if config.Metrics != nil && config.Metrics.Enabled {
enabled = true
}
// add links to service dashboards, making them clickable.
for i, d := range definition.Assets.Dashboards {
dashboardUuid := c.dashboardUuid(
cloudProvider, serviceId, d.Id,
)
if enabled {
definition.Assets.Dashboards[i].Url = fmt.Sprintf("/dashboard/%s", dashboardUuid)
} else {
definition.Assets.Dashboards[i].Url = "" // to unset the in-memory URL if enabled once and disabled afterwards
}
}
}
}
return &details, nil
}
type UpdateServiceConfigRequest struct {
CloudAccountId string `json:"cloud_account_id"`
Config integrationtypes.CloudServiceConfig `json:"config"`
}
func (u *UpdateServiceConfigRequest) Validate(def *services.Definition) error {
if def.Id != services.S3Sync && u.Config.Logs != nil && u.Config.Logs.S3Buckets != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "s3 buckets can only be added to service-type[%s]", services.S3Sync)
} else if def.Id == services.S3Sync && u.Config.Logs != nil && u.Config.Logs.S3Buckets != nil {
for region := range u.Config.Logs.S3Buckets {
if _, found := ValidAWSRegions[region]; !found {
return errors.NewInvalidInputf(CodeInvalidCloudRegion, "invalid cloud region: %s", region)
}
}
}
return nil
}
type UpdateServiceConfigResponse struct {
Id string `json:"id"`
Config integrationtypes.CloudServiceConfig `json:"config"`
}
func (c *Controller) UpdateServiceConfig(
ctx context.Context,
orgID string,
cloudProvider string,
serviceType string,
req *UpdateServiceConfigRequest,
) (*UpdateServiceConfigResponse, error) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
// can only update config for a valid service.
definition, err := services.GetServiceDefinition(cloudProvider, serviceType)
if err != nil {
return nil, err
}
if err := req.Validate(definition); err != nil {
return nil, err
}
// can only update config for a connected cloud account id
_, apiErr := c.accountsRepo.getConnectedCloudAccount(
ctx, orgID, cloudProvider, req.CloudAccountId,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't find connected cloud account")
}
updatedConfig, apiErr := c.serviceConfigRepo.upsert(
ctx, orgID, cloudProvider, req.CloudAccountId, serviceType, req.Config,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't update service config")
}
return &UpdateServiceConfigResponse{
Id: serviceType,
Config: *updatedConfig,
}, nil
}
// All dashboards that are available based on cloud integrations configuration
// across all cloud providers
func (c *Controller) AvailableDashboards(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) {
allDashboards := []*dashboardtypes.Dashboard{}
for _, provider := range []string{"aws"} {
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider)
if apiErr != nil {
return nil, model.WrapApiError(
apiErr, fmt.Sprintf("couldn't get available dashboards for %s", provider),
)
}
allDashboards = append(allDashboards, providerDashboards...)
}
return allDashboards, nil
}
func (c *Controller) AvailableDashboardsForCloudProvider(ctx context.Context, orgID valuer.UUID, cloudProvider string) ([]*dashboardtypes.Dashboard, *model.ApiError) {
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID.StringValue(), cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts")
}
// for v0, service dashboards are only available when metrics are enabled.
servicesWithAvailableMetrics := map[string]*time.Time{}
for _, ar := range accountRecords {
if ar.AccountID != nil {
configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount(
ctx, orgID.StringValue(), ar.ID.StringValue(),
)
if apiErr != nil {
return nil, apiErr
}
for svcId, config := range configsBySvcId {
if config.Metrics != nil && config.Metrics.Enabled {
servicesWithAvailableMetrics[svcId] = &ar.CreatedAt
}
}
}
}
allServices, apiErr := services.List(cloudProvider)
if apiErr != nil {
return nil, apiErr
}
svcDashboards := []*dashboardtypes.Dashboard{}
for _, svc := range allServices {
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
if serviceDashboardsCreatedAt != nil {
for _, d := range svc.Assets.Dashboards {
author := fmt.Sprintf("%s-integration", cloudProvider)
svcDashboards = append(svcDashboards, &dashboardtypes.Dashboard{
ID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
Locked: true,
Data: *d.Definition,
TimeAuditable: types.TimeAuditable{
CreatedAt: *serviceDashboardsCreatedAt,
UpdatedAt: *serviceDashboardsCreatedAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: author,
UpdatedBy: author,
},
OrgID: orgID,
})
}
servicesWithAvailableMetrics[svc.Id] = nil
}
}
return svcDashboards, nil
}
func (c *Controller) GetDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) {
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
if apiErr != nil {
return nil, apiErr
}
allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list available dashboards")
}
for _, d := range allDashboards {
if d.ID == dashboardUuid {
return d, nil
}
}
return nil, model.NotFoundError(fmt.Errorf("couldn't find dashboard with uuid: %s", dashboardUuid))
}
func (c *Controller) dashboardUuid(
cloudProvider string, svcId string, dashboardId string,
) string {
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
}
func (c *Controller) parseDashboardUuid(dashboardUuid string) (cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError) {
parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 || parts[0] != "cloud-integration" {
return "", "", "", model.BadRequest(fmt.Errorf("invalid cloud integration dashboard id"))
}
return parts[1], parts[2], parts[3], nil
}
func (c *Controller) IsCloudIntegrationDashboardUuid(dashboardUuid string) bool {
_, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
return apiErr == nil
}

View File

@@ -0,0 +1,331 @@
package implawsprovider
import (
"context"
"fmt"
"log/slog"
"net/url"
"slices"
"golang.org/x/exp/maps"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/baseprovider"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
integrationstore "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/store"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
CodeInvalidAWSRegion = errors.MustNewCode("invalid_aws_region")
)
type awsProvider struct {
baseprovider.BaseCloudProvider[*integrationtypes.AWSDefinition, *integrationtypes.AWSServiceConfig]
}
func NewAWSCloudProvider(
logger *slog.Logger,
accountsRepo integrationstore.CloudProviderAccountsRepository,
serviceConfigRepo integrationstore.ServiceConfigDatabase,
querier querier.Querier,
) (integrationtypes.CloudProvider, error) {
serviceDefinitions, err := services.NewAWSCloudProviderServices()
if err != nil {
return nil, err
}
return &awsProvider{
BaseCloudProvider: baseprovider.BaseCloudProvider[*integrationtypes.AWSDefinition, *integrationtypes.AWSServiceConfig]{
Logger: logger,
Querier: querier,
AccountsRepo: accountsRepo,
ServiceConfigRepo: serviceConfigRepo,
ServiceDefinitions: serviceDefinitions,
ProviderType: integrationtypes.CloudProviderAWS,
},
}, nil
}
func (a *awsProvider) AgentCheckIn(ctx context.Context, req *integrationtypes.PostableAgentCheckInPayload) (any, error) {
return baseprovider.AgentCheckIn(
&a.BaseCloudProvider,
ctx,
req,
a.getAWSAgentConfig,
)
}
func (a *awsProvider) getAWSAgentConfig(ctx context.Context, account *integrationtypes.CloudIntegration) (*integrationtypes.AWSAgentIntegrationConfig, error) {
// prepare and return integration config to be consumed by agent
agentConfig := &integrationtypes.AWSAgentIntegrationConfig{
EnabledRegions: []string{},
TelemetryCollectionStrategy: &integrationtypes.AWSCollectionStrategy{
Metrics: &integrationtypes.AWSMetricsStrategy{},
Logs: &integrationtypes.AWSLogsStrategy{},
S3Buckets: map[string][]string{},
},
}
accountConfig := new(integrationtypes.AWSAccountConfig)
err := integrationtypes.UnmarshalJSON([]byte(account.Config), accountConfig)
if err != nil {
return nil, err
}
if accountConfig.EnabledRegions != nil {
agentConfig.EnabledRegions = accountConfig.EnabledRegions
}
svcConfigs, err := a.ServiceConfigRepo.GetAllForAccount(
ctx, account.OrgID, account.ID.StringValue(),
)
if err != nil {
return nil, err
}
// accumulate config in a fixed order to ensure same config generated across runs
configuredServices := maps.Keys(svcConfigs)
slices.Sort(configuredServices)
for _, svcType := range configuredServices {
definition, err := a.ServiceDefinitions.GetServiceDefinition(ctx, svcType)
if err != nil {
continue
}
config := svcConfigs[svcType]
serviceConfig := new(integrationtypes.AWSServiceConfig)
err = integrationtypes.UnmarshalJSON(config, serviceConfig)
if err != nil {
continue
}
if serviceConfig.IsLogsEnabled() {
if svcType == integrationtypes.S3Sync {
// S3 bucket sync; No cloudwatch logs are appended for this service type;
// Though definition is populated with a custom cloudwatch group that helps in calculating logs connection status
agentConfig.TelemetryCollectionStrategy.S3Buckets = serviceConfig.Logs.S3Buckets
} else if definition.Strategy.Logs != nil { // services that includes a logs subscription
agentConfig.TelemetryCollectionStrategy.Logs.Subscriptions = append(
agentConfig.TelemetryCollectionStrategy.Logs.Subscriptions,
definition.Strategy.Logs.Subscriptions...,
)
}
}
if serviceConfig.IsMetricsEnabled() && definition.Strategy.Metrics != nil {
agentConfig.TelemetryCollectionStrategy.Metrics.StreamFilters = append(
agentConfig.TelemetryCollectionStrategy.Metrics.StreamFilters,
definition.Strategy.Metrics.StreamFilters...,
)
}
}
return agentConfig, nil
}
func (a *awsProvider) ListServices(ctx context.Context, orgID string, cloudAccountID *string) (any, error) {
svcConfigs := make(map[string]*integrationtypes.AWSServiceConfig)
if cloudAccountID != nil {
activeAccount, err := a.AccountsRepo.GetConnectedCloudAccount(ctx, orgID, a.GetName().String(), *cloudAccountID)
if err != nil {
return nil, err
}
serviceConfigs, err := a.ServiceConfigRepo.GetAllForAccount(ctx, orgID, activeAccount.ID.String())
if err != nil {
return nil, err
}
for svcType, config := range serviceConfigs {
serviceConfig := new(integrationtypes.AWSServiceConfig)
err = integrationtypes.UnmarshalJSON(config, serviceConfig)
if err != nil {
return nil, err
}
svcConfigs[svcType] = serviceConfig
}
}
summaries := make([]integrationtypes.AWSServiceSummary, 0)
definitions, err := a.ServiceDefinitions.ListServiceDefinitions(ctx)
if err != nil {
return nil, err
}
for _, def := range definitions {
summary := integrationtypes.AWSServiceSummary{
DefinitionMetadata: def.DefinitionMetadata,
Config: nil,
}
summary.Config = svcConfigs[summary.Id]
summaries = append(summaries, summary)
}
slices.SortFunc(summaries, func(a, b integrationtypes.AWSServiceSummary) int {
if a.DefinitionMetadata.Title < b.DefinitionMetadata.Title {
return -1
}
if a.DefinitionMetadata.Title > b.DefinitionMetadata.Title {
return 1
}
return 0
})
return &integrationtypes.GettableAWSServices{
Services: summaries,
}, nil
}
func (a *awsProvider) GetServiceDetails(ctx context.Context, req *integrationtypes.GetServiceDetailsReq) (any, error) {
details := new(integrationtypes.GettableAWSServiceDetails)
awsDefinition, err := a.ServiceDefinitions.GetServiceDefinition(ctx, req.ServiceId)
if err != nil {
return nil, err
}
details.Definition = *awsDefinition
if req.CloudAccountID == nil {
return details, nil
}
config, err := a.GetServiceConfig(ctx, awsDefinition, req.OrgID, req.ServiceId, *req.CloudAccountID)
if err != nil {
return nil, err
}
if config == nil {
return details, nil
}
details.Config = config
isMetricsEnabled := config.IsMetricsEnabled()
isLogsEnabled := config.IsLogsEnabled()
connectionStatus, err := a.GetServiceConnectionStatus(
ctx,
*req.CloudAccountID,
req.OrgID,
awsDefinition,
isMetricsEnabled,
isLogsEnabled,
)
if err != nil {
return nil, err
}
details.ConnectionStatus = connectionStatus
return details, nil
}
func (a *awsProvider) GenerateConnectionArtifact(ctx context.Context, req *integrationtypes.PostableConnectionArtifact) (any, error) {
connection := new(integrationtypes.PostableAWSConnectionUrl)
err := integrationtypes.UnmarshalJSON(req.Data, connection)
if err != nil {
return nil, err
}
if connection.AccountConfig != nil {
for _, region := range connection.AccountConfig.EnabledRegions {
if integrationtypes.ValidAWSRegions[region] {
continue
}
return nil, errors.NewInvalidInputf(CodeInvalidAWSRegion, "invalid aws region: %s", region)
}
}
config, err := integrationtypes.MarshalJSON(connection.AccountConfig)
if err != nil {
return nil, err
}
account, err := a.AccountsRepo.Upsert(
ctx, req.OrgID, integrationtypes.CloudProviderAWS.String(), nil, config,
nil, nil, nil,
)
if err != nil {
return nil, err
}
agentVersion := "v0.0.8"
if connection.AgentConfig.Version != "" {
agentVersion = connection.AgentConfig.Version
}
baseURL := fmt.Sprintf("https://%s.console.aws.amazon.com/cloudformation/home",
connection.AgentConfig.Region)
u, _ := url.Parse(baseURL)
q := u.Query()
q.Set("region", connection.AgentConfig.Region)
u.Fragment = "/stacks/quickcreate"
u.RawQuery = q.Encode()
q = u.Query()
q.Set("stackName", "signoz-integration")
q.Set("templateURL", fmt.Sprintf("https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json", agentVersion))
q.Set("param_SigNozIntegrationAgentVersion", agentVersion)
q.Set("param_SigNozApiUrl", connection.AgentConfig.SigNozAPIUrl)
q.Set("param_SigNozApiKey", connection.AgentConfig.SigNozAPIKey)
q.Set("param_SigNozAccountId", account.ID.StringValue())
q.Set("param_IngestionUrl", connection.AgentConfig.IngestionUrl)
q.Set("param_IngestionKey", connection.AgentConfig.IngestionKey)
return &integrationtypes.GettableAWSConnectionUrl{
AccountId: account.ID.StringValue(),
ConnectionUrl: u.String() + "?&" + q.Encode(), // this format is required by AWS
}, nil
}
func (a *awsProvider) UpdateAccountConfig(ctx context.Context, orgId valuer.UUID, accountId string, configBytes []byte) (any, error) {
config := new(integrationtypes.UpdatableAWSAccountConfig)
err := integrationtypes.UnmarshalJSON(configBytes, config)
if err != nil {
return nil, err
}
if config.Config == nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "account config can't be null")
}
for _, region := range config.Config.EnabledRegions {
if integrationtypes.ValidAWSRegions[region] {
continue
}
return nil, errors.NewInvalidInputf(CodeInvalidAWSRegion, "invalid aws region: %s", region)
}
// account must exist to update config, but it doesn't need to be connected
_, err = a.AccountsRepo.Get(ctx, orgId.String(), a.GetName().String(), accountId)
if err != nil {
return nil, err
}
configBytes, err = integrationtypes.MarshalJSON(config.Config)
if err != nil {
return nil, err
}
accountRecord, err := a.AccountsRepo.Upsert(
ctx, orgId.String(), a.GetName().String(), &accountId, configBytes, nil, nil, nil,
)
if err != nil {
return nil, err
}
return accountRecord.Account(a.GetName()), nil
}

View File

@@ -0,0 +1,368 @@
package implazureprovider
import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"golang.org/x/exp/maps"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/baseprovider"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/store"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
CodeInvalidAzureRegion = errors.MustNewCode("invalid_azure_region")
)
type azureProvider struct {
baseprovider.BaseCloudProvider[*integrationtypes.AzureDefinition, *integrationtypes.AzureServiceConfig]
}
func NewAzureCloudProvider(
logger *slog.Logger,
accountsRepo store.CloudProviderAccountsRepository,
serviceConfigRepo store.ServiceConfigDatabase,
querier querier.Querier,
) (integrationtypes.CloudProvider, error) {
azureServiceDefinitions, err := services.NewAzureCloudProviderServices()
if err != nil {
return nil, err
}
return &azureProvider{
BaseCloudProvider: baseprovider.BaseCloudProvider[*integrationtypes.AzureDefinition, *integrationtypes.AzureServiceConfig]{
Logger: logger,
Querier: querier,
AccountsRepo: accountsRepo,
ServiceConfigRepo: serviceConfigRepo,
ServiceDefinitions: azureServiceDefinitions,
ProviderType: integrationtypes.CloudProviderAzure,
},
}, nil
}
func (a *azureProvider) AgentCheckIn(ctx context.Context, req *integrationtypes.PostableAgentCheckInPayload) (any, error) {
return baseprovider.AgentCheckIn(
&a.BaseCloudProvider,
ctx,
req,
a.getAzureAgentConfig,
)
}
func (a *azureProvider) getAzureAgentConfig(ctx context.Context, account *integrationtypes.CloudIntegration) (*integrationtypes.AzureAgentIntegrationConfig, error) {
// prepare and return integration config to be consumed by agent
agentConfig := &integrationtypes.AzureAgentIntegrationConfig{
TelemetryCollectionStrategy: make(map[string]*integrationtypes.AzureCollectionStrategy),
}
accountConfig := new(integrationtypes.AzureAccountConfig)
err := integrationtypes.UnmarshalJSON([]byte(account.Config), accountConfig)
if err != nil {
return nil, err
}
if account.Config != "" {
agentConfig.DeploymentRegion = accountConfig.DeploymentRegion
agentConfig.EnabledResourceGroups = accountConfig.EnabledResourceGroups
}
svcConfigs, err := a.ServiceConfigRepo.GetAllForAccount(
ctx, account.OrgID, account.ID.StringValue(),
)
if err != nil {
return nil, err
}
// accumulate config in a fixed order to ensure same config generated across runs
configuredServices := maps.Keys(svcConfigs)
slices.Sort(configuredServices)
for _, svcType := range configuredServices {
definition, err := a.ServiceDefinitions.GetServiceDefinition(ctx, svcType)
if err != nil {
continue
}
config := svcConfigs[svcType]
serviceConfig := new(integrationtypes.AzureServiceConfig)
err = integrationtypes.UnmarshalJSON(config, serviceConfig)
if err != nil {
continue
}
metrics := make([]*integrationtypes.AzureMetricsStrategy, 0)
logs := make([]*integrationtypes.AzureLogsStrategy, 0)
metricsStrategyMap := make(map[string]*integrationtypes.AzureMetricsStrategy)
logsStrategyMap := make(map[string]*integrationtypes.AzureLogsStrategy)
if definition.Strategy != nil && definition.Strategy.Metrics != nil {
for _, metric := range definition.Strategy.Metrics {
metricsStrategyMap[metric.Name] = metric
}
}
if definition.Strategy != nil && definition.Strategy.Logs != nil {
for _, log := range definition.Strategy.Logs {
logsStrategyMap[log.Name] = log
}
}
if serviceConfig.Metrics != nil {
for _, metric := range serviceConfig.Metrics {
if metric.Enabled {
metrics = append(metrics, &integrationtypes.AzureMetricsStrategy{
CategoryType: metricsStrategyMap[metric.Name].CategoryType,
Name: metric.Name,
})
}
}
}
if serviceConfig.Logs != nil {
for _, log := range serviceConfig.Logs {
if log.Enabled {
logs = append(logs, &integrationtypes.AzureLogsStrategy{
CategoryType: logsStrategyMap[log.Name].CategoryType,
Name: log.Name,
})
}
}
}
strategy := &integrationtypes.AzureCollectionStrategy{
Metrics: metrics,
Logs: logs,
}
agentConfig.TelemetryCollectionStrategy[svcType] = strategy
}
return agentConfig, nil
}
func (a *azureProvider) ListServices(ctx context.Context, orgID string, cloudAccountID *string) (any, error) {
svcConfigs := make(map[string]*integrationtypes.AzureServiceConfig)
if cloudAccountID != nil {
activeAccount, err := a.AccountsRepo.GetConnectedCloudAccount(ctx, orgID, a.GetName().String(), *cloudAccountID)
if err != nil {
return nil, err
}
serviceConfigs, err := a.ServiceConfigRepo.GetAllForAccount(ctx, orgID, activeAccount.ID.StringValue())
if err != nil {
return nil, err
}
for svcType, config := range serviceConfigs {
serviceConfig := new(integrationtypes.AzureServiceConfig)
err = integrationtypes.UnmarshalJSON(config, serviceConfig)
if err != nil {
return nil, err
}
svcConfigs[svcType] = serviceConfig
}
}
summaries := make([]integrationtypes.AzureServiceSummary, 0)
definitions, err := a.ServiceDefinitions.ListServiceDefinitions(ctx)
if err != nil {
return nil, err
}
for _, def := range definitions {
summary := integrationtypes.AzureServiceSummary{
DefinitionMetadata: def.DefinitionMetadata,
Config: nil,
}
summary.Config = svcConfigs[summary.Id]
summaries = append(summaries, summary)
}
slices.SortFunc(summaries, func(a, b integrationtypes.AzureServiceSummary) int {
if a.DefinitionMetadata.Title < b.DefinitionMetadata.Title {
return -1
}
if a.DefinitionMetadata.Title > b.DefinitionMetadata.Title {
return 1
}
return 0
})
return &integrationtypes.GettableAzureServices{
Services: summaries,
}, nil
}
func (a *azureProvider) GetServiceDetails(ctx context.Context, req *integrationtypes.GetServiceDetailsReq) (any, error) {
details := new(integrationtypes.GettableAzureServiceDetails)
azureDefinition, err := a.ServiceDefinitions.GetServiceDefinition(ctx, req.ServiceId)
if err != nil {
return nil, err
}
details.Definition = *azureDefinition
if req.CloudAccountID == nil {
return details, nil
}
config, err := a.GetServiceConfig(ctx, azureDefinition, req.OrgID, req.ServiceId, *req.CloudAccountID)
if err != nil {
return nil, err
}
details.Config = config
// fill default values for config
if details.Config == nil {
cfg := new(integrationtypes.AzureServiceConfig)
logs := make([]*integrationtypes.AzureServiceLogsConfig, 0)
if azureDefinition.Strategy != nil && azureDefinition.Strategy.Logs != nil {
for _, log := range azureDefinition.Strategy.Logs {
logs = append(logs, &integrationtypes.AzureServiceLogsConfig{
Enabled: false,
Name: log.Name,
})
}
}
metrics := make([]*integrationtypes.AzureServiceMetricsConfig, 0)
if azureDefinition.Strategy != nil && azureDefinition.Strategy.Metrics != nil {
for _, metric := range azureDefinition.Strategy.Metrics {
metrics = append(metrics, &integrationtypes.AzureServiceMetricsConfig{
Enabled: false,
Name: metric.Name,
})
}
}
cfg.Logs = logs
cfg.Metrics = metrics
details.Config = cfg
}
isMetricsEnabled := details.Config != nil && details.Config.IsMetricsEnabled()
isLogsEnabled := details.Config != nil && details.Config.IsLogsEnabled()
connectionStatus, err := a.GetServiceConnectionStatus(
ctx,
*req.CloudAccountID,
req.OrgID,
azureDefinition,
isMetricsEnabled,
isLogsEnabled,
)
if err != nil {
return nil, err
}
details.ConnectionStatus = connectionStatus
return details, nil
}
func (a *azureProvider) GenerateConnectionArtifact(ctx context.Context, req *integrationtypes.PostableConnectionArtifact) (any, error) {
connection := new(integrationtypes.PostableAzureConnectionCommand)
err := integrationtypes.UnmarshalJSON(req.Data, connection)
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed unmarshal request data into AWS connection config")
}
// validate connection config
if connection.AccountConfig != nil {
if !integrationtypes.ValidAzureRegions[connection.AccountConfig.DeploymentRegion] {
return nil, errors.NewInvalidInputf(CodeInvalidAzureRegion, "invalid azure region: %s",
connection.AccountConfig.DeploymentRegion,
)
}
}
config, err := integrationtypes.MarshalJSON(connection.AccountConfig)
if err != nil {
return nil, err
}
account, err := a.AccountsRepo.Upsert(
ctx, req.OrgID, a.GetName().String(), nil, config,
nil, nil, nil,
)
if err != nil {
return nil, err
}
agentVersion := "v0.0.8"
if connection.AgentConfig.Version != "" {
agentVersion = connection.AgentConfig.Version
}
// TODO: improve the command and set url
cliCommand := []string{"az", "stack", "sub", "create", "--name", "SigNozIntegration", "--location",
connection.AccountConfig.DeploymentRegion, "--template-uri", fmt.Sprintf("<url>%s", agentVersion),
"--action-on-unmanage", "deleteAll", "--deny-settings-mode", "denyDelete", "--parameters", fmt.Sprintf("rgName=%s", "signoz-integration-rg"),
fmt.Sprintf("rgLocation=%s", connection.AccountConfig.DeploymentRegion)}
return &integrationtypes.GettableAzureConnectionCommand{
AccountId: account.ID.String(),
AzureShellConnectionCommand: "az create",
AzureCliConnectionCommand: strings.Join(cliCommand, " "),
}, nil
}
func (a *azureProvider) UpdateAccountConfig(ctx context.Context, orgId valuer.UUID, accountId string, configBytes []byte) (any, error) {
config := new(integrationtypes.UpdatableAzureAccountConfig)
err := integrationtypes.UnmarshalJSON(configBytes, config)
if err != nil {
return nil, err
}
if len(config.Config.EnabledResourceGroups) < 1 {
return nil, errors.NewInvalidInputf(CodeInvalidAzureRegion, "azure region and resource groups must be provided")
}
//for azure, preserve deployment region if already set
account, err := a.AccountsRepo.Get(ctx, orgId.String(), a.GetName().String(), accountId)
if err != nil {
return nil, err
}
storedConfig := new(integrationtypes.AzureAccountConfig)
err = integrationtypes.UnmarshalJSON([]byte(account.Config), storedConfig)
if err != nil {
return nil, err
}
if account.Config != "" {
config.Config.DeploymentRegion = storedConfig.DeploymentRegion
}
configBytes, err = integrationtypes.MarshalJSON(config.Config)
if err != nil {
return nil, err
}
accountRecord, err := a.AccountsRepo.Upsert(
ctx, orgId.String(), a.GetName().String(), &accountId, configBytes, nil, nil, nil,
)
if err != nil {
return nil, err
}
return accountRecord.Account(a.GetName()), nil
}

View File

@@ -1,94 +1 @@
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/integrationtypes"
)
type ServiceSummary struct {
services.Metadata
Config *integrationtypes.CloudServiceConfig `json:"config"`
}
type ServiceDetails struct {
services.Definition
Config *integrationtypes.CloudServiceConfig `json:"config"`
ConnectionStatus *ServiceConnectionStatus `json:"status,omitempty"`
}
type AccountStatus struct {
Integration AccountIntegrationStatus `json:"integration"`
}
type AccountIntegrationStatus struct {
LastHeartbeatTsMillis *int64 `json:"last_heartbeat_ts_ms"`
}
type LogsConfig struct {
Enabled bool `json:"enabled"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
}
type MetricsConfig struct {
Enabled bool `json:"enabled"`
}
type ServiceConnectionStatus struct {
Logs *SignalConnectionStatus `json:"logs"`
Metrics *SignalConnectionStatus `json:"metrics"`
}
type SignalConnectionStatus struct {
LastReceivedTsMillis int64 `json:"last_received_ts_ms"` // epoch milliseconds
LastReceivedFrom string `json:"last_received_from"` // resource identifier
}
type CompiledCollectionStrategy = services.CollectionStrategy
func NewCompiledCollectionStrategy(provider string) (*CompiledCollectionStrategy, error) {
if provider == "aws" {
return &CompiledCollectionStrategy{
Provider: "aws",
AWSMetrics: &services.AWSMetricsStrategy{},
AWSLogs: &services.AWSLogsStrategy{},
}, nil
}
return nil, errors.NewNotFoundf(services.CodeUnsupportedCloudProvider, "unsupported cloud provider: %s", provider)
}
// Helper for accumulating strategies for enabled services.
func AddServiceStrategy(serviceType string, cs *CompiledCollectionStrategy,
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)
}
if cs.Provider == "aws" {
if config.Logs != nil && config.Logs.Enabled {
if serviceType == services.S3Sync {
// S3 bucket sync; No cloudwatch logs are appended for this service type;
// Though definition is populated with a custom cloudwatch group that helps in calculating logs connection status
cs.S3Buckets = config.Logs.S3Buckets
} else if definitionStrat.AWSLogs != nil { // services that includes a logs subscription
cs.AWSLogs.Subscriptions = append(
cs.AWSLogs.Subscriptions,
definitionStrat.AWSLogs.Subscriptions...,
)
}
}
if config.Metrics != nil && config.Metrics.Enabled && definitionStrat.AWSMetrics != nil {
cs.AWSMetrics.StreamFilters = append(
cs.AWSMetrics.StreamFilters,
definitionStrat.AWSMetrics.StreamFilters...,
)
}
return nil
}
return errors.NewNotFoundf(services.CodeUnsupportedCloudProvider, "unsupported cloud provider: %s", cs.Provider)
}

View File

@@ -0,0 +1,37 @@
package cloudintegrations
import (
"log/slog"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/implawsprovider"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/implazureprovider"
integrationstore "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/store"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
)
func NewCloudProviderRegistry(
logger *slog.Logger,
store sqlstore.SQLStore,
querier querier.Querier,
) (map[integrationtypes.CloudProviderType]integrationtypes.CloudProvider, error) {
registry := make(map[integrationtypes.CloudProviderType]integrationtypes.CloudProvider)
accountsRepo := integrationstore.NewCloudProviderAccountsRepository(store)
serviceConfigRepo := integrationstore.NewServiceConfigRepository(store)
awsProviderImpl, err := implawsprovider.NewAWSCloudProvider(logger, accountsRepo, serviceConfigRepo, querier)
if err != nil {
return nil, err
}
registry[integrationtypes.CloudProviderAWS] = awsProviderImpl
azureProviderImpl, err := implazureprovider.NewAzureCloudProvider(logger, accountsRepo, serviceConfigRepo, querier)
if err != nil {
return nil, err
}
registry[integrationtypes.CloudProviderAzure] = azureProviderImpl
return registry, nil
}

View File

@@ -1,165 +0,0 @@
package cloudintegrations
import (
"context"
"database/sql"
"fmt"
"time"
"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 ServiceConfigDatabase interface {
get(
ctx context.Context,
orgID string,
cloudAccountId string,
serviceType string,
) (*integrationtypes.CloudServiceConfig, *model.ApiError)
upsert(
ctx context.Context,
orgID string,
cloudProvider string,
cloudAccountId string,
serviceId string,
config integrationtypes.CloudServiceConfig,
) (*integrationtypes.CloudServiceConfig, *model.ApiError)
getAllForAccount(
ctx context.Context,
orgID string,
cloudAccountId string,
) (
configsBySvcId map[string]*integrationtypes.CloudServiceConfig,
apiErr *model.ApiError,
)
}
func newServiceConfigRepository(store sqlstore.SQLStore) (
*serviceConfigSQLRepository, error,
) {
return &serviceConfigSQLRepository{
store: store,
}, nil
}
type serviceConfigSQLRepository struct {
store sqlstore.SQLStore
}
func (r *serviceConfigSQLRepository) get(
ctx context.Context,
orgID string,
cloudAccountId string,
serviceType string,
) (*integrationtypes.CloudServiceConfig, *model.ApiError) {
var result integrationtypes.CloudIntegrationService
err := r.store.BunDB().NewSelect().
Model(&result).
Join("JOIN cloud_integration ci ON ci.id = cis.cloud_integration_id").
Where("ci.org_id = ?", orgID).
Where("ci.id = ?", cloudAccountId).
Where("cis.type = ?", serviceType).
Scan(ctx)
if err == sql.ErrNoRows {
return nil, model.NotFoundError(fmt.Errorf(
"couldn't find config for cloud account %s",
cloudAccountId,
))
} else if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't query cloud service config: %w", err,
))
}
return &result.Config, nil
}
func (r *serviceConfigSQLRepository) upsert(
ctx context.Context,
orgID string,
cloudProvider string,
cloudAccountId string,
serviceId string,
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((*integrationtypes.CloudIntegration)(nil)).
Column("id").
Where("provider = ?", cloudProvider).
Where("account_id = ?", cloudAccountId).
Where("org_id = ?", orgID).
Where("removed_at is NULL").
Where("last_agent_report is not NULL").
Scan(ctx, &cloudIntegrationId)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't query cloud integration id: %w", err,
))
}
serviceConfig := integrationtypes.CloudIntegrationService{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Config: config,
Type: serviceId,
CloudIntegrationID: cloudIntegrationId,
}
_, err = r.store.BunDB().NewInsert().
Model(&serviceConfig).
On("conflict(cloud_integration_id, type) do update set config=excluded.config, updated_at=excluded.updated_at").
Exec(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"could not upsert cloud service config: %w", err,
))
}
return &serviceConfig.Config, nil
}
func (r *serviceConfigSQLRepository) getAllForAccount(
ctx context.Context,
orgID string,
cloudAccountId string,
) (map[string]*integrationtypes.CloudServiceConfig, *model.ApiError) {
serviceConfigs := []integrationtypes.CloudIntegrationService{}
err := r.store.BunDB().NewSelect().
Model(&serviceConfigs).
Join("JOIN cloud_integration ci ON ci.id = cis.cloud_integration_id").
Where("ci.id = ?", cloudAccountId).
Where("ci.org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"could not query service configs from db: %w", err,
))
}
result := map[string]*integrationtypes.CloudServiceConfig{}
for _, r := range serviceConfigs {
result[r.Type] = &r.Config
}
return result, nil
}

View File

@@ -7,6 +7,24 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_ApplicationELB_ConsumedLCUs_count",
"attributes": []
},
{
"key": "aws_ApplicationELB_ProcessedBytes_sum",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{

View File

@@ -7,6 +7,75 @@
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "rest_api",
"display_name": "REST API Metrics",
"checks": [
{
"key": "aws_ApiGateway_Count_count",
"attributes": [
{
"name": "ApiName",
"operator": "EXISTS",
"value": ""
}
]
}
]
},
{
"category": "http_api",
"display_name": "HTTP API Metrics",
"checks": [
{
"key": "aws_ApiGateway_Count_count",
"attributes": [
{
"name": "ApiId",
"operator": "EXISTS",
"value": ""
}
]
}
]
},
{
"category": "websocket_api",
"display_name": "Websocket API Metrics",
"checks": [
{
"key": "aws_ApiGateway_Count_count",
"attributes": [
{
"name": "ApiId",
"operator": "EXISTS",
"value": ""
}
]
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "aws.cloudwatch.log_group_name",
"operator": "ILIKE",
"value": "API-Gateway%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -148,6 +217,146 @@
"name": "aws_ApiGateway_Latency_sum",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4xx_sum",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4xx_max",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4xx_min",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4xx_count",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5xx_sum",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5xx_max",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5xx_min",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5xx_count",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_DataProcessed_sum",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_DataProcessed_max",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_DataProcessed_min",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_DataProcessed_count",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ExecutionError_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ExecutionError_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ExecutionError_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ExecutionError_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ClientError_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ClientError_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ClientError_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ClientError_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationError_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationError_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationError_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationError_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ConnectCount_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ConnectCount_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ConnectCount_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ConnectCount_count",
"unit": "Count",
"type": "Gauge"
}
],
"logs": [

View File

@@ -7,6 +7,24 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_DynamoDB_AccountMaxReads_max",
"attributes": []
},
{
"key": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -391,4 +409,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,24 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_EC2_CPUUtilization_max",
"attributes": []
},
{
"key": "aws_EC2_NetworkIn_max",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -515,4 +533,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,81 @@
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "overview",
"display_name": "Overview",
"checks": [
{
"key": "aws_ECS_CPUUtilization_max",
"attributes": []
},
{
"key": "aws_ECS_MemoryUtilization_max",
"attributes": []
}
]
},
{
"category": "containerinsights",
"display_name": "Container Insights",
"checks": [
{
"key": "aws_ECS_ContainerInsights_NetworkRxBytes_max",
"attributes": []
},
{
"key": "aws_ECS_ContainerInsights_StorageReadBytes_max",
"attributes": []
}
]
},
{
"category": "enhanced_containerinsights",
"display_name": "Enhanced Container Insights",
"checks": [
{
"key": "aws_ECS_ContainerInsights_ContainerCpuUtilization_max",
"attributes": [
{
"name": "TaskId",
"operator": "EXISTS",
"value": ""
}
]
},
{
"key": "aws_ECS_ContainerInsights_TaskMemoryUtilization_max",
"attributes": [
{
"name": "TaskId",
"operator": "EXISTS",
"value": ""
}
]
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "aws.cloudwatch.log_group_name",
"operator": "ILIKE",
"value": "%/ecs/%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{

View File

@@ -7,6 +7,20 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_ElastiCache_CacheHitRate_max",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics":[
{
@@ -1928,7 +1942,7 @@
"unit": "Percent",
"type": "Gauge",
"description": ""
}
}
]
},
"telemetry_collection_strategy": {
@@ -1951,4 +1965,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,37 @@
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_Lambda_Invocations_sum",
"attributes": []
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "aws.cloudwatch.log_group_name",
"operator": "ILIKE",
"value": "/aws/lambda%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{

View File

@@ -7,6 +7,20 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_Kafka_KafkaDataLogsDiskUsed_max",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -1088,4 +1102,3 @@
]
}
}

View File

@@ -7,6 +7,37 @@
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_RDS_CPUUtilization_max",
"attributes": []
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "resources.aws.cloudwatch.log_group_name",
"operator": "ILIKE",
"value": "/aws/rds%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -800,4 +831,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,20 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_SNS_NumberOfMessagesPublished_sum",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -127,4 +141,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,24 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_SQS_SentMessageSize_max",
"attributes": []
},
{
"key": "aws_SQS_NumberOfMessagesSent_sum",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -247,4 +265,4 @@
}
]
}
}
}

View File

@@ -0,0 +1 @@
<svg id="f2f04349-8aee-4413-84c9-a9053611b319" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><defs><linearGradient id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74" x1="9" y1="15.83" x2="9" y2="5.79" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b3b3b3" /><stop offset="0.26" stop-color="#c1c1c1" /><stop offset="1" stop-color="#e6e6e6" /></linearGradient></defs><title>Icon-storage-86</title><path d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z" fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)" /><path d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z" fill="#37c2b1" /><path d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z" fill="#fff" /><path d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z" fill="#37c2b1" /><path d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z" fill="#258277" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,293 @@
{
"id": "blobstorage",
"title": "Blob Storage",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supported_signals": {
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "placeholder",
"attributes": []
}
]
},
{
"category": "transactions",
"display_name": "Transactions",
"checks": [
{
"key": "placeholder",
"attributes": []
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "placeholder",
"operator": "ILIKE",
"value": "%/ecs/%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
}
],
"logs": [
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
},
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
},
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
},
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
}
]
},
"telemetry_collection_strategy": {
"azure_metrics": [
{
"category_type": "metrics",
"name": "Capacity"
},
{
"category_type": "metrics",
"name": "Transaction"
}
],
"azure_logs": [
{
"category_type": "logs",
"name": "StorageRead"
},
{
"category_type": "logs",
"name": "StorageWrite"
},
{
"category_type": "logs",
"name": "StorageDelete"
}
]
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "Blob Storage Overview",
"description": "Overview of Blob Storage",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -0,0 +1,2 @@
Monitor Azure Blob Storage with SigNoz
Collect key Blob Storage metrics and view them with an out of the box dashboard.

View File

@@ -0,0 +1 @@
<svg id="f2f04349-8aee-4413-84c9-a9053611b319" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><defs><linearGradient id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74" x1="9" y1="15.83" x2="9" y2="5.79" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b3b3b3" /><stop offset="0.26" stop-color="#c1c1c1" /><stop offset="1" stop-color="#e6e6e6" /></linearGradient></defs><title>Icon-storage-86</title><path d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z" fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)" /><path d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z" fill="#37c2b1" /><path d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z" fill="#fff" /><path d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z" fill="#37c2b1" /><path d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z" fill="#258277" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,289 @@
{
"id": "frontdoor",
"title": "Front Door",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supported_signals": {
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "overview",
"display_name": "Overview",
"checks": [
{
"key": "placeholder",
"attributes": []
}
]
},
{
"category": "insights",
"display_name": "Blob Storage Insights",
"checks": [
{
"key": "placeholder",
"attributes": []
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "placeholder",
"operator": "ILIKE",
"value": "%/ecs/%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
}
],
"logs": [
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
},
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
},
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
}
]
},
"telemetry_collection_strategy": {
"azure_metrics": [
{
"category_type": "metrics",
"name": "Capacity"
},
{
"category_type": "metrics",
"name": "Transaction"
}
],
"azure_logs": [
{
"category_type": "logs",
"name": "StorageRead"
},
{
"category_type": "logs",
"name": "StorageWrite"
},
{
"category_type": "logs",
"name": "StorageDelete"
}
]
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "Front Door Overview",
"description": "Overview of Blob Storage",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -0,0 +1,2 @@
Monitor Azure Front Door with SigNoz
Collect key Front Door metrics and view them with an out of the box dashboard.

View File

@@ -1,91 +0,0 @@
package services
import (
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
)
type Metadata struct {
Id string `json:"id"`
Title string `json:"title"`
Icon string `json:"icon"`
}
type Definition struct {
Metadata
Overview string `json:"overview"` // markdown
Assets Assets `json:"assets"`
SupportedSignals SupportedSignals `json:"supported_signals"`
DataCollected DataCollected `json:"data_collected"`
Strategy *CollectionStrategy `json:"telemetry_collection_strategy"`
}
type Assets struct {
Dashboards []Dashboard `json:"dashboards"`
}
type SupportedSignals struct {
Logs bool `json:"logs"`
Metrics bool `json:"metrics"`
}
type DataCollected struct {
Logs []CollectedLogAttribute `json:"logs"`
Metrics []CollectedMetric `json:"metrics"`
}
type CollectedLogAttribute struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
}
type CollectedMetric struct {
Name string `json:"name"`
Type string `json:"type"`
Unit string `json:"unit"`
Description string `json:"description"`
}
type CollectionStrategy struct {
Provider string `json:"provider"`
AWSMetrics *AWSMetricsStrategy `json:"aws_metrics,omitempty"`
AWSLogs *AWSLogsStrategy `json:"aws_logs,omitempty"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"` // Only available in S3 Sync Service Type
}
type AWSMetricsStrategy struct {
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
StreamFilters []struct {
// json tags here are in the shape expected by AWS API as detailed at
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
Namespace string `json:"Namespace"`
MetricNames []string `json:"MetricNames,omitempty"`
} `json:"cloudwatch_metric_stream_filters"`
}
type AWSLogsStrategy struct {
Subscriptions []struct {
// subscribe to all logs groups with specified prefix.
// eg: `/aws/rds/`
LogGroupNamePrefix string `json:"log_group_name_prefix"`
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
// "" implies no filtering is required.
FilterPattern string `json:"filter_pattern"`
} `json:"cloudwatch_logs_subscriptions"`
}
type Dashboard struct {
Id string `json:"id"`
Url string `json:"url"`
Title string `json:"title"`
Description string `json:"description"`
Image string `json:"image"`
Definition *dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
}

View File

@@ -2,128 +2,111 @@ package services
import (
"bytes"
"context"
"embed"
"encoding/json"
"fmt"
"io/fs"
"path"
"sort"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
koanfJson "github.com/knadh/koanf/parsers/json"
"golang.org/x/exp/maps"
)
const (
S3Sync = "s3sync"
)
var (
CodeUnsupportedCloudProvider = errors.MustNewCode("unsupported_cloud_provider")
CodeUnsupportedServiceType = errors.MustNewCode("unsupported_service_type")
CodeServiceDefinitionNotFound = errors.MustNewCode("service_definition_not_dound")
CodeUnsupportedCloudProvider = errors.MustNewCode("unsupported_cloud_provider")
CodeUnsupportedServiceType = errors.MustNewCode("unsupported_service_type")
)
func List(cloudProvider string) ([]Definition, *model.ApiError) {
cloudServices, found := supportedServices[cloudProvider]
if !found || cloudServices == nil {
return nil, model.NotFoundError(fmt.Errorf(
"unsupported cloud provider: %s", cloudProvider,
))
}
services := maps.Values(cloudServices)
sort.Slice(services, func(i, j int) bool {
return services[i].Id < services[j].Id
})
return services, nil
type ServicesProvider[T integrationtypes.Definition] struct {
definitions map[string]T
}
func Map(cloudProvider string) (map[string]Definition, error) {
cloudServices, found := supportedServices[cloudProvider]
if !found || cloudServices == nil {
return nil, errors.Newf(errors.TypeNotFound, CodeUnsupportedCloudProvider, "unsupported cloud provider: %s", cloudProvider)
func (a *ServicesProvider[T]) ListServiceDefinitions(ctx context.Context) (map[string]T, error) {
return a.definitions, nil
}
func (a *ServicesProvider[T]) GetServiceDefinition(ctx context.Context, serviceName string) (T, error) {
def, ok := a.definitions[serviceName]
if !ok {
return *new(T), errors.NewNotFoundf(CodeServiceDefinitionNotFound, "azure service definition not found: %s", serviceName)
}
return def, nil
}
func NewAWSCloudProviderServices() (*ServicesProvider[*integrationtypes.AWSDefinition], error) {
definitions, err := readAllServiceDefinitions(integrationtypes.CloudProviderAWS)
if err != nil {
return nil, err
}
serviceDefinitions := make(map[string]*integrationtypes.AWSDefinition)
for id, def := range definitions {
typedDef, ok := def.(*integrationtypes.AWSDefinition)
if !ok {
return nil, errors.NewInternalf(errors.CodeInternal, "invalid type for AWS service definition %s", id)
}
serviceDefinitions[id] = typedDef
}
return &ServicesProvider[*integrationtypes.AWSDefinition]{
definitions: serviceDefinitions,
}, nil
}
func NewAzureCloudProviderServices() (*ServicesProvider[*integrationtypes.AzureDefinition], error) {
definitions, err := readAllServiceDefinitions(integrationtypes.CloudProviderAzure)
if err != nil {
return nil, err
}
serviceDefinitions := make(map[string]*integrationtypes.AzureDefinition)
for id, def := range definitions {
typedDef, ok := def.(*integrationtypes.AzureDefinition)
if !ok {
return nil, errors.NewInternalf(errors.CodeInternal, "invalid type for Azure service definition %s", id)
}
serviceDefinitions[id] = typedDef
}
return &ServicesProvider[*integrationtypes.AzureDefinition]{
definitions: serviceDefinitions,
}, nil
}
// End of API. Logic for reading service definition files follows
//go:embed definitions/*
var definitionFiles embed.FS
func readAllServiceDefinitions(cloudProvider valuer.String) (map[string]any, error) {
rootDirName := "definitions"
cloudProviderDirPath := path.Join(rootDirName, cloudProvider.String())
cloudServices, err := readServiceDefinitionsFromDir(cloudProvider, cloudProviderDirPath)
if err != nil {
return nil, err
}
if len(cloudServices) < 1 {
return nil, errors.NewInternalf(errors.CodeInternal, "no service definitions found in %s", cloudProviderDirPath)
}
return cloudServices, nil
}
func GetServiceDefinition(cloudProvider, serviceType string) (*Definition, error) {
cloudServices := supportedServices[cloudProvider]
if cloudServices == nil {
return nil, errors.Newf(errors.TypeNotFound, CodeUnsupportedCloudProvider, "unsupported cloud provider: %s", cloudProvider)
}
svc, exists := cloudServices[serviceType]
if !exists {
return nil, errors.Newf(errors.TypeNotFound, CodeUnsupportedServiceType, "%s service not found: %s", cloudProvider, serviceType)
}
return &svc, nil
}
// End of API. Logic for reading service definition files follows
// Service details read from ./serviceDefinitions
// { "providerName": { "service_id": {...}} }
var supportedServices map[string]map[string]Definition
func init() {
err := readAllServiceDefinitions()
if err != nil {
panic(fmt.Errorf(
"couldn't read cloud service definitions: %w", err,
))
}
}
//go:embed definitions/*
var definitionFiles embed.FS
func readAllServiceDefinitions() error {
supportedServices = map[string]map[string]Definition{}
rootDirName := "definitions"
cloudProviderDirs, err := fs.ReadDir(definitionFiles, rootDirName)
if err != nil {
return fmt.Errorf("couldn't read dirs in %s: %w", rootDirName, err)
}
for _, d := range cloudProviderDirs {
if !d.IsDir() {
continue
}
cloudProvider := d.Name()
cloudProviderDirPath := path.Join(rootDirName, cloudProvider)
cloudServices, err := readServiceDefinitionsFromDir(cloudProvider, cloudProviderDirPath)
if err != nil {
return fmt.Errorf("couldn't read %s service definitions: %w", cloudProvider, err)
}
if len(cloudServices) < 1 {
return fmt.Errorf("no %s services could be read", cloudProvider)
}
supportedServices[cloudProvider] = cloudServices
}
return nil
}
func readServiceDefinitionsFromDir(cloudProvider string, cloudProviderDirPath string) (
map[string]Definition, error,
) {
func readServiceDefinitionsFromDir(cloudProvider valuer.String, cloudProviderDirPath string) (map[string]any, error) {
svcDefDirs, err := fs.ReadDir(definitionFiles, cloudProviderDirPath)
if err != nil {
return nil, fmt.Errorf("couldn't list integrations dirs: %w", err)
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't list integrations dirs")
}
svcDefs := map[string]Definition{}
svcDefs := make(map[string]any)
for _, d := range svcDefDirs {
if !d.IsDir() {
@@ -133,103 +116,73 @@ func readServiceDefinitionsFromDir(cloudProvider string, cloudProviderDirPath st
svcDirPath := path.Join(cloudProviderDirPath, d.Name())
s, err := readServiceDefinition(cloudProvider, svcDirPath)
if err != nil {
return nil, fmt.Errorf("couldn't read svc definition for %s: %w", d.Name(), err)
return nil, err
}
_, exists := svcDefs[s.Id]
_, exists := svcDefs[s.GetId()]
if exists {
return nil, fmt.Errorf(
"duplicate service definition for id %s at %s", s.Id, d.Name(),
)
return nil, errors.NewInternalf(errors.CodeInternal, "duplicate service definition for id %s at %s", s.GetId(), d.Name())
}
svcDefs[s.Id] = *s
svcDefs[s.GetId()] = s
}
return svcDefs, nil
}
func readServiceDefinition(cloudProvider string, svcDirpath string) (*Definition, error) {
func readServiceDefinition(cloudProvider valuer.String, svcDirpath string) (integrationtypes.Definition, error) {
integrationJsonPath := path.Join(svcDirpath, "integration.json")
serializedSpec, err := definitionFiles.ReadFile(integrationJsonPath)
if err != nil {
return nil, fmt.Errorf(
"couldn't find integration.json in %s: %w",
svcDirpath, err,
)
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read integration definition in %s", svcDirpath)
}
integrationSpec, err := koanfJson.Parser().Unmarshal(serializedSpec)
if err != nil {
return nil, fmt.Errorf(
"couldn't parse integration.json from %s: %w",
integrationJsonPath, err,
)
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't parse integration definition in %s", svcDirpath)
}
hydrated, err := integrations.HydrateFileUris(
integrationSpec, definitionFiles, svcDirpath,
)
hydrated, err := integrations.HydrateFileUris(integrationSpec, definitionFiles, svcDirpath)
if err != nil {
return nil, fmt.Errorf(
"couldn't hydrate files referenced in service definition %s: %w",
integrationJsonPath, err,
)
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't hydrate integration definition in %s", svcDirpath)
}
hydratedSpec := hydrated.(map[string]any)
serviceDef, err := ParseStructWithJsonTagsFromMap[Definition](hydratedSpec)
if err != nil {
return nil, fmt.Errorf(
"couldn't parse hydrated JSON spec read from %s: %w",
integrationJsonPath, err,
)
var serviceDef integrationtypes.Definition
switch cloudProvider {
case integrationtypes.CloudProviderAWS:
serviceDef = &integrationtypes.AWSDefinition{}
case integrationtypes.CloudProviderAzure:
serviceDef = &integrationtypes.AzureDefinition{}
default:
// ideally this shouldn't happen hence throwing internal error
return nil, errors.NewInternalf(errors.CodeInternal, "unsupported cloud provider: %s", cloudProvider)
}
err = validateServiceDefinition(serviceDef)
err = parseStructWithJsonTagsFromMap(hydratedSpec, serviceDef)
if err != nil {
return nil, fmt.Errorf("invalid service definition %s: %w", serviceDef.Id, err)
return nil, err
}
err = serviceDef.Validate()
if err != nil {
return nil, err
}
serviceDef.Strategy.Provider = cloudProvider
return serviceDef, nil
}
func validateServiceDefinition(s *Definition) error {
// Validate dashboard data
seenDashboardIds := map[string]interface{}{}
for _, dd := range s.Assets.Dashboards {
if _, seen := seenDashboardIds[dd.Id]; seen {
return fmt.Errorf("multiple dashboards found with id %s", dd.Id)
}
seenDashboardIds[dd.Id] = nil
}
if s.Strategy == nil {
return fmt.Errorf("telemetry_collection_strategy is required")
}
// potentially more to follow
return nil
}
func ParseStructWithJsonTagsFromMap[StructType any](data map[string]any) (
*StructType, error,
) {
func parseStructWithJsonTagsFromMap(data map[string]any, target interface{}) error {
mapJson, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("couldn't marshal map to json: %w", err)
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't marshal service definition json data")
}
var res StructType
decoder := json.NewDecoder(bytes.NewReader(mapJson))
decoder.DisallowUnknownFields()
err = decoder.Decode(&res)
err = decoder.Decode(target)
if err != nil {
return nil, fmt.Errorf("couldn't unmarshal json back to struct: %w", err)
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't unmarshal service definition json data")
}
return &res, nil
return nil
}

View File

@@ -1,35 +1,3 @@
package services
import (
"testing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/stretchr/testify/require"
)
func TestAvailableServices(t *testing.T) {
require := require.New(t)
// should be able to list available services.
_, apiErr := List("bad-cloud-provider")
require.NotNil(apiErr)
require.Equal(model.ErrorNotFound, apiErr.Type())
awsSvcs, apiErr := List("aws")
require.Nil(apiErr)
require.Greater(len(awsSvcs), 0)
// should be able to get details of a service
_, err := GetServiceDefinition(
"aws", "bad-service-id",
)
require.NotNil(err)
require.True(errors.Ast(err, errors.TypeNotFound))
svc, err := GetServiceDefinition(
"aws", awsSvcs[0].Id,
)
require.Nil(err)
require.Equal(*svc, awsSvcs[0])
}
// TODO: add more tests for services package

View File

@@ -1,4 +1,4 @@
package implcloudintegration
package store
import (
"context"
@@ -11,30 +11,47 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
cloudintegrationtypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var ErrCodeCloudIntegrationAccountNotFound = errors.MustNewCode("cloud_integration_account_not_found")
var (
CodeCloudIntegrationAccountNotFound errors.Code = errors.MustNewCode("cloud_integration_account_not_found")
)
type CloudProviderAccountsRepository interface {
ListConnected(ctx context.Context, orgId string, provider string) ([]integrationtypes.CloudIntegration, error)
Get(ctx context.Context, orgId string, provider string, id string) (*integrationtypes.CloudIntegration, error)
GetConnectedCloudAccount(ctx context.Context, orgId, provider string, accountID string) (*integrationtypes.CloudIntegration, error)
// Insert an account or update it by (cloudProvider, id)
// for specified non-empty fields
Upsert(
ctx context.Context,
orgId string,
provider string,
id *string,
config []byte,
accountId *string,
agentReport *integrationtypes.AgentReport,
removedAt *time.Time,
) (*integrationtypes.CloudIntegration, error)
}
func NewCloudProviderAccountsRepository(store sqlstore.SQLStore) CloudProviderAccountsRepository {
return &cloudProviderAccountsSQLRepository{store: store}
}
// cloudProviderAccountsSQLRepository is a SQL-backed implementation of CloudIntegrationAccountStore.
type cloudProviderAccountsSQLRepository struct {
store sqlstore.SQLStore
}
// NewSQLCloudIntegrationAccountStore constructs a SQL-backed CloudIntegrationAccountStore.
func NewSQLCloudIntegrationAccountStore(store sqlstore.SQLStore) cloudintegrationtypes.CloudIntegrationAccountStore {
return &cloudProviderAccountsSQLRepository{store: store}
}
// -----------------------------
// Account store implementation
// -----------------------------
func (r *cloudProviderAccountsSQLRepository) ListConnected(
ctx context.Context, orgId string, cloudProvider string,
) ([]cloudintegrationtypes.CloudIntegration, error) {
accounts := []cloudintegrationtypes.CloudIntegration{}
) ([]integrationtypes.CloudIntegration, error) {
accounts := []integrationtypes.CloudIntegration{}
err := r.store.BunDB().NewSelect().
Model(&accounts).
@@ -45,6 +62,7 @@ func (r *cloudProviderAccountsSQLRepository) ListConnected(
Where("last_agent_report is not NULL").
Order("created_at").
Scan(ctx)
if err != nil {
slog.ErrorContext(ctx, "error querying connected cloud accounts", "error", err)
return nil, errors.WrapInternalf(err, errors.CodeInternal, "could not query connected cloud accounts")
@@ -55,8 +73,8 @@ func (r *cloudProviderAccountsSQLRepository) ListConnected(
func (r *cloudProviderAccountsSQLRepository) Get(
ctx context.Context, orgId string, provider string, id string,
) (*cloudintegrationtypes.CloudIntegration, error) {
var result cloudintegrationtypes.CloudIntegration
) (*integrationtypes.CloudIntegration, error) {
var result integrationtypes.CloudIntegration
err := r.store.BunDB().NewSelect().
Model(&result).
@@ -64,11 +82,12 @@ func (r *cloudProviderAccountsSQLRepository) Get(
Where("provider = ?", provider).
Where("id = ?", id).
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.WrapNotFoundf(
err,
ErrCodeCloudIntegrationAccountNotFound,
CodeCloudIntegrationAccountNotFound,
"couldn't find account with Id %s", id,
)
}
@@ -81,8 +100,8 @@ func (r *cloudProviderAccountsSQLRepository) Get(
func (r *cloudProviderAccountsSQLRepository) GetConnectedCloudAccount(
ctx context.Context, orgId string, provider string, accountId string,
) (*cloudintegrationtypes.CloudIntegration, error) {
var result cloudintegrationtypes.CloudIntegration
) (*integrationtypes.CloudIntegration, error) {
var result integrationtypes.CloudIntegration
err := r.store.BunDB().NewSelect().
Model(&result).
@@ -94,7 +113,7 @@ func (r *cloudProviderAccountsSQLRepository) GetConnectedCloudAccount(
Scan(ctx)
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.WrapNotFoundf(err, ErrCodeCloudIntegrationAccountNotFound, "couldn't find connected cloud account %s", accountId)
return nil, errors.WrapNotFoundf(err, CodeCloudIntegrationAccountNotFound, "couldn't find connected cloud account %s", accountId)
} else if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't query cloud provider account")
}
@@ -109,9 +128,9 @@ func (r *cloudProviderAccountsSQLRepository) Upsert(
id *string,
config []byte,
accountId *string,
agentReport *cloudintegrationtypes.AgentReport,
agentReport *integrationtypes.AgentReport,
removedAt *time.Time,
) (*cloudintegrationtypes.CloudIntegration, error) {
) (*integrationtypes.CloudIntegration, error) {
// Insert
if id == nil {
temp := valuer.GenerateUUID().StringValue()
@@ -161,7 +180,7 @@ func (r *cloudProviderAccountsSQLRepository) Upsert(
)
}
integration := cloudintegrationtypes.CloudIntegration{
integration := integrationtypes.CloudIntegration{
OrgID: orgId,
Provider: provider,
Identifiable: types.Identifiable{ID: valuer.MustNewUUID(*id)},
@@ -179,6 +198,7 @@ func (r *cloudProviderAccountsSQLRepository) Upsert(
Model(&integration).
On(onConflictClause).
Exec(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't upsert cloud integration account")
}

View File

@@ -1,4 +1,4 @@
package implcloudintegration
package store
import (
"context"
@@ -8,25 +8,48 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
cloudintegrationtypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var ErrCodeServiceConfigNotFound = errors.MustNewCode("service_config_not_found")
var (
CodeServiceConfigNotFound = errors.MustNewCode("service_config_not_found")
)
// serviceConfigSQLRepository is a SQL-backed implementation of CloudIntegrationServiceStore.
type serviceConfigSQLRepository struct {
store sqlstore.SQLStore
type ServiceConfigDatabase interface {
Get(
ctx context.Context,
orgID string,
cloudAccountId string,
serviceType string,
) ([]byte, error)
Upsert(
ctx context.Context,
orgID string,
cloudProvider string,
cloudAccountId string,
serviceId string,
config []byte,
) ([]byte, error)
GetAllForAccount(
ctx context.Context,
orgID string,
cloudAccountId string,
) (
map[string][]byte,
error,
)
}
// NewSQLCloudIntegrationServiceStore constructs a SQL-backed CloudIntegrationServiceStore.
func NewSQLCloudIntegrationServiceStore(store sqlstore.SQLStore) cloudintegrationtypes.CloudIntegrationServiceStore {
func NewServiceConfigRepository(store sqlstore.SQLStore) ServiceConfigDatabase {
return &serviceConfigSQLRepository{store: store}
}
// -----------------------------
// Service config store implementation
// -----------------------------
type serviceConfigSQLRepository struct {
store sqlstore.SQLStore
}
func (r *serviceConfigSQLRepository) Get(
ctx context.Context,
@@ -34,7 +57,7 @@ func (r *serviceConfigSQLRepository) Get(
cloudAccountId string,
serviceType string,
) ([]byte, error) {
var result cloudintegrationtypes.CloudIntegrationService
var result integrationtypes.CloudIntegrationService
err := r.store.BunDB().NewSelect().
Model(&result).
@@ -45,7 +68,7 @@ func (r *serviceConfigSQLRepository) Get(
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.WrapNotFoundf(err, ErrCodeServiceConfigNotFound, "couldn't find config for cloud account %s", cloudAccountId)
return nil, errors.WrapNotFoundf(err, CodeServiceConfigNotFound, "couldn't find config for cloud account %s", cloudAccountId)
}
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't query cloud service config")
@@ -66,7 +89,7 @@ func (r *serviceConfigSQLRepository) Upsert(
// if the account is not connected, we don't need to upsert the config
var cloudIntegrationId string
err := r.store.BunDB().NewSelect().
Model((*cloudintegrationtypes.CloudIntegration)(nil)).
Model((*integrationtypes.CloudIntegration)(nil)).
Column("id").
Where("provider = ?", cloudProvider).
Where("account_id = ?", cloudAccountId).
@@ -78,14 +101,14 @@ func (r *serviceConfigSQLRepository) Upsert(
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.WrapNotFoundf(
err,
ErrCodeCloudIntegrationAccountNotFound,
CodeCloudIntegrationAccountNotFound,
"couldn't find active cloud integration account",
)
}
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't query cloud integration id")
}
serviceConfig := cloudintegrationtypes.CloudIntegrationService{
serviceConfig := integrationtypes.CloudIntegrationService{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
@@ -111,7 +134,7 @@ func (r *serviceConfigSQLRepository) GetAllForAccount(
orgID string,
cloudAccountId string,
) (map[string][]byte, error) {
var serviceConfigs []cloudintegrationtypes.CloudIntegrationService
var serviceConfigs []integrationtypes.CloudIntegrationService
err := r.store.BunDB().NewSelect().
Model(&serviceConfigs).

View File

@@ -6,11 +6,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/thirdpartyapi"
"github.com/SigNoz/signoz/pkg/queryparser"
"log/slog"
"io"
"math"
@@ -25,14 +21,19 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/errors"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/modules/thirdpartyapi"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/metricsexplorer"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/prometheus/prometheus/promql"
@@ -44,7 +45,6 @@ import (
"github.com/SigNoz/signoz/pkg/contextlinks"
traceFunnelsModule "github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/inframetrics"
queues2 "github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/queues"
"github.com/SigNoz/signoz/pkg/query-service/app/logs"
@@ -111,7 +111,7 @@ type APIHandler struct {
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
cloudIntegrationsRegistry map[integrationtypes.CloudProviderType]integrationtypes.CloudProvider
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
@@ -158,9 +158,6 @@ type APIHandlerOpts struct {
// Integrations
IntegrationsController *integrations.Controller
// Cloud Provider Integrations
CloudIntegrationsController *cloudintegrations.Controller
// Log parsing pipelines
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
@@ -174,6 +171,8 @@ type APIHandlerOpts struct {
QueryParserAPI *queryparser.API
Signoz *signoz.SigNoz
Logger *slog.Logger
}
// NewAPIHandler returns an APIHandler
@@ -209,12 +208,21 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
summaryService := metricsexplorer.NewSummaryService(opts.Reader, opts.RuleManager, opts.Signoz.Modules.Dashboard)
//quickFilterModule := quickfilter.NewAPI(opts.QuickFilterModule)
cloudIntegrationsRegistry, err := cloudintegrations.NewCloudProviderRegistry(
opts.Logger,
opts.Signoz.SQLStore,
opts.Signoz.Querier,
)
if err != nil {
return nil, err
}
aH := &APIHandler{
reader: opts.Reader,
temporalityMap: make(map[string]map[v3.Temporality]bool),
ruleManager: opts.RuleManager,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
cloudIntegrationsRegistry: cloudIntegrationsRegistry,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
querier: querier,
querierV2: querierv2,
@@ -1209,13 +1217,19 @@ func (aH *APIHandler) Get(rw http.ResponseWriter, r *http.Request) {
}
dashboard := new(dashboardtypes.Dashboard)
if aH.CloudIntegrationsController.IsCloudIntegrationDashboardUuid(id) {
cloudIntegrationDashboard, apiErr := aH.CloudIntegrationsController.GetDashboardById(ctx, orgID, id)
if apiErr != nil {
render.Error(rw, errorsV2.Wrapf(apiErr, errorsV2.TypeInternal, errorsV2.CodeInternal, "failed to get dashboard"))
if integrationtypes.IsCloudIntegrationDashboardUuid(id) {
cloudProvider, err := integrationtypes.GetCloudProviderFromDashboardID(id)
if err != nil {
render.Error(rw, err)
return
}
dashboard = cloudIntegrationDashboard
integrationDashboard, err := aH.cloudIntegrationsRegistry[cloudProvider].GetDashboard(ctx, id, orgID)
if err != nil {
render.Error(rw, err)
return
}
dashboard = integrationDashboard
} else if aH.IntegrationsController.IsInstalledIntegrationDashboardID(id) {
integrationDashboard, apiErr := aH.IntegrationsController.GetInstalledIntegrationDashboardById(ctx, orgID, id)
if apiErr != nil {
@@ -1279,11 +1293,13 @@ func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
dashboards = append(dashboards, installedIntegrationDashboards...)
}
cloudIntegrationDashboards, apiErr := aH.CloudIntegrationsController.AvailableDashboards(ctx, orgID)
if apiErr != nil {
zap.L().Error("failed to get dashboards for cloud integrations", zap.Error(apiErr))
} else {
dashboards = append(dashboards, cloudIntegrationDashboards...)
for _, provider := range aH.cloudIntegrationsRegistry {
cloudIntegrationDashboards, err := provider.GetAvailableDashboards(ctx, orgID)
if err != nil {
zap.L().Error("failed to get dashboards for cloud integrations", zap.Error(apiErr))
} else {
dashboards = append(dashboards, cloudIntegrationDashboards...)
}
}
gettableDashboards, err := dashboardtypes.NewGettableDashboardsFromDashboards(dashboards)
@@ -3259,15 +3275,15 @@ func (aH *APIHandler) GetIntegrationConnectionStatus(w http.ResponseWriter, r *h
lookbackSeconds = 15 * 60
}
connectionStatus, apiErr := aH.calculateConnectionStatus(
connectionStatus, err := aH.calculateConnectionStatus(
r.Context(), orgID, connectionTests, lookbackSeconds,
)
if apiErr != nil {
RespondError(w, apiErr, "Failed to calculate integration connection status")
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, connectionStatus)
render.Success(w, http.StatusOK, connectionStatus)
}
func (aH *APIHandler) calculateConnectionStatus(
@@ -3275,10 +3291,11 @@ func (aH *APIHandler) calculateConnectionStatus(
orgID valuer.UUID,
connectionTests *integrations.IntegrationConnectionTests,
lookbackSeconds int64,
) (*integrations.IntegrationConnectionStatus, *model.ApiError) {
) (*integrations.IntegrationConnectionStatus, error) {
// Calculate connection status for signals in parallel
result := &integrations.IntegrationConnectionStatus{}
// TODO: migrate to errors package
errors := []*model.ApiError{}
var resultLock sync.Mutex
@@ -3476,12 +3493,14 @@ func (aH *APIHandler) UninstallIntegration(w http.ResponseWriter, r *http.Reques
aH.Respond(w, map[string]interface{}{})
}
// cloud provider integrations
// RegisterCloudIntegrationsRoutes register routes for cloud provider integrations
func (aH *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
subRouter := router.PathPrefix("/api/v1/cloud-integrations").Subrouter()
subRouter.Use(middleware.NewRecovery(aH.Signoz.Instrumentation.Logger()).Wrap)
subRouter.HandleFunc(
"/{cloudProvider}/accounts/generate-connection-url", am.EditAccess(aH.CloudIntegrationsGenerateConnectionUrl),
"/{cloudProvider}/accounts/generate-connection-url", am.EditAccess(aH.CloudIntegrationsGenerateConnectionArtifact),
).Methods(http.MethodPost)
subRouter.HandleFunc(
@@ -3515,198 +3534,137 @@ func (aH *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *mi
subRouter.HandleFunc(
"/{cloudProvider}/services/{serviceId}/config", am.EditAccess(aH.CloudIntegrationsUpdateServiceConfig),
).Methods(http.MethodPost)
}
func (aH *APIHandler) CloudIntegrationsListConnectedAccounts(
w http.ResponseWriter, r *http.Request,
) {
cloudProvider := mux.Vars(r)["cloudProvider"]
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
resp, apiErr := aH.CloudIntegrationsController.ListConnectedAccounts(
r.Context(), claims.OrgID, cloudProvider,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
aH.Respond(w, resp)
}
func (aH *APIHandler) CloudIntegrationsGenerateConnectionUrl(
w http.ResponseWriter, r *http.Request,
) {
cloudProvider := mux.Vars(r)["cloudProvider"]
req := cloudintegrations.GenerateConnectionUrlRequest{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
result, apiErr := aH.CloudIntegrationsController.GenerateConnectionUrl(
r.Context(), claims.OrgID, cloudProvider, req,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
aH.Respond(w, result)
}
func (aH *APIHandler) CloudIntegrationsGetAccountStatus(
w http.ResponseWriter, r *http.Request,
) {
cloudProvider := mux.Vars(r)["cloudProvider"]
accountId := mux.Vars(r)["accountId"]
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
resp, apiErr := aH.CloudIntegrationsController.GetAccountStatus(
r.Context(), claims.OrgID, cloudProvider, accountId,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
aH.Respond(w, resp)
}
func (aH *APIHandler) CloudIntegrationsAgentCheckIn(
w http.ResponseWriter, r *http.Request,
) {
cloudProvider := mux.Vars(r)["cloudProvider"]
req := cloudintegrations.AgentCheckInRequest{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
result, err := aH.CloudIntegrationsController.CheckInAsAgent(
r.Context(), claims.OrgID, cloudProvider, req,
)
func (aH *APIHandler) CloudIntegrationsGenerateConnectionArtifact(w http.ResponseWriter, r *http.Request) {
cloudProviderString := mux.Vars(r)["cloudProvider"]
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, result)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
reqBody, err := io.ReadAll(r.Body)
if err != nil {
render.Error(w, errors.WrapInternalf(err, errors.CodeInternal, "failed to read request body"))
return
}
resp, err := aH.cloudIntegrationsRegistry[cloudProvider].GenerateConnectionArtifact(r.Context(), &integrationtypes.PostableConnectionArtifact{
OrgID: claims.OrgID,
Data: reqBody,
})
if err != nil {
aH.Signoz.Instrumentation.Logger().ErrorContext(r.Context(),
"failed to generate connection artifact for cloud integration",
slog.String("cloudProvider", cloudProviderString),
slog.String("orgID", claims.OrgID),
)
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, resp)
}
func (aH *APIHandler) CloudIntegrationsUpdateAccountConfig(
w http.ResponseWriter, r *http.Request,
) {
cloudProvider := mux.Vars(r)["cloudProvider"]
func (aH *APIHandler) CloudIntegrationsListConnectedAccounts(w http.ResponseWriter, r *http.Request) {
cloudProviderString := mux.Vars(r)["cloudProvider"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
resp, err := aH.cloudIntegrationsRegistry[cloudProvider].ListConnectedAccounts(r.Context(), claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, resp)
}
func (aH *APIHandler) CloudIntegrationsGetAccountStatus(w http.ResponseWriter, r *http.Request) {
cloudProviderString := mux.Vars(r)["cloudProvider"]
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
accountId := mux.Vars(r)["accountId"]
req := cloudintegrations.UpdateAccountConfigRequest{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
resp, err := aH.cloudIntegrationsRegistry[cloudProvider].GetAccountStatus(r.Context(), claims.OrgID, accountId)
if err != nil {
render.Error(w, err)
return
}
result, apiErr := aH.CloudIntegrationsController.UpdateAccountConfig(
r.Context(), claims.OrgID, cloudProvider, accountId, req,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
aH.Respond(w, result)
render.Success(w, http.StatusOK, resp)
}
func (aH *APIHandler) CloudIntegrationsDisconnectAccount(
w http.ResponseWriter, r *http.Request,
) {
cloudProvider := mux.Vars(r)["cloudProvider"]
accountId := mux.Vars(r)["accountId"]
func (aH *APIHandler) CloudIntegrationsAgentCheckIn(w http.ResponseWriter, r *http.Request) {
cloudProviderString := mux.Vars(r)["cloudProvider"]
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
result, apiErr := aH.CloudIntegrationsController.DisconnectAccount(
r.Context(), claims.OrgID, cloudProvider, accountId,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
req := new(integrationtypes.PostableAgentCheckInPayload)
if err = json.NewDecoder(r.Body).Decode(req); err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid request body"))
return
}
aH.Respond(w, result)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
req.OrgID = claims.OrgID
resp, err := aH.cloudIntegrationsRegistry[cloudProvider].AgentCheckIn(r.Context(), req)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, resp)
}
func (aH *APIHandler) CloudIntegrationsListServices(
w http.ResponseWriter, r *http.Request,
) {
cloudProvider := mux.Vars(r)["cloudProvider"]
func (aH *APIHandler) CloudIntegrationsUpdateAccountConfig(w http.ResponseWriter, r *http.Request) {
cloudProviderString := mux.Vars(r)["cloudProvider"]
var cloudAccountId *string
cloudAccountIdQP := r.URL.Query().Get("cloud_account_id")
if len(cloudAccountIdQP) > 0 {
cloudAccountId = &cloudAccountIdQP
}
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
resp, apiErr := aH.CloudIntegrationsController.ListServices(
r.Context(), claims.OrgID, cloudProvider, cloudAccountId,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
aH.Respond(w, resp)
}
func (aH *APIHandler) CloudIntegrationsGetServiceDetails(
w http.ResponseWriter, r *http.Request,
) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
@@ -3718,7 +3676,100 @@ func (aH *APIHandler) CloudIntegrationsGetServiceDetails(
return
}
cloudProvider := mux.Vars(r)["cloudProvider"]
accountId := mux.Vars(r)["accountId"]
reqBody, err := io.ReadAll(r.Body)
if err != nil {
render.Error(w, errors.WrapInternalf(err, errors.CodeInternal, "failed to read request body"))
return
}
resp, err := aH.cloudIntegrationsRegistry[cloudProvider].UpdateAccountConfig(r.Context(), orgID, accountId, reqBody)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, resp)
}
func (aH *APIHandler) CloudIntegrationsDisconnectAccount(w http.ResponseWriter, r *http.Request) {
cloudProviderString := mux.Vars(r)["cloudProvider"]
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
accountId := mux.Vars(r)["accountId"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
result, err := aH.cloudIntegrationsRegistry[cloudProvider].DisconnectAccount(r.Context(), claims.OrgID, accountId)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, result)
}
func (aH *APIHandler) CloudIntegrationsListServices(w http.ResponseWriter, r *http.Request) {
cloudProviderString := mux.Vars(r)["cloudProvider"]
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
var cloudAccountId *string
cloudAccountIdQP := r.URL.Query().Get("cloud_account_id")
if len(cloudAccountIdQP) > 0 {
cloudAccountId = &cloudAccountIdQP
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
resp, err := aH.cloudIntegrationsRegistry[cloudProvider].ListServices(r.Context(), claims.OrgID, cloudAccountId)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, resp)
}
func (aH *APIHandler) CloudIntegrationsGetServiceDetails(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
cloudProviderString := mux.Vars(r)["cloudProvider"]
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
serviceId := mux.Vars(r)["serviceId"]
var cloudAccountId *string
@@ -3728,270 +3779,58 @@ func (aH *APIHandler) CloudIntegrationsGetServiceDetails(
cloudAccountId = &cloudAccountIdQP
}
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
resp, err := aH.CloudIntegrationsController.GetServiceDetails(
r.Context(), claims.OrgID, cloudProvider, serviceId, cloudAccountId,
)
resp, err := aH.cloudIntegrationsRegistry[cloudProvider].GetServiceDetails(r.Context(), &integrationtypes.GetServiceDetailsReq{
OrgID: orgID,
ServiceId: serviceId,
CloudAccountID: cloudAccountId,
})
if err != nil {
render.Error(w, err)
return
}
// Add connection status for the 2 signals.
if cloudAccountId != nil {
connStatus, apiErr := aH.calculateCloudIntegrationServiceConnectionStatus(
r.Context(), orgID, cloudProvider, *cloudAccountId, resp,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
resp.ConnectionStatus = connStatus
}
aH.Respond(w, resp)
render.Success(w, http.StatusOK, resp)
}
func (aH *APIHandler) calculateCloudIntegrationServiceConnectionStatus(
ctx context.Context,
orgID valuer.UUID,
cloudProvider string,
cloudAccountId string,
svcDetails *cloudintegrations.ServiceDetails,
) (*cloudintegrations.ServiceConnectionStatus, *model.ApiError) {
if cloudProvider != "aws" {
// TODO(Raj): Make connection check generic for all providers in a follow up change
return nil, model.BadRequest(
fmt.Errorf("unsupported cloud provider: %s", cloudProvider),
)
}
func (aH *APIHandler) CloudIntegrationsUpdateServiceConfig(w http.ResponseWriter, r *http.Request) {
cloudProviderString := mux.Vars(r)["cloudProvider"]
telemetryCollectionStrategy := svcDetails.Strategy
if telemetryCollectionStrategy == nil {
return nil, model.InternalError(fmt.Errorf(
"service doesn't have telemetry collection strategy: %s", svcDetails.Id,
))
}
result := &cloudintegrations.ServiceConnectionStatus{}
errors := []*model.ApiError{}
var resultLock sync.Mutex
var wg sync.WaitGroup
// Calculate metrics connection status
if telemetryCollectionStrategy.AWSMetrics != nil {
wg.Add(1)
go func() {
defer wg.Done()
metricsConnStatus, apiErr := aH.calculateAWSIntegrationSvcMetricsConnectionStatus(
ctx, cloudAccountId, telemetryCollectionStrategy.AWSMetrics, svcDetails.DataCollected.Metrics,
)
resultLock.Lock()
defer resultLock.Unlock()
if apiErr != nil {
errors = append(errors, apiErr)
} else {
result.Metrics = metricsConnStatus
}
}()
}
// Calculate logs connection status
if telemetryCollectionStrategy.AWSLogs != nil {
wg.Add(1)
go func() {
defer wg.Done()
logsConnStatus, apiErr := aH.calculateAWSIntegrationSvcLogsConnectionStatus(
ctx, orgID, cloudAccountId, telemetryCollectionStrategy.AWSLogs,
)
resultLock.Lock()
defer resultLock.Unlock()
if apiErr != nil {
errors = append(errors, apiErr)
} else {
result.Logs = logsConnStatus
}
}()
}
wg.Wait()
if len(errors) > 0 {
return nil, errors[0]
}
return result, nil
}
func (aH *APIHandler) calculateAWSIntegrationSvcMetricsConnectionStatus(
ctx context.Context,
cloudAccountId string,
strategy *services.AWSMetricsStrategy,
metricsCollectedBySvc []services.CollectedMetric,
) (*cloudintegrations.SignalConnectionStatus, *model.ApiError) {
if strategy == nil || len(strategy.StreamFilters) < 1 {
return nil, nil
}
expectedLabelValues := map[string]string{
"cloud_provider": "aws",
"cloud_account_id": cloudAccountId,
}
metricsNamespace := strategy.StreamFilters[0].Namespace
metricsNamespaceParts := strings.Split(metricsNamespace, "/")
if len(metricsNamespaceParts) >= 2 {
expectedLabelValues["service_namespace"] = metricsNamespaceParts[0]
expectedLabelValues["service_name"] = metricsNamespaceParts[1]
} else {
// metrics for single word namespaces like "CWAgent" do not
// have the service_namespace label populated
expectedLabelValues["service_name"] = metricsNamespaceParts[0]
}
metricNamesCollectedBySvc := []string{}
for _, cm := range metricsCollectedBySvc {
metricNamesCollectedBySvc = append(metricNamesCollectedBySvc, cm.Name)
}
statusForLastReceivedMetric, apiErr := aH.reader.GetLatestReceivedMetric(
ctx, metricNamesCollectedBySvc, expectedLabelValues,
)
if apiErr != nil {
return nil, apiErr
}
if statusForLastReceivedMetric != nil {
return &cloudintegrations.SignalConnectionStatus{
LastReceivedTsMillis: statusForLastReceivedMetric.LastReceivedTsMillis,
LastReceivedFrom: "signoz-aws-integration",
}, nil
}
return nil, nil
}
func (aH *APIHandler) calculateAWSIntegrationSvcLogsConnectionStatus(
ctx context.Context,
orgID valuer.UUID,
cloudAccountId string,
strategy *services.AWSLogsStrategy,
) (*cloudintegrations.SignalConnectionStatus, *model.ApiError) {
if strategy == nil || len(strategy.Subscriptions) < 1 {
return nil, nil
}
logGroupNamePrefix := strategy.Subscriptions[0].LogGroupNamePrefix
if len(logGroupNamePrefix) < 1 {
return nil, nil
}
logsConnTestFilter := &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "cloud.account.id",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
Operator: "=",
Value: cloudAccountId,
},
{
Key: v3.AttributeKey{
Key: "aws.cloudwatch.log_group_name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
Operator: "like",
Value: logGroupNamePrefix + "%",
},
},
}
// TODO(Raj): Receive this as a param from UI in the future.
lookbackSeconds := int64(30 * 60)
qrParams := &v3.QueryRangeParamsV3{
Start: time.Now().UnixMilli() - (lookbackSeconds * 1000),
End: time.Now().UnixMilli(),
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelTypeList,
QueryType: v3.QueryTypeBuilder,
BuilderQueries: map[string]*v3.BuilderQuery{
"A": {
PageSize: 1,
Filters: logsConnTestFilter,
QueryName: "A",
DataSource: v3.DataSourceLogs,
Expression: "A",
AggregateOperator: v3.AggregateOperatorNoOp,
},
},
},
}
queryRes, _, err := aH.querier.QueryRange(
ctx, orgID, qrParams,
)
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"could not query for integration connection status: %w", err,
))
}
if len(queryRes) > 0 && queryRes[0].List != nil && len(queryRes[0].List) > 0 {
lastLog := queryRes[0].List[0]
return &cloudintegrations.SignalConnectionStatus{
LastReceivedTsMillis: lastLog.Timestamp.UnixMilli(),
LastReceivedFrom: "signoz-aws-integration",
}, nil
render.Error(w, err)
return
}
return nil, nil
}
func (aH *APIHandler) CloudIntegrationsUpdateServiceConfig(
w http.ResponseWriter, r *http.Request,
) {
cloudProvider := mux.Vars(r)["cloudProvider"]
serviceId := mux.Vars(r)["serviceId"]
req := cloudintegrations.UpdateServiceConfigRequest{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
result, err := aH.CloudIntegrationsController.UpdateServiceConfig(
r.Context(), claims.OrgID, cloudProvider, serviceId, &req,
)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, result)
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
reqBody, err := io.ReadAll(r.Body)
if err != nil {
render.Error(w, errors.WrapInternalf(err,
errors.CodeInternal,
"failed to read update service config request body",
))
return
}
result, err := aH.cloudIntegrationsRegistry[cloudProvider].UpdateServiceConfig(r.Context(), serviceId, orgID, reqBody)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, result)
}
// logs

View File

@@ -25,7 +25,6 @@ import (
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
@@ -70,11 +69,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
return nil, err
}
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
if err != nil {
return nil, err
}
cacheForTraceDetail, err := memorycache.New(context.TODO(), signoz.Instrumentation.ToProviderSettings(), cache.Config{
Provider: "memory",
Memory: cache.Memory{
@@ -126,13 +120,13 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
Reader: reader,
RuleManager: rm,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: nooplicensing.NewLicenseAPI(),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
Logger: signoz.Instrumentation.Logger(),
}, config)
if err != nil {
return nil, err

View File

@@ -0,0 +1,13 @@
package utils
import (
"runtime/debug"
)
func RecoverPanic(callback func(err interface{}, stack []byte)) {
if r := recover(); r != nil {
if callback != nil {
callback(r, debug.Stack())
}
}
}

View File

@@ -24,8 +24,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
"github.com/SigNoz/signoz/pkg/modules/services"
"github.com/SigNoz/signoz/pkg/modules/services/implservices"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
@@ -38,23 +36,22 @@ import (
)
type Handlers struct {
SavedView savedview.Handler
Apdex apdex.Handler
Dashboard dashboard.Handler
QuickFilter quickfilter.Handler
TraceFunnel tracefunnel.Handler
RawDataExport rawdataexport.Handler
SpanPercentile spanpercentile.Handler
Services services.Handler
MetricsExplorer metricsexplorer.Handler
Global global.Handler
FlaggerHandler flagger.Handler
GatewayHandler gateway.Handler
Fields fields.Handler
AuthzHandler authz.Handler
ZeusHandler zeus.Handler
QuerierHandler querier.Handler
ServiceAccountHandler serviceaccount.Handler
SavedView savedview.Handler
Apdex apdex.Handler
Dashboard dashboard.Handler
QuickFilter quickfilter.Handler
TraceFunnel tracefunnel.Handler
RawDataExport rawdataexport.Handler
SpanPercentile spanpercentile.Handler
Services services.Handler
MetricsExplorer metricsexplorer.Handler
Global global.Handler
FlaggerHandler flagger.Handler
GatewayHandler gateway.Handler
Fields fields.Handler
AuthzHandler authz.Handler
ZeusHandler zeus.Handler
QuerierHandler querier.Handler
}
func NewHandlers(
@@ -71,22 +68,21 @@ func NewHandlers(
zeusService zeus.Zeus,
) Handlers {
return Handlers{
SavedView: implsavedview.NewHandler(modules.SavedView),
Apdex: implapdex.NewHandler(modules.Apdex),
Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings),
QuickFilter: implquickfilter.NewHandler(modules.QuickFilter),
TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel),
RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport),
Services: implservices.NewHandler(modules.Services),
MetricsExplorer: implmetricsexplorer.NewHandler(modules.MetricsExplorer),
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),
Global: signozglobal.NewHandler(global),
FlaggerHandler: flagger.NewHandler(flaggerService),
GatewayHandler: gateway.NewHandler(gatewayService),
Fields: implfields.NewHandler(providerSettings, telemetryMetadataStore),
AuthzHandler: signozauthzapi.NewHandler(authz),
ZeusHandler: zeus.NewHandler(zeusService, licensing),
QuerierHandler: querierHandler,
ServiceAccountHandler: implserviceaccount.NewHandler(modules.ServiceAccount),
SavedView: implsavedview.NewHandler(modules.SavedView),
Apdex: implapdex.NewHandler(modules.Apdex),
Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings),
QuickFilter: implquickfilter.NewHandler(modules.QuickFilter),
TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel),
RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport),
Services: implservices.NewHandler(modules.Services),
MetricsExplorer: implmetricsexplorer.NewHandler(modules.MetricsExplorer),
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),
Global: signozglobal.NewHandler(global),
FlaggerHandler: flagger.NewHandler(flaggerService),
GatewayHandler: gateway.NewHandler(gatewayService),
Fields: implfields.NewHandler(providerSettings, telemetryMetadataStore),
AuthzHandler: signozauthzapi.NewHandler(authz),
ZeusHandler: zeus.NewHandler(zeusService, licensing),
QuerierHandler: querierHandler,
}
}

View File

@@ -27,8 +27,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
"github.com/SigNoz/signoz/pkg/modules/services"
"github.com/SigNoz/signoz/pkg/modules/services/implservices"
"github.com/SigNoz/signoz/pkg/modules/session"
@@ -68,7 +66,6 @@ type Modules struct {
SpanPercentile spanpercentile.Module
MetricsExplorer metricsexplorer.Module
Promote promote.Module
ServiceAccount serviceaccount.Module
}
func NewModules(
@@ -113,6 +110,5 @@ func NewModules(
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings),
}
}

View File

@@ -22,7 +22,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -60,7 +59,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ authz.Handler }{},
struct{ zeus.Handler }{},
struct{ querier.Handler }{},
struct{ serviceaccount.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -255,7 +255,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.AuthzHandler,
handlers.ZeusHandler,
handlers.QuerierHandler,
handlers.ServiceAccountHandler,
),
)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/querybuilder"
@@ -188,7 +189,9 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
}
// spatial_aggregation_cte
if frag, args := b.buildSpatialAggregationCTE(ctx, start, end, query, keys); frag != "" {
if frag, args, err := b.buildSpatialAggregationCTE(ctx, start, end, query, keys); err != nil {
return nil, err
} else if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -519,7 +522,14 @@ func (b *MetricQueryStatementBuilder) buildSpatialAggregationCTE(
_ uint64,
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
_ map[string][]*telemetrytypes.TelemetryFieldKey,
) (string, []any) {
) (string, []any, error) {
if query.Aggregations[0].SpaceAggregation.IsZero() {
return "", nil, errors.Newf(
errors.TypeInvalidInput,
errors.CodeInvalidInput,
"invalid space aggregation, should be one of the following: [`sum`, `avg`, `min`, `max`, `count`, `p50`, `p75`, `p90`, `p95`, `p99`]",
)
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("ts")
@@ -536,7 +546,7 @@ func (b *MetricQueryStatementBuilder) buildSpatialAggregationCTE(
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
q, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return fmt.Sprintf("__spatial_aggregation_cte AS (%s)", q), args
return fmt.Sprintf("__spatial_aggregation_cte AS (%s)", q), args, nil
}
func (b *MetricQueryStatementBuilder) BuildFinalSelect(

View File

@@ -122,7 +122,7 @@ func TestStatementBuilder(t *testing.T) {
expectedErr: nil,
},
{
name: "test_histogram_percentile1",
name: "test_histogram_percentile",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Signal: telemetrytypes.SignalMetrics,
@@ -132,7 +132,6 @@ func TestStatementBuilder(t *testing.T) {
MetricName: "signoz_latency",
Type: metrictypes.HistogramType,
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile95,
},
},
@@ -188,7 +187,7 @@ func TestStatementBuilder(t *testing.T) {
expectedErr: nil,
},
{
name: "test_histogram_percentile2",
name: "test_histogram_percentile",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Signal: telemetrytypes.SignalMetrics,
@@ -198,7 +197,6 @@ func TestStatementBuilder(t *testing.T) {
MetricName: "http_server_duration_bucket",
Type: metrictypes.HistogramType,
Temporality: metrictypes.Cumulative,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile95,
},
},
@@ -213,7 +211,7 @@ func TestStatementBuilder(t *testing.T) {
},
expected: qbtypes.Statement{
Query: "WITH __temporal_aggregation_cte AS (SELECT ts, `service.name`, `le`, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(30)) AS ts, `service.name`, `le`, max(value) AS per_series_value FROM signoz_metrics.distributed_samples_v4 AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'service.name') AS `service.name`, JSONExtractString(labels, 'le') AS `le` FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint, `service.name`, `le`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts, `service.name`, `le` ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, `service.name`, `le`, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts, `service.name`, `le`) SELECT ts, `service.name`, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.950) AS value FROM __spatial_aggregation_cte GROUP BY `service.name`, ts ORDER BY `service.name`, ts",
Args: []any{"http_server_duration_bucket", uint64(1747936800000), uint64(1747983420000), "cumulative", false, "http_server_duration_bucket", uint64(1747947360000), uint64(1747983420000), 0},
Args: []any{"http_server_duration_bucket", uint64(1747936800000), uint64(1747983420000), "cumulative", false, "http_server_duration_bucket", uint64(1747947390000), uint64(1747983420000), 0},
},
expectedErr: nil,
},

View File

@@ -1,42 +0,0 @@
package cloudintegrationtypes
import (
"context"
"time"
)
type CloudIntegrationAccountStore interface {
ListConnected(ctx context.Context, orgId string, provider string) ([]CloudIntegration, error)
Get(ctx context.Context, orgId string, provider string, id string) (*CloudIntegration, error)
GetConnectedCloudAccount(ctx context.Context, orgId, provider string, accountID string) (*CloudIntegration, error)
// Insert an account or update it by (cloudProvider, id)
// for specified non-empty fields
Upsert(
ctx context.Context,
orgId string,
provider string,
id *string,
config []byte,
accountId *string,
agentReport *AgentReport,
removedAt *time.Time,
) (*CloudIntegration, error)
}
type CloudIntegrationServiceStore interface {
Get(ctx context.Context, orgID, cloudAccountId, serviceType string) ([]byte, error)
Upsert(
ctx context.Context,
orgID,
cloudProvider,
cloudAccountId,
serviceId string,
config []byte,
) ([]byte, error)
GetAllForAccount(ctx context.Context, orgID, cloudAccountId string) (map[string][]byte, error)
}

View File

@@ -18,7 +18,6 @@ var (
var (
TemplateNameInvitationEmail = TemplateName{valuer.NewString("invitation")}
TemplateNameResetPassword = TemplateName{valuer.NewString("reset_password")}
TemplateNameAPIKeyEvent = TemplateName{valuer.NewString("api_key_event")}
)
type TemplateName struct{ valuer.String }
@@ -29,8 +28,6 @@ func NewTemplateName(name string) (TemplateName, error) {
return TemplateNameInvitationEmail, nil
case TemplateNameResetPassword.StringValue():
return TemplateNameResetPassword, nil
case TemplateNameAPIKeyEvent.StringValue():
return TemplateNameAPIKeyEvent, nil
default:
return TemplateName{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid template name: %s", name)
}

View File

@@ -1,84 +1,24 @@
// NOTE:
// - When Account keyword is used in struct names, it refers cloud integration account. CloudIntegration refers to DB schema.
// - When Account Config keyword is used in struct names, it refers to configuration for cloud integration accounts
// - When Service keyword is used in struct names, it refers to cloud integration service. CloudIntegrationService refers to DB schema.
// where `service` is services provided by each cloud provider like AWS S3, Azure BlobStorage etc.
// - When Service Config keyword is used in struct names, it refers to configuration for cloud integration services
package cloudintegrationtypes
package integrationtypes
import (
"context"
"database/sql/driver"
"encoding/json"
"strings"
"time"
"github.com/uptrace/bun"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
// CloudProviderType type alias
type CloudProviderType struct{ valuer.String }
var (
CloudProviderTypeAWS = CloudProviderType{valuer.NewString("aws")}
CloudProviderTypeAzure = CloudProviderType{valuer.NewString("azure")}
)
var ErrCodeCloudProviderInvalidInput = errors.MustNewCode("invalid_cloud_provider")
// NewCloudProvider returns a new CloudProviderType from a string. It validates the input and returns an error if the input is not valid.
func NewCloudProvider(provider string) (CloudProviderType, error) {
switch provider {
case CloudProviderTypeAWS.StringValue():
return CloudProviderTypeAWS, nil
case CloudProviderTypeAzure.StringValue():
return CloudProviderTypeAzure, nil
default:
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
}
}
var (
AWSIntegrationUserEmail = valuer.MustNewEmail("aws-integration@signoz.io")
AzureIntegrationUserEmail = valuer.MustNewEmail("azure-integration@signoz.io")
)
// CloudIntegrationUserEmails is the list of valid emails for Cloud One Click integrations.
// This is used for validation and restrictions in different contexts, across codebase.
var CloudIntegrationUserEmails = []valuer.Email{
AWSIntegrationUserEmail,
AzureIntegrationUserEmail,
}
func IsCloudIntegrationDashboardUuid(dashboardUuid string) bool {
parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 {
return false
}
return parts[0] == "cloud-integration"
}
// GetCloudIntegrationDashboardID returns the cloud provider from dashboard id, if it's a cloud integration dashboard id.
// throws an error if invalid format or invalid cloud provider is provided in the dashboard id.
func GetCloudProviderFromDashboardID(dashboardUuid string) (CloudProviderType, error) {
parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 {
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid dashboard uuid: %s", dashboardUuid)
}
providerStr := parts[1]
cloudProvider, err := NewCloudProvider(providerStr)
if err != nil {
return CloudProviderType{}, err
}
return cloudProvider, nil
}
// NOTE:
// - When Account keyword is used in struct names, it refers cloud integration account. CloudIntegration refers to DB schema.
// - When Account Config keyword is used in struct names, it refers to configuration for cloud integration accounts
// - When Service keyword is used in struct names, it refers to cloud integration service. CloudIntegrationService refers to DB schema.
// where `service` is services provided by each cloud provider like AWS S3, Azure BlobStorage etc.
// - When Service Config keyword is used in struct names, it refers to configuration for cloud integration services
// Generic utility functions for JSON serialization/deserialization
// this is helpful to return right errors from a common place and avoid repeating the same code in multiple places.
@@ -108,6 +48,42 @@ func MarshalJSON[T any](source *T) ([]byte, error) {
return serialized, nil
}
// CloudProvider defines the interface to be implemented by different cloud providers.
// This is generic interface so it will be accepting and returning generic types instead of concrete.
// It's the cloud provider's responsibility to cast them to appropriate types and validate
type CloudProvider interface {
GetName() CloudProviderType
// AgentCheckIn is called by agent to heartbeat and get latest config in response.
AgentCheckIn(ctx context.Context, req *PostableAgentCheckInPayload) (any, error)
// GenerateConnectionArtifact generates cloud provider specific connection information, client side handles how this information is shown
GenerateConnectionArtifact(ctx context.Context, req *PostableConnectionArtifact) (any, error)
// GetAccountStatus returns agent connection status for a cloud integration account
GetAccountStatus(ctx context.Context, orgID, accountID string) (*GettableAccountStatus, error)
// ListConnectedAccounts lists accounts where agent is connected
ListConnectedAccounts(ctx context.Context, orgID string) (*GettableConnectedAccountsList, error)
// LIstServices return list of services for a cloud provider attached with the accountID. This just returns a summary
ListServices(ctx context.Context, orgID string, accountID *string) (any, error) // returns either GettableAWSServices or GettableAzureServices
// GetServiceDetails returns service definition details for a serviceId. This returns config and other details required to show in service details page on client.
GetServiceDetails(ctx context.Context, req *GetServiceDetailsReq) (any, error)
// GetDashboard returns dashboard json for a give cloud integration service dashboard.
// this only returns the dashboard when account is connected and service is enabled
GetDashboard(ctx context.Context, id string, orgID valuer.UUID) (*dashboardtypes.Dashboard, error)
// GetAvailableDashboards returns list of available dashboards across all connected cloud integration accounts in the org.
// this list gets added to dashboard list page
GetAvailableDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
// UpdateAccountConfig updates cloud integration account config
UpdateAccountConfig(ctx context.Context, orgId valuer.UUID, accountId string, config []byte) (any, error)
// UpdateServiceConfig updates cloud integration service config
UpdateServiceConfig(ctx context.Context, serviceId string, orgID valuer.UUID, config []byte) (any, error)
// DisconnectAccount soft deletes/removes a cloud integration account.
DisconnectAccount(ctx context.Context, orgID, accountID string) (*CloudIntegration, error)
}
// GettableConnectedAccountsList is the response for listing connected accounts for a cloud provider.
type GettableConnectedAccountsList struct {
Accounts []*Account `json:"accounts"`
@@ -287,9 +263,9 @@ func (a *AzureServiceConfig) IsLogsEnabled() bool {
}
func (a *AWSServiceConfig) Validate(def *AWSDefinition) error {
if def.Id != S3Sync.String() && a.Logs != nil && a.Logs.S3Buckets != nil {
if def.Id != S3Sync && a.Logs != nil && a.Logs.S3Buckets != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "s3 buckets can only be added to service-type[%s]", S3Sync)
} else if def.Id == S3Sync.String() && a.Logs != nil && a.Logs.S3Buckets != nil {
} else if def.Id == S3Sync && a.Logs != nil && a.Logs.S3Buckets != nil {
for region := range a.Logs.S3Buckets {
if _, found := ValidAWSRegions[region]; !found {
return errors.NewInvalidInputf(CodeInvalidCloudRegion, "invalid cloud region: %s", region)
@@ -343,10 +319,8 @@ type UpdatableAccountConfigTyped[AccountConfigT any] struct {
Config *AccountConfigT `json:"config"`
}
type (
UpdatableAWSAccountConfig = UpdatableAccountConfigTyped[AWSAccountConfig]
UpdatableAzureAccountConfig = UpdatableAccountConfigTyped[AzureAccountConfig]
)
type UpdatableAWSAccountConfig = UpdatableAccountConfigTyped[AWSAccountConfig]
type UpdatableAzureAccountConfig = UpdatableAccountConfigTyped[AzureAccountConfig]
// AWSAccountConfig is the configuration for AWS cloud integration account
type AWSAccountConfig struct {
@@ -364,10 +338,8 @@ type GettableServices[ServiceSummaryT any] struct {
Services []ServiceSummaryT `json:"services"`
}
type (
GettableAWSServices = GettableServices[AWSServiceSummary]
GettableAzureServices = GettableServices[AzureServiceSummary]
)
type GettableAWSServices = GettableServices[AWSServiceSummary]
type GettableAzureServices = GettableServices[AzureServiceSummary]
// GetServiceDetailsReq is a req struct for getting service definition details
type GetServiceDetailsReq struct {
@@ -382,10 +354,8 @@ type ServiceSummary[ServiceConfigT any] struct {
Config *ServiceConfigT `json:"config"`
}
type (
AWSServiceSummary = ServiceSummary[AWSServiceConfig]
AzureServiceSummary = ServiceSummary[AzureServiceConfig]
)
type AWSServiceSummary = ServiceSummary[AWSServiceConfig]
type AzureServiceSummary = ServiceSummary[AzureServiceConfig]
// GettableServiceDetails is a generic struct for service details used in GetServiceDetails API
type GettableServiceDetails[DefinitionT any, ServiceConfigT any] struct {
@@ -394,10 +364,26 @@ type GettableServiceDetails[DefinitionT any, ServiceConfigT any] struct {
ConnectionStatus *ServiceConnectionStatus `json:"status,omitempty"`
}
type (
GettableAWSServiceDetails = GettableServiceDetails[AWSDefinition, *AWSServiceConfig]
GettableAzureServiceDetails = GettableServiceDetails[AzureDefinition, *AzureServiceConfig]
)
type GettableAWSServiceDetails = GettableServiceDetails[AWSDefinition, *AWSServiceConfig]
type GettableAzureServiceDetails = GettableServiceDetails[AzureDefinition, *AzureServiceConfig]
// Account represents a cloud integration account, this is used for business logic and API responses.
type Account struct {
Id string `json:"id"`
CloudAccountId string `json:"cloud_account_id"`
Config any `json:"config"` // AWSAccountConfig or AzureAccountConfig
Status AccountStatus `json:"status"`
}
// AccountStatus is generic struct for cloud integration account status
type AccountStatus struct {
Integration AccountIntegrationStatus `json:"integration"`
}
// AccountIntegrationStatus stores heartbeat information from agent check in
type AccountIntegrationStatus struct {
LastHeartbeatTsMillis *int64 `json:"last_heartbeat_ts_ms"`
}
// ServiceConnectionStatus represents integration connection status for a particular service
// this struct helps to check ingested data and determines connection status by whether data was ingested or not.
@@ -477,24 +463,6 @@ type CloudIntegration struct {
OrgID string `bun:"org_id,type:text,unique:provider_id"`
}
// Account represents a cloud integration account, this is used for business logic and API responses.
type Account struct {
Id string `json:"id"`
CloudAccountId string `json:"cloud_account_id"`
Config any `json:"config"` // AWSAccountConfig or AzureAccountConfig
Status AccountStatus `json:"status"`
}
// AccountStatus is generic struct for cloud integration account status
type AccountStatus struct {
Integration AccountIntegrationStatus `json:"integration"`
}
// AccountIntegrationStatus stores heartbeat information from agent check in
type AccountIntegrationStatus struct {
LastHeartbeatTsMillis *int64 `json:"last_heartbeat_ts_ms"`
}
func (a *CloudIntegration) Status() AccountStatus {
status := AccountStatus{}
if a.LastAgentReport != nil {
@@ -518,11 +486,11 @@ func (a *CloudIntegration) Account(cloudProvider CloudProviderType) *Account {
}
switch cloudProvider {
case CloudProviderTypeAWS:
case CloudProviderAWS:
config := new(AWSAccountConfig)
_ = UnmarshalJSON([]byte(a.Config), config)
ca.Config = config
case CloudProviderTypeAzure:
case CloudProviderAzure:
config := new(AzureAccountConfig)
_ = UnmarshalJSON([]byte(a.Config), config)
ca.Config = config

View File

@@ -1,4 +1,4 @@
package cloudintegrationtypes
package integrationtypes
import (
"fmt"
@@ -10,7 +10,9 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
)
var S3Sync = valuer.NewString("s3sync")
const (
S3Sync = "s3sync"
)
// Generic interface for cloud service definition.
// This is implemented by AWSDefinition and AzureDefinition, which represent service definitions for AWS and Azure respectively.
@@ -31,10 +33,8 @@ type AWSDefinition = ServiceDefinition[AWSCollectionStrategy]
type AzureDefinition = ServiceDefinition[AzureCollectionStrategy]
// Making AWSDefinition and AzureDefinition satisfy Definition interface, so that they can be used in a generic way
var (
_ Definition = &AWSDefinition{}
_ Definition = &AzureDefinition{}
)
var _ Definition = &AWSDefinition{}
var _ Definition = &AzureDefinition{}
// ServiceDefinition represents generic struct for cloud service, regardless of the cloud provider.
// this struct must satify Definition interface.
@@ -227,7 +227,7 @@ type Dashboard struct {
// GetCloudIntegrationDashboardID returns the dashboard id for a cloud integration, given the cloud provider, service id, and dashboard id.
// This is used to generate unique dashboard ids for cloud integration, and also to parse the dashboard id to get the cloud provider and service id when needed.
func GetCloudIntegrationDashboardID(cloudProvider CloudProviderType, svcId, dashboardId string) string {
func GetCloudIntegrationDashboardID(cloudProvider valuer.String, svcId, dashboardId string) string {
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
}

View File

@@ -1,4 +1,4 @@
package cloudintegrationtypes
package integrationtypes
import (
"github.com/SigNoz/signoz/pkg/errors"

View File

@@ -3,21 +3,74 @@ package integrationtypes
import (
"database/sql/driver"
"encoding/json"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type IntegrationUserEmail string
// CloudProviderType type alias
type CloudProviderType = valuer.String
const (
AWSIntegrationUserEmail IntegrationUserEmail = "aws-integration@signoz.io"
var (
CloudProviderAWS = valuer.NewString("aws")
CloudProviderAzure = valuer.NewString("azure")
)
var AllIntegrationUserEmails = []IntegrationUserEmail{
var (
CodeCloudProviderInvalidInput = errors.MustNewCode("invalid_cloud_provider")
)
// NewCloudProvider returns a new CloudProviderType from a string. It validates the input and returns an error if the input is not valid.
func NewCloudProvider(provider string) (CloudProviderType, error) {
switch provider {
case CloudProviderAWS.String(), CloudProviderAzure.String():
return valuer.NewString(provider), nil
default:
return CloudProviderType{}, errors.NewInvalidInputf(CodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
}
}
var (
AWSIntegrationUserEmail = valuer.MustNewEmail("aws-integration@signoz.io")
AzureIntegrationUserEmail = valuer.MustNewEmail("azure-integration@signoz.io")
)
// CloudIntegrationUserEmails is the list of valid emails for Cloud One Click integrations.
// This is used for validation and restrictions in different contexts, across codebase.
var CloudIntegrationUserEmails = []valuer.Email{
AWSIntegrationUserEmail,
AzureIntegrationUserEmail,
}
func IsCloudIntegrationDashboardUuid(dashboardUuid string) bool {
parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 {
return false
}
return parts[0] == "cloud-integration"
}
// GetCloudIntegrationDashboardID returns the cloud provider from dashboard id, if it's a cloud integration dashboard id.
// throws an error if invalid format or invalid cloud provider is provided in the dashboard id.
func GetCloudProviderFromDashboardID(dashboardUuid string) (CloudProviderType, error) {
parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 {
return valuer.String{}, errors.NewInvalidInputf(CodeCloudProviderInvalidInput, "invalid dashboard uuid: %s", dashboardUuid)
}
providerStr := parts[1]
cloudProvider, err := NewCloudProvider(providerStr)
if err != nil {
return CloudProviderType{}, err
}
return cloudProvider, nil
}
// --------------------------------------------------------------------------
@@ -34,10 +87,10 @@ type InstalledIntegration struct {
OrgID string `json:"org_id" bun:"org_id,type:text,unique:org_id_type,references:organizations(id),on_delete:cascade"`
}
type InstalledIntegrationConfig map[string]any
type InstalledIntegrationConfig map[string]interface{}
// For serializing from db
func (c *InstalledIntegrationConfig) Scan(src any) error {
// Scan scans data from db
func (c *InstalledIntegrationConfig) Scan(src interface{}) error {
var data []byte
switch v := src.(type) {
case []byte:
@@ -51,7 +104,7 @@ func (c *InstalledIntegrationConfig) Scan(src any) error {
return json.Unmarshal(data, c)
}
// For serializing to db
// Value serializes data to db
func (c *InstalledIntegrationConfig) Value() (driver.Value, error) {
filterSetJson, err := json.Marshal(c)
if err != nil {
@@ -59,190 +112,3 @@ func (c *InstalledIntegrationConfig) Value() (driver.Value, error) {
}
return filterSetJson, nil
}
// --------------------------------------------------------------------------
// Cloud integration uses the cloud_integration table
// and cloud_integrations_service table
// --------------------------------------------------------------------------
type CloudIntegration struct {
bun.BaseModel `bun:"table:cloud_integration"`
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"`
LastAgentReport *AgentReport `json:"last_agent_report" bun:"last_agent_report,type:text"`
RemovedAt *time.Time `json:"removed_at" bun:"removed_at,type:timestamp,nullzero"`
OrgID string `bun:"org_id,type:text,unique:provider_id"`
}
func (a *CloudIntegration) Status() AccountStatus {
status := AccountStatus{}
if a.LastAgentReport != nil {
lastHeartbeat := a.LastAgentReport.TimestampMillis
status.Integration.LastHeartbeatTsMillis = &lastHeartbeat
}
return status
}
func (a *CloudIntegration) Account() Account {
ca := Account{Id: a.ID.StringValue(), Status: a.Status()}
if a.AccountID != nil {
ca.CloudAccountId = *a.AccountID
}
if a.Config != nil {
ca.Config = *a.Config
} else {
ca.Config = DefaultAccountConfig()
}
return ca
}
type Account struct {
Id string `json:"id"`
CloudAccountId string `json:"cloud_account_id"`
Config AccountConfig `json:"config"`
Status AccountStatus `json:"status"`
}
type AccountStatus struct {
Integration AccountIntegrationStatus `json:"integration"`
}
type AccountIntegrationStatus struct {
LastHeartbeatTsMillis *int64 `json:"last_heartbeat_ts_ms"`
}
func DefaultAccountConfig() AccountConfig {
return AccountConfig{
EnabledRegions: []string{},
}
}
type AccountConfig struct {
EnabledRegions []string `json:"regions"`
}
// For serializing from db
func (c *AccountConfig) Scan(src any) error {
var data []byte
switch v := src.(type) {
case []byte:
data = v
case string:
data = []byte(v)
default:
return errors.NewInternalf(errors.CodeInternal, "tried to scan from %T instead of string or bytes", src)
}
return json.Unmarshal(data, c)
}
// For serializing to db
func (c *AccountConfig) Value() (driver.Value, error) {
if c == nil {
return nil, errors.NewInternalf(errors.CodeInternal, "cloud account config is nil")
}
serialized, err := json.Marshal(c)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't serialize cloud account config to JSON")
}
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytea
return string(serialized), nil
}
type AgentReport struct {
TimestampMillis int64 `json:"timestamp_millis"`
Data map[string]any `json:"data"`
}
// For serializing from db
func (r *AgentReport) Scan(src any) error {
var data []byte
switch v := src.(type) {
case []byte:
data = v
case string:
data = []byte(v)
default:
return errors.NewInternalf(errors.CodeInternal, "tried to scan from %T instead of string or bytes", src)
}
return json.Unmarshal(data, r)
}
// For serializing to db
func (r *AgentReport) Value() (driver.Value, error) {
if r == nil {
return nil, errors.NewInternalf(errors.CodeInternal, "agent report is nil")
}
serialized, err := json.Marshal(r)
if err != nil {
return nil, errors.WrapInternalf(
err, errors.CodeInternal, "couldn't serialize agent report to JSON",
)
}
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytea
return string(serialized), nil
}
type CloudIntegrationService struct {
bun.BaseModel `bun:"table:cloud_integration_service,alias:cis"`
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"`
}
type CloudServiceLogsConfig struct {
Enabled bool `json:"enabled"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
}
type CloudServiceMetricsConfig struct {
Enabled bool `json:"enabled"`
}
type CloudServiceConfig struct {
Logs *CloudServiceLogsConfig `json:"logs,omitempty"`
Metrics *CloudServiceMetricsConfig `json:"metrics,omitempty"`
}
// For serializing from db
func (c *CloudServiceConfig) Scan(src any) error {
var data []byte
switch src := src.(type) {
case []byte:
data = src
case string:
data = []byte(src)
default:
return errors.NewInternalf(errors.CodeInternal, "tried to scan from %T instead of string or bytes", src)
}
return json.Unmarshal(data, c)
}
// For serializing to db
func (c *CloudServiceConfig) Value() (driver.Value, error) {
if c == nil {
return nil, errors.NewInternalf(errors.CodeInternal, "cloud service config is nil")
}
serialized, err := json.Marshal(c)
if err != nil {
return nil, errors.WrapInternalf(
err, errors.CodeInternal, "couldn't serialize cloud service config to JSON",
)
}
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytea
return string(serialized), nil
}

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