Compare commits

..

3 Commits

Author SHA1 Message Date
Nagesh Bansal
67418bb132 docs: fix the signoz docs troubleshooting guide url 2026-06-25 07:42:57 +05:30
Nagesh Bansal
2e2517449d docs(deploy): title migration guide for both install script and deploy 2026-06-25 06:52:36 +05:30
Nagesh Bansal
0150c55361 docs(deploy): restructure migration guide and add Docker Swarm 2026-06-25 06:38:48 +05:30
15 changed files with 203 additions and 1732 deletions

View File

@@ -1,48 +1,76 @@
# Migrating from the install script to Foundry
# Migrating from the install script and `deploy/` to Foundry
The install script (`install.sh`) and the bundled Compose and Swarm files
under `deploy/` are deprecated in favor of [Foundry][foundry], the supported
way to install and manage SigNoz. This guide moves an existing Docker Compose
or Docker Swarm deployment to Foundry and reattaches your existing volumes, so
your data is preserved.
> [!IMPORTANT]
> The install script is now deprecated and will no longer receive updates.
> This guide is only for **existing** `install.sh` / `deploy/` deployments.
> Setting up SigNoz for the first time? Skip migration and install Foundry
> directly: [SigNoz install docs][install-docs].
This guide walks you through migrating an existing SigNoz deployment running via
Docker Compose to [Foundry](https://signoz.io/docs/install/docker/).
## How it works
> [!NOTE]
> Setting up SigNoz for the first time? You don't need this guide — follow the [SigNoz installation docs](https://signoz.io/docs/install/) instead.
Foundry splits a deployment into two commands:
## Overview
To stay up to date on new installation platforms and patterns, please refer to [Foundry](https://github.com/SigNoz/foundry).
- `foundryctl forge` generates the deployment manifests from a `casting.yaml`.
It never touches running containers, so it is safe to re-run while you
iterate.
- `foundryctl cast` applies those manifests: it (re)creates the containers and
reuses the volumes you point it at.
Two `foundryctl` commands are used throughout this guide:
- **`forge`** — generates deployment manifests from your `casting.yaml`. It does not touch running containers, so it is safe to re-run while you iterate.
- **`cast`** — applies the generated manifests: it creates and starts the containers (and pulls new images).
You write one `casting.yaml`, point a few patches at your existing data
volumes, then cast. The steps below are the same for Compose and Swarm; they
differ only in the casting (step 3) and how you stop the old stack (step 5).
## Prerequisites
- [ ] Install Foundry - `curl -fsSL https://signoz.io/foundry.sh | bash`
## Migration Steps
> [!WARNING]
> **Before proceeding, back up both:**
> - **Your docker volumes** — these hold your data.
> - **Your existing `docker-compose.yaml` (and any config it references)** — keep a copy somewhere safe. The compose manifests are no longer distributed by SigNoz, so this backup is your only way to roll back to your previous setup.
- An existing SigNoz deployment from `install.sh` or `deploy/` (Compose or
Swarm).
- `foundryctl` (installed in step 1).
1. Make a note of the volume names used by your existing deployment for the following components:
- ClickHouse
- SigNoz
- ZooKeeper
## Migrate
> If you used the docker compose file we provided, the volumes will be `signoz-clickhouse`, `signoz-sqlite`, and `signoz-zookeeper-1`.
### 1. Install Foundry
2. Generate your `casting.yaml`. Based on internal testing, the following casting should generate the manifests that mimic the legacy docker compose setup (compare against your backed-up `docker-compose.yaml`). Once created, run `foundryctl forge -f casting.yaml`.
```bash
curl -fsSL https://signoz.io/foundry.sh | bash
```
Foundry has a [Docker Compose example](https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose). Please use it as a reference.
### 2. Keep your rollback path
> [!WARNING]
> If your deployment had more than 1 shard or replica, you will need to adjust your manifest volumes accordingly.
This migration reattaches your existing volumes in place; it does not move or
delete your data. The only destructive action is passing `--volumes` / `-v`
when you stop the old stack (step 5), so avoid that flag.
> [!IMPORTANT]
> The `replica` and `shard` macros below are placeholders. Replace them with the values from your existing ClickHouse configuration (check the `macros` section of your current ClickHouse config, e.g. `config.xml`/`metrika.xml`), otherwise the generated manifests will not match your existing data.
> Keep a copy of your existing `docker-compose.yaml` / stack file (and any
> config it references). SigNoz no longer distributes these files, so this copy
> is your only way to roll back.
### 3. Write your `casting.yaml`
Use the casting for your deployment. Both reproduce the legacy single-node
setup (ClickHouse + ZooKeeper + SQLite) and reattach your existing volumes;
they differ only in `spec.deployment.flavor` and the volume-reuse patch
(Compose volumes have a `name` to replace; Swarm volumes are bare, so the whole
entry is replaced). If your deployment ran more than one shard or replica,
adjust the volume patches accordingly. The
[Docker Compose example][compose-example] is a useful reference.
> [!IMPORTANT]
> The `replica` and `shard` macros are placeholders. Replace them with the
> values from your existing ClickHouse config (the `macros` section of
> `config.xml` / `metrika.xml`), or the generated manifests will not match your
> existing data.
<details>
<summary><b>Docker Compose</b> casting.yaml</summary>
```yaml
# casting.yaml
apiVersion: v1alpha1
kind: Installation
metadata:
@@ -61,8 +89,8 @@ spec:
data:
config-0-0.yaml: |
macros:
replica: "example01-01-1" # replace with your existing ClickHouse replica macro (see legacy configuration files for reference)
shard: "01" # replace with your existing ClickHouse shard macro (see legacy configuration files for reference)
replica: "example01-01-1" # replace with your replica macro
shard: "01" # replace with your shard macro
patches:
- target: "deployment/compose.yaml"
operations:
@@ -80,50 +108,165 @@ spec:
value: root
```
> [!NOTE]
> The `user: root` patch on the ZooKeeper service is required so the container can read/write the data in your reused ZooKeeper volume, which was created with `root`-owned files by the legacy compose setup. Without it, ZooKeeper may fail to start with permission errors.
</details>
If you had custom configurations for features like SMTP or additional ingestion processors/receivers, you will need to include those in your casting file via [patches](https://github.com/SigNoz/foundry/blob/main/docs/concepts/patches.md), [custom configuration](https://github.com/SigNoz/foundry/blob/main/docs/concepts/moldings.md#custom-config-files) or [environment variables](https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md#molding-spec) based on your previous configuration.
<details>
<summary><b>Docker Swarm</b> casting.yaml</summary>
3. Review your manifests, we suggest executing the following checks on your manifests before proceeding:
- [ ] Validate the container images match what your deployment had, Foundry uses `latest` on generation by default.
- [ ] If your signoz version was older than latest, please check the [upgrade path](https://signoz.io/docs/operate/upgrade/) first.
- [ ] Check the produced manifests in `pours/deployment` match your older configurations. Extra consideration and review needs to be done on `compose.yaml` as this will be the main entry point for your new deployment.
- [ ] The configuration files for clickhouse are now in YAML so validate your custom settings are present.
```yaml
# casting.yaml
apiVersion: v1alpha1
kind: Installation
metadata:
name: signoz
spec:
deployment:
flavor: swarm
mode: docker
metastore:
kind: sqlite
telemetrykeeper:
kind: zookeeper
telemetrystore:
spec:
config:
data:
config-0-0.yaml: |
macros:
replica: "example01-01-1" # replace with your replica macro
shard: "01" # replace with your shard macro
patches:
- target: "deployment/compose.yaml"
operations:
- op: replace
path: /volumes/signoz-telemetrykeeper-0-data
value:
name: signoz-zookeeper-1
- op: replace
path: /volumes/signoz-telemetrystore-0-0-data
value:
name: signoz-clickhouse
- op: replace
path: /volumes/signoz-metastore-sqlite-0-data
value:
name: signoz-sqlite
- op: add
path: /services/signoz-telemetrykeeper-zookeeper-0/user
value: root
```
4. Execute a `docker compose down`. **Do not** include parameters such as `--volumes` (or `-v`), as it will wipe the volumes we need to maintain and reuse to avoid data loss.
</details>
> [!NOTE]
> This will generate downtime so please plan accordingly.
> The `user: root` patch on the ZooKeeper service lets the container read and
> write the data in your reused ZooKeeper volume, whose files the legacy setup
> created as `root`. Without it, ZooKeeper may fail to start with permission
> errors.
5. Validate the SigNoz containers are down with `docker ps -a`. Multiple containers cannot bind the same volume.
If you had custom configuration (SMTP, extra ingestion receivers/processors,
or custom ClickHouse settings), carry it over via [patches][patches],
[custom config files][custom-config], or [environment variables][env-vars].
6. Run `foundryctl cast -f casting.yaml`. This will recreate the containers based on the spec. This process will download new container images.
### 4. Generate and review the manifests
```bash
foundryctl forge -f casting.yaml
```
Review `pours/deployment/` before deploying:
- [ ] Container images match your current deployment. Foundry generates with
`latest` by default; if your SigNoz version was older than latest, check the
[upgrade path][upgrade-path] first.
- [ ] The generated manifests match your previous configuration, especially
`compose.yaml` (the new entry point for your deployment).
- [ ] The ClickHouse config is now YAML rather than XML; confirm your custom
settings carried over (see [ClickHouse configuration files][ch-config] for
the XML-to-YAML mapping).
### 5. Stop the old deployment
Use the command for your deployment. Do **not** pass `--volumes` / `-v`; that
would delete the data you are migrating.
```bash
docker compose down # Compose
docker stack rm signoz # Swarm
```
> [!NOTE]
> When `cast` is run, the migration container will execute its migrations.
> This causes downtime, so plan accordingly.
## Verifying the Migration
- SigNoz containers will be up and running.
- Log in to the SigNoz UI and verify that data is present.
- Signoz will run on localhost:8080
- Validate that your data ingestion is receiving data.
- Ingesters will receive data on localhost:4317(grpc) and localhost:4318(http)
- Review the logs from both ClickHouse and ZooKeeper; no errors should be present.
Confirm nothing is still bound to the volumes before continuing:
## Rolling Back
Because step 4 brought the legacy stack down *without* `-v`, your original volumes
are untouched and still hold your data. To roll back:
```bash
docker ps -a
```
- Stop and remove the containers created by Foundry (`docker compose down`, again without `-v`).
- Confirm the containers are gone with `docker ps -a` so nothing else is bound to the volumes.
- Reapply your original docker compose file (`docker compose up -d`). It will reattach to the
existing volumes and restore your prior state.
### 6. Deploy with Foundry
```bash
foundryctl cast -f casting.yaml
```
This recreates the containers against your existing volumes and pulls the
images. The migration container runs the schema migrations as part of `cast`.
**Prefer not to use `cast`?** The manifests in `pours/deployment/` are standard
Docker artifacts you can apply yourself. Run the command from that directory so
the relative config paths resolve:
```bash
cd pours/deployment
docker compose up -d # Compose
docker stack deploy -c compose.yaml signoz # Swarm
```
## Verify
- All SigNoz containers are running.
- The UI is reachable on `http://localhost:8080`, and OTLP on `4317` (gRPC)
and `4318` (HTTP), so already-instrumented apps and saved bookmarks keep
working.
- Your existing data is present in the UI, and new data is being ingested.
- ClickHouse and ZooKeeper logs show no errors.
## Roll back
Step 5 left your volumes untouched, so your data is intact. To return to the
previous setup:
1. Bring down the Foundry deployment (`docker compose down` or
`docker stack rm signoz`, again without `-v`).
2. Confirm the containers are gone with `docker ps -a`.
3. Re-apply your backed-up stack: `docker compose up -d` (Compose) or
`docker stack deploy -c docker-compose.yaml signoz` (Swarm). It reattaches
the same volumes and restores your prior state.
## Troubleshooting
- Please reach out to our community on [Slack](https://signoz.io/slack).
If the migration runs into trouble, see
[Troubleshooting Foundry][troubleshooting] for how to capture what we need to
help (the `--debug` output, the exit code, and your `casting.yaml`), then reach
out on [Slack][slack].
## References
- [SigNoz Docker installation docs](https://signoz.io/docs/install/docker/)
- [SigNoz documentation](https://signoz.io/docs)
- [Foundry](https://github.com/SigNoz/foundry)
- [Foundry][foundry]
- [Casting file reference][casting-ref]
- [Custom config files][custom-config]
- [Patches][patches]
- [SigNoz documentation][signoz-docs]
[foundry]: https://github.com/SigNoz/foundry
[install-docs]: https://signoz.io/docs/install/
[compose-example]: https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose
[patches]: https://github.com/SigNoz/foundry/blob/main/docs/concepts/patches.md
[custom-config]: https://github.com/SigNoz/foundry/blob/main/docs/concepts/moldings.md#custom-config-files
[env-vars]: https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md#molding-spec
[casting-ref]: https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md
[ch-config]: https://clickhouse.com/docs/operations/configuration-files
[upgrade-path]: https://signoz.io/docs/operate/upgrade/
[troubleshooting]: https://signoz.io/docs/setup/foundry/troubleshooting/faq/
[slack]: https://signoz.io/slack
[signoz-docs]: https://signoz.io/docs

View File

@@ -2859,13 +2859,6 @@ components:
publicDashboard:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
type: object
DashboardtypesGettablePublicDashboardDataV2:
properties:
dashboard:
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
publicDashboard:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
type: object
DashboardtypesHistogramBuckets:
properties:
bucketCount:
@@ -16426,138 +16419,6 @@ paths:
summary: Update my organization
tags:
- orgs
/api/v2/public/dashboards/{id}:
get:
deprecated: false
description: This endpoint returns the sanitized v2-shape dashboard data for
public access. Each panel query is reduced to a safe field subset, so filters
and raw query strings are not exposed.
operationId: GetPublicDashboardDataV2
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardDataV2'
status:
type: string
required:
- status
- data
type: object
description: OK
"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:
- anonymous:
- public-dashboard:read
summary: Get public dashboard data (v2)
tags:
- dashboard
/api/v2/public/dashboards/{id}/panels/{key}/query_range:
get:
deprecated: false
description: This endpoint returns query range results for a panel of a v2-shape
public dashboard. The panel is addressed by its key in spec.panels.
operationId: GetPublicDashboardPanelQueryRangeV2
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: key
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"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:
- anonymous:
- public-dashboard:read
summary: Get query range result (v2)
tags:
- dashboard
/api/v2/readyz:
get:
operationId: Readyz

View File

@@ -29,7 +29,6 @@ type module struct {
settings factory.ScopedProviderSettings
querier querier.Querier
licensing licensing.Licensing
tagModule tag.Module
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
@@ -42,7 +41,6 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
settings: scopedProviderSettings,
querier: querier,
licensing: licensing,
tagModule: tagModule,
}
}
@@ -134,49 +132,6 @@ func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.U
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
return nil, err
}
tags, err := module.tagModule.ListForResource(ctx, storableDashboard.OrgID, coretypes.KindDashboard, storableDashboard.ID)
if err != nil {
return nil, err
}
return storableDashboard.ToDashboardV2(tags)
}
func (module *module) GetPublicWidgetQueryRangeV2(ctx context.Context, id valuer.UUID, panelKey, startTimeRaw, endTimeRaw string) (*querybuildertypesv5.QueryRangeResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "dashboard",
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRangeV2",
})
dashboard, err := module.GetDashboardByPublicIDV2(ctx, id)
if err != nil {
return nil, err
}
publicDashboard, err := module.GetPublic(ctx, dashboard.OrgID, dashboard.ID)
if err != nil {
return nil, err
}
startTime, endTime, err := publicDashboard.ResolveTimeRange(startTimeRaw, endTimeRaw)
if err != nil {
return nil, err
}
query, err := dashboard.GetPanelQuery(startTime, endTime, panelKey)
if err != nil {
return nil, err
}
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publicDashboard *dashboardtypes.PublicDashboard) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {

View File

@@ -38,10 +38,6 @@ import type {
GetPublicDashboard200,
GetPublicDashboardData200,
GetPublicDashboardDataPathParameters,
GetPublicDashboardDataV2200,
GetPublicDashboardDataV2PathParameters,
GetPublicDashboardPanelQueryRangeV2200,
GetPublicDashboardPanelQueryRangeV2PathParameters,
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
@@ -1803,217 +1799,6 @@ export const useLockDashboardV2 = <
> => {
return useMutation(getLockDashboardV2MutationOptions(options));
};
/**
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
* @summary Get public dashboard data (v2)
*/
export const getPublicDashboardDataV2 = (
{ id }: GetPublicDashboardDataV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardDataV2200>({
url: `/api/v2/public/dashboards/${id}`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardDataV2QueryKey = ({
id,
}: GetPublicDashboardDataV2PathParameters) => {
return [`/api/v2/public/dashboards/${id}`] as const;
};
export const getGetPublicDashboardDataV2QueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetPublicDashboardDataV2QueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>
> = ({ signal }) => getPublicDashboardDataV2({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardDataV2QueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>
>;
export type GetPublicDashboardDataV2QueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get public dashboard data (v2)
*/
export function useGetPublicDashboardDataV2<
TData = Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardDataV2QueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get public dashboard data (v2)
*/
export const invalidateGetPublicDashboardDataV2 = async (
queryClient: QueryClient,
{ id }: GetPublicDashboardDataV2PathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardDataV2QueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint returns query range results for a panel of a v2-shape public dashboard. The panel is addressed by its key in spec.panels.
* @summary Get query range result (v2)
*/
export const getPublicDashboardPanelQueryRangeV2 = (
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardPanelQueryRangeV2200>({
url: `/api/v2/public/dashboards/${id}/panels/${key}/query_range`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardPanelQueryRangeV2QueryKey = ({
id,
key,
}: GetPublicDashboardPanelQueryRangeV2PathParameters) => {
return [`/api/v2/public/dashboards/${id}/panels/${key}/query_range`] as const;
};
export const getGetPublicDashboardPanelQueryRangeV2QueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
> = ({ signal }) => getPublicDashboardPanelQueryRangeV2({ id, key }, signal);
return {
queryKey,
queryFn,
enabled: !!(id && key),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardPanelQueryRangeV2QueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
>;
export type GetPublicDashboardPanelQueryRangeV2QueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get query range result (v2)
*/
export function useGetPublicDashboardPanelQueryRangeV2<
TData = Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardPanelQueryRangeV2QueryOptions(
{ id, key },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get query range result (v2)
*/
export const invalidateGetPublicDashboardPanelQueryRangeV2 = async (
queryClient: QueryClient,
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key }) },
options,
);
return queryClient;
};
/**
* Same as ListDashboardsV2 but personalized for the calling user: each dashboard carries the caller's `pinned` state, and pinned dashboards float to the top of the requested ordering. Supports the same filter DSL, sort, order, and pagination.
* @summary List dashboards for the current user (v2)

View File

@@ -4878,11 +4878,6 @@ export interface DashboardtypesGettablePublicDashboardDataDTO {
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export interface DashboardtypesGettablePublicDashboardDataV2DTO {
dashboard?: DashboardtypesGettableDashboardV2DTO;
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export enum DashboardtypesPatchOpDTO {
add = 'add',
remove = 'remove',
@@ -10505,29 +10500,6 @@ export type GetMyOrganization200 = {
status: string;
};
export type GetPublicDashboardDataV2PathParameters = {
id: string;
};
export type GetPublicDashboardDataV2200 = {
data: DashboardtypesGettablePublicDashboardDataV2DTO;
/**
* @type string
*/
status: string;
};
export type GetPublicDashboardPanelQueryRangeV2PathParameters = {
id: string;
key: string;
};
export type GetPublicDashboardPanelQueryRangeV2200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status: string;
};
export type Readyz200 = {
data: FactoryResponseDTO;
/**

View File

@@ -421,61 +421,5 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/public/dashboards/{id}", handler.New(provider.authzMiddleware.CheckWithoutClaims(
provider.dashboardHandler.GetPublicDataV2,
authtypes.Relation{Verb: coretypes.VerbRead},
coretypes.ResourceMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
return nil, valuer.UUID{}, err
}
return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs)
}, []string{}), handler.OpenAPIDef{
ID: "GetPublicDashboardDataV2",
Tags: []string{"dashboard"},
Summary: "Get public dashboard data (v2)",
Description: "This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.",
Request: nil,
RequestContentType: "",
Response: new(dashboardtypes.GettablePublicDashboardDataV2),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newAnonymousSecuritySchemes([]string{coretypes.ResourceMetaResourcePublicDashboard.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/public/dashboards/{id}/panels/{key}/query_range", handler.New(provider.authzMiddleware.CheckWithoutClaims(
provider.dashboardHandler.GetPublicWidgetQueryRangeV2,
authtypes.Relation{Verb: coretypes.VerbRead},
coretypes.ResourceMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
return nil, valuer.UUID{}, err
}
return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs)
}, []string{}), handler.OpenAPIDef{
ID: "GetPublicDashboardPanelQueryRangeV2",
Tags: []string{"dashboard"},
Summary: "Get query range result (v2)",
Description: "This endpoint returns query range results for a panel of a v2-shape public dashboard. The panel is addressed by its key in spec.panels.",
Request: nil,
RequestContentType: "",
Response: new(querybuildertypesv5.QueryRangeResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newAnonymousSecuritySchemes([]string{coretypes.ResourceMetaResourcePublicDashboard.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -81,12 +81,6 @@ type Module interface {
DeletePreferencesForUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error
// get the v2 dashboard data by public dashboard id
GetDashboardByPublicIDV2(context.Context, valuer.UUID) (*dashboardtypes.DashboardV2, error)
// gets the query results by panel key and public shared id for a v2 dashboard
GetPublicWidgetQueryRangeV2(ctx context.Context, id valuer.UUID, panelKey, startTimeRaw, endTimeRaw string) (*querybuildertypesv5.QueryRangeResponse, error)
CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error)
ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error)
@@ -105,10 +99,6 @@ type Handler interface {
GetPublicWidgetQueryRange(http.ResponseWriter, *http.Request)
GetPublicDataV2(http.ResponseWriter, *http.Request)
GetPublicWidgetQueryRangeV2(http.ResponseWriter, *http.Request)
UpdatePublic(http.ResponseWriter, *http.Request)
DeletePublic(http.ResponseWriter, *http.Request)

View File

@@ -247,14 +247,6 @@ func (module *module) GetPublicWidgetQueryRange(context.Context, valuer.UUID, ui
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}
func (module *module) GetDashboardByPublicIDV2(_ context.Context, _ valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}
func (module *module) GetPublicWidgetQueryRangeV2(context.Context, valuer.UUID, string, string, string) (*qbtypes.QueryRangeResponse, error) {
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}
func (module *module) GetPublicDashboardSelectorsAndOrg(_ context.Context, _ valuer.UUID, _ []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
return nil, valuer.UUID{}, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}

View File

@@ -376,53 +376,3 @@ func (handler *handler) DeleteV2(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) GetPublicDataV2(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := handler.module.GetDashboardByPublicIDV2(ctx, id)
if err != nil {
render.Error(rw, err)
return
}
publicDashboard, err := handler.module.GetPublic(ctx, dashboard.OrgID, dashboard.ID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, dashboardtypes.NewPublicDashboardDataFromDashboardV2(dashboard, publicDashboard))
}
func (handler *handler) GetPublicWidgetQueryRangeV2(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
panelKey, ok := mux.Vars(r)["key"]
if !ok || panelKey == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "panel key is missing from the path"))
return
}
queryRangeResults, err := handler.module.GetPublicWidgetQueryRangeV2(ctx, id, panelKey, r.URL.Query().Get("startTime"), r.URL.Query().Get("endTime"))
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, queryRangeResults)
}

View File

@@ -1,209 +0,0 @@
package dashboardtypes
import (
"github.com/SigNoz/signoz/pkg/errors"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
)
// ════════════════════════════════════════════════════════════════════════
// Gettable
// ════════════════════════════════════════════════════════════════════════
// GettablePublicDashboardDataV2 is the anonymous-facing payload of a v2 dashboard.
type GettablePublicDashboardDataV2 struct {
Dashboard *GettableDashboardV2 `json:"dashboard"`
PublicDashboard *GettablePublicDasbhboard `json:"publicDashboard"`
}
// NewPublicDashboardDataFromDashboardV2 builds the anonymous v2 payload: panel queries
// are redacted, and only the body fields v1 exposed (name, metadata, tags, spec) are set.
func NewPublicDashboardDataFromDashboardV2(dashboard *DashboardV2, publicDashboard *PublicDashboard) *GettablePublicDashboardDataV2 {
spec := dashboard.Spec
redactPanelQueries(&spec)
return &GettablePublicDashboardDataV2{
Dashboard: &GettableDashboardV2{
DashboardV2MetadataBase: dashboard.DashboardV2MetadataBase,
Name: dashboard.Name,
Tags: tagtypes.NewGettableTagsFromTags(dashboard.Tags),
Spec: spec,
},
PublicDashboard: &GettablePublicDasbhboard{
TimeRangeEnabled: publicDashboard.TimeRangeEnabled,
DefaultTimeRange: publicDashboard.DefaultTimeRange,
PublicPath: publicDashboard.PublicPath(),
},
}
}
// ════════════════════════════════════════════════════════════════════════
// Redaction
// ════════════════════════════════════════════════════════════════════════
func redactPanelQueries(spec *DashboardSpec) {
panels := make(map[string]*Panel, len(spec.Panels))
for key, panel := range spec.Panels {
if panel == nil {
panels[key] = nil
continue
}
redacted := *panel
queries := make([]Query, len(redacted.Spec.Queries))
for i, query := range redacted.Spec.Queries {
query.Spec.Plugin.Spec = redactQuery(query.Spec.Plugin.Spec)
queries[i] = query
}
redacted.Spec.Queries = queries
panels[key] = &redacted
}
spec.Panels = panels
}
func redactQuery(spec any) any {
switch s := spec.(type) {
case *qb.CompositeQuery:
if s == nil {
return spec
}
queries := make([]qb.QueryEnvelope, len(s.Queries))
for i, envelope := range s.Queries {
envelope.Spec = redactLeafQuery(envelope.Spec)
queries[i] = envelope
}
return &qb.CompositeQuery{Queries: queries}
case *BuilderQuerySpec:
if s == nil {
return spec
}
return &BuilderQuerySpec{Spec: redactLeafQuery(s.Spec)}
case *qb.PromQuery:
return redactQueryPtr(s)
case *qb.ClickHouseQuery:
return redactQueryPtr(s)
case *qb.QueryBuilderFormula:
return redactQueryPtr(s)
case *qb.QueryBuilderTraceOperator:
return redactQueryPtr(s)
}
return spec
}
func redactQueryPtr[T any](s *T) any {
if s == nil {
return s
}
redacted := redactLeafQuery(*s).(T)
return &redacted
}
func redactLeafQuery(spec any) any {
switch s := spec.(type) {
case qb.QueryBuilderQuery[qb.LogAggregation]:
return redactBuilderQuery(s)
case qb.QueryBuilderQuery[qb.MetricAggregation]:
return redactBuilderQuery(s)
case qb.QueryBuilderQuery[qb.TraceAggregation]:
return redactBuilderQuery(s)
case qb.PromQuery:
return qb.PromQuery{Name: s.Name, Legend: s.Legend}
case qb.ClickHouseQuery:
return qb.ClickHouseQuery{Name: s.Name, Legend: s.Legend}
case qb.QueryBuilderFormula:
return qb.QueryBuilderFormula{Name: s.Name, Expression: s.Expression, Legend: s.Legend}
case qb.QueryBuilderTraceOperator:
return qb.QueryBuilderTraceOperator{
Name: s.Name,
Expression: s.Expression,
Aggregations: s.Aggregations,
GroupBy: s.GroupBy,
Legend: s.Legend,
}
}
return spec
}
func redactBuilderQuery[T any](q qb.QueryBuilderQuery[T]) qb.QueryBuilderQuery[T] {
return qb.QueryBuilderQuery[T]{
Name: q.Name,
Signal: q.Signal,
Source: q.Source,
Aggregations: q.Aggregations,
GroupBy: q.GroupBy,
Legend: q.Legend,
}
}
// ════════════════════════════════════════════════════════════════════════
// Panel query
// ════════════════════════════════════════════════════════════════════════
func (d *DashboardV2) GetPanelQuery(startTime, endTime uint64, panelKey string) (*qb.QueryRangeRequest, error) {
panel, ok := d.Spec.Panels[panelKey]
if !ok || panel == nil {
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidInput, "panel with key %q doesn't exist", panelKey)
}
// Validator guarantees exactly one query per panel.
if len(panel.Spec.Queries) != 1 {
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "panel %q must have exactly one query", panelKey)
}
query := panel.Spec.Queries[0]
composite, err := buildV5CompositeQueryFromPlugin(query.Spec.Plugin)
if err != nil {
return nil, err
}
// fillGaps lives on the panel visualization; only timeseries and bar chart carry it.
fillGaps := false
switch panelSpec := panel.Spec.Plugin.Spec.(type) {
case *TimeSeriesPanelSpec:
if panelSpec != nil {
fillGaps = panelSpec.Visualization.FillSpans
}
case *BarChartPanelSpec:
if panelSpec != nil {
fillGaps = panelSpec.Visualization.FillSpans
}
}
return &qb.QueryRangeRequest{
SchemaVersion: "v1",
Start: startTime,
End: endTime,
RequestType: query.Kind,
CompositeQuery: composite,
FormatOptions: &qb.FormatOptions{
FillGaps: fillGaps,
FormatTableResultForUI: panel.Spec.Plugin.Kind == PanelKindTable,
},
}, nil
}
func buildV5CompositeQueryFromPlugin(plugin QueryPlugin) (qb.CompositeQuery, error) {
switch spec := plugin.Spec.(type) {
case *qb.CompositeQuery:
if spec == nil {
return qb.CompositeQuery{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "composite query is empty")
}
return *spec, nil
case *BuilderQuerySpec:
if spec == nil {
return qb.CompositeQuery{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "builder query is empty")
}
return wrapEnvelope(qb.QueryTypeBuilder, spec.Spec), nil
case *qb.PromQuery:
return wrapEnvelope(qb.QueryTypePromQL, *spec), nil
case *qb.ClickHouseQuery:
return wrapEnvelope(qb.QueryTypeClickHouseSQL, *spec), nil
case *qb.QueryBuilderFormula:
return wrapEnvelope(qb.QueryTypeFormula, *spec), nil
case *qb.QueryBuilderTraceOperator:
return wrapEnvelope(qb.QueryTypeTraceOperator, *spec), nil
}
return qb.CompositeQuery{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "unsupported query kind %q", plugin.Kind)
}
func wrapEnvelope(queryType qb.QueryType, spec any) qb.CompositeQuery {
return qb.CompositeQuery{Queries: []qb.QueryEnvelope{{Type: queryType, Spec: spec}}}
}

View File

@@ -1,331 +0,0 @@
package dashboardtypes
import (
"testing"
"github.com/SigNoz/signoz/pkg/errors"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDashboardV2GetPanelQuery(t *testing.T) {
t.Run("returns error when the panel does not exist", func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{
Plugin: PanelPlugin{Kind: PanelKindTimeSeries},
Queries: []Query{
{
Kind: qb.RequestTypeTimeSeries,
Spec: QuerySpec{
Plugin: QueryPlugin{
Kind: QueryKindBuilder,
Spec: &BuilderQuerySpec{Spec: qb.QueryBuilderQuery[qb.MetricAggregation]{Name: "A"}},
},
},
},
},
},
},
},
},
}
_, err := dashboard.GetPanelQuery(1, 2, "wrongPanelKey")
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
})
t.Run("returns error when the panel is nil", func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": nil,
},
},
}
_, err := dashboard.GetPanelQuery(1, 2, "panel-1")
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
})
t.Run("returns error unless the panel has exactly one query", func(t *testing.T) {
cases := []struct {
description string
queries []Query
}{
{description: "zero queries", queries: nil},
{description: "two queries", queries: []Query{{}, {}}},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{Queries: tc.queries},
},
},
},
}
_, err := dashboard.GetPanelQuery(1, 2, "panel-1")
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
})
}
})
t.Run("builds a single-envelope request for a builder query", func(t *testing.T) {
builder := qb.QueryBuilderQuery[qb.MetricAggregation]{Name: "A"}
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{
Plugin: PanelPlugin{Kind: PanelKindTimeSeries},
Queries: []Query{
{
Kind: qb.RequestTypeTimeSeries,
Spec: QuerySpec{
Plugin: QueryPlugin{
Kind: QueryKindBuilder,
Spec: &BuilderQuerySpec{Spec: builder},
},
},
},
},
},
},
},
},
}
req, err := dashboard.GetPanelQuery(100, 200, "panel-1")
require.NoError(t, err)
assert.Equal(t, "v1", req.SchemaVersion)
assert.Equal(t, uint64(100), req.Start)
assert.Equal(t, uint64(200), req.End)
assert.Equal(t, qb.RequestTypeTimeSeries, req.RequestType)
require.Len(t, req.CompositeQuery.Queries, 1)
assert.Equal(t, qb.QueryTypeBuilder, req.CompositeQuery.Queries[0].Type)
assert.Equal(t, builder, req.CompositeQuery.Queries[0].Spec)
require.NotNil(t, req.FormatOptions)
assert.False(t, req.FormatOptions.FormatTableResultForUI)
})
t.Run("uses a composite query as-is", func(t *testing.T) {
composite := &qb.CompositeQuery{Queries: []qb.QueryEnvelope{
{Type: qb.QueryTypeBuilder, Spec: qb.QueryBuilderQuery[qb.MetricAggregation]{Name: "A"}},
{Type: qb.QueryTypePromQL, Spec: qb.PromQuery{Name: "B"}},
}}
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{
Plugin: PanelPlugin{Kind: PanelKindTimeSeries},
Queries: []Query{
{
Kind: qb.RequestTypeTimeSeries,
Spec: QuerySpec{
Plugin: QueryPlugin{
Kind: QueryKindComposite,
Spec: composite,
},
},
},
},
},
},
},
},
}
req, err := dashboard.GetPanelQuery(1, 2, "panel-1")
require.NoError(t, err)
assert.Equal(t, composite.Queries, req.CompositeQuery.Queries)
})
t.Run("wraps a leaf query in a single typed envelope", func(t *testing.T) {
cases := []struct {
description string
plugin QueryPlugin
expectedType qb.QueryType
}{
{
description: "promql",
plugin: QueryPlugin{Kind: QueryKindPromQL, Spec: &qb.PromQuery{Name: "A", Query: "up"}},
expectedType: qb.QueryTypePromQL,
},
{
description: "clickhouse",
plugin: QueryPlugin{Kind: QueryKindClickHouseSQL, Spec: &qb.ClickHouseQuery{Name: "A", Query: "SELECT 1"}},
expectedType: qb.QueryTypeClickHouseSQL,
},
{
description: "formula",
plugin: QueryPlugin{Kind: QueryKindFormula, Spec: &qb.QueryBuilderFormula{Name: "F1", Expression: "A / B"}},
expectedType: qb.QueryTypeFormula,
},
{
description: "trace operator",
plugin: QueryPlugin{Kind: QueryKindTraceOperator, Spec: &qb.QueryBuilderTraceOperator{Name: "T1", Expression: "A => B"}},
expectedType: qb.QueryTypeTraceOperator,
},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{
Plugin: PanelPlugin{Kind: PanelKindTimeSeries},
Queries: []Query{
{
Kind: qb.RequestTypeTimeSeries,
Spec: QuerySpec{Plugin: tc.plugin},
},
},
},
},
},
},
}
req, err := dashboard.GetPanelQuery(1, 2, "panel-1")
require.NoError(t, err)
require.Len(t, req.CompositeQuery.Queries, 1)
assert.Equal(t, tc.expectedType, req.CompositeQuery.Queries[0].Type)
})
}
})
t.Run("sets FormatTableResultForUI only for table panels", func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{
Plugin: PanelPlugin{Kind: PanelKindTable},
Queries: []Query{
{
Kind: qb.RequestTypeScalar,
Spec: QuerySpec{
Plugin: QueryPlugin{
Kind: QueryKindBuilder,
Spec: &BuilderQuerySpec{Spec: qb.QueryBuilderQuery[qb.MetricAggregation]{Name: "A"}},
},
},
},
},
},
},
},
},
}
req, err := dashboard.GetPanelQuery(1, 2, "panel-1")
require.NoError(t, err)
require.NotNil(t, req.FormatOptions)
assert.True(t, req.FormatOptions.FormatTableResultForUI)
})
t.Run("sets FillGaps from the panel visualization", func(t *testing.T) {
cases := []struct {
description string
panelPlugin PanelPlugin
expectedFillGaps bool
}{
{
description: "timeseries with fillSpans enabled",
panelPlugin: PanelPlugin{Kind: PanelKindTimeSeries, Spec: &TimeSeriesPanelSpec{Visualization: TimeSeriesVisualization{FillSpans: true}}},
expectedFillGaps: true,
},
{
description: "timeseries with fillSpans disabled",
panelPlugin: PanelPlugin{Kind: PanelKindTimeSeries, Spec: &TimeSeriesPanelSpec{Visualization: TimeSeriesVisualization{FillSpans: false}}},
expectedFillGaps: false,
},
{
description: "bar chart with fillSpans enabled",
panelPlugin: PanelPlugin{Kind: PanelKindBarChart, Spec: &BarChartPanelSpec{Visualization: BarChartVisualization{FillSpans: true}}},
expectedFillGaps: true,
},
{
description: "table panel has no fillSpans",
panelPlugin: PanelPlugin{Kind: PanelKindTable, Spec: &TablePanelSpec{}},
expectedFillGaps: false,
},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{
Plugin: tc.panelPlugin,
Queries: []Query{
{
Kind: qb.RequestTypeTimeSeries,
Spec: QuerySpec{
Plugin: QueryPlugin{
Kind: QueryKindBuilder,
Spec: &BuilderQuerySpec{Spec: qb.QueryBuilderQuery[qb.MetricAggregation]{Name: "A"}},
},
},
},
},
},
},
},
},
}
req, err := dashboard.GetPanelQuery(1, 2, "panel-1")
require.NoError(t, err)
require.NotNil(t, req.FormatOptions)
assert.Equal(t, tc.expectedFillGaps, req.FormatOptions.FillGaps)
})
}
})
t.Run("returns error for an unsupported plugin spec", func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{
Plugin: PanelPlugin{Kind: PanelKindTimeSeries},
Queries: []Query{
{
Kind: qb.RequestTypeTimeSeries,
Spec: QuerySpec{
Plugin: QueryPlugin{
Kind: QueryKindBuilder,
Spec: "not-a-query",
},
},
},
},
},
},
},
},
}
_, err := dashboard.GetPanelQuery(1, 2, "panel-1")
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
})
}

View File

@@ -1,206 +0,0 @@
package dashboardtypes
import (
"testing"
"time"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRedactLeafQuery(t *testing.T) {
t.Run("builder query drops filter, having, limit and keeps display fields", func(t *testing.T) {
input := qb.QueryBuilderQuery[qb.MetricAggregation]{
Name: "A",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qb.MetricAggregation{{MetricName: "system_cpu_usage"}},
GroupBy: []qb.GroupByKey{{}},
Legend: "cpu",
Disabled: true,
StepInterval: qb.Step{Duration: time.Minute},
Filter: &qb.Filter{Expression: "service.name = 'checkout'"},
Having: &qb.Having{},
Limit: 100,
}
redacted, ok := redactLeafQuery(input).(qb.QueryBuilderQuery[qb.MetricAggregation])
require.True(t, ok)
// dropped (not in the v1 whitelist)
assert.Nil(t, redacted.Filter)
assert.Nil(t, redacted.Having)
assert.Zero(t, redacted.Limit)
assert.Zero(t, redacted.StepInterval)
assert.False(t, redacted.Disabled)
// kept (display)
assert.Equal(t, "A", redacted.Name)
assert.Equal(t, telemetrytypes.SignalMetrics, redacted.Signal)
assert.Equal(t, "cpu", redacted.Legend)
require.Len(t, redacted.Aggregations, 1)
assert.Equal(t, "system_cpu_usage", redacted.Aggregations[0].MetricName)
assert.Len(t, redacted.GroupBy, 1)
})
t.Run("promql query drops the raw query, step and disabled", func(t *testing.T) {
redacted, ok := redactLeafQuery(qb.PromQuery{
Name: "A",
Query: "sum(rate(http_requests_total[5m]))",
Step: qb.Step{Duration: time.Minute},
Disabled: true,
Legend: "rps",
}).(qb.PromQuery)
require.True(t, ok)
assert.Empty(t, redacted.Query)
assert.Zero(t, redacted.Step)
assert.False(t, redacted.Disabled)
assert.Equal(t, "A", redacted.Name)
assert.Equal(t, "rps", redacted.Legend)
})
t.Run("clickhouse query drops the raw query string", func(t *testing.T) {
redacted, ok := redactLeafQuery(qb.ClickHouseQuery{
Name: "A",
Query: "SELECT * FROM signoz_logs WHERE user = 'admin'",
Legend: "logs",
}).(qb.ClickHouseQuery)
require.True(t, ok)
assert.Empty(t, redacted.Query)
assert.Equal(t, "A", redacted.Name)
assert.Equal(t, "logs", redacted.Legend)
})
t.Run("formula keeps its expression but drops limit and having", func(t *testing.T) {
redacted, ok := redactLeafQuery(qb.QueryBuilderFormula{
Name: "F1",
Expression: "A / B",
Legend: "ratio",
Limit: 50,
Having: &qb.Having{},
}).(qb.QueryBuilderFormula)
require.True(t, ok)
assert.Equal(t, "A / B", redacted.Expression)
assert.Equal(t, "F1", redacted.Name)
assert.Equal(t, "ratio", redacted.Legend)
assert.Zero(t, redacted.Limit)
assert.Nil(t, redacted.Having)
})
t.Run("trace operator drops filter but keeps expression and aggregations", func(t *testing.T) {
redacted, ok := redactLeafQuery(qb.QueryBuilderTraceOperator{
Name: "T1",
Expression: "A => B",
Aggregations: []qb.TraceAggregation{{}},
Legend: "spans",
Filter: &qb.Filter{Expression: "http.status_code = 500"},
ReturnSpansFrom: "A",
Limit: 10,
}).(qb.QueryBuilderTraceOperator)
require.True(t, ok)
assert.Nil(t, redacted.Filter)
assert.Empty(t, redacted.ReturnSpansFrom)
assert.Zero(t, redacted.Limit)
assert.Equal(t, "A => B", redacted.Expression)
assert.Equal(t, "T1", redacted.Name)
assert.Len(t, redacted.Aggregations, 1)
})
t.Run("unknown value is returned unchanged", func(t *testing.T) {
assert.Equal(t, "passthrough", redactLeafQuery("passthrough"))
})
}
func TestRedactQueryPluginWrappers(t *testing.T) {
t.Run("builder plugin pointer is redacted and stays a pointer", func(t *testing.T) {
plugin := &BuilderQuerySpec{Spec: qb.QueryBuilderQuery[qb.LogAggregation]{
Name: "A",
Filter: &qb.Filter{Expression: "body contains 'secret'"},
}}
result, ok := redactQuery(plugin).(*BuilderQuerySpec)
require.True(t, ok)
builder, ok := result.Spec.(qb.QueryBuilderQuery[qb.LogAggregation])
require.True(t, ok)
assert.Nil(t, builder.Filter)
assert.Equal(t, "A", builder.Name)
})
t.Run("composite plugin redacts every sub-query envelope", func(t *testing.T) {
composite := &qb.CompositeQuery{Queries: []qb.QueryEnvelope{
{Type: qb.QueryTypeBuilder, Spec: qb.QueryBuilderQuery[qb.MetricAggregation]{Name: "A", Filter: &qb.Filter{Expression: "x = 1"}}},
{Type: qb.QueryTypePromQL, Spec: qb.PromQuery{Name: "B", Query: "up"}},
}}
result, ok := redactQuery(composite).(*qb.CompositeQuery)
require.True(t, ok)
require.Len(t, result.Queries, 2)
builder, ok := result.Queries[0].Spec.(qb.QueryBuilderQuery[qb.MetricAggregation])
require.True(t, ok)
assert.Nil(t, builder.Filter)
prom, ok := result.Queries[1].Spec.(qb.PromQuery)
require.True(t, ok)
assert.Empty(t, prom.Query)
})
t.Run("promql plugin pointer drops the raw query", func(t *testing.T) {
result, ok := redactQuery(&qb.PromQuery{Name: "A", Query: "up"}).(*qb.PromQuery)
require.True(t, ok)
assert.Empty(t, result.Query)
assert.Equal(t, "A", result.Name)
})
t.Run("nil composite pointer is returned unchanged without panicking", func(t *testing.T) {
var nilComposite *qb.CompositeQuery
assert.Equal(t, nilComposite, redactQuery(nilComposite))
})
t.Run("unknown plugin spec is returned unchanged", func(t *testing.T) {
assert.Equal(t, 42, redactQuery(42))
})
}
func TestRedactPanelQueries(t *testing.T) {
t.Run("redacts panel queries without mutating the source spec", func(t *testing.T) {
sourcePlugin := &BuilderQuerySpec{Spec: qb.QueryBuilderQuery[qb.MetricAggregation]{
Name: "A",
Filter: &qb.Filter{Expression: "service.name = 'payments'"},
}}
sourcePanel := &Panel{Spec: PanelSpec{
Queries: []Query{{Spec: QuerySpec{Plugin: QueryPlugin{Kind: QueryKindBuilder, Spec: sourcePlugin}}}},
}}
spec := DashboardSpec{Panels: map[string]*Panel{"panel-1": sourcePanel}}
redactPanelQueries(&spec)
// redacted output has no filter
redactedPanel := spec.Panels["panel-1"]
require.NotNil(t, redactedPanel)
redactedBuilder := redactedPanel.Spec.Queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec).Spec.(qb.QueryBuilderQuery[qb.MetricAggregation])
assert.Nil(t, redactedBuilder.Filter)
// source is untouched: original plugin still carries the filter
sourceBuilder := sourcePlugin.Spec.(qb.QueryBuilderQuery[qb.MetricAggregation])
require.NotNil(t, sourceBuilder.Filter)
assert.Equal(t, "service.name = 'payments'", sourceBuilder.Filter.Expression)
assert.NotSame(t, sourcePanel, redactedPanel)
})
t.Run("preserves nil panels without panicking", func(t *testing.T) {
spec := DashboardSpec{Panels: map[string]*Panel{"panel-1": nil}}
redactPanelQueries(&spec)
panel, ok := spec.Panels["panel-1"]
assert.True(t, ok)
assert.Nil(t, panel)
})
}

View File

@@ -2,7 +2,6 @@ package dashboardtypes
import (
"encoding/json"
"strconv"
"time"
"github.com/SigNoz/signoz/pkg/errors"
@@ -213,30 +212,6 @@ func (typ *PublicDashboard) PublicPath() string {
return "/public/dashboard/" + typ.ID.StringValue()
}
// ResolveTimeRange returns the [start, end] window in epoch millis for a public
// widget/panel query: the caller-supplied range when the dashboard allows it,
// otherwise now minus the configured default range.
func (typ *PublicDashboard) ResolveTimeRange(startTimeRaw, endTimeRaw string) (uint64, uint64, error) {
if typ.TimeRangeEnabled {
startTime, err := strconv.ParseUint(startTimeRaw, 10, 64)
if err != nil {
return 0, 0, errors.New(errors.TypeInvalidInput, ErrCodePublicDashboardInvalidInput, "invalid startTime")
}
endTime, err := strconv.ParseUint(endTimeRaw, 10, 64)
if err != nil {
return 0, 0, errors.New(errors.TypeInvalidInput, ErrCodePublicDashboardInvalidInput, "invalid endTime")
}
return startTime, endTime, nil
}
timeRange, err := time.ParseDuration(typ.DefaultTimeRange)
if err != nil {
return 0, 0, errors.WrapInternalf(err, errors.CodeInternal, "stored defaultTimeRange %q is not a valid duration", typ.DefaultTimeRange)
}
now := time.Now()
return uint64(now.Add(-timeRange).UnixMilli()), uint64(now.UnixMilli()), nil
}
func (typ *PostablePublicDashboard) UnmarshalJSON(data []byte) error {
type alias PostablePublicDashboard
var temp alias

View File

@@ -1,117 +0,0 @@
package dashboardtypes
import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPublicDashboardResolveTimeRange(t *testing.T) {
t.Run("returns the explicit range when time range is enabled", func(t *testing.T) {
cases := []struct {
description string
startTimeRaw string
endTimeRaw string
expectedStart uint64
expectedEnd uint64
}{
{
description: "valid epoch millis",
startTimeRaw: "1700000000000",
endTimeRaw: "1700000600000",
expectedStart: 1700000000000,
expectedEnd: 1700000600000,
},
{
description: "zero start is allowed",
startTimeRaw: "0",
endTimeRaw: "1700000600000",
expectedStart: 0,
expectedEnd: 1700000600000,
},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
publicDashboard := &PublicDashboard{TimeRangeEnabled: true}
startTime, endTime, err := publicDashboard.ResolveTimeRange(tc.startTimeRaw, tc.endTimeRaw)
require.NoError(t, err)
assert.Equal(t, tc.expectedStart, startTime)
assert.Equal(t, tc.expectedEnd, endTime)
})
}
})
t.Run("rejects an invalid explicit range when time range is enabled", func(t *testing.T) {
cases := []struct {
description string
startTimeRaw string
endTimeRaw string
}{
{description: "non-numeric startTime", startTimeRaw: "abc", endTimeRaw: "1700000600000"},
{description: "empty startTime", startTimeRaw: "", endTimeRaw: "1700000600000"},
{description: "negative startTime", startTimeRaw: "-1", endTimeRaw: "1700000600000"},
{description: "non-numeric endTime", startTimeRaw: "1700000000000", endTimeRaw: "xyz"},
{description: "empty endTime", startTimeRaw: "1700000000000", endTimeRaw: ""},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
publicDashboard := &PublicDashboard{TimeRangeEnabled: true}
_, _, err := publicDashboard.ResolveTimeRange(tc.startTimeRaw, tc.endTimeRaw)
assert.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
})
}
})
t.Run("derives the range from now and the default when time range is disabled", func(t *testing.T) {
cases := []struct {
description string
defaultTimeRange string
expectedWidthMS uint64
}{
{description: "one hour", defaultTimeRange: "1h", expectedWidthMS: uint64(time.Hour.Milliseconds())},
{description: "thirty minutes", defaultTimeRange: "30m", expectedWidthMS: uint64((30 * time.Minute).Milliseconds())},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
publicDashboard := &PublicDashboard{TimeRangeEnabled: false, DefaultTimeRange: tc.defaultTimeRange}
before := uint64(time.Now().UnixMilli())
startTime, endTime, err := publicDashboard.ResolveTimeRange("ignored", "ignored")
after := uint64(time.Now().UnixMilli())
require.NoError(t, err)
// end is "now"; both bounds share the same instant, so the width is exact.
assert.GreaterOrEqual(t, endTime, before)
assert.LessOrEqual(t, endTime, after)
assert.Equal(t, tc.expectedWidthMS, endTime-startTime)
})
}
})
t.Run("ignores caller-supplied bounds when time range is disabled", func(t *testing.T) {
publicDashboard := &PublicDashboard{TimeRangeEnabled: false, DefaultTimeRange: "1h"}
startTime, endTime, err := publicDashboard.ResolveTimeRange("123", "456")
require.NoError(t, err)
assert.NotEqual(t, uint64(123), startTime)
assert.NotEqual(t, uint64(456), endTime)
assert.Equal(t, uint64(time.Hour.Milliseconds()), endTime-startTime)
})
t.Run("returns an internal error for an unparseable stored default range", func(t *testing.T) {
publicDashboard := &PublicDashboard{TimeRangeEnabled: false, DefaultTimeRange: "not-a-duration"}
_, _, err := publicDashboard.ResolveTimeRange("", "")
assert.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeInternal))
})
}

View File

@@ -1,233 +0,0 @@
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
import requests
from wiremock.resources.mappings import Mapping
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
from fixtures.metrics import Metrics
from fixtures.types import Operation, SigNoz, TestContainerDocker
V2_BASE_URL = "/api/v2/dashboards"
PANEL_KEY = "24e2697b"
def test_apply_license(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
make_http_mocks: Callable[[TestContainerDocker, list[Mapping]], None],
get_token: Callable[[str, str], str],
) -> None:
"""
Public dashboards are a licensed feature, so a license must be present.
"""
add_license(signoz, make_http_mocks, get_token)
def test_public_dashboard_v2(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[list[Metrics]], None],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Insert metric data so the panel query resolves to a result.
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
insert_metrics(
[
Metrics(
metric_name="system.cpu.time",
labels={"service.name": "sampleapp"},
timestamp=now - timedelta(minutes=minutes),
value=value,
temporality="Cumulative",
)
for minutes, value in ((5, 100.0), (3, 200.0), (1, 300.0))
]
)
# Create a v2 dashboard with one panel whose builder query carries a filter,
# so we can assert the filter is redacted from the anonymous payload.
create_response = requests.post(
signoz.self.host_configs["8080"].get(V2_BASE_URL),
json={
"schemaVersion": "v6",
"name": "v2-public-sample",
"tags": [{"key": "team", "value": "pulse"}],
"spec": {
"display": {"name": "Sample Dashboard", "description": "Used for integration tests"},
"duration": "1h",
"variables": [],
"panels": {
PANEL_KEY: {
"kind": "Panel",
"spec": {
"display": {"name": "total"},
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {"visualization": {"fillSpans": True}}},
"queries": [
{
"kind": "time_series",
"spec": {
"plugin": {
"kind": "signoz/BuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"aggregations": [
{
"metricName": "system.cpu.time",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate",
}
],
"filter": {"expression": "service.name = 'sampleapp'"},
"groupBy": [{"name": "service.name", "fieldDataType": "string", "fieldContext": "tag"}],
},
}
},
}
],
},
}
},
"layouts": [
{
"kind": "Grid",
"spec": {
"items": [
{
"x": 0,
"y": 0,
"width": 6,
"height": 6,
"content": {"$ref": f"#/spec/panels/{PANEL_KEY}"},
}
]
},
}
],
},
},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert create_response.status_code == HTTPStatus.CREATED, create_response.text
dashboard_id = create_response.json()["data"]["id"]
# Enable public sharing (the public-config endpoint is shape-agnostic, still v1).
public_response = requests.post(
signoz.self.host_configs["8080"].get(f"/api/v1/dashboards/{dashboard_id}/public"),
json={"timeRangeEnabled": True, "defaultTimeRange": "10m"},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert public_response.status_code == HTTPStatus.CREATED, public_response.text
config_response = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v1/dashboards/{dashboard_id}/public"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert config_response.status_code == HTTPStatus.OK, config_response.text
public_id = config_response.json()["data"]["publicPath"].split("/public/dashboard/")[-1]
# ── anonymous public data (no Authorization header) ──────────────────────
data_response = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/public/dashboards/{public_id}"),
timeout=5,
)
assert data_response.status_code == HTTPStatus.OK, data_response.text
body = data_response.json()
assert body["status"] == "success"
dashboard = body["data"]["dashboard"]
assert dashboard["schemaVersion"] == "v6"
assert dashboard["spec"]["display"]["name"] == "Sample Dashboard"
assert {"key": "team", "value": "pulse"} in dashboard["tags"]
# Identity/audit fields are not exposed to anonymous viewers.
assert dashboard["createdBy"] == ""
assert dashboard["updatedBy"] == ""
# The public config is echoed back.
assert body["data"]["publicDashboard"]["timeRangeEnabled"] is True
# The builder query is redacted: the filter is gone, display fields remain.
builder = dashboard["spec"]["panels"][PANEL_KEY]["spec"]["queries"][0]["spec"]["plugin"]["spec"]
assert "filter" not in builder
assert builder["name"] == "A"
assert builder["signal"] == "metrics"
assert builder["aggregations"][0]["metricName"] == "system.cpu.time"
assert builder["groupBy"][0]["name"] == "service.name"
# ── anonymous panel query range ──────────────────────────────────────────
start_time = int((now - timedelta(minutes=15)).timestamp() * 1000)
end_time = int(now.timestamp() * 1000)
query_response = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/public/dashboards/{public_id}/panels/{PANEL_KEY}/query_range"),
params={"startTime": start_time, "endTime": end_time},
timeout=10,
)
assert query_response.status_code == HTTPStatus.OK, query_response.text
query_body = query_response.json()
assert query_body["status"] == "success"
# The inserted metric is returned as a time series for query "A".
result = query_body["data"]
assert result["type"] == "time_series"
results = result["data"]["results"]
result_a = next((r for r in results if r.get("queryName") == "A"), None)
assert result_a is not None, results
series = result_a["aggregations"][0]["series"]
assert len(series) >= 1, result_a
assert len(series[0]["values"]) >= 1, series[0]
# With timeRangeEnabled, the bounds are required.
missing_range = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/public/dashboards/{public_id}/panels/{PANEL_KEY}/query_range"),
timeout=5,
)
assert missing_range.status_code == HTTPStatus.BAD_REQUEST, missing_range.text
# An unknown panel key is rejected.
unknown_panel = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/public/dashboards/{public_id}/panels/does-not-exist/query_range"),
params={"startTime": start_time, "endTime": end_time},
timeout=5,
)
assert unknown_panel.status_code == HTTPStatus.BAD_REQUEST, unknown_panel.text
# A bogus public id is rejected before any data is served.
bogus = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/public/dashboards/not-a-real-id/panels/{PANEL_KEY}/query_range"),
params={"startTime": start_time, "endTime": end_time},
timeout=5,
)
assert bogus.status_code >= HTTPStatus.BAD_REQUEST
# ── deleting the dashboard removes public access ─────────────────────────
# DeleteV2 drops the public-config row in the same transaction, so the public
# id no longer resolves to a dashboard.
delete_response = requests.delete(
signoz.self.host_configs["8080"].get(f"{V2_BASE_URL}/{dashboard_id}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert delete_response.status_code == HTTPStatus.NO_CONTENT, delete_response.text
deleted_data = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/public/dashboards/{public_id}"),
timeout=5,
)
assert deleted_data.status_code == HTTPStatus.NOT_FOUND, deleted_data.text
deleted_query = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/public/dashboards/{public_id}/panels/{PANEL_KEY}/query_range"),
params={"startTime": start_time, "endTime": end_time},
timeout=5,
)
assert deleted_query.status_code == HTTPStatus.NOT_FOUND, deleted_query.text