Compare commits

..

1 Commits

Author SHA1 Message Date
aks07
2aed4821c8 chore: update e2e code owners 2026-06-30 12:05:02 +05:30
97 changed files with 778 additions and 5685 deletions

2
.github/CODEOWNERS vendored
View File

@@ -138,7 +138,7 @@ go.mod @therealpandey
/tests/integration/ @therealpandey
# e2e tests
/tests/e2e/ @AshwinBhatkal
/tests/e2e/ @SigNoz/frontend-maintainers
# Flagger Owners

View File

@@ -1,190 +1,197 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/readme-assets/signoz-hero-dark.png" width="900">
<source media="(prefers-color-scheme: light)" srcset="docs/readme-assets/signoz-hero-light.png" width="900">
<img alt="SigNoz - Observability nach deinen Bedingungen, basierend auf offenen Standards." src="docs/readme-assets/signoz-hero-light.png" width="900">
</picture>
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
<p align="center">Überwache deine Anwendungen und behebe Probleme in deinen bereitgestellten Anwendungen. SigNoz ist eine Open Source Alternative zu DataDog, New Relic, etc.</p>
</p>
<p align="center">
<a href="README.md">English</a> ·
<a href="README.zh-cn.md">中文</a> ·
<a href="README.pt-br.md">Português</a>
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Downloads"> </a>
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
</p>
<p align="center">
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
<a href="https://github.com/SigNoz/signoz/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/SigNoz/signoz?label=release"></a>
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
<a href="https://www.linkedin.com/company/signozio/"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin-SigNoz-0A66C2?logo=linkedin&logoColor=white"></a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability"><img alt="Tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"></a>
</p>
<h3 align="center">
<a href="https://signoz.io/docs"><b>Dokumentation</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.md"><b>Readme auf Englisch </b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.zh-cn.md"><b>ReadMe auf Chinesisch</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.pt-br.md"><b>ReadMe auf Portugiesisch</b></a> &bull;
<a href="https://signoz.io/slack"><b>Slack Community</b></a> &bull;
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
</h3>
SigNoz ist eine Open-Source-Observability-Plattform auf Basis von OpenTelemetry. Wir bauen eine Enterprise-taugliche Alternative zu fragmentierten Monitoring-Stacks, mit Logs, Metriken, Traces, Alerts und Dashboards an einem Ort.
##
### Wähle, wie du SigNoz betreibst
SigNoz hilft Entwicklern, Anwendungen zu überwachen und Probleme in ihren bereitgestellten Anwendungen zu beheben. Mit SigNoz können Sie Folgendes tun:
#### SigNoz Cloud (empfohlen)
👉 Visualisieren Sie Metriken, Traces und Logs in einer einzigen Oberfläche.
Vollständig verwaltetes SigNoz mit 30 Tagen kostenloser Testphase, ohne Kreditkarte, nutzungsbasierter Preisgestaltung ab 49 USD und regionalem Datenhosting.
👉 Sie können Metriken wie die p99-Latenz, Fehlerquoten für Ihre Dienste, externe API-Aufrufe und individuelle Endpunkte anzeigen.
[**Kostenlos starten →**](https://signoz.io/teams/)
👉 Sie können die Ursache des Problems ermitteln, indem Sie zu den genauen Traces gehen, die das Problem verursachen, und detaillierte Flammenbilder einzelner Anfragetraces anzeigen.
#### Enterprise
👉 Führen Sie Aggregationen auf Trace-Daten durch, um geschäftsrelevante Metriken zu erhalten.
Enterprise Cloud, BYOC oder Enterprise Self-Hosted mit Compliance, Support, benutzerdefinierter Aufbewahrung, RBAC, Ingestion Controls, Datenresidenz und Regionsauswahl.
👉 Filtern und Abfragen von Logs, Erstellen von Dashboards und Benachrichtigungen basierend auf Attributen in den Logs.
[**Enterprise entdecken →**](https://signoz.io/enterprise/)
👉 Automatische Aufzeichnung von Ausnahmen in Python, Java, Ruby und Javascript.
#### Community
👉 Einfache Einrichtung von Benachrichtigungen mit dem selbst erstellbaren Abfrage-Builder.
Kostenloses Open-Source-SigNoz, das in deiner eigenen Infrastruktur läuft. Deployment mit Docker, Kubernetes oder Linux, während du die volle Kontrolle über deine Datenebene behältst.
##
[**SigNoz installieren →**](https://signoz.io/docs/install/self-host/)
### Anwendung Metriken
### Was kannst du überwachen?
![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png)
SigNoz hilft Teams, Produktionsprobleme schneller zu debuggen, indem Logs, Metriken, Traces, Alerts, Dashboards, Exceptions und agent-native Workflows an einem Ort verbunden werden.
### Verteiltes Tracing
#### APM-Überblick
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
Überwache Service-Latenz, Fehlerrate, Durchsatz, Apdex, wichtige Endpunkte, Datenbankaufrufe und externe Aufrufe.
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
<p align="center">
<img alt="SigNoz APM-Dashboard mit Latenz, Durchsatz, Apdex und wichtigen Operationen" src="docs/readme-assets/monitor/apm.png" width="900">
</p>
### Log Verwaltung
Mehr erfahren: [APM-Dokumentation](https://signoz.io/docs/instrumentation/overview/)
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
#### Log-Management
### Infrastruktur Überwachung
Erfasse, suche, aggregiere und korreliere Logs mit Traces und Metriken über einen visuellen Query Builder.
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
<p align="center">
<img alt="SigNoz Logs Explorer mit Filtern, Frequenzdiagramm und Log-Zeilen" src="docs/readme-assets/monitor/log-management.svg" width="900">
</p>
### Exceptions Monitoring
Mehr erfahren: [Log-Management-Dokumentation](https://signoz.io/docs/logs-management/overview/)
![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.png)
#### Metriken und Dashboards
### Alarme
Erstelle Dashboards für Anwendungs-, Infrastruktur- und benutzerdefinierte Metriken mit Query Builder, PromQL oder ClickHouse SQL.
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
<p align="center">
<img alt="SigNoz Host-Metrics-Dashboard mit Systemlast- und Netzwerkdiagrammen" src="docs/readme-assets/monitor/metrics.png" width="900">
</p>
<br /><br />
Mehr erfahren: [Metriken-Dokumentation](https://signoz.io/docs/metrics-management/overview/)
## Werde Teil unserer Slack Community
#### Infrastruktur-Monitoring
Sag Hi zu uns auf [Slack](https://signoz.io/slack) 👋
Überwache Kubernetes-Cluster, Pods, Nodes, Workloads sowie Host-CPU, Arbeitsspeicher, Festplatten, Netzwerk, Logs und Traces.
<br /><br />
<p align="center">
<img alt="SigNoz Kubernetes-Infrastruktur-Dashboard mit Pod- und Node-Metriken" src="docs/readme-assets/monitor/infrastructure.png" width="900">
</p>
## Funktionen:
Mehr erfahren: [Infrastruktur-Monitoring-Dokumentation](https://signoz.io/docs/infrastructure-monitoring/overview/)
- Einheitliche Benutzeroberfläche für Metriken, Traces und Logs. Keine Notwendigkeit, zwischen Prometheus und Jaeger zu wechseln, um Probleme zu debuggen oder ein separates Log-Tool wie Elastic neben Ihrer Metriken- und Traces-Stack zu verwenden.
- Überblick über Anwendungsmetriken wie RPS, Latenzzeiten des 50tes/90tes/99tes Perzentils und Fehlerquoten.
- Langsamste Endpunkte in Ihrer Anwendung.
- Zeigen Sie genaue Anfragetraces an, um Probleme in nachgelagerten Diensten, langsamen Datenbankabfragen oder Aufrufen von Drittanbieterdiensten wie Zahlungsgateways zu identifizieren.
- Filtern Sie Traces nach Dienstname, Operation, Latenz, Fehler, Tags/Annotationen.
- Führen Sie Aggregationen auf Trace-Daten (Ereignisse/Spans) durch, um geschäftsrelevante Metriken zu erhalten. Beispielsweise können Sie die Fehlerquote und die 99tes Perzentillatenz für `customer_type: gold` oder `deployment_version: v2` oder `external_call: paypal` erhalten.
- Native Unterstützung für OpenTelemetry-Logs, erweiterten Log-Abfrage-Builder und automatische Log-Sammlung aus dem Kubernetes-Cluster.
- Blitzschnelle Log-Analytik ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
- End-to-End-Sichtbarkeit der Infrastrukturleistung, Aufnahme von Metriken aus allen Arten von Host-Umgebungen.
- Einfache Einrichtung von Benachrichtigungen mit dem selbst erstellbaren Abfrage-Builder.
#### LLM- und AI-Observability
<br /><br />
Verfolge LLM-Apps, RAG-Pipelines, Prompts, Tool Calls, Tokens, Latenz und Kosten zusammen mit Anwendungs- und Infrastruktur-Telemetrie.
## Wieso SigNoz?
<p align="center">
<img alt="SigNoz LLM-Observability-Dashboard für Traces, Token-Nutzung, Latenz und Kosten" src="docs/readme-assets/monitor/llm.png" width="900">
</p>
Als Entwickler fanden wir es anstrengend, uns für jede kleine Funktion, die wir haben wollten, auf Closed Source SaaS Anbieter verlassen zu müssen. Closed Source Anbieter überraschen ihre Kunden zum Monatsende oft mit hohen Rechnungen, die keine Transparenz bzgl. der Kostenaufteilung bieten.
Mehr erfahren: [LLM-Observability-Dokumentation](https://signoz.io/docs/llm-observability/)
Wir wollten eine selbst gehostete, Open Source Variante von Lösungen wie DataDog, NewRelic für Firmen anbieten, die Datenschutz und Sicherheitsbedenken haben, bei der Weitergabe von Kundendaten an Drittanbieter.
#### Agent-Native Observability und MCP
Open Source gibt dir außerdem die totale Kontrolle über deine Konfiguration, Stichprobenentnahme und Betriebszeit. Du kannst des Weiteren neue Module auf Basis von SigNoz bauen, die erweiterte, geschäftsspezifische Funktionen anbieten.
Nutze den SigNoz MCP-Server, um Telemetrie in Coding Agents zu bringen, oder nutze Noz in SigNoz, um Incidents zu untersuchen, Alerts zu verbessern und Dashboards mit Produktionskontext zu erstellen. [Noz](https://signoz.io/docs/ai/noz/) ist nur in SigNoz Cloud verfügbar.
### Languages supported:
<p align="center">
<img alt="SigNoz Noz-Oberfläche neben einem MCP-gestützten Agent-Workflow" src="docs/readme-assets/monitor/agent-native.png" width="900">
</p>
Wir unterstützen [OpenTelemetry](https://opentelemetry.io) als Bibliothek, mit der Sie Ihre Anwendungen instrumentieren können. Daher wird jedes von OpenTelemetry unterstützte Framework und jede Sprache auch von SignNoz unterstützt. Einige der wichtigsten unterstützten Sprachen sind:
Mehr erfahren: [SigNoz MCP-Server-Dokumentation](https://signoz.io/docs/ai/signoz-mcp-server/) · [Agent-Skills-Dokumentation](https://signoz.io/docs/ai/agent-skills/#install-the-plugin)
- Java
- Python
- NodeJS
- Go
- PHP
- .NET
- Ruby
- Elixir
- Rust
#### Distributed Tracing
Hier findest du die vollständige Liste von unterstützten Programmiersprachen - https://opentelemetry.io/docs/
Verfolge Requests über Services hinweg mit Flamegraphs, Waterfalls, Span Events, Filtern und Trace Analytics.
<br /><br />
<p align="center">
<img alt="SigNoz Distributed-Tracing-Ansicht mit Flamegraph und Waterfall-Spans" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
</p>
## Erste Schritte mit SigNoz
Mehr erfahren: [Distributed-Tracing-Dokumentation](https://signoz.io/docs/instrumentation/)
### Bereitstellung mit Docker
#### Trace Funnels
Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
Erstelle Funnels aus Traces, um Drop-offs im Request-Flow, fehlgeschlagene Übergänge und systemische Workflow-Probleme zu verstehen.
Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshooting/) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
<p align="center">
<img alt="SigNoz Trace Funnels mit Request-Flow-Drop-offs und fehlgeschlagenen Übergängen" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
</p>
<p>&nbsp </p>
Mehr erfahren: [Trace-Funnels-Dokumentation](https://signoz.io/docs/trace-funnels/overview/)
### Deploy in Kubernetes using Helm
Du kannst außerdem [**Exceptions**](https://signoz.io/docs/userguide/exceptions/), [**Alerts**](https://signoz.io/docs/alerts/), [**externe APIs**](https://signoz.io/docs/external-api-monitoring/overview/) und [**Integrationen**](https://signoz.io/docs/integrations/integrations-list/) für OpenTelemetry, Prometheus, Kubernetes, Cloud-Anbieter, Sprach-SDKs, Application Frameworks, Datenbanken und LLM-Tools überwachen.
Bitte folge den [hier](https://signoz.io/docs/deployment/helm_chart) aufgelisteten Schritten, um deine Anwendung mit Helm Charts bereitzustellen.
### Warum Teams SigNoz nutzen
<br /><br />
1. **OpenTelemetry-native**<br>
Einmal mit offenen Standards instrumentieren und die Kontrolle über deine Telemetrie behalten.
2. **Korrelierte Signale**<br>
Von Service-Charts zu Traces, Logs, Infrastrukturmetriken und Exceptions wechseln, ohne das Tool zu wechseln.
3. **Eine einzelne spaltenorientierte Datenbank**<br>
Gebaut für hochkardinale Observability-Workloads mit hohem Volumen.
4. **Vorhersehbare Preise**<br>
Keine Preise pro Host, keine Preise pro Nutzerplatz und keine Sonderpreise für Custom Metrics.
5. **Enterprise-ready**<br>
SOC 2 Type II und HIPAA Compliance, RBAC, Ingestion Controls, benutzerdefinierte Aufbewahrung, Support, BYOC und Self-Hosting.
## Vergleiche mit bekannten Tools
### Erste Schritte
### SigNoz vs Prometheus
#### Mit Cloud starten
Prometheus ist gut, falls du dich nur für Metriken interessierst. Wenn du eine nahtlose Integration von Metriken und Einzelschritt-Fehlersuchen haben möchtest, ist die Kombination aus Prometheus und Jaeger nicht das Richtige für dich.
Erstelle einen verwalteten SigNoz-Workspace und erhalte dein erstes Dashboard, ohne Observability-Infrastruktur betreiben zu müssen.
Unser Ziel ist es, eine integrierte Benutzeroberfläche aus Metriken und Einzelschritt-Fehlersuchen anzubieten, ähnlich wie es SaaS Anbieter wie Datadog tun, mit der Möglichkeit von erweitertem filtern und aggregieren von Fehlersuchen. Etwas, was in Jaeger aktuell fehlt.
[**Kostenlos mit SigNoz Cloud starten**](https://signoz.io/teams/)
<p>&nbsp </p>
#### SigNoz selbst hosten
### SigNoz vs Jaeger
Betreibe SigNoz in deiner eigenen Infrastruktur mit Foundry, Docker, Kubernetes oder Linux.
Jaeger kümmert sich nur um verteilte Einzelschritt-Fehlersuche. SigNoz erstellt sowohl Metriken als auch Einzelschritt-Fehlersuche, daneben haben wir auch Protokoll Verwaltung auf unserem Plan.
[**Foundry**](https://github.com/SigNoz/foundry) · [**Docker**](https://signoz.io/docs/install/docker/) · [**Kubernetes**](https://signoz.io/docs/install/kubernetes/) · [**Linux**](https://signoz.io/docs/install/linux/)
Außerdem hat SigNoz noch mehr spezielle Funktionen im Vergleich zu Jaeger:
#### Daten senden
- Jaeger UI zeigt keine Metriken für Einzelschritt-Fehlersuchen oder für gefilterte Einzelschritt-Fehlersuchen an.
- Jaeger erstellt keine Aggregate für gefilterte Einzelschritt-Fehlersuchen, z. B. die P99 Latenz von Abfragen mit dem Tag `customer_type=premium`, was hingegen mit SigNoz leicht umsetzbar ist.
Instrumentiere Anwendungen und Infrastruktur mit OpenTelemetry, Prometheus, Sprach-SDKs und Integrationen.
<p>&nbsp </p>
[**Instrumentation**](https://signoz.io/docs/instrumentation/) · [**Integrationen**](https://signoz.io/docs/integrations/integrations-list/)
### SigNoz vs Elastic
### Vergleich mit bekannten Tools
- Die Verwaltung von SigNoz-Protokollen basiert auf 'ClickHouse', einem spaltenbasierten OLAP-Datenspeicher, der aggregierte Protokollanalyseabfragen wesentlich effizienter macht.
- 50 % geringerer Ressourcenbedarf im Vergleich zu Elastic während der Aufnahme.
SigNoz wird häufig von Teams eingeführt, die von einzelnen Spezialtools oder kommerziellen Plattformen mit unvorhersehbarer Preisgestaltung wechseln.
Wir haben Benchmarks veröffentlicht, die Elastic mit SignNoz vergleichen. Schauen Sie es sich [hier](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
**Prometheus**<br>
Gut, wenn du nur Metriken brauchst. SigNoz hält Metriken, Logs, Traces, Dashboards und Alerts zusammen, damit Teams mit korreliertem Kontext debuggen können.
<p>&nbsp </p>
**Jaeger**<br>
Jaeger macht ausschließlich Distributed Tracing. SigNoz ergänzt Metriken, Logs, Trace Analytics, Dashboards, Alerts, Exceptions und Trace-to-Log-Workflows.
### SigNoz vs Loki
**Elastic**<br>
SigNoz nutzt eine spaltenorientierte Datenbank für effiziente Observability-Analysen und hochkardinale Log-Workloads, mit 50 % geringerem Ressourcenbedarf gegenüber Elastic während der Ingestion. Lies die [detaillierte Studie](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
- SigNoz unterstützt Aggregationen von Daten mit hoher Kardinalität über ein großes Volumen, Loki hingegen nicht.
- SigNoz unterstützt Indizes über Daten mit hoher Kardinalität und hat keine Beschränkungen hinsichtlich der Anzahl der Indizes, während Loki maximale Streams erreicht, wenn ein paar Indizes hinzugefügt werden.
- Das Durchsuchen großer Datenmengen ist in Loki im Vergleich zu SigNoz schwierig und langsam.
**Loki**<br>
Im verlinkten Benchmark indexierte SigNoz alle Keys im Test-Setup, während Loki beim Hinzufügen weiterer Labels Max-Stream-Fehler erreichte. Lies die [detaillierte Studie](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
Wir haben Benchmarks veröffentlicht, die Loki mit SigNoz vergleichen. Schauen Sie es sich [hier](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
## Mitwirken
<br /><br />
Wir freuen uns über große und kleine Beiträge. Lies bitte [CONTRIBUTING.md](CONTRIBUTING.md), um mit Beiträgen zu SigNoz loszulegen.
## Zum Projekt beitragen
Nicht sicher, wie du anfangen sollst? **Schreib uns einfach im Channel `#contributing` in unserer [Slack Community](https://signoz.io/slack).**
Wir ❤️ Beiträge zum Projekt, egal ob große oder kleine. Bitte lies dir zuerst die [CONTRIBUTING.md](CONTRIBUTING.md), durch, bevor du anfängst, Beiträge zu SigNoz zu machen.
Du bist dir nicht sicher, wie du anfangen sollst? Schreib uns einfach auf dem #contributing Kanal in unserer [slack community](https://signoz.io/slack)
Wie immer: Danke an unsere großartigen Contributors!
<br /><br />
## Dokumentation
Du findest unsere Dokumentation unter https://signoz.io/docs/. Falls etwas unverständlich ist oder fehlt, öffne gerne ein Github Issue mit dem Label `documentation` oder schreib uns über den Community Slack Channel.
<br /><br />
## Gemeinschaft
Werde Teil der [slack community](https://signoz.io/slack) um mehr über verteilte Einzelschritt-Fehlersuche, Messung von Systemzuständen oder SigNoz zu erfahren und sich mit anderen Nutzern und Mitwirkenden in Verbindung zu setzen.
Falls du irgendwelche Ideen, Fragen oder Feedback hast, kannst du sie gerne über unsere [Github Discussions](https://github.com/SigNoz/signoz/discussions) mit uns teilen.
Wie immer, Dank an unsere großartigen Mitwirkenden!
<a href="https://github.com/signoz/signoz/graphs/contributors">
<img alt="SigNoz Contributors" src="https://contrib.rocks/image?repo=signoz/signoz" />
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

278
README.md
View File

@@ -1,190 +1,244 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/readme-assets/signoz-hero-dark.png" width="900">
<source media="(prefers-color-scheme: light)" srcset="docs/readme-assets/signoz-hero-light.png" width="900">
<img alt="SigNoz - Observability on Your Terms, Powered by Open Standards." src="docs/readme-assets/signoz-hero-light.png" width="900">
</picture>
</p>
<h1 align="center" style="border-bottom: none">
<a href="https://signoz.io" target="_blank">
<img alt="SigNoz" src="https://github.com/user-attachments/assets/ef9a33f7-12d7-4c94-8908-0a02b22f0c18" width="100" height="100">
</a>
<br>SigNoz
</h1>
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
<p align="center">
<a href="README.zh-cn.md">中文</a> ·
<a href="README.de-de.md">Deutsch</a> ·
<a href="README.pt-br.md">Português</a>
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
</p>
<h3 align="center">
<a href="https://signoz.io/docs"><b>Documentation</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.zh-cn.md"><b>ReadMe in Chinese</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.de-de.md"><b>ReadMe in German</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.pt-br.md"><b>ReadMe in Portuguese</b></a> &bull;
<a href="https://signoz.io/slack"><b>Slack Community</b></a> &bull;
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
</h3>
<p align="center">
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
<a href="https://github.com/SigNoz/signoz/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/SigNoz/signoz?label=release"></a>
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
<a href="https://www.linkedin.com/company/signozio/"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin-SigNoz-0A66C2?logo=linkedin&logoColor=white"></a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability"><img alt="Tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"></a>
</p>
## Features
SigNoz is an open-source observability platform built on OpenTelemetry. Were building an enterprise-grade alternative to fragmented monitoring stacks, with logs, metrics, traces, alerts, and dashboards in one place.
### Choose how to run SigNoz
### Application Performance Monitoring
#### SigNoz Cloud (Recommended)
Use SigNoz APM to monitor your applications and services. It comes with out-of-box charts for key application metrics like p99 latency, error rate, Apdex and operations per second. You can also monitor the database and external calls made from your application. Read [more](https://signoz.io/application-performance-monitoring/).
Fully managed SigNoz with a 30-day free trial, no credit card required, usage-based pricing that starts at $49, and regional data hosting.
You can [instrument](https://signoz.io/docs/instrumentation/) your application with OpenTelemetry to get started.
[**Start free →**](https://signoz.io/teams/)
![apm-cover](https://github.com/user-attachments/assets/fa5c0396-0854-4c8b-b972-9b62fd2a70d2)
#### Enterprise
Enterprise Cloud, BYOC, or Enterprise Self-Hosted with compliance, support, custom retention, RBAC, ingestion controls, data residency, and region selection.
### Logs Management
[**Explore Enterprise →**](https://signoz.io/enterprise/)
SigNoz can be used as a centralized log management solution. We use ClickHouse (used by likes of Uber & Cloudflare) as a datastore, ⎯ an extremely fast and highly optimized storage for logs data. Instantly search through all your logs using quick filters and a powerful query builder.
#### Community
You can also create charts on your logs and monitor them with customized dashboards. Read [more](https://signoz.io/log-management/).
Free open-source SigNoz that runs in your own infrastructure. Deploy with Docker, Kubernetes, or Linux and keep full control of your data plane.
![logs-management-cover](https://github.com/user-attachments/assets/343588ee-98fb-4310-b3d2-c5bacf9c7384)
[**Install SigNoz →**](https://signoz.io/docs/install/self-host/)
### What can you monitor?
### Distributed Tracing
SigNoz helps teams debug production issues faster by connecting logs, metrics, traces, alerts, dashboards, exceptions, and agent-native workflows in one place.
Distributed Tracing is essential to troubleshoot issues in microservices applications. Powered by OpenTelemetry, distributed tracing in SigNoz can help you track user requests across services to help you identify performance bottlenecks.
#### APM Overview
See user requests in a detailed breakdown with the help of Flamegraphs and Gantt Charts. Click on any span to see the entire trace represented beautifully, which will help you make sense of where issues actually occurred in the flow of requests.
Monitor service latency, error rate, throughput, Apdex, top endpoints, database calls, and external calls.
Read [more](https://signoz.io/distributed-tracing/).
<p align="center">
<img alt="SigNoz APM dashboard showing latency, throughput, Apdex, and key operations" src="docs/readme-assets/monitor/apm.png" width="900">
</p>
![distributed-tracing-cover](https://github.com/user-attachments/assets/9bfe060a-0c40-4922-9b55-8a97e1a4076c)
Learn more: [APM documentation](https://signoz.io/docs/instrumentation/overview/)
#### Log Management
Ingest, search, aggregate, and correlate logs with traces and metrics using a visual query builder.
### Metrics and Dashboards
<p align="center">
<img alt="SigNoz logs explorer with filters, frequency chart, and log lines" src="docs/readme-assets/monitor/log-management.svg" width="900">
</p>
Ingest metrics from your infrastructure or applications and create customized dashboards to monitor them. Create visualization that suits your needs with a variety of panel types like pie chart, time-series, bar chart, etc.
Learn more: [Log management documentation](https://signoz.io/docs/logs-management/overview/)
Create queries on your metrics data quickly with an easy-to-use metrics query builder. Add multiple queries and combine those queries with formulae to create really complex queries quickly.
#### Metrics and Dashboards
Read [more](https://signoz.io/metrics-and-dashboards/).
Build dashboards for application, infrastructure, and custom metrics using Query Builder, PromQL, or ClickHouse SQL.
![metrics-n-dashboards-cover](https://github.com/user-attachments/assets/a536fd71-1d2c-4681-aa7e-516d754c47a5)
<p align="center">
<img alt="SigNoz host metrics dashboard with system load and network charts" src="docs/readme-assets/monitor/metrics.png" width="900">
</p>
### LLM Observability
Learn more: [Metrics documentation](https://signoz.io/docs/metrics-management/overview/)
Monitor and debug your LLM applications with comprehensive observability. Track LLM calls, analyze token usage, monitor performance, and gain insights into your AI application's behavior in production.
#### Infrastructure Monitoring
SigNoz LLM observability helps you understand how your language models are performing, identify issues with prompts and responses, track token usage and costs, and optimize your AI applications for better performance and reliability.
Monitor Kubernetes clusters, pods, nodes, workloads, and host-level CPU, memory, disk, network, logs, and traces.
[Get started with LLM Observability →](https://signoz.io/docs/llm-observability/)
<p align="center">
<img alt="SigNoz Kubernetes infrastructure dashboard with pod and node metrics" src="docs/readme-assets/monitor/infrastructure.png" width="900">
</p>
![llm-observability-cover](https://github.com/user-attachments/assets/a6cc0ca3-59df-48f9-9c16-7c843fccff96)
Learn more: [Infrastructure monitoring documentation](https://signoz.io/docs/infrastructure-monitoring/overview/)
#### LLM and AI Observability
### Alerts
Trace LLM apps, RAG pipelines, prompts, tool calls, tokens, latency, and costs alongside application and infrastructure telemetry.
Use alerts in SigNoz to get notified when anything unusual happens in your application. You can set alerts on any type of telemetry signal (logs, metrics, traces), create thresholds and set up a notification channel to get notified. Advanced features like alert history and anomaly detection can help you create smarter alerts.
<p align="center">
<img alt="SigNoz LLM observability dashboard for traces, token usage, latency, and costs" src="docs/readme-assets/monitor/llm.png" width="900">
</p>
Alerts in SigNoz help you identify issues proactively so that you can address them before they reach your customers.
Learn more: [LLM observability documentation](https://signoz.io/docs/llm-observability/)
Read [more](https://signoz.io/alerts-management/).
#### Agent-Native Observability and MCP
![alerts-cover](https://github.com/user-attachments/assets/03873bb8-1b62-4adf-8f56-28bb7b1750ea)
Use the SigNoz MCP server to bring telemetry into coding agents, or use Noz inside SigNoz to investigate incidents, tune alerts, and build dashboards with production context. [Noz](https://signoz.io/docs/ai/noz/) is available only on SigNoz Cloud.
### Exceptions Monitoring
<p align="center">
<img alt="SigNoz Noz interface alongside MCP-powered agent workflow" src="docs/readme-assets/monitor/agent-native.png" width="900">
</p>
Monitor exceptions automatically in Python, Java, Ruby, and Javascript. For other languages, just drop in a few lines of code and start monitoring exceptions.
Learn more: [SigNoz MCP server docs](https://signoz.io/docs/ai/signoz-mcp-server/) · [Agent skills docs](https://signoz.io/docs/ai/agent-skills/#install-the-plugin)
See the detailed stack trace for all exceptions caught in your application. You can also log in custom attributes to add more context to your exceptions. For example, you can add attributes to identify users for which exceptions occurred.
#### Distributed Tracing
Read [more](https://signoz.io/exceptions-monitoring/).
Follow requests across services with flamegraphs, waterfalls, span events, filters, and trace analytics.
<p align="center">
<img alt="SigNoz distributed trace view with flamegraph and waterfall spans" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
</p>
![exceptions-cover](https://github.com/user-attachments/assets/4be37864-59f2-4e8a-8d6e-e29ad04298c5)
Learn more: [Distributed tracing documentation](https://signoz.io/docs/instrumentation/)
#### Trace Funnels
<br /><br />
Create funnels from traces to understand request-flow drop-offs, failed transitions, and systemic workflow issues.
## Why SigNoz?
<p align="center">
<img alt="SigNoz trace funnels showing request-flow drop-offs and failed transitions" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
</p>
SigNoz is a single tool for all your monitoring and observability needs. Here are a few reasons why you should choose SigNoz:
Learn more: [Trace funnels documentation](https://signoz.io/docs/trace-funnels/overview/)
- Single tool for observability(logs, metrics, and traces)
Also monitor: [**exceptions**](https://signoz.io/docs/userguide/exceptions/), [**alerts**](https://signoz.io/docs/alerts/), [**external APIs**](https://signoz.io/docs/external-api-monitoring/overview/), and [**integrations**](https://signoz.io/docs/integrations/integrations-list/) for OpenTelemetry, Prometheus, Kubernetes, cloud providers, language SDKs, application frameworks, databases, and LLM tools.
- Built on top of [OpenTelemetry](https://opentelemetry.io/), the open-source standard which frees you from any type of vendor lock-in
### Why teams use SigNoz
- Correlated logs, metrics and traces for much richer context while debugging
1. **OpenTelemetry-native**<br>
Instrument once with open standards and keep ownership of your telemetry.
2. **Correlated signals**<br>
Move from service charts to traces, logs, infra metrics, and exceptions without switching tools.
3. **Single columnar database**<br>
Built for high-cardinality, high-volume observability workloads.
4. **Predictable pricing**<br>
No per-host pricing, no user-seat pricing, and no special pricing for custom metrics.
5. **Enterprise ready**<br>
SOC 2 Type II and HIPAA compliance, RBAC, ingestion controls, custom retention, support, BYOC, and self-hosting.
- Uses ClickHouse (used by likes of Uber & Cloudflare) as datastore - an extremely fast and highly optimized storage for observability data
### Getting started
- DIY Query builder, PromQL, and ClickHouse queries to fulfill all your use-cases around querying observability data
#### Start on Cloud
- Open-Source - you can use open-source, our [cloud service](https://signoz.io/teams/) or a mix of both based on your use case
Create a managed SigNoz workspace and get your first dashboard without running observability infrastructure.
[**Start free on SigNoz Cloud**](https://signoz.io/teams/)
## Getting Started
#### Self-host SigNoz
### Create a SigNoz Cloud Account
Run SigNoz in your own infrastructure with Foundry, Docker, Kubernetes, or Linux.
SigNoz cloud is the easiest way to get started with SigNoz. Our cloud service is for those users who want to spend more time in getting insights for their application performance without worrying about maintenance.
[**Foundry**](https://github.com/SigNoz/foundry) · [**Docker**](https://signoz.io/docs/install/docker/) · [**Kubernetes**](https://signoz.io/docs/install/kubernetes/) · [**Linux**](https://signoz.io/docs/install/linux/)
[Get started for free](https://signoz.io/teams/)
#### Send data
### Deploy using Docker(self-hosted)
Instrument applications and infrastructure with OpenTelemetry, Prometheus, language SDKs, and integrations.
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
[**Instrumentation**](https://signoz.io/docs/instrumentation/) · [**Integrations**](https://signoz.io/docs/integrations/integrations-list/)
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
### Comparisons to familiar tools
<p>&nbsp </p>
### Deploy in Kubernetes using Helm(self-hosted)
SigNoz is often adopted by teams moving from a stack of single-purpose tools or commercial platforms with unpredictable pricing.
Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_chart) to install using helm charts
**Prometheus**<br>
Good if you just need metrics. SigNoz keeps metrics, logs, traces, dashboards, and alerts together so teams can debug with correlated context.
<br /><br />
**Jaeger**<br>
Jaeger only does distributed tracing. SigNoz adds metrics, logs, trace analytics, dashboards, alerts, exceptions, and trace-to-log workflows.
We also offer managed services in your infra. Check our [pricing plans](https://signoz.io/pricing/) for all details.
**Elastic**<br>
SigNoz uses columnar database for efficient observability analytics and high-cardinality log workloads, with 50% lower resource requirement compared to Elastic during ingestion. Check the [detailed study](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
**Loki**<br>
In the linked benchmark, SigNoz indexed all keys in the test setup, while Loki hit max stream errors when more labels were added. Check the [detailed study](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
## Join our Slack community
Come say Hi to us on [Slack](https://signoz.io/slack) 👋
<br /><br />
### Languages supported:
SigNoz supports all major programming languages for monitoring. Any framework and language supported by OpenTelemetry is supported by SigNoz. Find instructions for instrumenting different languages below:
- [Java](https://signoz.io/docs/instrumentation/java/)
- [Python](https://signoz.io/docs/instrumentation/python/)
- [Node.js or Javascript](https://signoz.io/docs/instrumentation/javascript/)
- [Go](https://signoz.io/docs/instrumentation/golang/)
- [PHP](https://signoz.io/docs/instrumentation/php/)
- [.NET](https://signoz.io/docs/instrumentation/dotnet/)
- [Ruby](https://signoz.io/docs/instrumentation/ruby-on-rails/)
- [Elixir](https://signoz.io/docs/instrumentation/elixir/)
- [Rust](https://signoz.io/docs/instrumentation/rust/)
- [Swift](https://signoz.io/docs/instrumentation/swift/)
You can find our entire documentation [here](https://signoz.io/docs/introduction/).
<br /><br />
## Comparisons to Familiar Tools
### SigNoz vs Prometheus
Prometheus is good if you want to do just metrics. But if you want to have a seamless experience between metrics, logs and traces, then current experience of stitching together Prometheus & other tools is not great.
SigNoz is a one-stop solution for metrics and other telemetry signals. And because you will use the same standard(OpenTelemetry) to collect all telemetry signals, you can also correlate these signals to troubleshoot quickly.
For example, if you see that there are issues with infrastructure metrics of your k8s cluster at a timestamp, you can jump to other signals like logs and traces to understand the issue quickly.
<p>&nbsp </p>
### SigNoz vs Jaeger
Jaeger only does distributed tracing. SigNoz supports metrics, traces and logs - all the 3 pillars of observability.
Moreover, SigNoz has few more advanced features wrt Jaeger:
- Jaegar UI doesnt show any metrics on traces or on filtered traces
- Jaeger cant get aggregates on filtered traces. For example, p99 latency of requests which have tag - customer_type='premium'. This can be done easily on SigNoz
- You can also go from traces to logs easily in SigNoz
<p>&nbsp </p>
### SigNoz vs Elastic
- SigNoz Logs management are based on ClickHouse, a columnar OLAP datastore which makes aggregate log analytics queries much more efficient
- 50% lower resource requirement compared to Elastic during ingestion
We have published benchmarks comparing Elastic with SigNoz. Check it out [here](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
<p>&nbsp </p>
### SigNoz vs Loki
- SigNoz supports aggregations on high-cardinality data over a huge volume while loki doesnt.
- SigNoz supports indexes over high cardinality data and has no limitations on the number of indexes, while Loki reaches max streams with a few indexes added to it.
- Searching over a huge volume of data is difficult and slow in Loki compared to SigNoz
We have published benchmarks comparing Loki with SigNoz. Check it out [here](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
<br /><br />
## Contributing
We ❤️ contributions big or small. Please read [CONTRIBUTING.md](CONTRIBUTING.md) to get started with making contributions to SigNoz.
Not sure how to get started? **Just ping us on `#contributing` in our [slack community](https://signoz.io/slack).**
Not sure how to get started? Just ping us on `#contributing` in our [slack community](https://signoz.io/slack)
<br /><br />
## Documentation
You can find docs at https://signoz.io/docs/. If you need any clarification or find something missing, feel free to raise a GitHub issue with the label `documentation` or reach out to us at the community slack channel.
<br /><br />
## Community
Join the [slack community](https://signoz.io/slack) to know more about distributed tracing, observability, or SigNoz and to connect with other users and contributors.
If you have any ideas, questions, or any feedback, please share on our [Github Discussions](https://github.com/SigNoz/signoz/discussions)
As always, thanks to our amazing contributors!
<a href="https://github.com/signoz/signoz/graphs/contributors">
<img alt="SigNoz contributors" src="https://contrib.rocks/image?repo=signoz/signoz" />
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

View File

@@ -1,190 +1,158 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/readme-assets/signoz-hero-dark.png" width="900">
<source media="(prefers-color-scheme: light)" srcset="docs/readme-assets/signoz-hero-light.png" width="900">
<img alt="SigNoz - Observabilidade nos seus termos, baseada em padrões abertos." src="docs/readme-assets/signoz-hero-light.png" width="900">
</picture>
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
<p align="center">Monitore seus aplicativos e solucione problemas em seus aplicativos implantados, uma alternativa de código aberto para soluções como DataDog, New Relic, entre outras.</p>
</p>
<p align="center">
<a href="README.md">English</a> ·
<a href="README.zh-cn.md">中文</a> ·
<a href="README.de-de.md">Deutsch</a>
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/frontend?label=Downloads"> </a>
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
</p>
<h3 align="center">
<a href="https://signoz.io/docs"><b>Documentação</b></a> &bull;
<a href="https://signoz.io/slack"><b>Comunidade no Slack</b></a> &bull;
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
</h3>
<p align="center">
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
<a href="https://github.com/SigNoz/signoz/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/SigNoz/signoz?label=release"></a>
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
<a href="https://www.linkedin.com/company/signozio/"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin-SigNoz-0A66C2?logo=linkedin&logoColor=white"></a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability"><img alt="Tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"></a>
</p>
##
SigNoz é uma plataforma de observabilidade open-source construída sobre OpenTelemetry. Estamos criando uma alternativa de nível empresarial a stacks de monitoramento fragmentadas, com logs, métricas, traces, alertas e dashboards em um só lugar.
SigNoz auxilia os desenvolvedores a monitorarem aplicativos e solucionar problemas em seus aplicativos implantados. SigNoz usa rastreamento distribuído para obter visibilidade em sua pilha de software.
### Escolha como executar o SigNoz
👉 Você pode verificar métricas como latência p99, taxas de erro em seus serviços, requisições às APIs externas e endpoints individuais.
#### SigNoz Cloud (recomendado)
👉 Você pode encontrar a causa raiz do problema acessando os rastreamentos exatos que estão causando o problema e verificar os quadros detalhados de cada requisição individual.
SigNoz totalmente gerenciado, com teste gratuito de 30 dias, sem cartão de crédito, preço baseado em uso a partir de US$ 49 e hospedagem de dados por região.
👉 Execute agregações em dados de rastreamento para obter métricas de negócios relevantes.
[**Comece gratuitamente →**](https://signoz.io/teams/)
#### Enterprise
![SigNoz Feature](https://signoz-public.s3.us-east-2.amazonaws.com/signoz_hero_github.png)
Enterprise Cloud, BYOC ou Enterprise Self-Hosted com compliance, suporte, retenção personalizada, RBAC, controles de ingestão, residência de dados e seleção de região.
<br /><br />
[**Conheça o Enterprise →**](https://signoz.io/enterprise/)
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
#### Community
## Junte-se à nossa comunidade no Slack
SigNoz open-source gratuito, executado na sua própria infraestrutura. Faça o deploy com Docker, Kubernetes ou Linux e mantenha controle total sobre o seu plano de dados.
Venha dizer oi para nós no [Slack](https://signoz.io/slack) 👋
[**Instale o SigNoz →**](https://signoz.io/docs/install/self-host/)
<br /><br />
### O que você pode monitorar?
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Features.svg" width="50px" />
O SigNoz ajuda equipes a depurar problemas de produção mais rapidamente ao conectar logs, métricas, traces, alertas, dashboards, exceções e fluxos agent-native em um só lugar.
## Funções:
#### Visão geral de APM
- Métricas de visão geral do aplicativo, como RPS, latências de percentual 50/90/99 e taxa de erro
- Endpoints mais lentos em seu aplicativo
- Visualize o rastreamento preciso de requisições de rede para descobrir problemas em serviços downstream, consultas lentas de banco de dados, chamadas para serviços de terceiros, como gateways de pagamento, etc.
- Filtre os rastreamentos por nome de serviço, operação, latência, erro, tags / anotações.
- Execute agregações em dados de rastreamento (eventos / extensões) para obter métricas de negócios relevantes, como por exemplo, você pode obter a taxa de erro e a latência do 99º percentil de `customer_type: gold` or `deployment_version: v2` or `external_call: paypal`
- Interface de Usuário unificada para métricas e rastreios. Não há necessidade de mudar de Prometheus para Jaeger para depurar problemas.
Monitore latência de serviço, taxa de erro, throughput, Apdex, principais endpoints, chamadas ao banco de dados e chamadas externas.
<br /><br />
<p align="center">
<img alt="Dashboard de APM do SigNoz mostrando latência, throughput, Apdex e operações principais" src="docs/readme-assets/monitor/apm.png" width="900">
</p>
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/WhatsCool.svg" width="50px" />
Saiba mais: [documentação de APM](https://signoz.io/docs/instrumentation/overview/)
## Por que escolher SigNoz?
#### Gerenciamento de logs
Sendo desenvolvedores, achamos irritante contar com fornecedores de SaaS de código fechado para cada pequeno recurso que queríamos. Fornecedores de código fechado costumam surpreendê-lo com enormes contas no final do mês de uso sem qualquer transparência .
Ingira, pesquise, agregue e correlacione logs com traces e métricas usando um construtor visual de consultas.
Queríamos fazer uma versão auto-hospedada e de código aberto de ferramentas como DataDog, NewRelic para empresas que têm preocupações com privacidade e segurança em ter dados de clientes indo para serviços de terceiros.
<p align="center">
<img alt="Explorador de logs do SigNoz com filtros, gráfico de frequência e linhas de log" src="docs/readme-assets/monitor/log-management.svg" width="900">
</p>
Ser open source também oferece controle completo de sua configuração, amostragem e tempos de atividade. Você também pode construir módulos sobre o SigNoz para estender recursos específicos do negócio.
Saiba mais: [documentação de gerenciamento de logs](https://signoz.io/docs/logs-management/overview/)
### Linguagens Suportadas:
#### Métricas e dashboards
Nós apoiamos a biblioteca [OpenTelemetry](https://opentelemetry.io) como a biblioteca que você pode usar para instrumentar seus aplicativos. Em outras palavras, SigNoz oferece suporte a qualquer framework e linguagem que suporte a biblioteca OpenTelemetry. As principais linguagens suportadas incluem:
Crie dashboards para métricas de aplicação, infraestrutura e métricas personalizadas usando Query Builder, PromQL ou ClickHouse SQL.
- Java
- Python
- NodeJS
- Go
<p align="center">
<img alt="Dashboard de métricas de host do SigNoz com gráficos de carga do sistema e rede" src="docs/readme-assets/monitor/metrics.png" width="900">
</p>
Você pode encontrar a lista completa de linguagens aqui - https://opentelemetry.io/docs/
Saiba mais: [documentação de métricas](https://signoz.io/docs/metrics-management/overview/)
<br /><br />
#### Monitoramento de infraestrutura
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Philosophy.svg" width="50px" />
Monitore clusters Kubernetes, pods, nodes, workloads e CPU, memória, disco, rede, logs e traces em nível de host.
## Iniciando
### Implantar usando Docker
<p align="center">
<img alt="Dashboard de infraestrutura Kubernetes do SigNoz com métricas de pods e nodes" src="docs/readme-assets/monitor/infrastructure.png" width="900">
</p>
Siga as etapas listadas [aqui](https://signoz.io/docs/install/docker/) para instalar usando o Docker.
Saiba mais: [documentação de monitoramento de infraestrutura](https://signoz.io/docs/infrastructure-monitoring/overview/)
Esse [guia para solução de problemas](https://signoz.io/docs/install/troubleshooting/) pode ser útil se você enfrentar quaisquer problemas.
#### Observabilidade de LLM e AI
<p>&nbsp </p>
### Implentar no Kubernetes usando Helm
Rastreie apps LLM, pipelines RAG, prompts, chamadas de ferramentas, tokens, latência e custos junto com telemetria de aplicação e infraestrutura.
Siga as etapas listadas [aqui](https://signoz.io/docs/deployment/helm_chart) para instalar usando helm charts.
<p align="center">
<img alt="Dashboard de observabilidade de LLM do SigNoz para traces, uso de tokens, latência e custos" src="docs/readme-assets/monitor/llm.png" width="900">
</p>
<br /><br />
Saiba mais: [documentação de observabilidade de LLM](https://signoz.io/docs/llm-observability/)
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/UseSigNoz.svg" width="50px" />
#### Observabilidade agent-native e MCP
## Comparações com ferramentas similares
Use o servidor MCP do SigNoz para levar telemetria aos agentes de programação, ou use o Noz dentro do SigNoz para investigar incidentes, ajustar alertas e criar dashboards com contexto de produção. O [Noz](https://signoz.io/docs/ai/noz/) está disponível apenas no SigNoz Cloud.
### SigNoz ou Prometheus
<p align="center">
<img alt="Interface Noz do SigNoz ao lado de um fluxo agent via MCP" src="docs/readme-assets/monitor/agent-native.png" width="900">
</p>
Prometheus é bom se você quiser apenas fazer métricas. Mas se você quiser ter uma experiência perfeita entre métricas e rastreamentos, a experiência atual de unir Prometheus e Jaeger não é ótima.
Saiba mais: [documentação do servidor MCP do SigNoz](https://signoz.io/docs/ai/signoz-mcp-server/) · [documentação de agent skills](https://signoz.io/docs/ai/agent-skills/#install-the-plugin)
Nosso objetivo é fornecer uma interface do usuário integrada entre métricas e rastreamentos - semelhante ao que fornecedores de SaaS como o Datadog fornecem - e fornecer filtragem e agregação avançada sobre rastreamentos, algo que a Jaeger atualmente carece.
#### Tracing distribuído
<p>&nbsp </p>
Acompanhe requisições entre serviços com flamegraphs, waterfalls, eventos de span, filtros e análise de traces.
### SigNoz ou Jaeger
<p align="center">
<img alt="Visualização de tracing distribuído do SigNoz com flamegraph e spans em waterfall" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
</p>
Jaeger só faz rastreamento distribuído. SigNoz faz métricas e rastreia, e também temos gerenciamento de log em nossos planos.
Saiba mais: [documentação de tracing distribuído](https://signoz.io/docs/instrumentation/)
Além disso, SigNoz tem alguns recursos mais avançados do que Jaeger:
#### Trace Funnels
- A interface de usuário do Jaegar não mostra nenhuma métrica em traces ou em traces filtrados
- Jaeger não pode obter agregados em rastros filtrados. Por exemplo, latência p99 de solicitações que possuem tag - customer_type='premium'. Isso pode ser feito facilmente com SigNoz.
Crie funis a partir de traces para entender quedas no fluxo de requisições, transições com falha e problemas sistêmicos de workflow.
<br /><br />
<p align="center">
<img alt="Trace Funnels do SigNoz mostrando quedas no fluxo de requisições e transições com falha" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
</p>
Saiba mais: [documentação de Trace Funnels](https://signoz.io/docs/trace-funnels/overview/)
Também monitore: [**exceções**](https://signoz.io/docs/userguide/exceptions/), [**alertas**](https://signoz.io/docs/alerts/), [**APIs externas**](https://signoz.io/docs/external-api-monitoring/overview/) e [**integrações**](https://signoz.io/docs/integrations/integrations-list/) para OpenTelemetry, Prometheus, Kubernetes, provedores de nuvem, SDKs de linguagem, frameworks de aplicação, bancos de dados e ferramentas de LLM.
### Por que equipes usam o SigNoz
1. **Nativo em OpenTelemetry**<br>
Instrumente uma vez com padrões abertos e mantenha a posse da sua telemetria.
2. **Sinais correlacionados**<br>
Vá de gráficos de serviço para traces, logs, métricas de infraestrutura e exceções sem trocar de ferramenta.
3. **Um único banco de dados colunar**<br>
Construído para workloads de observabilidade de alto volume e alta cardinalidade.
4. **Preço previsível**<br>
Sem cobrança por host, sem cobrança por usuário e sem preço especial para métricas personalizadas.
5. **Pronto para enterprise**<br>
Compliance SOC 2 Type II e HIPAA, RBAC, controles de ingestão, retenção personalizada, suporte, BYOC e self-hosting.
### Primeiros passos
#### Comece na Cloud
Crie um workspace gerenciado do SigNoz e obtenha seu primeiro dashboard sem operar infraestrutura de observabilidade.
[**Comece gratuitamente no SigNoz Cloud**](https://signoz.io/teams/)
#### Self-host SigNoz
Execute o SigNoz na sua própria infraestrutura com Foundry, Docker, Kubernetes ou Linux.
[**Foundry**](https://github.com/SigNoz/foundry) · [**Docker**](https://signoz.io/docs/install/docker/) · [**Kubernetes**](https://signoz.io/docs/install/kubernetes/) · [**Linux**](https://signoz.io/docs/install/linux/)
#### Envie dados
Instrumente aplicações e infraestrutura com OpenTelemetry, Prometheus, SDKs de linguagem e integrações.
[**Instrumentação**](https://signoz.io/docs/instrumentation/) · [**Integrações**](https://signoz.io/docs/integrations/integrations-list/)
### Comparações com ferramentas conhecidas
O SigNoz é frequentemente adotado por equipes que estão migrando de ferramentas de propósito único ou plataformas comerciais com preços imprevisíveis.
**Prometheus**<br>
Bom se você precisa apenas de métricas. O SigNoz mantém métricas, logs, traces, dashboards e alertas juntos para que equipes possam depurar com contexto correlacionado.
**Jaeger**<br>
Jaeger faz apenas tracing distribuído. O SigNoz adiciona métricas, logs, análise de traces, dashboards, alertas, exceções e fluxos de trace para log.
**Elastic**<br>
O SigNoz usa banco de dados colunar para análises de observabilidade eficientes e workloads de logs de alta cardinalidade, com 50% menos necessidade de recursos em comparação ao Elastic durante a ingestão. Confira o [estudo detalhado](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
**Loki**<br>
No benchmark vinculado, o SigNoz indexou todas as chaves na configuração de teste, enquanto o Loki atingiu erros de max streams ao adicionar mais labels. Confira o [estudo detalhado](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributors.svg" width="50px" />
## Contribuindo
Adoramos contribuições grandes ou pequenas. Leia [CONTRIBUTING.md](CONTRIBUTING.md) para começar a contribuir com o SigNoz.
Não sabe como começar? **Fale conosco no `#contributing` na nossa [comunidade Slack](https://signoz.io/slack).**
Nós ❤️ contribuições grandes ou pequenas. Leia [CONTRIBUTING.md](CONTRIBUTING.md) para começar a fazer contribuições para o SigNoz.
Como sempre, obrigado aos nossos incríveis contribuidores!
Não sabe como começar? Basta enviar um sinal para nós no canal `#contributing` em nossa [comunidade no Slack.](https://signoz.io/slack)
<br /><br />
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/DevelopingLocally.svg" width="50px" />
## Documentação
Você pode encontrar a documentação em https://signoz.io/docs/. Se você tiver alguma dúvida ou sentir falta de algo, sinta-se à vontade para criar uma issue com a tag `documentation` no GitHub ou entre em contato conosco no canal da comunidade no Slack.
<br /><br />
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
## Comunidade
Junte-se a [comunidade no Slack](https://signoz.io/slack) para saber mais sobre rastreamento distribuído, observabilidade ou SigNoz e para se conectar com outros usuários e colaboradores.
Se você tiver alguma ideia, pergunta ou feedback, compartilhe em nosso [Github Discussões](https://github.com/SigNoz/signoz/discussions)
Como sempre, obrigado aos nossos incríveis colaboradores!
<a href="https://github.com/signoz/signoz/graphs/contributors">
<img alt="Contribuidores do SigNoz" src="https://contrib.rocks/image?repo=signoz/signoz" />
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

View File

@@ -1,190 +1,208 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/readme-assets/signoz-hero-dark.png" width="900">
<source media="(prefers-color-scheme: light)" srcset="docs/readme-assets/signoz-hero-light.png" width="900">
<img alt="SigNoz - 按你的方式运行的可观测性,由开放标准驱动。" src="docs/readme-assets/signoz-hero-light.png" width="900">
</picture>
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
<p align="center">监控你的应用,并且可排查已部署应用的问题,这是一个可替代 DataDog、NewRelic 的开源方案</p>
</p>
<p align="center">
<a href="README.md">English</a> ·
<a href="README.de-de.md">Deutsch</a> ·
<a href="README.pt-br.md">Português</a>
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker Downloads"> </a>
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
</p>
<p align="center">
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
<a href="https://github.com/SigNoz/signoz/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/SigNoz/signoz?label=release"></a>
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
<a href="https://www.linkedin.com/company/signozio/"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin-SigNoz-0A66C2?logo=linkedin&logoColor=white"></a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability"><img alt="Tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"></a>
</p>
<h3 align="center">
<a href="https://signoz.io/docs"><b>文档</b></a>
<a href="https://github.com/SigNoz/signoz/blob/main/README.zh-cn.md"><b>中文ReadMe</b></a>
<a href="https://github.com/SigNoz/signoz/blob/main/README.de-de.md"><b>德文ReadMe</b></a>
<a href="https://github.com/SigNoz/signoz/blob/main/README.pt-br.md"><b>葡萄牙语ReadMe</b></a>
<a href="https://signoz.io/slack"><b>Slack 社区</b></a>
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
</h3>
SigNoz 是一个基于 OpenTelemetry 构建的开源可观测性平台。我们正在构建一个企业级替代方案,用来替代分散的监控工具栈,把日志、指标、链路追踪、告警和仪表盘放在同一个地方。
##
### 选择 SigNoz 的运行方式
SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可以使用 SigNoz 实现如下能力:
#### SigNoz Cloud推荐
👉 在同一块面板上,可视化 Metrics, Traces 和 Logs 内容。
完全托管的 SigNoz提供 30 天免费试用,无需信用卡,按用量计费,起价为 49 美元,并支持区域化数据托管
👉 你可以关注服务的 p99 延迟和错误率, 包括外部 API 调用和个别的端点
[**免费开始 →**](https://signoz.io/teams/)
👉 你可以找到问题的根因,通过提取相关问题的 traces 日志、单独查看请求 traces 的火焰图详情。
#### 企业版
👉 执行 trace 数据聚合,以获取业务相关的 metrics
Enterprise Cloud、BYOC 或 Enterprise Self-Hosted提供合规、支持、自定义保留期、RBAC、摄取控制、数据驻留和区域选择。
👉 对日志过滤和查询,通过日志的属性建立看板和告警
[**了解企业版 →**](https://signoz.io/enterprise/)
👉 通过 PythonjavaRuby 和 Javascript 自动记录异常
#### 社区版
👉 轻松的自定义查询和设置告警
免费的开源 SigNoz可运行在你自己的基础设施中。使用 Docker、Kubernetes 或 Linux 部署,并完全掌控你的数据平面。
### 应用 Metrics 展示
[**安装 SigNoz →**](https://signoz.io/docs/install/self-host/)
![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png)
### 你可以监控什么?
### 分布式追踪
SigNoz 将日志、指标、链路追踪、告警、仪表盘、异常和面向 Agent 的工作流连接在一起,帮助团队更快地调试生产问题。
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
#### APM 概览
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
监控服务延迟、错误率、吞吐量、Apdex、核心端点、数据库调用和外部调用。
### 日志管理
<p align="center">
<img alt="SigNoz APM 仪表盘展示延迟、吞吐量、Apdex 和关键操作" src="docs/readme-assets/monitor/apm.png" width="900">
</p>
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
了解更多:[APM 文档](https://signoz.io/docs/instrumentation/overview/)
### 基础设施监控
#### 日志管理
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
摄取、搜索、聚合日志,并通过可视化查询构建器将日志与链路追踪和指标关联起来。
### 异常监控
<p align="center">
<img alt="SigNoz 日志浏览器,包含过滤器、频率图和日志行" src="docs/readme-assets/monitor/log-management.svg" width="900">
</p>
![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.png)
了解更多:[日志管理文档](https://signoz.io/docs/logs-management/overview/)
### 告警
#### 指标和仪表盘
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
使用 Query Builder、PromQL 或 ClickHouse SQL 为应用、基础设施和自定义指标构建仪表盘。
<br /><br />
<p align="center">
<img alt="SigNoz 主机指标仪表盘,展示系统负载和网络图表" src="docs/readme-assets/monitor/metrics.png" width="900">
</p>
## 加入我们 Slack 社区
了解更多:[指标文档](https://signoz.io/docs/metrics-management/overview/)
来 [Slack](https://signoz.io/slack) 和我们打招呼吧 👋
#### 基础设施监控
<br /><br />
监控 Kubernetes 集群、Pod、节点、工作负载以及主机级 CPU、内存、磁盘、网络、日志和链路追踪。
## 特性:
<p align="center">
<img alt="SigNoz Kubernetes 基础设施仪表盘,展示 Pod 和节点指标" src="docs/readme-assets/monitor/infrastructure.png" width="900">
</p>
- 为 metrics, traces and logs 制定统一的 UI。 无需切换 Prometheus 到 Jaeger 去查找问题,也无需使用想 Elastic 这样的日志工具分开你的 metrics 和 traces
了解更多:[基础设施监控文档](https://signoz.io/docs/infrastructure-monitoring/overview/)
- 默认统计应用的 metrics 数据,像 RPS (每秒请求数) 50th/90th/99th 的分位数延迟数据,还有相关的错误率
#### LLM 和 AI 可观测性
- 找到应用中最慢的端点
追踪 LLM 应用、RAG 流水线、Prompt、工具调用、Token、延迟和成本并与应用和基础设施遥测数据放在一起分析。
- 查看准确的请求跟踪数据,找到下游服务的问题了,比如 DB 慢查询,或者调用第三方的支付网关等
<p align="center">
<img alt="SigNoz LLM 可观测性仪表盘展示链路追踪、Token 使用、延迟和成本" src="docs/readme-assets/monitor/llm.png" width="900">
</p>
- 通过 服务名、操作方式、延迟、错误、标签/注释 过滤 traces 数据
了解更多:[LLM 可观测性文档](https://signoz.io/docs/llm-observability/)
- 通过聚合 trace 数据而获得业务相关的 metrics。 比如你可以通过 `customer_type: gold` 或者 `deployment_version: v2` 或者 `external_call: paypal` 获取错误率和 P99 延迟数据
#### Agent 原生可观测性和 MCP
- 原生支持 OpenTelemetry 日志,高级日志查询,自动收集 k8s 相关日志
使用 SigNoz MCP server 将遥测数据带入编程 Agent或在 SigNoz 中使用 Noz基于生产上下文调查事故、优化告警并构建仪表盘。[Noz](https://signoz.io/docs/ai/noz/) 仅适用于 SigNoz Cloud。
- 快如闪电的日志分析 ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
<p align="center">
<img alt="SigNoz Noz 界面与基于 MCP 的 Agent 工作流" src="docs/readme-assets/monitor/agent-native.png" width="900">
</p>
- 可视化点到点的基础设施性能,提取有所有类型机器的 metrics 数据
了解更多:[SigNoz MCP server 文档](https://signoz.io/docs/ai/signoz-mcp-server/) · [Agent skills 文档](https://signoz.io/docs/ai/agent-skills/#install-the-plugin)
- 轻易自定义告警查询
#### 分布式链路追踪
<br /><br />
通过火焰图、瀑布图、Span 事件、过滤器和 Trace 分析,跟踪请求在各个服务之间的流转。
## 为什么使用 SigNoz?
<p align="center">
<img alt="SigNoz 分布式链路追踪视图,包含火焰图和瀑布图 Span" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
</p>
作为开发者, 我们发现 SaaS 厂商对一些大家想要的小功能都是闭源的,这种行为真的让人有点恼火。 闭源厂商还会在月底给你一张没有明细的巨额账单。
了解更多:[分布式链路追踪文档](https://signoz.io/docs/instrumentation/)
我们想做一个自托管并且可开源的工具,像 DataDog 和 NewRelic 那样, 为那些担心数据隐私和安全的公司提供第三方服务。
#### Trace Funnels
作为开源的项目,你完全可以自己掌控你的配置、样本和更新。你同样可以基于 SigNoz 拓展特定的业务模块。
基于链路追踪创建漏斗,用于理解请求流中的掉点、失败转换和系统性工作流问题。
### 支持的编程语言:
<p align="center">
<img alt="SigNoz Trace Funnels展示请求流掉点和失败转换" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
</p>
我们支持 [OpenTelemetry](https://opentelemetry.io)。作为一个观测你应用的库文件。所以任何 OpenTelemetry 支持的框架和语言,对于 SigNoz 也同样支持。 一些主要支持的语言如下:
了解更多:[Trace Funnels 文档](https://signoz.io/docs/trace-funnels/overview/)
- Java
- Python
- NodeJS
- Go
- PHP
- .NET
- Ruby
- Elixir
- Rust
可以监控:[**异常**](https://signoz.io/docs/userguide/exceptions/)、[**告警**](https://signoz.io/docs/alerts/)、[**外部 API**](https://signoz.io/docs/external-api-monitoring/overview/),以及面向 OpenTelemetry、Prometheus、Kubernetes、云服务商、语言 SDK、应用框架、数据库和 LLM 工具的[**集成**](https://signoz.io/docs/integrations/integrations-list/)。
你可以在这里找到全部支持的语言列表 - https://opentelemetry.io/docs/
### 为什么团队选择 SigNoz
<br /><br />
1. **OpenTelemetry 原生**<br>
用开放标准完成一次接入,并保持对遥测数据的所有权。
2. **信号关联**<br>
在服务图表、链路追踪、日志、基础设施指标和异常之间切换时,不需要更换工具。
3. **单一列式数据库**<br>
为高基数、高吞吐量的可观测性工作负载而构建。
4. **可预测的定价**<br>
不按主机收费,不按用户席位收费,也不对自定义指标设置特殊价格。
5. **企业就绪**<br>
SOC 2 Type II 和 HIPAA 合规、RBAC、摄取控制、自定义保留期、支持、BYOC 和自托管。
## 让我们开始吧
### 快速开始
### 使用 Docker 部署
#### 从 Cloud 开始
请一步步跟随 [这里](https://signoz.io/docs/install/docker/) 通过 docker 来安装。
创建一个托管的 SigNoz 工作区,无需运行可观测性基础设施,即可获得第一个仪表盘
这个 [排障说明书](https://signoz.io/docs/install/troubleshooting/) 可以帮助你解决碰到的问题
[**免费开始使用 SigNoz Cloud**](https://signoz.io/teams/)
<p>&nbsp </p>
#### 自托管 SigNoz
### 使用 Helm 在 Kubernetes 部署
在你自己的基础设施中通过 Foundry、Docker、Kubernetes 或 Linux 运行 SigNoz。
请一步步跟随 [这里](https://signoz.io/docs/deployment/helm_chart) 通过 helm 来安装
[**Foundry**](https://github.com/SigNoz/foundry) · [**Docker**](https://signoz.io/docs/install/docker/) · [**Kubernetes**](https://signoz.io/docs/install/kubernetes/) · [**Linux**](https://signoz.io/docs/install/linux/)
<br /><br />
#### 发送数据
## 比较相似的工具
使用 OpenTelemetry、Prometheus、语言 SDK 和集成来接入应用与基础设施。
### SigNoz vs Prometheus
[**接入文档**](https://signoz.io/docs/instrumentation/) · [**集成列表**](https://signoz.io/docs/integrations/integrations-list/)
Prometheus 是一个针对 metrics 监控的强大工具。但是如果你想无缝的切换 metrics 和 traces 查询,你当前大概率需要在 Prometheus 和 Jaeger 之间切换。
### 与常见工具的对比
我们的目标是提供一个客户观测 metrics 和 traces 整合的 UI。就像 SaaS 供应商 DataDog它提供很多 jaeger 缺失的功能,比如针对 traces 过滤功能和聚合功能。
许多团队在从单一用途工具或价格不可预测的商业平台迁移时,会选择 SigNoz。
<p>&nbsp </p>
**Prometheus**<br>
如果你只需要指标Prometheus 很合适。SigNoz 将指标、日志、链路追踪、仪表盘和告警放在一起,让团队可以通过关联上下文进行调试。
### SigNoz vs Jaeger
**Jaeger**<br>
Jaeger 只做分布式链路追踪。SigNoz 增加了指标、日志、Trace 分析、仪表盘、告警、异常和 Trace 到日志的工作流。
Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metrics, traces 和 logs 所有的观测。
**Elastic**<br>
SigNoz 使用列式数据库来高效处理可观测性分析和高基数日志工作负载。在摄取阶段,相比 Elastic 可降低 50% 的资源需求。查看[详细评测](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)。
而且, SigNoz 相较于 Jaeger 拥有更对的高级功能:
**Loki**<br>
在链接的评测中SigNoz 在测试设置中索引了所有键,而 Loki 在增加更多标签时遇到了 max stream 错误。查看[详细评测](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)。
- Jaegar UI 不能提供任何基于 traces 的 metrics 查询和过滤。
- Jaeger 不能针对过滤的 traces 做聚合。 比如, p99 延迟的请求有个标签是 customer_type='premium'。 而这些在 SigNoz 可以轻松做到。
<p>&nbsp </p>
### SigNoz vs Elastic
- SigNoz 的日志管理是基于 ClickHouse 实现的,可以使日志的聚合更加高效,因为它是基于 OLAP 的数据仓储。
- 与 Elastic 相比,可以节省 50% 的资源成本
我们已经公布了 Elastic 和 SigNoz 的性能对比。 请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
<p>&nbsp </p>
### SigNoz vs Loki
- SigNoz 支持大容量高基数的聚合,但是 loki 是不支持的。
- SigNoz 支持索引的高基数查询,并且对索引没有数量限制,而 Loki 会在添加部分索引后到达最大上限。
- 相较于 SigNozLoki 在搜索大量数据下既困难又缓慢。
我们已经发布了基准测试对比 Loki 和 SigNoz 性能。请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
<br /><br />
## 贡献
无论贡献大小,我们都非常欢迎。请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)开始 SigNoz 做贡献。
我们 ❤️ 你的贡献,无论大小。 请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)开始 SigNoz 做贡献。
不确定如何开始?**可以在我们的 [Slack 社区](https://signoz.io/slack)通过 `#contributing` 联系我们。**
如果你不知道如何开始? 只需要在 [slack 社区](https://signoz.io/slack) 通过 `#contributing` 频道联系我们。
一如既往,感谢所有出色的贡献者!
<br /><br />
## 文档
你可以通过 https://signoz.io/docs/ 找到相关文档。如果你需要阐述问题或者发现一些确实的事件, 通过标签为 `documentation` 提交 Github 问题。或者通过 slack 社区频道。
<br /><br />
## 社区
加入 [slack 社区](https://signoz.io/slack) 去了解更多关于分布式追踪、可观测性系统 。或者与 SigNoz 其他用户和贡献者交流。
如果你有任何想法、问题、或者任何反馈, 请通过 [Github Discussions](https://github.com/SigNoz/signoz/discussions) 分享。
不管怎么样,感谢这个项目的所有贡献者!
<a href="https://github.com/signoz/signoz/graphs/contributors">
<img alt="SigNoz 贡献者" src="https://contrib.rocks/image?repo=signoz/signoz" />
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

View File

@@ -71,10 +71,6 @@ web:
sentry:
# Whether to enable Sentry in web.
enabled: false
# The Sentry DSN.
dsn: ""
# The Sentry tunnel URL.
tunnel: ""
pylon:
# Whether to enable Pylon in web.
enabled: false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -4,7 +4,5 @@ VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
VITE_PYLON_APP_ID="pylon-app-id"
VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
VITE_SENTRY_DSN="sentry-dsn"
VITE_TUNNEL_URL="sentry-tunnel-url"
CI="1"

View File

@@ -35,7 +35,6 @@ import { PreferenceContextProvider } from 'providers/preferences/context/Prefere
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { extractDomain } from 'utils/app';
import { getWebSettings } from 'utils/bootSettings';
import { Home } from './pageComponents';
import PrivateRoute from './Private';
@@ -334,19 +333,24 @@ function App(): JSX.Element {
}, [user, isFetchingUser, isCloudUser, enableAnalytics]);
useEffect(() => {
const webSettings = getWebSettings();
if (isCloudUser || isEnterpriseSelfHostedUser) {
if ((webSettings?.posthog.enabled ?? true) && process.env.POSTHOG_KEY) {
if (
(window.signozBootData?.settings?.posthog.enabled ?? true) &&
process.env.POSTHOG_KEY
) {
posthog.init(process.env.POSTHOG_KEY, {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
});
}
if (!isSentryInitialized && (webSettings?.sentry.enabled ?? true)) {
if (
!isSentryInitialized &&
(window.signozBootData?.settings?.sentry.enabled ?? true)
) {
Sentry.init({
dsn: webSettings?.sentry.dsn,
tunnel: webSettings?.sentry.tunnel,
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,
environment: process.env.ENVIRONMENT,
release: process.env.VERSION,
integrations: [

View File

@@ -20,8 +20,6 @@ interface ConfigPaneProps {
/** The panel spec — the single editing surface (title/description + section slices). */
spec: DashboardtypesPanelSpecDTO;
onChangeSpec: (next: DashboardtypesPanelSpecDTO) => void;
/** Switch the panel to another visualization kind. */
onChangePanelKind: (kind: PanelKind) => void;
/** Panel's resolved series, provided to sections that need them (legend colors). */
legendSeries: LegendSeries[];
/** Table panel's resolved value columns, for the table-only editors. */
@@ -38,14 +36,13 @@ function ConfigPane({
panelKind,
spec,
onChangeSpec,
onChangePanelKind,
legendSeries,
tableColumns,
}: ConfigPaneProps): JSX.Element {
const definition = getPanelDefinition(panelKind);
const sections = definition.sections;
const signal = getBuilderQueries(spec.queries)[0]?.signal as
const signal = getBuilderQueries(spec.queries || [])[0]?.signal as
| TelemetrytypesSignalDTO
| undefined;
@@ -98,8 +95,6 @@ function ConfigPane({
legendSeries={legendSeries}
tableColumns={tableColumns}
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
/>
))}
</div>

View File

@@ -1,6 +0,0 @@
// Matches ConfigPane's `.field` so the switcher lines up with the title/description fields.
.field {
display: flex;
flex-direction: column;
gap: 8px;
}

View File

@@ -1,53 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { getPanelDefinition } from '../../../Panels/registry';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { PANEL_TYPES } from '../../../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/constants';
import ConfigSelect from '../controls/ConfigSelect/ConfigSelect';
import styles from './PanelTypeSwitcher.module.scss';
interface PanelTypeSwitcherProps {
/** The current panel kind (selected value). */
panelKind: PanelKind;
/** Panel's current datasource — drives the disabled rule. */
signal?: TelemetrytypesSignalDTO;
onChange: (kind: PanelKind) => void;
}
/**
* Visualization-type selector (rendered inside the Visualization section). Types whose
* supported signals exclude the panel's current datasource are disabled (V1 parity —
* e.g. List needs logs/traces, not metrics). The datasource is unknown for
* PromQL/ClickHouse queries, in which case no type is disabled.
*/
function PanelTypeSwitcher({
panelKind,
signal,
onChange,
}: PanelTypeSwitcherProps): JSX.Element {
const items = PANEL_TYPES.map(({ panelKind, label, Icon }) => {
const definition = getPanelDefinition(panelKind);
return {
value: panelKind,
label,
icon: <Icon size={14} />,
disabled: !!signal && !definition.supportedSignals.includes(signal),
};
});
return (
<div className={styles.field}>
<Typography.Text>Panel Type</Typography.Text>
<ConfigSelect
testId="panel-editor-v2-type-switcher"
value={panelKind}
items={items}
onChange={(value): void => onChange(value)}
/>
</div>
);
}
export default PanelTypeSwitcher;

View File

@@ -1,73 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
import PanelTypeSwitcher from '../PanelTypeSwitcher';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
jest.mock('pages/DashboardPageV2/DashboardContainer/Panels/registry', () => ({
getPanelDefinition: jest.fn(),
}));
const mockGetPanelDefinition = getPanelDefinition as unknown as jest.Mock;
function openDropdown(): void {
fireEvent.mouseDown(screen.getByRole('combobox'));
}
describe('PanelTypeSwitcher', () => {
beforeEach(() => {
jest.clearAllMocks();
// List supports only logs/traces; every other kind also supports metrics.
mockGetPanelDefinition.mockImplementation((kind: string) => ({
supportedSignals:
kind === 'signoz/ListPanel'
? ['logs', 'traces']
: ['metrics', 'logs', 'traces'],
}));
});
it('fires onChange with the chosen plugin kind', () => {
const onChange = jest.fn();
render(
<PanelTypeSwitcher panelKind="signoz/TimeSeriesPanel" onChange={onChange} />,
);
openDropdown();
fireEvent.click(screen.getByText('List'));
expect(onChange).toHaveBeenCalledWith('signoz/ListPanel');
});
it('disables types whose supported signals exclude the current datasource', () => {
render(
<PanelTypeSwitcher
panelKind="signoz/TimeSeriesPanel"
signal={TelemetrytypesSignalDTO.metrics}
onChange={jest.fn()}
/>,
);
openDropdown();
const disabled = Array.from(
document.querySelectorAll('.ant-select-item-option-disabled'),
).map((el) => el.textContent);
// List can't render a metrics query, so it's disabled; Time Series stays enabled.
expect(disabled).toContain('List');
expect(disabled).not.toContain('Time Series');
});
it('does not disable any type when the datasource is unknown', () => {
render(
<PanelTypeSwitcher
panelKind="signoz/TimeSeriesPanel"
onChange={jest.fn()}
/>,
);
openDropdown();
expect(
document.querySelectorAll('.ant-select-item-option-disabled'),
).toHaveLength(0);
});
});

View File

@@ -6,10 +6,8 @@ import {
type PanelFormattingSlice,
SECTION_METADATA,
type SectionConfig,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { PanelKind } from '../../../Panels/types/panelKind';
import type { LegendSeries } from '../../hooks/useLegendSeries';
import type { TableColumnOption } from '../../hooks/useTableColumns';
import { resolveSectionEditor } from '../sectionRegistry';
@@ -25,9 +23,6 @@ interface SectionSlotProps {
tableColumns: TableColumnOption[];
/** Panel's telemetry signal, for editors that fetch field suggestions (List columns). */
signal?: TelemetrytypesSignalDTO;
/** Current panel kind + switch handler, for the visualization section's type switcher. */
panelKind: PanelKind;
onChangePanelKind: (kind: PanelKind) => void;
}
/**
@@ -43,8 +38,6 @@ function SectionSlot({
legendSeries,
tableColumns,
signal,
panelKind,
onChangePanelKind,
}: SectionSlotProps): JSX.Element | null {
// A kind can hide a section based on current spec state (e.g. Histogram legend once
// queries are merged) — skip it before resolving the editor.
@@ -67,12 +60,7 @@ function SectionSlot({
.formatting?.unit;
return (
<SettingsSection
title={title}
icon={<Icon size={15} />}
// Open Visualization by default so the type switcher is visible.
defaultOpen={config.kind === SectionKind.Visualization}
>
<SettingsSection title={title} icon={<Icon size={15} />}>
<Component
value={get(spec)}
controls={controls}
@@ -81,8 +69,6 @@ function SectionSlot({
yAxisUnit={yAxisUnit}
tableColumns={tableColumns}
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
/>
</SettingsSection>
);

View File

@@ -21,7 +21,6 @@ function renderConfigPane(
panelKind: 'signoz/TimeSeriesPanel',
spec: spec(),
onChangeSpec: jest.fn(),
onChangePanelKind: jest.fn(),
legendSeries: [],
tableColumns: [],
...overrides,

View File

@@ -1,5 +1,5 @@
.group {
width: 100%;
width: min(350px, 100%);
}
.segment {

View File

@@ -1,22 +1,21 @@
import type { ReactNode } from 'react';
import { Select } from 'antd';
import { SegmentIcon, type SegmentIconName } from '../segmentIcons';
import styles from './ConfigSelect.module.scss';
export interface ConfigSelectItem<T extends string = string> {
value: T;
export interface ConfigSelectItem {
value: string;
label: string;
/** Optional leading icon node rendered before the label. */
icon?: ReactNode;
disabled?: boolean;
icon?: SegmentIconName;
}
interface ConfigSelectProps<T extends string = string> {
interface ConfigSelectProps {
testId: string;
value: T | undefined;
value: string | undefined;
placeholder?: string;
items: ConfigSelectItem<T>[];
onChange: (value: T) => void;
items: ConfigSelectItem[];
onChange: (value: string) => void;
}
/**
@@ -24,15 +23,15 @@ interface ConfigSelectProps<T extends string = string> {
* `Select` so it matches the rest of the editor's antd controls; the menu portals to
* `document.body` (antd default) so the surrounding `overflow:auto` pane can't clip it.
*/
function ConfigSelect<T extends string = string>({
function ConfigSelect({
testId,
value,
placeholder,
items,
onChange,
}: ConfigSelectProps<T>): JSX.Element {
}: ConfigSelectProps): JSX.Element {
return (
<Select<T>
<Select<string>
className={styles.select}
data-testid={testId}
value={value}
@@ -41,10 +40,9 @@ function ConfigSelect<T extends string = string>({
virtual={false}
options={items.map((item) => ({
value: item.value,
disabled: item.disabled,
label: item.icon ? (
<span className={styles.item}>
{item.icon}
<SegmentIcon name={item.icon} />
{item.label}
</span>
) : (

View File

@@ -8,12 +8,12 @@ import type {
DashboardtypesPanelSpecDTO,
DashboardtypesTimeSeriesChartAppearanceDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
import type {
AnyThreshold,
PanelFormattingSlice,
SectionEditorProps,
SectionKind,
type AnyThreshold,
type PanelFormattingSlice,
type SectionEditorProps,
type SectionSpecMap,
SectionSpecMap,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import AxesSection from './sections/AxesSection/AxesSection';
@@ -69,27 +69,27 @@ function updatePluginSlice(
export const SECTION_REGISTRY: {
[K in SectionKind]?: SectionDescriptor<K>;
} = {
[SectionKind.Formatting]: {
formatting: {
Component: FormattingSection,
get: (spec): PanelFormattingSlice | undefined =>
getPluginSlice<PanelFormattingSlice>(spec, 'formatting'),
update: (spec, formatting): PanelSpec =>
updatePluginSlice(spec, 'formatting', formatting),
},
[SectionKind.Axes]: {
axes: {
Component: AxesSection,
get: (spec): DashboardtypesAxesDTO | undefined =>
getPluginSlice<DashboardtypesAxesDTO>(spec, 'axes'),
update: (spec, axes): PanelSpec => updatePluginSlice(spec, 'axes', axes),
},
[SectionKind.Legend]: {
legend: {
Component: LegendSection,
get: (spec): DashboardtypesLegendDTO | undefined =>
getPluginSlice<DashboardtypesLegendDTO>(spec, 'legend'),
update: (spec, legend): PanelSpec =>
updatePluginSlice(spec, 'legend', legend),
},
[SectionKind.ChartAppearance]: {
chartAppearance: {
Component: ChartAppearanceSection,
get: (spec): DashboardtypesTimeSeriesChartAppearanceDTO | undefined =>
getPluginSlice<DashboardtypesTimeSeriesChartAppearanceDTO>(
@@ -99,7 +99,7 @@ export const SECTION_REGISTRY: {
update: (spec, chartAppearance): PanelSpec =>
updatePluginSlice(spec, 'chartAppearance', chartAppearance),
},
[SectionKind.Visualization]: {
visualization: {
Component: VisualizationSection,
get: (spec): DashboardtypesBarChartVisualizationDTO | undefined =>
getPluginSlice<DashboardtypesBarChartVisualizationDTO>(
@@ -109,14 +109,14 @@ export const SECTION_REGISTRY: {
update: (spec, visualization): PanelSpec =>
updatePluginSlice(spec, 'visualization', visualization),
},
[SectionKind.Buckets]: {
buckets: {
Component: BucketsSection,
get: (spec): DashboardtypesHistogramBucketsDTO | undefined =>
getPluginSlice<DashboardtypesHistogramBucketsDTO>(spec, 'histogramBuckets'),
update: (spec, buckets): PanelSpec =>
updatePluginSlice(spec, 'histogramBuckets', buckets),
},
[SectionKind.ContextLinks]: {
contextLinks: {
Component: ContextLinksSection,
// Panel-level slice (spec.links), not under the plugin spec — no cast needed.
get: (spec): DashboardLinkDTO[] | undefined => spec.links,
@@ -125,7 +125,7 @@ export const SECTION_REGISTRY: {
// One editor for every threshold variant (label / comparison / table); the kind's
// `controls.variant` picks the row editor + element shape. All persist to the same
// plugin.spec.thresholds key.
[SectionKind.Thresholds]: {
thresholds: {
Component: ThresholdsSection,
get: (spec): AnyThreshold[] | undefined =>
getPluginSlice<AnyThreshold[]>(spec, 'thresholds'),
@@ -157,10 +157,6 @@ export interface ErasedSectionDescriptor {
// The panel's telemetry signal; read by editors that fetch field-key
// suggestions scoped to it (List column picker).
signal?: unknown;
// Current panel kind + switch handler; read by the visualization section's
// type switcher.
panelKind?: unknown;
onChangePanelKind?: unknown;
}>;
get: (spec: PanelSpec) => unknown;
update: (spec: PanelSpec, value: unknown) => PanelSpec;

View File

@@ -1,10 +1,7 @@
import type { ChangeEvent } from 'react';
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
@@ -25,7 +22,7 @@ function AxesSection({
value,
controls,
onChange,
}: SectionEditorProps<SectionKind.Axes>): JSX.Element {
}: SectionEditorProps<'axes'>): JSX.Element {
// An empty field clears the bound (null); otherwise parse to a number, ignoring
// transient non-numeric input (e.g. a lone "-") by leaving the bound unset.
const handleBound =

View File

@@ -2,10 +2,7 @@ import type { ChangeEvent } from 'react';
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
import type { DashboardtypesHistogramBucketsDTO } from 'api/generated/services/sigNoz.schemas';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
@@ -26,7 +23,7 @@ function BucketsSection({
value,
controls,
onChange,
}: SectionEditorProps<SectionKind.Buckets>): JSX.Element {
}: SectionEditorProps<'buckets'>): JSX.Element {
// Empty clears the bound to null (chart auto-sizes); otherwise parse to a number,
// ignoring transient non-numeric input by leaving it unset.
const handleNumber =

View File

@@ -6,15 +6,11 @@ import {
DashboardtypesLineInterpolationDTO,
DashboardtypesLineStyleDTO,
} from 'api/generated/services/sigNoz.schemas';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
import ConfigSelect from '../../controls/ConfigSelect/ConfigSelect';
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
import { SegmentIcon } from '../../controls/segmentIcons';
import styles from './ChartAppearanceSection.module.scss';
@@ -35,22 +31,22 @@ const LINE_INTERPOLATION_OPTIONS = [
{
value: DashboardtypesLineInterpolationDTO.linear,
label: 'Linear',
icon: <SegmentIcon name="interp-linear" />,
icon: 'interp-linear' as const,
},
{
value: DashboardtypesLineInterpolationDTO.spline,
label: 'Spline',
icon: <SegmentIcon name="interp-spline" />,
icon: 'interp-spline' as const,
},
{
value: DashboardtypesLineInterpolationDTO.step_before,
label: 'Step before',
icon: <SegmentIcon name="interp-step-before" />,
icon: 'interp-step-before' as const,
},
{
value: DashboardtypesLineInterpolationDTO.step_after,
label: 'Step after',
icon: <SegmentIcon name="interp-step-after" />,
icon: 'interp-step-after' as const,
},
];
@@ -81,7 +77,7 @@ function ChartAppearanceSection({
value,
controls,
onChange,
}: SectionEditorProps<SectionKind.ChartAppearance>): JSX.Element {
}: SectionEditorProps<'chartAppearance'>): JSX.Element {
// `spanGaps.fillLessThan` is a stringified seconds threshold: empty means "connect
// every gap" (the chart default), a number means "only bridge gaps shorter than this".
const handleSpanGaps = (e: ChangeEvent<HTMLInputElement>): void => {
@@ -118,7 +114,7 @@ function ChartAppearanceSection({
onChange={(next): void =>
onChange({
...value,
lineInterpolation: next,
lineInterpolation: next as DashboardtypesLineInterpolationDTO,
})
}
/>

View File

@@ -4,10 +4,7 @@ import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
import type { DashboardLinkDTO } from 'api/generated/services/sigNoz.schemas';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import styles from './ContextLinksSection.module.scss';
@@ -20,7 +17,7 @@ import styles from './ContextLinksSection.module.scss';
function ContextLinksSection({
value,
onChange,
}: SectionEditorProps<SectionKind.ContextLinks>): JSX.Element {
}: SectionEditorProps<'contextLinks'>): JSX.Element {
const links = value ?? [];
const updateAt = (index: number, patch: Partial<DashboardLinkDTO>): void =>

View File

@@ -2,10 +2,7 @@ import { Typography } from '@signozhq/ui/typography';
import { DashboardtypesPrecisionOptionDTO } from 'api/generated/services/sigNoz.schemas';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { TableColumnOption } from '../../../hooks/useTableColumns';
import ConfigSelect from '../../controls/ConfigSelect/ConfigSelect';
@@ -13,7 +10,7 @@ import ColumnUnits from './ColumnUnits';
import styles from './FormattingSection.module.scss';
type FormattingSectionProps = SectionEditorProps<SectionKind.Formatting> & {
type FormattingSectionProps = SectionEditorProps<'formatting'> & {
/** Table panel's resolved value columns; required for the column-units editor. */
tableColumns?: TableColumnOption[];
};
@@ -68,7 +65,7 @@ function FormattingSection({
onChange={(next): void =>
onChange({
...value,
decimalPrecision: next,
decimalPrecision: next as DashboardtypesPrecisionOptionDTO,
})
}
/>

View File

@@ -1,9 +1,6 @@
import { Typography } from '@signozhq/ui/typography';
import { DashboardtypesLegendPositionDTO } from 'api/generated/services/sigNoz.schemas';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
import LegendColors from '../../controls/LegendColors/LegendColors';
@@ -11,7 +8,7 @@ import type { LegendSeries } from '../../../hooks/useLegendSeries';
import styles from './LegendSection.module.scss';
type LegendSectionProps = SectionEditorProps<SectionKind.Legend> & {
type LegendSectionProps = SectionEditorProps<'legend'> & {
/** Panel's resolved series, forwarded by SectionSlot for the colors control. */
legendSeries?: LegendSeries[];
};

View File

@@ -1,51 +1,26 @@
import { Typography } from '@signozhq/ui/typography';
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import { DashboardtypesTimePreferenceDTO } from 'api/generated/services/sigNoz.schemas';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { PanelKind } from '../../../../Panels/types/panelKind';
import ConfigSelect from '../../controls/ConfigSelect/ConfigSelect';
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
import PanelTypeSwitcher from '../../PanelTypeSwitcher/PanelTypeSwitcher';
import { TIME_PREFERENCE_OPTIONS } from './timePreferenceOptions';
import styles from './VisualizationSection.module.scss';
type VisualizationSectionProps =
SectionEditorProps<SectionKind.Visualization> & {
/** Current panel kind + switch handler, forwarded by SectionSlot for the type switcher. */
panelKind?: PanelKind;
onChangePanelKind?: (kind: PanelKind) => void;
/** Panel's datasource, forwarded by SectionSlot — scopes the switcher's disabled types. */
signal?: TelemetrytypesSignalDTO;
};
/**
* Edits the `visualization` slice: the panel-type switcher (`switchPanelKind`, every
* kind), the per-panel time preference, bar stacking (`stackedBarChart`, Bar only), and
* gap filling (`fillSpans`, TimeSeries only). Each control is gated by its `controls`
* flag, so a kind only renders — and only writes — the fields its spec supports.
* Edits the `visualization` slice: the per-panel time preference (all kinds), bar
* stacking (`stackedBarChart`, Bar only), and gap filling (`fillSpans`, TimeSeries
* only). Each control is gated by its `controls` flag, so a kind only renders — and only
* writes — the visualization fields its spec actually supports.
*/
function VisualizationSection({
value,
controls,
onChange,
panelKind,
onChangePanelKind,
signal,
}: VisualizationSectionProps): JSX.Element {
}: SectionEditorProps<'visualization'>): JSX.Element {
return (
<>
{controls.switchPanelKind && panelKind && onChangePanelKind && (
<PanelTypeSwitcher
panelKind={panelKind}
signal={signal}
onChange={onChangePanelKind}
/>
)}
{controls.timePreference && (
<div className={styles.field}>
<Typography.Text>Panel time preference</Typography.Text>
@@ -57,7 +32,7 @@ function VisualizationSection({
onChange={(next): void =>
onChange({
...value,
timePreference: next,
timePreference: next as DashboardtypesTimePreferenceDTO,
})
}
/>

View File

@@ -4,14 +4,6 @@ import { DashboardtypesTimePreferenceDTO } from 'api/generated/services/sigNoz.s
import VisualizationSection from '../VisualizationSection';
// The type switcher resolves each kind's supported signals; stub it so the test
// doesn't pull the whole panel registry (renderers, chart libs).
jest.mock('pages/DashboardPageV2/DashboardContainer/Panels/registry', () => ({
getPanelDefinition: jest.fn(() => ({
supportedSignals: ['metrics', 'logs', 'traces'],
})),
}));
// Open the antd Select by clicking its selector, then pick the option by label.
async function pickOption(triggerTestId: string, label: string): Promise<void> {
const user = userEvent.setup();
@@ -25,12 +17,7 @@ describe('VisualizationSection', () => {
render(
<VisualizationSection
value={undefined}
controls={{
switchPanelKind: true,
timePreference: true,
stacking: true,
fillSpans: true,
}}
controls={{ timePreference: true, stacking: true, fillSpans: true }}
onChange={jest.fn()}
/>,
);
@@ -48,10 +35,7 @@ describe('VisualizationSection', () => {
render(
<VisualizationSection
value={undefined}
controls={{
switchPanelKind: true,
timePreference: true,
}}
controls={{ timePreference: true }}
onChange={jest.fn()}
/>,
);
@@ -72,10 +56,7 @@ describe('VisualizationSection', () => {
render(
<VisualizationSection
value={undefined}
controls={{
switchPanelKind: true,
timePreference: true,
}}
controls={{ timePreference: true }}
onChange={onChange}
/>,
);
@@ -93,10 +74,7 @@ describe('VisualizationSection', () => {
timePreference: DashboardtypesTimePreferenceDTO.global_time,
stackedBarChart: false,
}}
controls={{
switchPanelKind: true,
stacking: true,
}}
controls={{ stacking: true }}
onChange={onChange}
/>,
);
@@ -114,10 +92,7 @@ describe('VisualizationSection', () => {
render(
<VisualizationSection
value={{ fillSpans: false }}
controls={{
switchPanelKind: true,
fillSpans: true,
}}
controls={{ fillSpans: true }}
onChange={onChange}
/>,
);
@@ -126,43 +101,4 @@ describe('VisualizationSection', () => {
expect(onChange).toHaveBeenCalledWith({ fillSpans: true });
});
it('renders the type switcher and switches kind when switchPanelKind is set', async () => {
const onChangePanelKind = jest.fn();
render(
<VisualizationSection
value={undefined}
controls={{ switchPanelKind: true }}
onChange={jest.fn()}
panelKind="signoz/TimeSeriesPanel"
onChangePanelKind={onChangePanelKind}
/>,
);
expect(
screen.getByTestId('panel-editor-v2-type-switcher'),
).toBeInTheDocument();
await pickOption('panel-editor-v2-type-switcher', 'Table');
expect(onChangePanelKind).toHaveBeenCalledWith('signoz/TablePanel');
});
it('hides the type switcher when switchPanelKind is not set', () => {
render(
<VisualizationSection
value={undefined}
controls={{
switchPanelKind: false,
timePreference: true,
}}
onChange={jest.fn()}
panelKind="signoz/TimeSeriesPanel"
onChangePanelKind={jest.fn()}
/>,
);
expect(
screen.queryByTestId('panel-editor-v2-type-switcher'),
).not.toBeInTheDocument();
});
});

View File

@@ -4,19 +4,15 @@ import type { ConfigSelectItem } from '../../controls/ConfigSelect/ConfigSelect'
// Per-panel time scope. "Global Time" follows the dashboard's time picker; the rest pin
// the panel to a fixed relative window regardless of the dashboard range (V1 parity).
export const TIME_PREFERENCE_OPTIONS: ConfigSelectItem<DashboardtypesTimePreferenceDTO>[] =
[
{ value: DashboardtypesTimePreferenceDTO.global_time, label: 'Global Time' },
{ value: DashboardtypesTimePreferenceDTO.last_5_min, label: 'Last 5 min' },
{ value: DashboardtypesTimePreferenceDTO.last_15_min, label: 'Last 15 min' },
{ value: DashboardtypesTimePreferenceDTO.last_30_min, label: 'Last 30 min' },
{ value: DashboardtypesTimePreferenceDTO.last_1_hr, label: 'Last 1 hr' },
{ value: DashboardtypesTimePreferenceDTO.last_6_hr, label: 'Last 6 hr' },
{ value: DashboardtypesTimePreferenceDTO.last_1_day, label: 'Last 1 day' },
{ value: DashboardtypesTimePreferenceDTO.last_3_days, label: 'Last 3 days' },
{ value: DashboardtypesTimePreferenceDTO.last_1_week, label: 'Last 1 week' },
{
value: DashboardtypesTimePreferenceDTO.last_1_month,
label: 'Last 1 month',
},
];
export const TIME_PREFERENCE_OPTIONS: ConfigSelectItem[] = [
{ value: DashboardtypesTimePreferenceDTO.global_time, label: 'Global Time' },
{ value: DashboardtypesTimePreferenceDTO.last_5_min, label: 'Last 5 min' },
{ value: DashboardtypesTimePreferenceDTO.last_15_min, label: 'Last 15 min' },
{ value: DashboardtypesTimePreferenceDTO.last_30_min, label: 'Last 30 min' },
{ value: DashboardtypesTimePreferenceDTO.last_1_hr, label: 'Last 1 hr' },
{ value: DashboardtypesTimePreferenceDTO.last_6_hr, label: 'Last 6 hr' },
{ value: DashboardtypesTimePreferenceDTO.last_1_day, label: 'Last 1 day' },
{ value: DashboardtypesTimePreferenceDTO.last_3_days, label: 'Last 3 days' },
{ value: DashboardtypesTimePreferenceDTO.last_1_week, label: 'Last 1 week' },
{ value: DashboardtypesTimePreferenceDTO.last_1_month, label: 'Last 1 month' },
];

View File

@@ -1,98 +0,0 @@
import {
TelemetrytypesSignalDTO,
type DashboardtypesPanelSpecDTO,
} from 'api/generated/services/sigNoz.schemas';
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
import { defaultColumnsForSignal } from '../ListColumnsEditor/selectFields';
import { getSwitchedPluginSpec } from '../getSwitchedPluginSpec';
jest.mock('pages/DashboardPageV2/DashboardContainer/Panels/registry', () => ({
getPanelDefinition: jest.fn(),
}));
jest.mock('../ListColumnsEditor/selectFields', () => ({
defaultColumnsForSignal: jest.fn(),
}));
const mockGetPanelDefinition = getPanelDefinition as unknown as jest.Mock;
const mockDefaultColumnsForSignal =
defaultColumnsForSignal as unknown as jest.Mock;
function specWith(pluginSpec: unknown): DashboardtypesPanelSpecDTO {
return {
display: { name: 'Panel' },
plugin: { kind: 'signoz/TablePanel', spec: pluginSpec },
queries: [],
} as unknown as DashboardtypesPanelSpecDTO;
}
describe('getSwitchedPluginSpec', () => {
beforeEach(() => {
jest.clearAllMocks();
mockDefaultColumnsForSignal.mockReturnValue([]);
});
it('carries only unit + decimalPrecision when the new kind has a formatting section', () => {
mockGetPanelDefinition.mockReturnValue({
sections: [{ kind: 'formatting', controls: { unit: true, decimals: true } }],
});
const old = specWith({
formatting: { unit: 'ms', decimalPrecision: 2, columnUnits: { A: 'bytes' } },
axes: { logScale: true },
});
const result = getSwitchedPluginSpec(
old,
'signoz/TimeSeriesPanel',
TelemetrytypesSignalDTO.logs,
);
expect(result.formatting).toStrictEqual({ unit: 'ms', decimalPrecision: 2 });
// Type-specific config from the old kind is dropped.
expect((result as { axes?: unknown }).axes).toBeUndefined();
});
it('does not carry formatting when the new kind has no formatting section', () => {
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
const old = specWith({ formatting: { unit: 'ms' } });
const result = getSwitchedPluginSpec(
old,
'signoz/ListPanel',
TelemetrytypesSignalDTO.logs,
);
expect(result.formatting).toBeUndefined();
});
it('seeds List columns from the signal when switching into a List', () => {
const columns = [{ name: 'body' }];
mockDefaultColumnsForSignal.mockReturnValue(columns);
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
const result = getSwitchedPluginSpec(
specWith({}),
'signoz/ListPanel',
TelemetrytypesSignalDTO.logs,
);
expect(mockDefaultColumnsForSignal).toHaveBeenCalledWith(
TelemetrytypesSignalDTO.logs,
);
expect(result.selectFields).toBe(columns);
});
it('includes the kind section defaults (e.g. legend position)', () => {
mockGetPanelDefinition.mockReturnValue({
sections: [{ kind: 'legend', controls: { position: true } }],
});
const result = getSwitchedPluginSpec(
specWith({}),
'signoz/PieChartPanel',
TelemetrytypesSignalDTO.logs,
);
expect(result.legend?.position).toBe('bottom');
});
});

View File

@@ -1,70 +0,0 @@
import type {
DashboardtypesPanelSpecDTO,
TelemetrytypesSignalDTO,
TelemetrytypesTelemetryFieldKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
import type { PanelKind } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelKind';
import {
SectionKind,
type PanelFormattingSlice,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import {
buildDefaultPluginSpec,
type DefaultPluginSpec,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/buildDefaultPluginSpec';
import { defaultColumnsForSignal } from './ListColumnsEditor/selectFields';
/**
* Plugin spec produced on a first-time switch to a new kind. A partial cross-section
* of the per-kind spec union; the caller assigns it to `plugin.spec` (typed `unknown`)
* at the boundary.
*/
export interface SwitchedPluginSpec extends DefaultPluginSpec {
formatting?: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'>;
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
}
/**
* Builds the plugin spec for a first-visit switch to `newKind`: the kind's declared
* section defaults (so the config pane opens populated, matching new-panel seeding) plus
* the only cross-kind config worth keeping — unit + decimal precision. Switching into a
* List seeds the current signal's default columns so the columns control isn't empty.
*
* Revisiting a kind restores its stashed spec instead, so this runs only on first visit.
*/
export function getSwitchedPluginSpec(
oldSpec: DashboardtypesPanelSpecDTO,
newKind: PanelKind,
signal: TelemetrytypesSignalDTO,
): SwitchedPluginSpec {
const sections = getPanelDefinition(newKind).sections;
const result: SwitchedPluginSpec = buildDefaultPluginSpec(sections);
if (sections.some((section) => section.kind === SectionKind.Formatting)) {
const oldFormatting = (
oldSpec.plugin.spec as {
formatting?: PanelFormattingSlice;
}
).formatting;
const carried: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'> = {
...(oldFormatting?.unit !== undefined && { unit: oldFormatting.unit }),
...(oldFormatting?.decimalPrecision !== undefined && {
decimalPrecision: oldFormatting.decimalPrecision,
}),
};
if (Object.keys(carried).length > 0) {
result.formatting = carried;
}
}
if (sections.some((section) => section.kind === SectionKind.Columns)) {
const columns = defaultColumnsForSignal(signal);
if (columns.length > 0) {
result.selectFields = columns;
}
}
return result;
}

View File

@@ -1,190 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { handleQueryChange } from 'container/NewWidget/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { getBuilderQueries } from '../../../Panels/utils/getBuilderQueries';
import { toPerses } from '../../../queryV5/persesQueryAdapters';
import { getSwitchedPluginSpec } from '../../getSwitchedPluginSpec';
import { usePanelTypeSwitch } from '../usePanelTypeSwitch';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
jest.mock('container/NewWidget/utils', () => ({
handleQueryChange: jest.fn(),
PANEL_TYPE_TO_QUERY_TYPES: {
graph: ['builder', 'clickhouse', 'promql'],
table: ['builder', 'clickhouse'],
list: ['builder'],
value: ['builder', 'clickhouse', 'promql'],
bar: ['builder', 'clickhouse', 'promql'],
pie: ['builder', 'clickhouse'],
histogram: ['builder', 'clickhouse', 'promql'],
},
}));
jest.mock('../../../queryV5/persesQueryAdapters', () => ({
toPerses: jest.fn(),
}));
jest.mock('../../getSwitchedPluginSpec', () => ({
getSwitchedPluginSpec: jest.fn(),
}));
jest.mock('../../../Panels/utils/getBuilderQueries', () => ({
getBuilderQueries: jest.fn(),
}));
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
const mockHandleQueryChange = handleQueryChange as unknown as jest.Mock;
const mockToPerses = toPerses as unknown as jest.Mock;
const mockGetSwitchedPluginSpec = getSwitchedPluginSpec as unknown as jest.Mock;
const mockGetBuilderQueries = getBuilderQueries as unknown as jest.Mock;
// Opaque sentinels — the leaf utilities are mocked, so only identity matters.
const TABLE_PLUGIN_SPEC = { table: true } as unknown;
const TABLE_QUERIES = [{ id: 'table-q' }] as unknown as NonNullable<
DashboardtypesPanelSpecDTO['queries']
>;
const LIST_PLUGIN_SPEC = { list: true } as unknown;
const LIST_QUERIES = [{ id: 'list-q' }] as unknown as NonNullable<
DashboardtypesPanelSpecDTO['queries']
>;
const TRANSFORMED = {
id: 'transformed',
queryType: 'builder',
} as unknown as Query;
const CONVERTED = [{ id: 'converted' }] as unknown as NonNullable<
DashboardtypesPanelSpecDTO['queries']
>;
const SWITCHED_SPEC = { switched: true } as unknown;
function makeSpec(
kind: string,
pluginSpec: unknown,
queries: NonNullable<DashboardtypesPanelSpecDTO['queries']>,
): DashboardtypesPanelSpecDTO {
return {
display: { name: 'Panel' },
plugin: { kind, spec: pluginSpec },
queries,
} as unknown as DashboardtypesPanelSpecDTO;
}
const tableSpec = makeSpec(
'signoz/TablePanel',
TABLE_PLUGIN_SPEC,
TABLE_QUERIES,
);
const listSpec = makeSpec('signoz/ListPanel', LIST_PLUGIN_SPEC, LIST_QUERIES);
function builderState(currentQuery: Query): {
currentQuery: Query;
redirectWithQueryBuilderData: jest.Mock;
} {
return { currentQuery, redirectWithQueryBuilderData: jest.fn() };
}
describe('usePanelTypeSwitch', () => {
beforeEach(() => {
jest.clearAllMocks();
mockHandleQueryChange.mockReturnValue(TRANSFORMED);
mockToPerses.mockReturnValue(CONVERTED);
mockGetSwitchedPluginSpec.mockReturnValue(SWITCHED_SPEC);
mockGetBuilderQueries.mockReturnValue([{ signal: 'logs' }]);
});
it('does nothing when switching to the current kind', () => {
const setSpec = jest.fn();
const state = builderState({ id: 'q', queryType: 'builder' } as Query);
mockUseQueryBuilder.mockReturnValue(state);
const { result } = renderHook(() =>
usePanelTypeSwitch({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
}),
);
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
expect(setSpec).not.toHaveBeenCalled();
expect(state.redirectWithQueryBuilderData).not.toHaveBeenCalled();
});
it('on first visit: transforms the query and resets the spec to the new kind', () => {
const setSpec = jest.fn();
const tableQuery = { id: 'table-current', queryType: 'builder' } as Query;
const state = builderState(tableQuery);
mockUseQueryBuilder.mockReturnValue(state);
const { result } = renderHook(() =>
usePanelTypeSwitch({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
}),
);
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
expect(setSpec).toHaveBeenCalledTimes(1);
const next = setSpec.mock.calls[0][0] as DashboardtypesPanelSpecDTO;
expect(next.plugin.kind).toBe('signoz/ListPanel');
expect(next.plugin.spec).toBe(SWITCHED_SPEC);
expect(next.queries).toBe(CONVERTED);
expect(state.redirectWithQueryBuilderData).toHaveBeenCalledWith(TRANSFORMED);
});
it('coerces the query type when the new kind disallows it (promql → List)', () => {
const setSpec = jest.fn();
const promQuery = { id: 'prom', queryType: 'promql' } as Query;
mockUseQueryBuilder.mockReturnValue(builderState(promQuery));
const { result } = renderHook(() =>
usePanelTypeSwitch({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
}),
);
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
// List allows only Query Builder, so the promql query is coerced to 'builder'.
const [, queryArg] = mockHandleQueryChange.mock.calls[0];
expect((queryArg as Query).queryType).toBe('builder');
});
it('restores the original kind verbatim on switch-back (reversibility)', () => {
const setSpec = jest.fn();
const tableQuery = { id: 'table-current', queryType: 'builder' } as Query;
const listQuery = { id: 'list-current', queryType: 'builder' } as Query;
let state = builderState(tableQuery);
mockUseQueryBuilder.mockImplementation(() => state);
const { result, rerender } = renderHook(
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
usePanelTypeSwitch({ ...props, setSpec }),
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
);
// Leave Table for List (stashes Table in its pristine state).
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
// Parent re-renders as a List panel; the builder now holds the List query.
state = builderState(listQuery);
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
// Switch back to Table → restored from the stash, not re-transformed.
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
const restored = setSpec.mock.calls[
setSpec.mock.calls.length - 1
][0] as DashboardtypesPanelSpecDTO;
expect(restored.plugin.kind).toBe('signoz/TablePanel');
expect(restored.plugin.spec).toBe(TABLE_PLUGIN_SPEC);
expect(restored.queries).toBe(TABLE_QUERIES);
expect(state.redirectWithQueryBuilderData).toHaveBeenCalledWith(tableQuery);
// The restore path must not run the query transform again.
expect(mockHandleQueryChange).toHaveBeenCalledTimes(1);
});
});

View File

@@ -40,7 +40,7 @@ export function useLegendSeries(
getTimeSeriesResults(data?.response),
data.legendMap,
);
const builderQueries = getBuilderQueries(panel.spec.queries);
const builderQueries = getBuilderQueries(panel?.spec?.queries || []);
const byLabel = new Map<string, string>();
series.forEach((s) => {

View File

@@ -57,8 +57,8 @@ export function usePanelEditorQuerySync({
const { currentQuery, stagedQuery, handleRunQuery } = useQueryBuilder();
// Saved queries, captured once: seed the builder and serve as the restore target.
const savedQueries = draft.spec.queries;
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only snapshot
const savedQueries = useMemo(() => draft.spec?.queries ?? [], []);
// A new panel has no saved query: seed from the kind's first supported signal
// instead of letting `fromPerses` fall back to the metrics default (which List
// doesn't support).
@@ -93,7 +93,7 @@ export function usePanelEditorQuerySync({
// No-op guard at the V5 envelope level: equivalent wrappers (bare
// `signoz/BuilderQuery` vs `signoz/CompositeQuery`) unwrap to the same
// envelopes, so a structural compare would falsely dirty the draft.
const current = draft.spec.queries;
const current = draft.spec?.queries ?? [];
if (isEqual(toQueryEnvelopes(next), toQueryEnvelopes(current))) {
return false;
}

View File

@@ -1,137 +0,0 @@
import { useCallback, useRef } from 'react';
import type {
DashboardtypesPanelPluginDTO,
DashboardtypesPanelSpecDTO,
DashboardtypesQueryDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
handleQueryChange,
PANEL_TYPE_TO_QUERY_TYPES,
type PartialPanelTypes,
} from 'container/NewWidget/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
PANEL_KIND_TO_PANEL_TYPE,
type PanelKind,
} from '../../Panels/types/panelKind';
import { getBuilderQueries } from '../../Panels/utils/getBuilderQueries';
import { toPerses } from '../../queryV5/persesQueryAdapters';
import {
getSwitchedPluginSpec,
type SwitchedPluginSpec,
} from '../getSwitchedPluginSpec';
/** What a kind looks like when you leave it; restored verbatim if you return. */
interface KindState {
pluginSpec: DashboardtypesPanelPluginDTO['spec'];
queries: DashboardtypesQueryDTO[];
builderQuery: Query;
}
interface UsePanelTypeSwitchArgs {
spec: DashboardtypesPanelSpecDTO;
panelType: PANEL_TYPES;
setSpec: (next: DashboardtypesPanelSpecDTO) => void;
}
interface UsePanelTypeSwitchApi {
/** Switch the panel to `newKind`, transforming/restoring its query + spec. */
onChangePanelKind: (newKind: PanelKind) => void;
}
/**
* Switches the edited panel's visualization kind. Mutating `plugin.kind` re-derives the
* renderer, config sections, query-builder tabs and request type for free; this hook adds
* the two things that don't: a per-kind session cache that makes switching reversible
* (`Table → List → Table` restores the original query + spec), and, on first visit to a
* kind, a query rebuild (`handleQueryChange`) + spec reset (`getSwitchedPluginSpec`).
*/
export function usePanelTypeSwitch({
spec,
panelType,
setSpec,
}: UsePanelTypeSwitchArgs): UsePanelTypeSwitchApi {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const cacheRef = useRef<Map<PanelKind, KindState>>(new Map());
// Latest spec/query/type, read inside the stable callback without re-subscribing.
const specRef = useRef(spec);
specRef.current = spec;
const queryRef = useRef(currentQuery);
queryRef.current = currentQuery;
const panelTypeRef = useRef(panelType);
panelTypeRef.current = panelType;
const onChangePanelKind = useCallback(
(newKind: PanelKind): void => {
const currentSpec = specRef.current;
const oldKind = currentSpec.plugin.kind as PanelKind;
if (newKind === oldKind) {
return;
}
const query = queryRef.current;
cacheRef.current.set(oldKind, {
pluginSpec: currentSpec.plugin.spec,
queries: currentSpec.queries,
builderQuery: query,
});
const newPanelType = PANEL_KIND_TO_PANEL_TYPE[newKind];
// Only `plugin` needs a cast: it's a discriminated union over `kind`, and a
// dynamically-chosen kind can't be correlated with its spec statically (as in
// `createDefaultPanel`). The surrounding spec stays fully typed.
const buildSpec = (
pluginSpec: DashboardtypesPanelPluginDTO['spec'] | SwitchedPluginSpec,
queries: DashboardtypesQueryDTO[],
): DashboardtypesPanelSpecDTO => ({
...currentSpec,
plugin: {
...currentSpec.plugin,
kind: newKind,
spec: pluginSpec,
} as DashboardtypesPanelPluginDTO,
queries,
});
// Revisit → restore the stash verbatim (the reversibility path).
const cached = cacheRef.current.get(newKind);
if (cached) {
setSpec(buildSpec(cached.pluginSpec, cached.queries));
redirectWithQueryBuilderData(cached.builderQuery);
return;
}
// First visit → coerce the query type if the new panel disallows it, then
// rebuild the builder query for the new type.
const supported = PANEL_TYPE_TO_QUERY_TYPES[newPanelType] ?? [];
const queryType = supported.includes(query.queryType)
? query.queryType
: supported[0];
const transformed = handleQueryChange(
newPanelType as keyof PartialPanelTypes,
{ ...query, queryType },
panelTypeRef.current,
);
const signal = getBuilderQueries(currentSpec.queries)[0]
?.signal as TelemetrytypesSignalDTO;
setSpec(
buildSpec(
getSwitchedPluginSpec(currentSpec, newKind, signal),
toPerses(transformed, newPanelType),
),
);
redirectWithQueryBuilderData(transformed);
},
[setSpec, redirectWithQueryBuilderData],
);
return { onChangePanelKind };
}

View File

@@ -29,7 +29,6 @@ import { usePanelQuery } from '../hooks/usePanelQuery';
import { usePanelEditorDraft } from './hooks/usePanelEditorDraft';
import { usePanelEditorQuerySync } from './hooks/usePanelEditorQuerySync';
import { usePanelEditorSave } from './hooks/usePanelEditorSave';
import { usePanelTypeSwitch } from './hooks/usePanelTypeSwitch';
import { useSeedNewListColumns } from './hooks/useSeedNewListColumns';
import { useSwitchColumnsOnSignalChange } from './hooks/useSwitchColumnsOnSignalChange';
import { useTableColumns } from './hooks/useTableColumns';
@@ -114,9 +113,6 @@ function PanelEditorContainer({
signal: defaultSignal,
});
// Switch the panel's visualization kind in place (reversible per session).
const { onChangePanelKind } = usePanelTypeSwitch({ spec, panelType, setSpec });
// Spec and query dirtiness are tracked independently so query re-serialization
// never false-dirties. A new panel is always savable (you're creating it).
const isDirty = isNew || isSpecDirty || isQueryDirty;
@@ -125,8 +121,8 @@ function PanelEditorContainer({
// values; cast at this boundary (as ConfigPane does) so the columns editor's
// field-key lookup is typed.
const listSignal =
(getBuilderQueries(spec.queries)[0]?.signal as TelemetrytypesSignalDTO) ||
TelemetrytypesSignalDTO.logs;
(getBuilderQueries(spec.queries || [])[0]
?.signal as TelemetrytypesSignalDTO) || TelemetrytypesSignalDTO.logs;
// Swap the List panel's columns to the new signal's defaults on signal change
// (V1 had a per-signal field list; V2 has one `selectFields`).
@@ -230,7 +226,6 @@ function PanelEditorContainer({
panelKind={draft.spec.plugin.kind}
spec={spec}
onChangeSpec={setSpec}
onChangePanelKind={onChangePanelKind}
legendSeries={legendSeries}
tableColumns={tableColumns}
/>

View File

@@ -49,7 +49,7 @@ function BarPanelRenderer({
);
const builderQueries = useMemo(
() => getBuilderQueries(panel.spec.queries),
() => getBuilderQueries(panel.spec.queries || []),
[panel.spec.queries],
);

View File

@@ -1,15 +1,12 @@
import { SectionKind, type SectionConfig } from '../../types/sections';
import type { SectionConfig } from '../../types/sections';
// Bar stacking lives in `visualization.stackedBarChart`, so it's a `visualization`
// control, not `chartAppearance`. fillSpans is TimeSeries-only, so Bar omits it (V1 parity).
export const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true, stacking: true },
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
{ kind: SectionKind.Legend, controls: { position: true } },
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
{ kind: SectionKind.ContextLinks },
{ kind: 'visualization', controls: { timePreference: true, stacking: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'axes', controls: { minMax: true, logScale: true } },
{ kind: 'legend', controls: { position: true } },
{ kind: 'thresholds', controls: { variant: 'label' } },
{ kind: 'contextLinks' },
];

View File

@@ -41,7 +41,7 @@ function HistogramPanelRenderer({
);
const builderQueries = useMemo(
() => getBuilderQueries(panel.spec.queries),
() => getBuilderQueries(panel.spec.queries || []),
[panel.spec.queries],
);

View File

@@ -1,14 +1,10 @@
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { SectionKind, type SectionConfig } from '../../types/sections';
import type { SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true },
},
{
kind: SectionKind.Legend,
kind: 'legend',
controls: { position: true },
// Merging all queries collapses to one distribution with no legend.
isHidden: (spec): boolean =>
@@ -18,8 +14,8 @@ export const sections: SectionConfig[] = [
),
},
{
kind: SectionKind.Buckets,
kind: 'buckets',
controls: { count: true, width: true, mergeQueries: true },
},
{ kind: SectionKind.ContextLinks },
{ kind: 'contextLinks' },
];

View File

@@ -52,7 +52,7 @@ function ListPanelRenderer({
// and row-click behavior. Cast is safe — the query carries the same string values.
const signal = useMemo(
() =>
(getBuilderQueries(panel.spec.queries)[0]
(getBuilderQueries(panel.spec.queries || [])[0]
?.signal as TelemetrytypesSignalDTO) || TelemetrytypesSignalDTO.logs,
[panel.spec.queries],
);

View File

@@ -20,7 +20,7 @@ function panelWith(
): PanelOfKind<'signoz/ListPanel'> {
return {
kind: 'Panel',
spec: { plugin: { kind: 'signoz/ListPanel', spec }, queries: [] },
spec: { plugin: { kind: 'signoz/ListPanel', spec } },
} as unknown as PanelOfKind<'signoz/ListPanel'>;
}

View File

@@ -1,8 +1,3 @@
import { SectionKind, type SectionConfig } from '../../types/sections';
import type { SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true },
},
];
export const sections: SectionConfig[] = [];

View File

@@ -1,11 +1,8 @@
import { SectionKind, type SectionConfig } from '../../types/sections';
import type { SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true },
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Thresholds, controls: { variant: 'comparison' } },
{ kind: SectionKind.ContextLinks },
{ kind: 'visualization', controls: { timePreference: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'thresholds', controls: { variant: 'comparison' } },
{ kind: 'contextLinks' },
];

View File

@@ -1,13 +1,10 @@
import { SectionKind, type SectionConfig } from '../../types/sections';
import type { SectionConfig } from '../../types/sections';
// Pie has no axes, thresholds, or stacking — just value formatting and a legend.
// Legend `colors` is omitted: the pie legend is always interactive swatches.
export const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true },
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Legend, controls: { position: true } },
{ kind: SectionKind.ContextLinks },
{ kind: 'visualization', controls: { timePreference: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'legend', controls: { position: true } },
{ kind: 'contextLinks' },
];

View File

@@ -1,17 +1,11 @@
import { SectionKind, type SectionConfig } from '../../types/sections';
import type { SectionConfig } from '../../types/sections';
// A table panel renders one scalar result (the V5 backend joins every query into a
// single column set). It exposes the per-panel time scope, formatting (decimals +
// per-column units), per-column thresholds, and context links.
export const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true },
},
{
kind: SectionKind.Formatting,
controls: { decimals: true, columnUnits: true },
},
{ kind: SectionKind.Thresholds, controls: { variant: 'table' } },
{ kind: SectionKind.ContextLinks },
{ kind: 'visualization', controls: { timePreference: true } },
{ kind: 'formatting', controls: { decimals: true, columnUnits: true } },
{ kind: 'thresholds', controls: { variant: 'table' } },
{ kind: 'contextLinks' },
];

View File

@@ -49,7 +49,7 @@ function TimeSeriesPanelRenderer({
);
const builderQueries = useMemo(
() => getBuilderQueries(panel.spec.queries),
() => getBuilderQueries(panel.spec.queries || []),
[panel.spec.queries],
);

View File

@@ -1,15 +1,12 @@
import { SectionKind, type SectionConfig } from '../../types/sections';
import type { SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{ kind: 'visualization', controls: { timePreference: true, fillSpans: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'axes', controls: { minMax: true, logScale: true } },
{ kind: 'legend', controls: { position: true, colors: true } },
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true, fillSpans: true },
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
{
kind: SectionKind.ChartAppearance,
kind: 'chartAppearance',
controls: {
lineStyle: true,
lineInterpolation: true,
@@ -18,6 +15,6 @@ export const sections: SectionConfig[] = [
spanGaps: true,
},
},
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
{ kind: SectionKind.ContextLinks },
{ kind: 'thresholds', controls: { variant: 'label' } },
{ kind: 'contextLinks' },
];

View File

@@ -35,22 +35,6 @@ export interface SectionMetadata {
description?: string;
}
/**
* Discriminant for each config section (the `kind` field of a `SectionConfig`). The
* string values match the keys each slice persists under in the plugin spec.
*/
export enum SectionKind {
Formatting = 'formatting',
Axes = 'axes',
Legend = 'legend',
ChartAppearance = 'chartAppearance',
Buckets = 'buckets',
Visualization = 'visualization',
Thresholds = 'thresholds',
ContextLinks = 'contextLinks',
Columns = 'columns',
}
/**
* Which threshold editor a kind uses. All three variants persist to the same
* `plugin.spec.thresholds` key with different element shapes:
@@ -76,17 +60,17 @@ export type PanelFormattingSlice = DashboardtypesPanelFormattingDTO &
Pick<DashboardtypesTableFormattingDTO, 'columnUnits'>;
export interface SectionSpecMap {
[SectionKind.Formatting]: PanelFormattingSlice; // spec.plugin.spec.formatting
[SectionKind.Axes]: DashboardtypesAxesDTO; // spec.plugin.spec.axes
[SectionKind.Legend]: DashboardtypesLegendDTO; // spec.plugin.spec.legend
[SectionKind.ChartAppearance]: DashboardtypesTimeSeriesChartAppearanceDTO; // spec.plugin.spec.chartAppearance
[SectionKind.Buckets]: DashboardtypesHistogramBucketsDTO; // spec.plugin.spec.histogramBuckets
formatting: PanelFormattingSlice; // spec.plugin.spec.formatting
axes: DashboardtypesAxesDTO; // spec.plugin.spec.axes
legend: DashboardtypesLegendDTO; // spec.plugin.spec.legend
chartAppearance: DashboardtypesTimeSeriesChartAppearanceDTO; // spec.plugin.spec.chartAppearance
buckets: DashboardtypesHistogramBucketsDTO; // spec.plugin.spec.histogramBuckets
// spec.plugin.spec.visualization — typed as the Bar shape (widest superset);
// the `controls` bag gates which fields each kind writes.
[SectionKind.Visualization]: DashboardtypesBarChartVisualizationDTO;
[SectionKind.Thresholds]: AnyThreshold[]; // spec.plugin.spec.thresholds (variant picks the editor)
[SectionKind.ContextLinks]: DashboardLinkDTO[]; // spec.links (PANEL-level)
[SectionKind.Columns]: TelemetrytypesTelemetryFieldKeyDTO[]; // spec.plugin.spec.selectFields (List)
visualization: DashboardtypesBarChartVisualizationDTO;
thresholds: AnyThreshold[]; // spec.plugin.spec.thresholds (variant picks the editor)
contextLinks: DashboardLinkDTO[]; // spec.links (PANEL-level)
columns: TelemetrytypesTelemetryFieldKeyDTO[]; // spec.plugin.spec.selectFields (List)
}
/**
@@ -94,42 +78,33 @@ export interface SectionSpecMap {
* analogue of V1's `allowSoftMinMax` / `allowLegendColors` flags).
*/
export interface SectionControls {
[SectionKind.Formatting]: {
unit?: boolean;
decimals?: boolean;
columnUnits?: boolean;
};
[SectionKind.Axes]: { minMax?: boolean; logScale?: boolean }; // minMax → softMin/softMax
[SectionKind.Legend]: { position?: boolean; colors?: boolean }; // colors → customColors
[SectionKind.ChartAppearance]: {
formatting: { unit?: boolean; decimals?: boolean; columnUnits?: boolean };
axes: { minMax?: boolean; logScale?: boolean }; // minMax → softMin/softMax
legend: { position?: boolean; colors?: boolean }; // colors → customColors
chartAppearance: {
lineStyle?: boolean;
lineInterpolation?: boolean;
fillMode?: boolean;
showPoints?: boolean;
spanGaps?: boolean;
};
[SectionKind.Buckets]: {
count?: boolean;
width?: boolean;
mergeQueries?: boolean;
};
// switchPanelKind → the visualization-type switcher (every kind, so you can switch
// away from any panel); stacking → stackedBarChart (Bar); fillSpans → fill gaps with
// 0 (TimeSeries).
[SectionKind.Visualization]: {
switchPanelKind: boolean;
buckets: { count?: boolean; width?: boolean; mergeQueries?: boolean };
// stacking → stackedBarChart (Bar); fillSpans → fill gaps with 0 (TimeSeries).
visualization: {
timePreference?: boolean;
stacking?: boolean;
fillSpans?: boolean;
};
// Editor discriminator (not a spec field): which threshold variant a kind edits.
[SectionKind.Thresholds]: { variant?: ThresholdVariant };
thresholds: { variant?: ThresholdVariant };
}
export type ControlledSectionKind = keyof SectionControls;
/** Atomic sections — no sub-controls; a kind either shows them or not. */
export type AtomicSectionKind = SectionKind.ContextLinks | SectionKind.Columns;
export type AtomicSectionKind = 'contextLinks' | 'columns';
export type SectionKind = ControlledSectionKind | AtomicSectionKind;
/** Predicate to hide a section from the current spec; returning true removes it. */
export type SectionVisibilityPredicate = (
@@ -153,15 +128,15 @@ export type SectionConfig =
// Per-section title + sidebar icon. Pure data; the editor component + spec lens
// live in the ConfigPane section registry.
export const SECTION_METADATA = {
[SectionKind.Formatting]: { title: 'Formatting', icon: Hash },
[SectionKind.Axes]: { title: 'Axes', icon: Ruler },
[SectionKind.Legend]: { title: 'Legend', icon: Layers },
[SectionKind.ChartAppearance]: { title: 'Chart appearance', icon: Palette },
[SectionKind.Visualization]: { title: 'Visualization', icon: LayoutDashboard },
[SectionKind.Buckets]: { title: 'Histogram / Buckets', icon: BarChart },
[SectionKind.Thresholds]: { title: 'Thresholds', icon: SlidersHorizontal },
[SectionKind.ContextLinks]: { title: 'Context Links', icon: Link },
[SectionKind.Columns]: { title: 'Columns', icon: Columns3 },
formatting: { title: 'Formatting', icon: Hash },
axes: { title: 'Axes', icon: Ruler },
legend: { title: 'Legend', icon: Layers },
chartAppearance: { title: 'Chart appearance', icon: Palette },
visualization: { title: 'Visualization', icon: LayoutDashboard },
buckets: { title: 'Histogram / Buckets', icon: BarChart },
thresholds: { title: 'Thresholds', icon: SlidersHorizontal },
contextLinks: { title: 'Context Links', icon: Link },
columns: { title: 'Columns', icon: Columns3 },
} as const satisfies Record<SectionKind, SectionMetadata>;
/**

View File

@@ -10,7 +10,7 @@ import { sections as barSections } from '../../kinds/BarChartPanel/sections';
import { sections as histogramSections } from '../../kinds/HistogramPanel/sections';
import { sections as listSections } from '../../kinds/ListPanel/sections';
import { sections as timeSeriesSections } from '../../kinds/TimeSeriesPanel/sections';
import { SectionKind, type SectionConfig } from '../../types/sections';
import type { SectionConfig } from '../../types/sections';
import { buildDefaultPluginSpec } from '../buildDefaultPluginSpec';
describe('buildDefaultPluginSpec', () => {
@@ -50,17 +50,17 @@ describe('buildDefaultPluginSpec', () => {
it('does not seed controls that already show a clear default', () => {
// `axes` and `formatting` stay unset — their empty state is the chart default.
const sections: SectionConfig[] = [
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
{ kind: SectionKind.ContextLinks },
{ kind: 'axes', controls: { minMax: true, logScale: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'thresholds', controls: { variant: 'label' } },
{ kind: 'contextLinks' },
];
expect(buildDefaultPluginSpec(sections)).toStrictEqual({});
});
it('only seeds the legend position when the kind exposes that control', () => {
const sections: SectionConfig[] = [
{ kind: SectionKind.Legend, controls: { colors: true } },
{ kind: 'legend', controls: { colors: true } },
];
expect(buildDefaultPluginSpec(sections)).toStrictEqual({});
});

View File

@@ -6,11 +6,7 @@ import {
DashboardtypesTimePreferenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
SectionKind,
type SectionConfig,
type SectionSpecMap,
} from '../types/sections';
import type { SectionConfig, SectionSpecMap } from '../types/sections';
/**
* Seeded plugin-spec slices, typed as canonical section slices so each value is
@@ -18,9 +14,9 @@ import {
* so the union cast stays localized to `createDefaultPanel`.
*/
export interface DefaultPluginSpec {
visualization?: SectionSpecMap[SectionKind.Visualization];
legend?: SectionSpecMap[SectionKind.Legend];
chartAppearance?: SectionSpecMap[SectionKind.ChartAppearance];
visualization?: SectionSpecMap['visualization'];
legend?: SectionSpecMap['legend'];
chartAppearance?: SectionSpecMap['chartAppearance'];
}
/**
@@ -35,20 +31,20 @@ export function buildDefaultPluginSpec(
sections.forEach((section) => {
switch (section.kind) {
case SectionKind.Visualization:
case 'visualization':
if (section.controls.timePreference) {
spec.visualization = {
timePreference: DashboardtypesTimePreferenceDTO.global_time,
};
}
break;
case SectionKind.Legend:
case 'legend':
if (section.controls.position) {
spec.legend = { position: DashboardtypesLegendPositionDTO.bottom };
}
break;
case SectionKind.ChartAppearance: {
const chartAppearance: SectionSpecMap[SectionKind.ChartAppearance] = {};
case 'chartAppearance': {
const chartAppearance: SectionSpecMap['chartAppearance'] = {};
if (section.controls.lineStyle) {
chartAppearance.lineStyle = DashboardtypesLineStyleDTO.solid;
}

View File

@@ -10,15 +10,21 @@ import type { BuilderQuery } from 'types/api/v5/queryRange';
export function getBuilderQueries(
queries: DashboardtypesQueryDTO[],
): BuilderQuery[] {
if (!queries) {
return [];
}
const flattened: BuilderQuery[] = [];
queries.forEach((envelope) => {
const plugin = envelope.spec.plugin;
const plugin = envelope?.spec?.plugin;
if (!plugin) {
return;
}
if (plugin.kind === 'signoz/BuilderQuery') {
flattened.push(plugin.spec as BuilderQuery);
return;
}
if (plugin.kind === 'signoz/CompositeQuery') {
(plugin.spec.queries || []).forEach((sub) => {
(plugin.spec.queries ?? []).forEach((sub) => {
if (sub.type === 'builder_query') {
flattened.push(sub.spec as BuilderQuery);
}

View File

@@ -12,7 +12,7 @@ import { deriveQueryType } from '../../queryV5/persesQueryAdapters';
export function getPanelQueryType(
panel: DashboardtypesPanelDTO,
): EQueryType | undefined {
const envelopes = toQueryEnvelopes(panel.spec.queries);
const envelopes = toQueryEnvelopes(panel.spec.queries || []);
if (envelopes.length === 0) {
return undefined;
}

View File

@@ -55,7 +55,7 @@ function PanelBody({
// react-query keeps the previous response during refetches, so its presence is
// the "have something to show" signal — only fail hard when there's nothing.
const hasData = !!data.response;
const queries = panel.spec.queries;
const queries = panel.spec.queries || [];
// Not-configured panel: no runnable query, so nothing to error/load on.
if (!hasRunnableQueries(queries)) {

View File

@@ -8,7 +8,7 @@ import styles from './PanelTypeSelectionModal.module.scss';
interface PanelTypeSelectionModalProps {
open: boolean;
onClose: () => void;
onSelect: (panelKind: PanelKind) => void;
onSelect: (pluginKind: PanelKind) => void;
}
function PanelTypeSelectionModal({
@@ -25,17 +25,17 @@ function PanelTypeSelectionModal({
destroyOnClose
>
<div className={styles.grid}>
{PANEL_TYPES.map(({ panelKind, label, Icon }) => (
{PANEL_TYPES.map((type) => (
<Button
key={panelKind}
key={type.pluginKind}
type="button"
variant="ghost"
className={styles.typeButton}
data-testid={`panel-type-${panelKind}`}
onClick={(): void => onSelect(panelKind)}
data-testid={`panel-type-${type.pluginKind}`}
onClick={(): void => onSelect(type.pluginKind)}
>
<Icon size={14} />
{label}
{type.icon}
{type.label}
</Button>
))}
</div>

View File

@@ -1,24 +0,0 @@
import {
BarChart,
ChartLine,
ChartPie,
Hash,
List,
Table,
} from '@signozhq/icons';
import type { PanelType } from './types';
export const PANEL_TYPES: PanelType[] = [
{
panelKind: 'signoz/TimeSeriesPanel',
label: 'Time Series',
Icon: ChartLine,
},
{ panelKind: 'signoz/NumberPanel', label: 'Number', Icon: Hash },
{ panelKind: 'signoz/TablePanel', label: 'Table', Icon: Table },
{ panelKind: 'signoz/BarChartPanel', label: 'Bar Chart', Icon: BarChart },
{ panelKind: 'signoz/PieChartPanel', label: 'Pie Chart', Icon: ChartPie },
{ panelKind: 'signoz/HistogramPanel', label: 'Histogram', Icon: BarChart },
{ panelKind: 'signoz/ListPanel', label: 'List', Icon: List },
];

View File

@@ -0,0 +1,36 @@
import {
BarChart,
ChartLine,
ChartPie,
Hash,
List,
Table,
} from '@signozhq/icons';
import type { PanelType } from './types';
export const PANEL_TYPES: PanelType[] = [
{
pluginKind: 'signoz/TimeSeriesPanel',
label: 'Time Series',
icon: <ChartLine size={16} />,
},
{ pluginKind: 'signoz/NumberPanel', label: 'Value', icon: <Hash size={16} /> },
{ pluginKind: 'signoz/TablePanel', label: 'Table', icon: <Table size={16} /> },
{
pluginKind: 'signoz/BarChartPanel',
label: 'Bar Chart',
icon: <BarChart size={16} />,
},
{
pluginKind: 'signoz/PieChartPanel',
label: 'Pie Chart',
icon: <ChartPie size={16} />,
},
{
pluginKind: 'signoz/HistogramPanel',
label: 'Histogram',
icon: <BarChart size={16} />,
},
{ pluginKind: 'signoz/ListPanel', label: 'List', icon: <List size={16} /> },
];

View File

@@ -1,16 +1,7 @@
import type { IconSize } from '@signozhq/icons';
import type { ComponentType, SVGProps } from 'react';
import type { PanelKind } from '../../../Panels/types/panelKind';
type IconProps = Omit<SVGProps<SVGSVGElement>, 'ref'> & {
size?: number | IconSize;
strokeWidth?: number;
};
export interface PanelType {
panelKind: PanelKind;
pluginKind: PanelKind;
label: string;
/** Icon component — the consumer renders it and controls size/color/etc. */
Icon: ComponentType<IconProps>;
icon: JSX.Element;
}

View File

@@ -63,10 +63,9 @@ describe('useClonePanel', () => {
op: 'add',
path: '/spec/layouts/0/spec/items/-',
value: {
// Same dimensions as the source panel (p1: 8x5). The last row is
// full (8 + 4 = 12 cols), so the 8-wide clone wraps to a fresh row
// at the section bottom: max(y + height) = 5.
// Same dimensions as the source panel (p1: 8x5).
x: 0,
// Bottom of the section: max(y + height) over existing items = 5.
y: 5,
width: 8,
height: 5,
@@ -76,27 +75,6 @@ describe('useClonePanel', () => {
]);
});
it('places the clone beside the last row when it fits', async () => {
const oneNarrowItem: DashboardSection[] = [
{
id: 'section-0',
layoutIndex: 0,
title: 'Overview',
repeatVariable: undefined,
items: [{ id: 'p1', x: 0, y: 0, width: 4, height: 5, panel: sourcePanel }],
},
];
const { result } = renderHook(() =>
useClonePanel({ sections: oneNarrowItem }),
);
await result.current({ panelId: 'p1', layoutIndex: 0 });
const ops = mockPatch.mock.calls[0][1];
// Room in the last row (4 + 4 = 8 ≤ 12 cols) → sits to the right at y:0.
expect(ops[1].value).toMatchObject({ x: 4, y: 0, width: 4, height: 5 });
});
it('deep-copies the spec — the cloned value is not the same object reference', async () => {
const { result } = renderHook(() => useClonePanel({ sections: sections() }));

View File

@@ -5,11 +5,7 @@ import { v4 as uuid } from 'uuid';
import { patchDashboardV2 } from 'api/generated/services/dashboard';
import {
addPanelToSectionOps,
findFreeSlot,
panelRef,
} from '../../../patchOps';
import { addPanelToSectionOps, panelRef } from '../../../patchOps';
import { useDashboardStore } from '../../../store/useDashboardStore';
import type { DashboardSection } from '../../../utils';
@@ -24,9 +20,8 @@ export interface ClonePanelArgs {
/**
* Duplicates a panel: deep-copies the source spec under a fresh id and drops a
* same-size grid item into the section via `findFreeSlot` (beside the last row
* if it fits, else a fresh row), as one atomic patch. Mirrors V1's clone
* (verbatim spec copy, no rename).
* same-size grid item at the bottom of the section, as one atomic patch. Mirrors
* V1's clone (verbatim spec copy, no rename).
*/
export function useClonePanel({
sections,
@@ -43,7 +38,10 @@ export function useClonePanel({
}
const newPanelId = uuid();
const { x, y } = findFreeSlot(section.items, source.width);
const nextY = section.items.reduce(
(max, i) => Math.max(max, i.y + i.height),
0,
);
const clone = patchDashboardV2(
{ id: dashboardId },
@@ -52,8 +50,8 @@ export function useClonePanel({
panel: cloneDeep(source.panel),
layoutIndex,
item: {
x,
y,
x: 0,
y: nextY,
width: source.width,
height: source.height,
content: { $ref: panelRef(newPanelId) },

View File

@@ -77,7 +77,7 @@ export function usePanelQuery({
const fullKind = panel.spec.plugin.kind;
const panelType =
(fullKind && PANEL_KIND_TO_PANEL_TYPE[fullKind]) ?? PANEL_TYPES.TIME_SERIES;
const queries = panel.spec.queries;
const queries = useMemo(() => panel.spec.queries || [], [panel.spec.queries]);
// V1 parity: a list query with an explicit `limit` shows without a server pager; without
// one it pages server-side at a user-selectable size.

View File

@@ -132,16 +132,13 @@ const NEW_PANEL_SIZE = { width: 6, height: 6 };
/** Columns in the section grid — mirrors `cols` on SectionGrid's GridLayout. */
const GRID_COLS = 12;
/** Minimal placement fields shared by grid-item DTOs and flattened `GridItem`s. */
type PlacedItem = Pick<DashboardGridItemDTO, 'x' | 'y' | 'width' | 'height'>;
/**
* Placement for a new grid item: drop it right of the last row if there's room,
* else wrap to a fresh row at the bottom. Only the last row is considered (items
* sharing the greatest top-y); gaps in earlier rows are left alone.
*/
export function findFreeSlot(
items: PlacedItem[],
function findFreeSlot(
items: DashboardGridItemDTO[],
width: number,
): { x: number; y: number } {
const w = Math.min(width, GRID_COLS);

View File

@@ -59,7 +59,7 @@ export function toQueryEnvelopes(
queries: DashboardtypesQueryDTO[],
): Querybuildertypesv5QueryEnvelopeDTO[] {
// Backend invariant: panel.queries.length === 1. Only the first entry is consumed.
if (queries.length === 0) {
if (!queries || queries.length === 0) {
return [];
}
const plugin = queries[0].spec.plugin;

View File

@@ -81,7 +81,7 @@ export function fromPerses(
queries: DashboardtypesQueryDTO[],
panelType: PANEL_TYPES,
): Query {
const envelopes = toQueryEnvelopes(queries);
const envelopes = toQueryEnvelopes(queries ?? []);
if (envelopes.length === 0) {
return initialQueriesMap[DataSource.METRICS];
}

View File

@@ -148,7 +148,7 @@ function AnalyticsPanel({
className="floating-panel__drag-handle"
/>
<div className={styles.body} data-testid="trace-analytics-panel">
<div className={styles.body}>
<TabsRoot defaultValue="exec-time" onValueChange={onTabChange}>
<TabsList variant="secondary">
<TabsTrigger value="exec-time" variant="secondary">

View File

@@ -60,7 +60,7 @@ function DockModeSwitcher({
{DOCK_OPTIONS.map((option) => (
<TooltipRoot key={option.value}>
<TooltipTrigger asChild>
<span data-testid={`dock-mode-${option.value}`}>
<span>
<ToggleGroupItem value={option.value}>{option.icon}</ToggleGroupItem>
</span>
</TooltipTrigger>

View File

@@ -64,11 +64,7 @@ export function SpanTooltipContent({
{previewRows && previewRows.length > 0 && (
<div className={styles.preview}>
{previewRows.map((row) => (
<div
key={row.key}
className={styles.row}
data-testid={`span-hover-card-preview-${row.key}`}
>
<div key={row.key} className={styles.row}>
<span className={styles.previewKey}>{row.key}:</span>{' '}
<span className={styles.previewValue}>{row.value}</span>
</div>

View File

@@ -12,7 +12,6 @@ import { useFlamegraphCrosshair } from './hooks/useFlamegraphCrosshair';
import { useFlamegraphDrag } from './hooks/useFlamegraphDrag';
import { useFlamegraphDraw } from './hooks/useFlamegraphDraw';
import { useFlamegraphHover } from './hooks/useFlamegraphHover';
import { useFlamegraphTestHook } from './hooks/useFlamegraphTestHook';
import { useFlamegraphZoom } from './hooks/useFlamegraphZoom';
import { useScrollToSpan } from './hooks/useScrollToSpan';
import { EventRect, FlamegraphCanvasProps, SpanRect } from './types';
@@ -160,14 +159,6 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
useCanvasSetup(canvasRef, containerRef, drawFlamegraph, overlayCanvasRef);
// E2E-only: expose the live span→rect map so specs can target canvas bars.
// No-op unless window.__SIGNOZ_E2E__ is set (Playwright addInitScript).
useFlamegraphTestHook({
canvasRef,
containerRef,
spanRectsRef,
});
const {
cursorXPercent,
cursorX,

View File

@@ -1,101 +0,0 @@
import { MutableRefObject, useEffect } from 'react';
import { SpanRect } from '../types';
/**
* E2E test hook for the canvas flamegraph. The flamegraph is `<canvas>`, so
* individual bars have no DOM nodes to target — but `spanRectsRef` already
* holds the live span→rectangle map (CSS pixels) used for hit-testing. This
* exposes a thin, read-only view of it on `window.__sigTraceFlame__` so a
* Playwright spec can resolve a span's on-screen point and drive real
* hover/click events at it (see tests/e2e/helpers/trace-details.ts).
*
* Gated on `window.__SIGNOZ_E2E__` (set by Playwright via addInitScript), so
* nothing is attached in normal runtime — the e2e build is a production build,
* so this must be a RUNTIME flag, not a NODE_ENV/mode check.
*/
interface Point {
x: number;
y: number;
}
interface FlamegraphTestApi {
getSpanPoint: (spanId: string) => Point | null;
isSpanInView: (spanId: string) => boolean;
// Resting group color of a span's bar — changes when colour-by changes.
getSpanColor: (spanId: string) => string | null;
}
declare global {
interface Window {
__SIGNOZ_E2E__?: boolean;
__sigTraceFlame__?: FlamegraphTestApi;
}
}
// Inverse of `getCanvasPointer` in useFlamegraphHover: a CSS-space span rect
// maps back to a viewport point at the bar's center.
function rectToViewportCenter(canvas: HTMLCanvasElement, r: SpanRect): Point {
const box = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const cssWidth = canvas.width / dpr;
const cssHeight = canvas.height / dpr;
const cssX = r.x + r.width / 2;
const cssY = r.y + r.height / 2;
return {
x: box.left + cssX * (box.width / cssWidth),
y: box.top + cssY * (box.height / cssHeight),
};
}
interface UseFlamegraphTestHookParams {
canvasRef: MutableRefObject<HTMLCanvasElement | null>;
containerRef: MutableRefObject<HTMLDivElement | null>;
spanRectsRef: MutableRefObject<SpanRect[]>;
}
export function useFlamegraphTestHook({
canvasRef,
containerRef,
spanRectsRef,
}: UseFlamegraphTestHookParams): void {
useEffect(() => {
if (!window.__SIGNOZ_E2E__) {
return undefined;
}
// Reads `.current` at call time, so it always reflects the latest draw.
const findRect = (spanId: string): SpanRect | undefined =>
spanRectsRef.current.find((r) => r.span.spanId === spanId);
window.__sigTraceFlame__ = {
getSpanPoint: (spanId): Point | null => {
const canvas = canvasRef.current;
const rect = findRect(spanId);
return canvas && rect ? rectToViewportCenter(canvas, rect) : null;
},
isSpanInView: (spanId): boolean => {
const canvas = canvasRef.current;
const container = containerRef.current;
const rect = findRect(spanId);
if (!canvas || !container || !rect) {
return false;
}
const pt = rectToViewportCenter(canvas, rect);
const box = container.getBoundingClientRect();
return (
pt.x >= box.left &&
pt.x <= box.right &&
pt.y >= box.top &&
pt.y <= box.bottom
);
},
getSpanColor: (spanId): string | null => findRect(spanId)?.color ?? null,
};
return (): void => {
delete window.__sigTraceFlame__;
};
}, [canvasRef, containerRef, spanRectsRef]);
}

View File

@@ -28,9 +28,6 @@ export interface SpanRect {
width: number;
height: number;
level: number;
// Resting fill color for the current colour-by grouping. Optional: only the
// draw path sets it; consumers (e.g. the e2e colour-by hook) read it.
color?: string;
}
export interface EventRect {

View File

@@ -279,9 +279,6 @@ export function drawSpanBar(args: DrawSpanBarArgs): void {
width,
height: metrics.SPAN_BAR_HEIGHT,
level: levelIndex,
// Resting group color (selected/hovered bars override the fill, but this
// still reflects the colour-by grouping — used by the e2e colour-by hook).
color: isDarkMode ? color : colorDark,
});
span.event?.forEach((event) => {

View File

@@ -259,10 +259,7 @@ function Filters({
);
const highlightErrorsToggle = (
<div
className={styles.highlightErrorsToggle}
data-testid="highlight-errors-toggle"
>
<div className={styles.highlightErrorsToggle}>
<Typography.Text>Highlight errors</Typography.Text>
<Switch
color="cherry"

View File

@@ -246,19 +246,6 @@ const SpanOverview = memo(function SpanOverview({
onAddSpanToFunnel(span);
};
// e2e hook: expose the filter highlight/dim state as a stable attribute, since
// the styles.* classes are hashed at build time and can't be asserted.
let spanState = 'default';
if (isHighlighted) {
spanState = 'highlighted';
} else if (isDimmed) {
spanState = 'dimmed';
} else if (isSelectedNonMatching) {
spanState = 'selected-non-matching';
} else if (isSelected) {
spanState = 'selected';
}
return (
<div
className={cx(styles.spanOverview, {
@@ -267,7 +254,6 @@ const SpanOverview = memo(function SpanOverview({
[styles.isSelectedNonMatching]: isSelectedNonMatching,
[styles.isDimmed]: isDimmed,
})}
data-span-state={spanState}
onClick={(): void => handleSpanClick(span)}
onMouseEnter={(): void => onHoverEnter(span.span_id)}
onMouseLeave={(): void => onHoverLeave()}
@@ -315,7 +301,6 @@ const SpanOverview = memo(function SpanOverview({
{span.has_children && (
<span
className={styles.treeArrow}
data-testid={`cell-collapse-${span.span_id}`}
onClick={(event): void => {
event.stopPropagation();
event.preventDefault();

View File

@@ -46,20 +46,12 @@
},
"Sentry": {
"required": [
"enabled",
"dsn",
"tunnel"
"enabled"
],
"additionalProperties": false,
"properties": {
"dsn": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"tunnel": {
"type": "string"
}
},
"type": "object"

View File

@@ -16,7 +16,5 @@ export interface Pylon {
enabled: boolean;
}
export interface Sentry {
dsn: string;
enabled: boolean;
tunnel: string;
}

View File

@@ -1,9 +0,0 @@
import type { WebSettings } from 'types/generated/webSettings';
/**
* Returns the web settings injected by the backend into index.html at runtime
* (window.signozBootData.settings), or null when unavailable.
*/
export function getWebSettings(): WebSettings | null {
return window.signozBootData?.settings ?? null;
}

View File

@@ -20,6 +20,8 @@ interface ImportMetaEnv {
readonly VITE_SENTRY_AUTH_TOKEN: string;
readonly VITE_SENTRY_ORG: string;
readonly VITE_SENTRY_PROJECT_ID: string;
readonly VITE_SENTRY_DSN: string;
readonly VITE_TUNNEL_URL: string;
readonly VITE_TUNNEL_DOMAIN: string;
readonly VITE_DOCS_BASE_URL: string;
readonly VITE_ENVIRONMENT: string;

View File

@@ -31,11 +31,7 @@ function devBootDataPlugin(env: Record<string, string>): Plugin {
const settings = {
posthog: { enabled: env.VITE_POSTHOG_ENABLED !== 'false' },
appcues: { enabled: env.VITE_APPCUES_ENABLED !== 'false' },
sentry: {
enabled: env.VITE_SENTRY_ENABLED !== 'false',
dsn: env.VITE_SENTRY_DSN || '',
tunnel: env.VITE_TUNNEL_URL || '',
},
sentry: { enabled: env.VITE_SENTRY_ENABLED !== 'false' },
pylon: { enabled: env.VITE_PYLON_ENABLED !== 'false' },
};
return html.replaceAll('[[.Settings]]', JSON.stringify(settings));
@@ -169,6 +165,8 @@ export default defineConfig(({ mode }): UserConfig => {
'process.env.POSTHOG_KEY': JSON.stringify(env.VITE_POSTHOG_KEY),
'process.env.SENTRY_ORG': JSON.stringify(env.VITE_SENTRY_ORG),
'process.env.SENTRY_PROJECT_ID': JSON.stringify(env.VITE_SENTRY_PROJECT_ID),
'process.env.SENTRY_DSN': JSON.stringify(env.VITE_SENTRY_DSN),
'process.env.TUNNEL_URL': JSON.stringify(env.VITE_TUNNEL_URL),
'process.env.TUNNEL_DOMAIN': JSON.stringify(env.VITE_TUNNEL_DOMAIN),
'process.env.DOCS_BASE_URL': JSON.stringify(env.VITE_DOCS_BASE_URL),
'process.env.ENVIRONMENT': JSON.stringify(env.VITE_ENVIRONMENT),

View File

@@ -36,9 +36,7 @@ type AppcuesConfig struct {
}
type SentryConfig struct {
Enabled bool `mapstructure:"enabled"`
DSN string `mapstructure:"dsn"`
Tunnel string `mapstructure:"tunnel"`
Enabled bool `mapstructure:"enabled"`
}
type PylonConfig struct {

View File

@@ -48,20 +48,17 @@ func TestSettingsConfigWithEnvProvider(t *testing.T) {
testCases := []struct {
name string
env string
value string
expected SettingsConfig
}{
{name: "posthog", env: "SIGNOZ_WEB_SETTINGS_POSTHOG_ENABLED", value: "true", expected: SettingsConfig{Posthog: PosthogConfig{Enabled: true}}},
{name: "appcues", env: "SIGNOZ_WEB_SETTINGS_APPCUES_ENABLED", value: "true", expected: SettingsConfig{Appcues: AppcuesConfig{Enabled: true}}},
{name: "sentry_enabled", env: "SIGNOZ_WEB_SETTINGS_SENTRY_ENABLED", value: "true", expected: SettingsConfig{Sentry: SentryConfig{Enabled: true}}},
{name: "sentry_dsn", env: "SIGNOZ_WEB_SETTINGS_SENTRY_DSN", value: "https://examplePublicKey@o0.ingest.sentry.io/0", expected: SettingsConfig{Sentry: SentryConfig{DSN: "https://examplePublicKey@o0.ingest.sentry.io/0"}}},
{name: "sentry_tunnel", env: "SIGNOZ_WEB_SETTINGS_SENTRY_TUNNEL", value: "https://example.com/tunnel", expected: SettingsConfig{Sentry: SentryConfig{Tunnel: "https://example.com/tunnel"}}},
{name: "pylon", env: "SIGNOZ_WEB_SETTINGS_PYLON_ENABLED", value: "true", expected: SettingsConfig{Pylon: PylonConfig{Enabled: true}}},
{name: "posthog", env: "SIGNOZ_WEB_SETTINGS_POSTHOG_ENABLED", expected: SettingsConfig{Posthog: PosthogConfig{Enabled: true}}},
{name: "appcues", env: "SIGNOZ_WEB_SETTINGS_APPCUES_ENABLED", expected: SettingsConfig{Appcues: AppcuesConfig{Enabled: true}}},
{name: "sentry", env: "SIGNOZ_WEB_SETTINGS_SENTRY_ENABLED", expected: SettingsConfig{Sentry: SentryConfig{Enabled: true}}},
{name: "pylon", env: "SIGNOZ_WEB_SETTINGS_PYLON_ENABLED", expected: SettingsConfig{Pylon: PylonConfig{Enabled: true}}},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Setenv(testCase.env, testCase.value)
t.Setenv(testCase.env, "true")
conf, err := config.New(
context.Background(),

View File

@@ -121,21 +121,11 @@ func TestServeTemplatedIndex(t *testing.T) {
Settings: web.SettingsConfig{
Posthog: web.PosthogConfig{Enabled: true},
Appcues: web.AppcuesConfig{Enabled: true},
Sentry: web.SentryConfig{
Enabled: true,
DSN: "https://examplePublicKey@o0.ingest.sentry.io/0",
Tunnel: "https://example.com/tunnel",
},
},
},
expected: expectedHTML("/", web.Settings{
Posthog: web.Posthog{Enabled: true},
Appcues: web.Appcues{Enabled: true},
Sentry: web.Sentry{
Enabled: true,
DSN: "https://examplePublicKey@o0.ingest.sentry.io/0",
Tunnel: "https://example.com/tunnel",
},
}),
},
}

View File

@@ -16,9 +16,7 @@ type Appcues struct {
}
type Sentry struct {
Enabled bool `json:"enabled" required:"true"`
DSN string `json:"dsn" required:"true"`
Tunnel string `json:"tunnel" required:"true"`
Enabled bool `json:"enabled" required:"true"`
}
type Pylon struct {
@@ -35,8 +33,6 @@ func NewSettings(config Config) Settings {
},
Sentry: Sentry{
Enabled: config.Settings.Sentry.Enabled,
DSN: config.Settings.Sentry.DSN,
Tunnel: config.Settings.Sentry.Tunnel,
},
Pylon: Pylon{
Enabled: config.Settings.Pylon.Enabled,

View File

@@ -1,37 +0,0 @@
import type { Page } from '@playwright/test';
// Shared helpers used across feature-specific helper modules (dashboards,
// trace-details, …). Keep this to genuinely cross-feature utilities.
// ─── Seeder ────────────────────────────────────────────────────────────────
// Base URL of the HTTP seeder container the pytest harness brings up (exposes
// POST/DELETE on /telemetry/{traces,logs,metrics}). Written to
// `tests/e2e/.env.local` as `SIGNOZ_E2E_SEEDER_URL` and read here from the env.
export function seederUrl(): string {
const url = process.env.SIGNOZ_E2E_SEEDER_URL;
if (!url) {
throw new Error(
'SIGNOZ_E2E_SEEDER_URL not set — pytest test_setup must be running.',
);
}
return url;
}
// ─── Auth ────────────────────────────────────────────────────────────────
// Read the app JWT from the context's stored auth state. No navigation needed:
// the auth fixture loads the admin storageState (localStorage AUTH_TOKEN) into
// the context at creation, so storageState() returns it regardless of the page's
// current URL. Server-side APIs need this as a Bearer token (auth is
// JWT-in-localStorage, not cookies, so request.* doesn't carry it automatically).
export async function authToken(page: Page): Promise<string> {
const state = await page.context().storageState();
for (const origin of state.origins) {
const entry = origin.localStorage.find((e) => e.name === 'AUTH_TOKEN');
if (entry) {
return entry.value;
}
}
throw new Error('AUTH_TOKEN not found in storage state — is the page authed?');
}

View File

@@ -1,405 +0,0 @@
import { randomBytes } from 'crypto';
import type { APIRequestContext, Page } from '@playwright/test';
import largeTraceRecords from '../testdata/traces/large-trace.json';
import { authToken, seederUrl } from './common';
// ── Seeder: insert traces via POST /telemetry/traces ─────────────────────────
// Shape accepted by the seeder's POST /telemetry/traces endpoint
// (mirrors `Traces.from_dict` in tests/fixtures/traces.py). One object per span;
// spans sharing a `trace_id` form one trace, linked into a tree via
// `parent_span_id`. NOTE: the endpoint does NOT ingest span events/links.
export interface SeederSpan {
timestamp: string; // ISO-8601, e.g. new Date().toISOString()
trace_id: string; // 32 hex chars
span_id: string; // 16 hex chars
parent_span_id?: string; // empty/omitted = root span
name?: string;
kind?: number; // 1=internal 2=server 3=client 4=producer 5=consumer
status_code?: number; // 0=unset 1=ok 2=error
status_message?: string;
duration?: string; // ISO-8601 duration, e.g. "PT0.12S" (default PT1S)
resources?: Record<string, string>; // include 'service.name'
attributes?: Record<string, unknown>;
}
// 16-byte trace id / 8-byte span id, matching tests/fixtures/traces.py.
export const randomTraceId = (): string => randomBytes(16).toString('hex');
export const randomSpanId = (): string => randomBytes(8).toString('hex');
// Insert spans into the backend via the seeder. No auth needed (direct seeder
// call), so any APIRequestContext works — `page.request` or a standalone
// `playwright.request.newContext()` (cheaper than a full browser page for a
// pure API call).
//
// The seeder shares a single ClickHouse client, so concurrent POSTs from
// parallel workers collide with a 500 "concurrent queries within the same
// session". That's transient, so retry with backoff; any other error is real.
export async function seedTracesViaSeeder(
request: APIRequestContext,
spans: SeederSpan[],
): Promise<void> {
const url = `${seederUrl()}/telemetry/traces`;
const maxAttempts = 6;
let lastStatus = 0;
let lastText = '';
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
// eslint-disable-next-line no-await-in-loop
const res = await request.post(url, {
data: spans,
headers: { 'Content-Type': 'application/json' },
});
if (res.ok()) {
return;
}
lastStatus = res.status();
// eslint-disable-next-line no-await-in-loop
lastText = await res.text();
if (!(lastStatus === 500 && lastText.includes('concurrent'))) {
break;
}
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 150 * (attempt + 1) + Math.floor(Math.random() * 100));
});
}
throw new Error(`seeder POST /telemetry/traces ${lastStatus}: ${lastText}`);
}
// ── Navigation ───────────────────────────────────────────────────────────────
// Pages that already had the e2e test-hook init script registered, so
// gotoTraceUntilLoaded adds it at most once per Page (addInitScript re-runs on
// every navigation, and the script would otherwise stack up across calls).
const e2eHookRegistered = new WeakSet<Page>();
// Open a seeded trace and wait until the waterfall has rendered. The trace page
// fetches once on load, so if the seed isn't query-able yet (ClickHouse lag, worse
// under parallel load) it lands on the NoData state and never refetches — this
// reloads until the given row testid appears. Makes seeded-trace specs
// deterministic in the full parallel run, not just when run alone.
export async function gotoTraceUntilLoaded(
page: Page,
url: string,
readyTestId: string,
{ attempts = 5, perAttemptTimeoutMs = 8000 } = {},
): Promise<void> {
// Enable e2e-only test hooks (e.g. the flamegraph span→rect map in
// useFlamegraphTestHook) before the first navigation. Registered here because
// every trace-detail spec loads the page through this helper, so the flag is
// set without a dedicated fixture. Guarded to once per Page — addInitScript
// re-runs on every navigation, so re-registering would stack duplicates.
if (!e2eHookRegistered.has(page)) {
await page.addInitScript(() => {
(window as unknown as { __SIGNOZ_E2E__?: boolean }).__SIGNOZ_E2E__ = true;
});
// Dock the left nav so it doesn't fly out on hover and overlay the trace
// content's left strip (which otherwise makes left-edge hover/click targets
// land on the sidebar). Once per Page, before the first navigation.
await pinSidenav(page);
e2eHookRegistered.add(page);
}
for (let i = 0; i < attempts; i += 1) {
// eslint-disable-next-line no-await-in-loop
await page.goto(url);
try {
// eslint-disable-next-line no-await-in-loop
await page
.getByTestId(readyTestId)
.waitFor({ state: 'visible', timeout: perAttemptTimeoutMs });
return;
} catch {
// not loaded yet (NoData / seed lag) — reload and retry
}
}
// final navigation so the test's own assertion surfaces a clear failure
await page.goto(url);
}
// ── Trace options menu ─────────────────────────────────────────────────────
// Change the colour-by field via the trace options menu (Trace options → Colour
// by → field). colour-by is a per-user preference that persists, so tests should
// set a known field explicitly rather than assume the default. `fieldName` is a
// COLOR_BY_OPTIONS label (service.name | service.namespace | host.name |
// k8s.node.name | k8s.container.name); exact match avoids service.name matching
// service.namespace.
export async function changeColourByViaMenu(
page: Page,
fieldName: string,
): Promise<void> {
await page.getByRole('button', { name: 'Trace options' }).click();
await page.getByRole('menuitem', { name: /colour by/i }).click();
await page
.getByRole('menuitemradio', { name: fieldName, exact: true })
.click();
}
// ── Large trace fixture (tests/e2e/testdata/traces/large-trace.json) ─────────
// One deep, realistic trace: 100 spans across 18 services, nested ~34 levels,
// 8 error spans, a wide duration spread, and db/http/llm/messaging attributes —
// enough to drive the flamegraph, waterfall, filters and drawer off one seed.
// Converted once from a real getWaterfallV4 capture. `loadLargeTrace()` stamps
// fresh ids per run (parallel isolation), rebases the timeline to ~now, and
// derives landmark span ids so specs target rows without hardcoding ids.
// Shape of each record in large-trace.json.
interface LargeTraceRecord {
span_id: string;
parent_span_id: string; // empty = root
name: string;
kind: number;
status_code: number;
duration: string; // ISO-8601, e.g. "PT0.080000S"
offset_ms: number; // start offset from the root span
resources: Record<string, string>;
attributes: Record<string, unknown>;
}
const LARGE_TRACE_RECORDS = largeTraceRecords as LargeTraceRecord[];
export interface LargeTrace {
traceId: string;
spans: SeederSpan[];
// landmark span ids — already stamped — for targeting rows / the drawer
landmarks: {
root: string;
errors: string[];
db: string;
http: string;
llm: string;
messaging: string;
deepLeaf: string;
};
}
// Depth of a record via its parent chain (the JSON doesn't store level).
function recordDepth(
rec: LargeTraceRecord,
byId: Map<string, LargeTraceRecord>,
): number {
let depth = 0;
let cur: LargeTraceRecord | undefined = rec;
while (cur && cur.parent_span_id) {
cur = byId.get(cur.parent_span_id);
depth += 1;
}
return depth;
}
// Build a seedable copy of the large trace with fresh, isolated ids.
export function loadLargeTrace(): LargeTrace {
const traceId = randomTraceId();
// Stamp a fresh span id for every original id, preserving the tree links.
const idMap = new Map<string, string>();
LARGE_TRACE_RECORDS.forEach((r) => idMap.set(r.span_id, randomSpanId()));
// Sit the whole trace ~1 min in the past so all timestamps stay <= now.
const baseStartMs = Date.now() - 60_000;
const spans: SeederSpan[] = LARGE_TRACE_RECORDS.map((r) => {
const span: SeederSpan = {
timestamp: new Date(baseStartMs + r.offset_ms).toISOString(),
trace_id: traceId,
span_id: idMap.get(r.span_id) as string,
name: r.name,
kind: r.kind,
status_code: r.status_code,
duration: r.duration,
resources: r.resources,
attributes: r.attributes,
};
if (r.parent_span_id) {
span.parent_span_id = idMap.get(r.parent_span_id);
}
return span;
});
const byId = new Map(LARGE_TRACE_RECORDS.map((r) => [r.span_id, r]));
const stamp = (r: LargeTraceRecord | undefined): string =>
r ? (idMap.get(r.span_id) as string) : '';
const firstWithAttr = (key: string): LargeTraceRecord | undefined =>
LARGE_TRACE_RECORDS.find((r) => key in r.attributes);
const deepest = LARGE_TRACE_RECORDS.reduce((a, b) =>
recordDepth(b, byId) > recordDepth(a, byId) ? b : a,
);
const landmarks = {
root: stamp(LARGE_TRACE_RECORDS.find((r) => !r.parent_span_id)),
errors: LARGE_TRACE_RECORDS.filter((r) => r.status_code === 2).map((r) =>
stamp(r),
),
db: stamp(firstWithAttr('db.system')),
http: stamp(firstWithAttr('http.method')),
llm: stamp(firstWithAttr('gen_ai.request.model')),
messaging: stamp(firstWithAttr('messaging.system')),
deepLeaf: stamp(deepest),
};
return { traceId, spans, landmarks };
}
// ── Flamegraph canvas test hook ──────────────────────────────────────────────
// The flamegraph is canvas-rendered, so individual bars have no DOM nodes. The
// frontend exposes a read-only span→rect view on window.__sigTraceFlame__
// (useFlamegraphTestHook), present only when __SIGNOZ_E2E__ is set — which
// gotoTraceUntilLoaded injects via addInitScript.
// Mirror of the API exposed by useFlamegraphTestHook.
interface FlamegraphTestApi {
getSpanPoint: (spanId: string) => { x: number; y: number } | null;
isSpanInView: (spanId: string) => boolean;
getSpanColor: (spanId: string) => string | null;
}
interface FlameWindow {
__sigTraceFlame__?: FlamegraphTestApi;
}
// Resolve a span's on-canvas viewport point, waiting through the first paint
// (the hook + spanRects populate only after the flamegraph's draw rAF).
async function spanPoint(
page: Page,
spanId: string,
): Promise<{ x: number; y: number }> {
const handle = await page.waitForFunction(
(id) =>
(window as unknown as FlameWindow).__sigTraceFlame__?.getSpanPoint(id) ??
null,
spanId,
{ timeout: 10_000 },
);
const point = await handle.jsonValue();
if (!point) {
throw new Error(`flamegraph span "${spanId}" is not drawn on the canvas`);
}
return point;
}
// Hover the flamegraph bar for `spanId` (opens its SpanHoverCard).
export async function hoverFlamegraphSpan(
page: Page,
spanId: string,
): Promise<void> {
const { x, y } = await spanPoint(page, spanId);
await page.mouse.move(x, y);
}
// Click the flamegraph bar for `spanId` (selects the span / opens the drawer).
export async function clickFlamegraphSpan(
page: Page,
spanId: string,
): Promise<void> {
const { x, y } = await spanPoint(page, spanId);
await page.mouse.move(x, y);
await page.mouse.click(x, y);
}
// Whether `spanId`'s bar is currently drawn AND inside the viewport container.
export async function isFlamegraphSpanInView(
page: Page,
spanId: string,
): Promise<boolean> {
return page.evaluate(
(id) =>
(window as unknown as FlameWindow).__sigTraceFlame__?.isSpanInView(id) ??
false,
spanId,
);
}
// Resting group color of a span's bar — used to assert colour-by recolor.
export async function getFlamegraphSpanColor(
page: Page,
spanId: string,
): Promise<string | null> {
return page.evaluate(
(id) =>
(window as unknown as FlameWindow).__sigTraceFlame__?.getSpanColor(id) ??
null,
spanId,
);
}
// ── User preferences (server-side, per-user) ─────────────────────────────────
// Trace-detail user-preference keys (mirror frontend constants/userPreferences.ts).
export const TRACE_PREFERENCE = {
COLOR_BY: 'span_details_color_by_attribute',
PREVIEW_FIELDS: 'span_details_preview_attributes',
PINNED_ATTRIBUTES: 'span_details_pinned_attributes',
} as const;
// Whether the left nav is docked/pinned (mirror USER_PREFERENCES.SIDENAV_PINNED).
const SIDENAV_PINNED = 'sidenav_pinned';
// A telemetry field key as persisted in the preview-fields preference. Only
// `name` is required by the store (derivePreviewFields), but fieldContext /
// fieldDataType match how the UI persists them.
export interface PreviewFieldKey {
name: string;
fieldContext?: string;
fieldDataType?: string;
}
// PUT a single user preference (server-side, per-user). Call BEFORE navigating
// to the trace page so its on-mount preference fetch returns the seeded value.
//
// NOTE: user preferences are GLOBAL PER USER, not per-test — they persist on the
// server for the admin user. Reset them (resetTracePreferences) in afterAll, and
// be aware other specs run by the same user in parallel share this state.
export async function setUserPreference(
page: Page,
name: string,
value: unknown,
): Promise<void> {
const token = await authToken(page);
const res = await page.request.put(`/api/v1/user/preferences/${name}`, {
data: { value },
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok()) {
throw new Error(
`PUT /api/v1/user/preferences/${name} ${res.status()}: ${await res.text()}`,
);
}
}
// Persist the flamegraph color-by field. `fieldName` must be one of
// COLOR_BY_OPTIONS (service.name | service.namespace | host.name |
// k8s.node.name | k8s.container.name); '' falls back to the default.
export async function setColorByPreference(
page: Page,
fieldName: string,
): Promise<void> {
await setUserPreference(page, TRACE_PREFERENCE.COLOR_BY, fieldName);
}
// Persist the span-details preview fields (shown as rows in the hover card).
export async function setPreviewFieldsPreference(
page: Page,
fields: PreviewFieldKey[],
): Promise<void> {
await setUserPreference(page, TRACE_PREFERENCE.PREVIEW_FIELDS, fields);
}
// Reset trace-detail prefs to defaults. Run in afterAll so a prefs spec doesn't
// leak color-by / preview-field state into other specs for the same user.
export async function resetTracePreferences(page: Page): Promise<void> {
await setColorByPreference(page, '');
await setPreviewFieldsPreference(page, []);
}
// Pin (dock) the left nav. When unpinned it's a collapsed rail that flies out on
// hover as an absolute OVERLAY, covering the trace content's left strip — so
// hover/click on left-edge targets (the waterfall collapse arrow, flamegraph
// bars) lands on the sidebar instead. Pinned, it's a flex child that reserves
// layout space, so nothing is occluded. Set before navigating: the server pref
// wins over localStorage once preferences load.
export async function pinSidenav(page: Page): Promise<void> {
await setUserPreference(page, SIDENAV_PINNED, true);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +0,0 @@
import { test, expect } from '../../fixtures/auth';
import {
gotoTraceUntilLoaded,
loadLargeTrace,
seedTracesViaSeeder,
} from '../../helpers/trace-details';
// One shared trace for the whole file, seeded once. Unique ids per run keep this
// isolated from other parallel specs; the global teardown clears the traces signal.
const trace = loadLargeTrace();
test.describe('Trace details — span details drawer', () => {
test.beforeAll(async ({ playwright }) => {
// Seed once via a disposable request context — no auth needed (direct
// seeder call), and cheaper than spinning up a full browser page.
const request = await playwright.request.newContext();
await seedTracesViaSeeder(request, trace.spans);
await request.dispose();
});
test.beforeEach(async ({ authedPage: page }) => {
// open the trace, reloading until the waterfall renders (seed→query lag)
await gotoTraceUntilLoaded(
page,
`/trace/${trace.traceId}`,
`cell-0-${trace.landmarks.root}`,
);
});
test('TC-01 the floating drawer can be dragged', async ({
authedPage: page,
}) => {
await page.getByTestId(`cell-0-${trace.landmarks.root}`).click();
await page.getByTestId('dock-mode-dialog').click();
const handle = page.locator('.floating-panel__drag-handle');
await expect(handle).toBeVisible();
const zero = { x: 0, y: 0, width: 0, height: 0 };
const before = (await handle.boundingBox()) ?? zero;
// Drag from the left of the header (title area) to avoid the action buttons.
const startX = before.x + 30;
const startY = before.y + before.height / 2;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(startX - 120, startY + 80, { steps: 8 });
await page.mouse.up();
await expect
.poll(async () => Math.round(((await handle.boundingBox()) ?? before).x))
.toBeLessThan(Math.round(before.x));
});
test('TC-02 a dock-mode change persists and is restored on reload', async ({
authedPage: page,
}) => {
// §0 prefs-boot, UI-first: switch to floating via the dock-mode UI (which
// persists the variant), then reload and confirm it's restored — the drawer
// boots floating, not the docked-right default.
await page.getByTestId(`cell-0-${trace.landmarks.root}`).click();
await page.getByTestId('dock-mode-dialog').click();
await expect(page.locator('.floating-panel__drag-handle')).toBeVisible();
await page.reload();
await page.getByTestId(`cell-0-${trace.landmarks.root}`).click();
await expect(page.locator('.floating-panel__drag-handle')).toBeVisible();
});
});

View File

@@ -1,114 +0,0 @@
import { test, expect } from '../../fixtures/auth';
import { newAdminContext } from '../../helpers/auth';
import {
changeColourByViaMenu,
clickFlamegraphSpan,
getFlamegraphSpanColor,
gotoTraceUntilLoaded,
hoverFlamegraphSpan,
isFlamegraphSpanInView,
loadLargeTrace,
seedTracesViaSeeder,
setColorByPreference,
} from '../../helpers/trace-details';
// The flamegraph is canvas-rendered, so individual bars have no DOM nodes. These
// specs drive it through the window.__sigTraceFlame__ test hook (enabled by
// gotoTraceUntilLoaded) — see helpers/trace-details.ts — which resolves a span's
// on-canvas point from the live span→rect map and dispatches real mouse events.
//
// One shared trace for the file, seeded once. Random ids per run isolate it from
// other parallel specs; the global teardown clears the traces signal.
//
// Colour-by recolor is asserted via the hook's getSpanColor (the resting group
// color per bar), since canvas pixels aren't directly assertable.
//
// Deferred: sampled large trace — sampling needs >100k spans
// (FLAMEGRAPH_SPAN_LIMIT), which is the deferred large-trace work.
const trace = loadLargeTrace();
test.describe('Trace details — flamegraph', () => {
test.beforeAll(async ({ playwright }) => {
const request = await playwright.request.newContext();
await seedTracesViaSeeder(request, trace.spans);
await request.dispose();
});
test.afterAll(async ({ browser }) => {
// TC-04 changes colour-by — a per-user pref. Reset it so it doesn't leak to
// other specs (afterAll can't use the test-scoped authedPage fixture).
const ctx = await newAdminContext(browser);
const page = await ctx.newPage();
await setColorByPreference(page, '');
await ctx.close();
});
test.beforeEach(async ({ authedPage: page }) => {
await gotoTraceUntilLoaded(
page,
`/trace/${trace.traceId}`,
`cell-0-${trace.landmarks.root}`,
);
});
test('TC-01 hovering an error bar opens its hover card with status/start/duration', async ({
authedPage: page,
}) => {
await hoverFlamegraphSpan(page, trace.landmarks.errors[0]);
// "status: error" only renders in the hover card (not in waterfall rows),
// so it proves both that the card opened and that we hovered the right
// (error) span — the bar was targeted by id via the span→rect map.
await expect(page.getByText('status: error')).toBeVisible();
await expect(page.getByText(/start: [\d.]+ ms/)).toBeVisible();
await expect(page.getByText(/duration: [\d.]+/)).toBeVisible();
});
test('TC-02 clicking a bar selects the span, opens the drawer, and syncs the waterfall row', async ({
authedPage: page,
}) => {
await clickFlamegraphSpan(page, trace.landmarks.db);
// selection is reflected in the shared URL state...
await expect(page).toHaveURL(new RegExp(`spanId=${trace.landmarks.db}`));
// ...the drawer opens (Overview tab is drawer-only)...
await expect(page.getByRole('tab', { name: /overview/i })).toBeVisible();
// ...and the same span's waterfall row is present (views share selection).
await expect(page.getByTestId(`cell-0-${trace.landmarks.db}`)).toBeVisible();
});
test('TC-03 deep-linking a deeply-nested span scrolls it into view on the flamegraph', async ({
authedPage: page,
}) => {
// Open pre-pointed at a deep (level ~34) span; useScrollToSpan should
// center it, so its bar becomes drawn and inside the viewport container.
await gotoTraceUntilLoaded(
page,
`/trace/${trace.traceId}?spanId=${trace.landmarks.deepLeaf}`,
`cell-0-${trace.landmarks.deepLeaf}`,
);
await expect
.poll(() => isFlamegraphSpanInView(page, trace.landmarks.deepLeaf))
.toBe(true);
});
test('TC-04 changing colour-by recolors the flamegraph bars', async ({
authedPage: page,
}) => {
// colour-by persists per-user, so set an explicit baseline rather than
// assuming the default. Root's color under service.name:
await changeColourByViaMenu(page, 'service.name');
const colorByService = await getFlamegraphSpanColor(
page,
trace.landmarks.root,
);
expect(colorByService).not.toBeNull();
// Switch to host.name → root groups by a different value → new color.
await changeColourByViaMenu(page, 'host.name');
await expect
.poll(() => getFlamegraphSpanColor(page, trace.landmarks.root))
.not.toBe(colorByService);
});
});

View File

@@ -1,57 +0,0 @@
import { test, expect } from '../../fixtures/auth';
import {
gotoTraceUntilLoaded,
loadLargeTrace,
seedTracesViaSeeder,
} from '../../helpers/trace-details';
// §1 header — the Analytics FloatingPanel. The action cluster (Analytics button
// + options menu) only renders once trace data is loaded, which gotoTraceUntilLoaded
// guarantees by waiting for the root waterfall row.
//
// Not covered here: subheader summary (presentational → unit test), colour-by /
// options menu / trace-id copy (unit), Noz button (feature-flagged, lives in the
// filter bar). Resize is deferred — react-rnd's resize handles have no stable hook.
const trace = loadLargeTrace();
test.describe('Trace details — header analytics panel', () => {
test.beforeAll(async ({ playwright }) => {
const request = await playwright.request.newContext();
await seedTracesViaSeeder(request, trace.spans);
await request.dispose();
});
test.beforeEach(async ({ authedPage: page }) => {
await gotoTraceUntilLoaded(
page,
`/trace/${trace.traceId}`,
`cell-0-${trace.landmarks.root}`,
);
});
test('TC-01 the analytics panel can be dragged by its header', async ({
authedPage: page,
}) => {
await page.getByRole('button', { name: 'Analytics' }).click();
const panel = page.getByTestId('trace-analytics-panel');
await expect(panel).toBeVisible();
const zero = { x: 0, y: 0, width: 0, height: 0 };
const before = (await panel.boundingBox()) ?? zero;
const hb =
(await page.locator('.floating-panel__drag-handle').boundingBox()) ?? zero;
// Drag the header left + down.
await page.mouse.move(hb.x + hb.width / 2, hb.y + hb.height / 2);
await page.mouse.down();
await page.mouse.move(hb.x + hb.width / 2 - 120, hb.y + hb.height / 2 + 60, {
steps: 8,
});
await page.mouse.up();
// Panel shifted left.
await expect
.poll(async () => Math.round(((await panel.boundingBox()) ?? before).x))
.toBeLessThan(Math.round(before.x));
});
});

View File

@@ -1,88 +0,0 @@
import { test, expect } from '../../fixtures/auth';
import { newAdminContext } from '../../helpers/auth';
import {
gotoTraceUntilLoaded,
hoverFlamegraphSpan,
loadLargeTrace,
resetTracePreferences,
seedTracesViaSeeder,
setPreviewFieldsPreference,
} from '../../helpers/trace-details';
// §6 — preview fields. A configured preview field appears as a row in the span
// hover card (SpanTooltipContent, testid span-hover-card-preview-<key>). The
// waterfall variant is covered at the unit/integration level; this spec keeps
// the flamegraph (canvas) case, which can't run in jsdom.
//
// Preview fields are a server-side, per-user preference, so each test seeds them
// via the API before navigating; afterAll resets them so the state doesn't leak
// into other specs run by the same admin user.
const trace = loadLargeTrace();
// The db landmark span carries db.system="redis"; seed db.system as a preview
// field so its value renders in the hover card.
const PREVIEW_FIELD = 'db.system';
const PREVIEW_VALUE = 'redis';
const PREVIEW_TESTID = `span-hover-card-preview-${PREVIEW_FIELD}`;
// Skipped wholesale until the flamegraph preview-fields fetch race (FE bug, see
// the TC-01 FIXME + sprint task) is fixed — the only case here is that flamegraph
// hover test, which can't pass reliably yet. The waterfall variant moved to
// unit/integration. Re-enable (and un-fixme TC-01) once the flamegraph
// gates/refetches on previewFields.
test.describe.skip('Trace details — preview fields in the hover card', () => {
// Run serially in one worker: preview fields are a per-user preference, so
// the afterAll reset must not race a sibling test still using them on another
// worker (which intermittently wiped the preview row mid-test).
test.describe.configure({ mode: 'serial' });
test.beforeAll(async ({ playwright }) => {
const request = await playwright.request.newContext();
await seedTracesViaSeeder(request, trace.spans);
await request.dispose();
});
test.afterAll(async ({ browser }) => {
// Reset prefs to defaults (afterAll can't use the authedPage fixture).
const ctx = await newAdminContext(browser);
const page = await ctx.newPage();
await resetTracePreferences(page);
await ctx.close();
});
test.beforeEach(async ({ authedPage: page }) => {
// Seed the preview field BEFORE navigating so the on-mount prefs fetch
// returns it and the hover card renders the row.
// db.system is a span ATTRIBUTE (fieldContext 'attribute', not 'span') —
// the flamegraph fetches fields selectively, so the wrong context means
// the bar's span wouldn't carry the value and the hover row wouldn't render.
await setPreviewFieldsPreference(page, [
{ name: PREVIEW_FIELD, fieldContext: 'attribute', fieldDataType: 'string' },
]);
await gotoTraceUntilLoaded(
page,
`/trace/${trace.traceId}`,
`cell-0-${trace.landmarks.root}`,
);
});
// FIXME: blocked by a frontend bug — the flamegraph fires its span fetch
// (POST /flamegraph) with selectFields = color-by only, before previewFields
// syncs into the store, and does NOT refetch when the preference lands. So the
// flamegraph span never carries the preview attribute (e.g. db.system) and its
// hover card can't render the row. Intermittent (passes only when prefs are
// cache-warm before the first fetch). Re-enable once the flamegraph
// gates/refetches on previewFields. See sprint task.
test.fixme('TC-01 flamegraph hover card shows the configured preview field', async ({
authedPage: page,
}) => {
const previewRow = page.getByTestId(PREVIEW_TESTID).first();
await expect(async () => {
await page.mouse.move(0, 0);
await hoverFlamegraphSpan(page, trace.landmarks.db);
await expect(previewRow).toBeVisible({ timeout: 1500 });
}).toPass({ timeout: 15_000 });
await expect(previewRow).toContainText(PREVIEW_VALUE);
});
});

View File

@@ -1,50 +0,0 @@
import { test, expect } from '../../fixtures/auth';
import {
gotoTraceUntilLoaded,
loadLargeTrace,
seedTracesViaSeeder,
} from '../../helpers/trace-details';
const trace = loadLargeTrace();
test.describe('Trace details — waterfall', () => {
test.beforeAll(async ({ playwright }) => {
const request = await playwright.request.newContext();
await seedTracesViaSeeder(request, trace.spans);
await request.dispose();
});
test('TC-01 deep-link ?spanId auto-selects the span and opens the drawer', async ({
authedPage: page,
}) => {
// Open the trace pre-pointed at a specific span via the URL, reloading
// until the waterfall renders (seed→query lag).
const errorSpan = trace.landmarks.errors[0];
await gotoTraceUntilLoaded(
page,
`/trace/${trace.traceId}?spanId=${errorSpan}`,
`cell-0-${errorSpan}`,
);
// the deep-linked span's row renders...
await expect(page.getByTestId(`cell-0-${errorSpan}`)).toBeVisible();
// ...and it auto-selects → the drawer is open (Overview tab is drawer-only)
await expect(page.getByRole('tab', { name: /overview/i })).toBeVisible();
});
test('TC-02 deep-linking a deeply-nested span auto-expands ancestors and scrolls it into view', async ({
authedPage: page,
}) => {
// deepLeaf sits ~34 levels down; rendering its row at all proves every
// ancestor auto-expanded and the waterfall scrolled it into view.
const deep = trace.landmarks.deepLeaf;
await gotoTraceUntilLoaded(
page,
`/trace/${trace.traceId}?spanId=${deep}`,
`cell-0-${deep}`,
);
await expect(page.getByTestId(`cell-0-${deep}`)).toBeVisible();
await expect(page).toHaveURL(new RegExp(`spanId=${deep}`));
});
});