Compare commits

...

12 Commits

Author SHA1 Message Date
Nikhil Soni
432723335f fix: remove module doc from skills
These file could go stale leading to wrong info
to the agents
2026-03-02 18:31:38 +05:30
Nikhil Soni
32110e7718 Remove non standard attribute usage 2026-02-20 00:16:29 +05:30
Nikhil Soni
c295ef386d chore(agent): merge and compact traces skill into single reference doc
Combines trace-detail-architecture.md and TRACES_MODULE.md into one
concise traces-module.md (196 lines, down from 1119 combined).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:56:20 +05:30
Nikhil Soni
bf0394cc28 chore(agent): add clickhouse-query skill, project settings, and update existing skills
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 18:27:12 +05:30
Nikhil Soni
fa08ca2fac chore(agent): add skill to code review 2026-02-17 14:08:58 +05:30
Nikhil Soni
08c53fe7e8 docs: add few modules implemtation details
Generated by claude code
2026-01-27 22:33:49 +05:30
Nikhil Soni
c1fac00d2e feat: add claude.md and github commands 2026-01-27 22:33:12 +05:30
Aditya Singh
15161c09e8 Feat: show (cmd + return) as helper text in Run Query button (#10082)
* feat: create common run query btn

* feat: update run query in explorer

* feat: comment

* feat: fix styles

* feat: fix styles

* feat: update style

* feat: update btn in alerts

* feat: added test cases

* feat: replace run query btn

* feat: bg change run query
2026-01-27 14:52:03 +05:30
Ashwin Bhatkal
ee5fbe41eb chore: add eslint rules for no-unused-vars (#10072)
* chore: updated eslint base config with comments

* chore: add eslint rules for no-else-return and curly

* chore: add eslint rules for no-console

* chore: add eslint rules for no-unused-vars

* chore: fix more cases
2026-01-27 14:14:26 +05:30
Vikrant Gupta
f2f3a7b24a chore(lint): enable wastedassign (#10103)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-01-26 20:40:49 +05:30
Ashwin Bhatkal
dd0738ac70 chore: add eslint rules for no-console (#10071)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-01-23 19:06:05 +00:00
Pandey
1c4dfc931f chore: move to clone instead of json marshal (#10076) 2026-01-23 16:34:30 +00:00
50 changed files with 875 additions and 244 deletions

136
.claude/CLAUDE.md Normal file
View File

@@ -0,0 +1,136 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
SigNoz is an open-source observability platform (APM, logs, metrics, traces) built on OpenTelemetry and ClickHouse. It provides a unified solution for monitoring applications with features including distributed tracing, log management, metrics dashboards, and alerting.
## Build and Development Commands
### Development Environment Setup
```bash
make devenv-up # Start ClickHouse and OTel Collector for local dev
make devenv-clickhouse # Start only ClickHouse
make devenv-signoz-otel-collector # Start only OTel Collector
make devenv-clickhouse-clean # Clean ClickHouse data
```
### Backend (Go)
```bash
make go-run-community # Run community backend server
make go-run-enterprise # Run enterprise backend server
make go-test # Run all Go unit tests
go test -race ./pkg/... # Run tests for specific package
go test -race ./pkg/querier/... # Example: run querier tests
```
### Integration Tests (Python)
```bash
cd tests/integration
uv sync # Install dependencies
make py-test-setup # Start test environment (keep running with --reuse)
make py-test # Run all integration tests
make py-test-teardown # Stop test environment
# Run specific test
uv run pytest --basetemp=./tmp/ -vv --reuse src/<suite>/<file>.py::test_name
```
### Code Quality
```bash
# Go linting (golangci-lint)
golangci-lint run
# Python formatting/linting
make py-fmt # Format with black
make py-lint # Run isort, autoflake, pylint
```
### OpenAPI Generation
```bash
go run cmd/enterprise/*.go generate openapi
```
## Architecture Overview
### Backend Structure
The Go backend follows a **provider pattern** for dependency injection:
- **`pkg/signoz/`** - IoC container that wires all providers together
- **`pkg/modules/`** - Business logic modules (user, organization, dashboard, etc.)
- **`pkg/<provider>/`** - Provider implementations following consistent structure:
- `<name>.go` - Interface definition
- `config.go` - Configuration (implements `factory.Config`)
- `<implname><name>/provider.go` - Implementation
- `<name>test/` - Mock implementations for testing
### Key Packages
- **`pkg/querier/`** - Query engine for telemetry data (logs, traces, metrics)
- **`pkg/telemetrystore/`** - ClickHouse telemetry storage interface
- **`pkg/sqlstore/`** - Relational database (SQLite/PostgreSQL) for metadata
- **`pkg/apiserver/`** - HTTP API server with OpenAPI integration
- **`pkg/alertmanager/`** - Alert management
- **`pkg/authn/`, `pkg/authz/`** - Authentication and authorization
- **`pkg/flagger/`** - Feature flags (OpenFeature-based)
- **`pkg/errors/`** - Structured error handling
### Enterprise vs Community
- **`cmd/community/`** - Community edition entry point
- **`cmd/enterprise/`** - Enterprise edition entry point
- **`ee/`** - Enterprise-only features
## Code Conventions
### Error Handling
Use the custom `pkg/errors` package instead of standard library:
```go
errors.New(typ, code, message) // Instead of errors.New()
errors.Newf(typ, code, message, args...) // Instead of fmt.Errorf()
errors.Wrapf(err, typ, code, msg) // Wrap with context
```
Define domain-specific error codes:
```go
var CodeThingNotFound = errors.MustNewCode("thing_not_found")
```
### HTTP Handlers
Handlers are thin adapters in modules that:
1. Extract auth context from request
2. Decode request body using `binding` package
3. Call module functions
4. Return responses using `render` package
Register routes in `pkg/apiserver/signozapiserver/` with `handler.New()` and `OpenAPIDef`.
### SQL/Database
- Use Bun ORM via `sqlstore.BunDBCtx(ctx)`
- Star schema with `organizations` as central entity
- All tables have `id`, `created_at`, `updated_at`, `org_id` columns
- Write idempotent migrations in `pkg/sqlmigration/`
- No `ON CASCADE` deletes - handle in application logic
### REST Endpoints
- Use plural resource names: `/v1/organizations`, `/v1/users`
- Use `me` for current user/org: `/v1/organizations/me/users`
- Follow RESTful conventions for CRUD operations
### Linting Rules (from .golangci.yml)
- Don't use `errors` package - use `pkg/errors`
- Don't use `zap` logger - use `slog`
- Don't use `fmt.Errorf` or `fmt.Print*`
## Testing
### Unit Tests
- Run with race detector: `go test -race ./...`
- Provider mocks are in `<provider>test/` packages
### Integration Tests
- Located in `tests/integration/`
- Use pytest with testcontainers
- Files prefixed with numbers for execution order (e.g., `01_database.py`)
- Always use `--reuse` flag during development
- Fixtures in `tests/integration/fixtures/`

View File

@@ -0,0 +1,37 @@
---
name: commit
description: Create a conventional commit with staged changes
allowed-tools: Bash(git commit:*)
---
# Create Conventional Commit
Commit staged changes using conventional commit format: `type(scope): description`
## Types
- `feat:` - New feature
- `fix:` - Bug fix
- `chore:` - Maintenance/refactor/tooling
- `test:` - Tests only
- `docs:` - Documentation
## Process
1. Review staged changes: `git diff --cached`
2. Determine type, optional scope, and description (imperative, <70 chars)
3. Commit using HEREDOC:
```bash
git commit -m "$(cat <<'EOF'
type(scope): description
EOF
)"
```
4. Verify: `git log -1`
## Notes
- Description: imperative mood, lowercase, no period
- Body: explain WHY, not WHAT (code shows what). Keep it concise and brief.
- Do not include co-authored by claude in commit message, we want ownership and accountability to remain with the human contributor.
- Do not automatically add files to stage unless asked to.

View File

@@ -0,0 +1,55 @@
---
name: raise-pr
description: Create a pull request with auto-filled template. Pass 'commit' to commit staged changes first.
allowed-tools: Bash(gh:*, git:*), Read
argument-hint: [commit?]
---
# Raise Pull Request
Create a PR with auto-filled template from commits after origin/main.
## Arguments
- No argument: Create PR with existing commits
- `commit`: Commit staged changes first, then create PR
## Process
1. **If `$ARGUMENTS` is "commit"**: Review staged changes and commit with descriptive message
- Check for staged changes: `git diff --cached --stat`
- If changes exist:
- Review the changes: `git diff --cached`
- Use commit skill for making the commit, i.e. follow conventional commit practices
- Commit command: `git commit -m "message"`
2. **Analyze commits since origin/main**:
- `git log origin/main..HEAD --pretty=format:"%s%n%b"` - get commit messages
- `git diff origin/main...HEAD --stat` - see changes
3. **Read template**: `.github/pull_request_template.md`
4. **Generate PR**:
- **Title**: Short (<70 chars), from commit messages or main change
- **Body**: Fill template sections based on commits/changes, keep these minimal and to the point:
- Summary (why/what/approach) - end with "Closes #<issue_number>" if issue number is available from branch name (git branch --show-current)
- Change Type checkboxes
- Bug Context (if applicable)
- Testing Strategy
- Risk Assessment
- Changelog (if user-facing)
- Checklist
5. **Create PR**:
```bash
git push -u origin $(git branch --show-current)
gh pr create --base main --title "..." --body "..."
gh pr view
```
## Notes
- Analyze ALL commits messages from origin/main to HEAD
- Fill template sections based on commit messages, look into code changes if messages doesn't have all the context.
- Leave template sections as they are if you can't determine the content
- Don't add the changes to git stage, only commit or push whatever user has already staged

View File

@@ -0,0 +1,228 @@
---
name: review
description: Review code changes for bugs, performance issues, and SigNoz convention compliance
allowed-tools: Bash(git:*, gh:*), Read, Glob, Grep
---
# Review Command
Perform a thorough code review following SigNoz's coding conventions and contributing guidelines and for feature intent completion.
## Usage
Invoke this command to review code changes with actionable and concise feedback.
## Process
1. **Determine scope**:
- Ask user what to review if not specified:
- Current git diff (staged or unstaged)
- All changes since origin/main or a commit range
2. **Gather context**:
```bash
# For current changes
git diff --cached # Staged changes
git diff # Unstaged changes
# For commit range
git diff origin/main...HEAD # All changes since main
# for last commit only
git diff HEAD~1..HEAD
```
3. **Read all relevant files thoroughly**:
- Understand the context and purpose of changes
4. **Review against SigNoz guidelines**:
- **Frontend**: Check [Frontend Guidelines](../../frontend/CONTRIBUTIONS.md)
- **Backend/Architecture**: Check [CLAUDE.md](../CLAUDE.md) for provider pattern, error handling, SQL, REST, and linting conventions
- **General**: Check [Contributing Guidelines](../../CONTRIBUTING.md)
- **Commits**: Verify [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
5. **Verify feature intent**:
- Read the PR description, commit message, or linked issue to understand *what* the change claims to do
- Trace the code path end-to-end to confirm the change actually achieves its stated goal
- Check that the happy path works as described
- Identify any scenarios where the feature silently does nothing or produces wrong results
6. **Review for bug introduction**:
- **Regressions**: Does the change break existing behavior? Check callers of modified functions/interfaces
- **Edge cases**: Empty inputs, nil/undefined values, boundary conditions, concurrent access
- **Error paths**: Are all error cases handled? Can errors be swallowed silently?
- **State management**: Are state transitions correct? Can state become inconsistent?
- **Race conditions**: Shared mutable state, async operations, missing locks or guards
- **Type mismatches**: Unsafe casts, implicit conversions, `any` usage hiding real types
7. **Review for performance implications**:
- **Backend**: N+1 queries, missing indexes, unbounded result sets, large allocations in hot paths, unnecessary DB round-trips
- **Frontend**: Unnecessary re-renders from inline objects/functions as props, missing memoization on expensive computations, large bundle imports that should be lazy-loaded, unthrottled event handlers
- **General**: O(n²) or worse algorithms on potentially large datasets, unnecessary network calls, missing pagination or limits
8. **Provide actionable, concise feedback** in structured format
## Review Checklist
For coding conventions and style, refer to the linked guideline docs. This checklist focuses on **review-specific concerns** that guidelines alone don't catch.
### Correctness & Intent
- [ ] Change achieves what the PR/commit/issue describes
- [ ] Happy path works end-to-end
- [ ] Edge cases handled (empty, nil, boundary, concurrent)
- [ ] Error paths don't swallow failures silently
- [ ] No regressions to existing callers of modified code
### Security
- [ ] No exposed secrets, API keys, credentials
- [ ] No sensitive data in logs
- [ ] Input validation at system boundaries
- [ ] Authentication/authorization checked for new endpoints
- [ ] No SQL injection or XSS risks
### Performance
- [ ] No N+1 queries or unbounded result sets
- [ ] No unnecessary re-renders (inline objects/functions as props, missing memoization)
- [ ] No large imports that should be lazy-loaded
- [ ] No O(n²) on potentially large datasets
- [ ] Pagination/limits present where needed
### Testing
- [ ] Edge cases and error paths tested
- [ ] Tests are deterministic (no flakiness)
## Output Format
Provide feedback in this structured format:
```markdown
## Code Review
**Scope**: [What was reviewed]
**Overall**: [1-2 sentence summary and general sentiment]
---
### 🚨 Critical Issues (Must Fix)
1. **[Category]** `file:line`
**Problem**: [What's wrong]
**Why**: [Why it matters]
**Fix**: [Specific solution]
```[language]
// Example fix if helpful
```
### ⚠️ Suggestions (Should Consider)
1. **[Category]** `file:line`
**Issue**: [What could be improved]
**Suggestion**: [Concrete improvement]
---
**References**:
- [Relevant guideline links]
```
## Review Categories
Use these categories for issues:
- **Bug / Regression**: Logic errors, edge cases, race conditions, broken existing behavior
- **Feature Gap**: Change doesn't fully achieve its stated intent
- **Security Risk**: Authentication, authorization, data exposure, injection
- **Performance Issue**: Inefficient queries, unnecessary re-renders, memory leaks, unbounded data
- **Convention Violation**: Style, patterns, architectural guidelines (link to relevant guideline doc)
- **Code Quality**: Complexity, duplication, naming, type safety
- **Testing**: Missing tests, inadequate coverage, flaky tests
## Example Review
```markdown
## Code Review
**Scope**: Changes in `frontend/src/pages/TraceDetail/` (3 files, 245 additions)
**Overall**: Good implementation of pagination feature. Found 2 critical issues and 3 suggestions.
---
### 🚨 Critical Issues (Must Fix)
1. **Security Risk** `TraceList.tsx:45`
**Problem**: API token exposed in client-side code
**Why**: Security vulnerability - tokens should never be in frontend
**Fix**: Move authentication to backend, use session-based auth
2. **Performance Issue** `TraceList.tsx:89`
**Problem**: Inline function passed as prop causes unnecessary re-renders
**Why**: Violates frontend guideline, degrades performance with large lists
**Fix**:
```typescript
const handleTraceClick = useCallback((traceId: string) => {
navigate(`/trace/${traceId}`);
}, [navigate]);
```
### ⚠️ Suggestions (Should Consider)
1. **Code Quality** `TraceList.tsx:120-180`
**Issue**: Function exceeds 40-line guideline
**Suggestion**: Extract into smaller functions:
- `filterTracesByTimeRange()`
- `aggregateMetrics()`
- `renderChartData()`
2. **Type Safety** `types.ts:23`
**Issue**: Using `any` for trace attributes
**Suggestion**: Define proper interface for TraceAttributes
3. **Convention** `TraceList.tsx:12`
**Issue**: File imports not organized
**Suggestion**: Let simple-import-sort auto-organize (will happen on save)
---
**References**:
- [Frontend Guidelines](../../frontend/CONTRIBUTIONS.md)
- [useCallback best practices](https://kentcdodds.com/blog/usememo-and-usecallback)
```
## Tone Guidelines
- **Be respectful**: Focus on code, not the person
- **Be specific**: Always reference exact file:line locations
- **Be concise**: Get to the point, avoid verbosity
- **Be actionable**: Every comment should have clear resolution path
- **Be educational**: Explain why something is an issue, link to guidelines
## Priority Levels
1. **Critical (🚨)**: Security, bugs, data corruption, crashes
2. **Important (⚠️)**: Performance, maintainability, convention violations
3. **Nice to have (💡)**: Style preferences, micro-optimizations
## Important Notes
- **Reference specific guidelines** from docs when applicable
- **Provide code examples** for fixes when helpful
- **Ask questions** if code intent is unclear
- **Link to external resources** for educational value
- **Distinguish** must-fix from should-consider
- **Be concise** - reviewers value their time
## Critical Rules
- **NEVER** be vague - always specify file and line number
- **NEVER** just point out problems - suggest solutions
- **NEVER** review without reading the actual code
- **ALWAYS** check against SigNoz's specific guidelines
- **ALWAYS** provide rationale for each comment
- **ALWAYS** be constructive and respectful
## Reference Documents
- [Frontend Guidelines](../../frontend/CONTRIBUTIONS.md) - React, TypeScript, styling
- [Contributing Guidelines](../../CONTRIBUTING.md) - Workflow, commit conventions
- [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - Commit format
- [CLAUDE.md](../CLAUDE.md) - Project architecture and conventions

View File

@@ -12,6 +12,7 @@ linters:
- misspell
- nilnil
- sloglint
- wastedassign
- unparam
- unused
settings:

View File

@@ -38,7 +38,7 @@ module.exports = {
'import', // Import/export linting
'sonarjs', // Code quality/complexity
// TODO: Uncomment after running: yarn add -D eslint-plugin-spellcheck
// 'spellcheck',
// 'spellcheck', // Correct spellings
],
settings: {
react: {
@@ -60,12 +60,18 @@ module.exports = {
'no-debugger': 'error', // Disallows debugger statements in production code
curly: 'error', // Requires curly braces for all control statements
eqeqeq: ['error', 'always', { null: 'ignore' }], // Enforces === and !== (allows == null for null/undefined check)
// TODO: Enable after fixing ~15 console.log statements
// 'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
// TypeScript rules
'@typescript-eslint/explicit-function-return-type': 'error', // Requires explicit return types on functions
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
// Disallows unused variables/args
'error',
{
argsIgnorePattern: '^_', // Allows unused args prefixed with _ (e.g., _unusedParam)
varsIgnorePattern: '^_', // Allows unused vars prefixed with _ (e.g., _unusedVar)
},
],
'@typescript-eslint/no-explicit-any': 'warn', // Warns when using 'any' type (consider upgrading to error)
// TODO: Change to 'error' after fixing ~80 empty function placeholders in providers/contexts
'@typescript-eslint/no-empty-function': 'off', // Disallows empty function bodies

View File

@@ -41,7 +41,7 @@ export const getConsumerLagDetails = async (
> => {
const { detailType, ...restProps } = props;
const response = await axios.post(
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
`/messaging-queues/kafka/consumer-lag/${detailType}`,
{
...restProps,
},

View File

@@ -43,16 +43,17 @@ export const omitIdFromQuery = (query: Query | null): any => ({
builder: {
...query?.builder,
queryData: query?.builder.queryData.map((queryData) => {
const { id, ...rest } = queryData.aggregateAttribute || {};
const { id: _aggregateAttributeId, ...rest } =
queryData.aggregateAttribute || {};
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id, ...rest } = groupByAttribute;
const { id: _groupByAttributeId, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters?.items?.map((item) => {
const { id, ...newItem } = item;
const { id: _itemId, ...newItem } = item;
if (item.key) {
const { id, ...rest } = item.key;
const { id: _keyId, ...rest } = item.key;
return {
...newItem,
key: rest,

View File

@@ -45,7 +45,6 @@ function Pre({
}
function Code({
node,
inline,
className = 'blog-code',
children,

View File

@@ -29,7 +29,6 @@ import {
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
queryOperatorSuggestions,
} from 'constants/antlrQueryConstants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
@@ -208,8 +207,6 @@ function QuerySearch({
const lastValueRef = useRef<string>('');
const isMountedRef = useRef<boolean>(true);
const { handleRunQuery } = useQueryBuilder();
const { selectedDashboard } = useDashboard();
const dynamicVariables = useMemo(

View File

@@ -87,7 +87,7 @@ function TraceOperatorEditor({
// Track if the query was changed externally (from props) vs internally (user input)
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalValue, setLastExternalValue] = useState<string>('');
const { currentQuery, handleRunQuery } = useQueryBuilder();
const { currentQuery } = useQueryBuilder();
const queryOptions = useMemo(
() =>

View File

@@ -5,7 +5,6 @@ import { EditorView } from '@uiw/react-codemirror';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as UseQBModule from 'hooks/queryBuilder/useQueryBuilder';
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
@@ -121,13 +120,8 @@ jest.mock('api/querySuggestions/getValueSuggestion', () => ({
// Note: We're NOT mocking CodeMirror here - using the real component
// This provides integration testing with the actual CodeMirror editor
const handleRunQueryMock = ((UseQBModule as unknown) as {
handleRunQuery: jest.MockedFunction<() => void>;
}).handleRunQuery;
const SAMPLE_KEY_TYPING = 'http.';
const SAMPLE_VALUE_TYPING_INCOMPLETE = "service.name = '";
const SAMPLE_VALUE_TYPING_COMPLETE = "service.name = 'frontend'";
const SAMPLE_STATUS_QUERY = "http.status_code = '200'";
describe('QuerySearch (Integration with Real CodeMirror)', () => {

View File

@@ -796,12 +796,12 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
});
const {
aggregateAttribute,
aggregateOperator,
timeAggregation,
spaceAggregation,
reduceTo,
filters,
aggregateAttribute: _aggregateAttribute,
aggregateOperator: _aggregateOperator,
timeAggregation: _timeAggregation,
spaceAggregation: _spaceAggregation,
reduceTo: _reduceTo,
filters: _filters,
...retainedQuery
} = query;

View File

@@ -1,14 +1,5 @@
import './Slider.styles.scss';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
interface ISliderProps {
filter: IQuickFiltersConfig;
}
// not needed for now build when required
export default function Slider(props: ISliderProps): JSX.Element {
const { filter } = props;
console.log(filter);
export default function Slider(): JSX.Element {
return <div>Slider</div>;
}

View File

@@ -303,15 +303,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
);
case FiltersType.DURATION:
return (
<Duration
filter={filter}
onFilterChange={onFilterChange}
source={source}
/>
);
return <Duration filter={filter} onFilterChange={onFilterChange} />;
case FiltersType.SLIDER:
return <Slider filter={filter} />;
return <Slider />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return (

View File

@@ -1,7 +1,6 @@
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';

View File

@@ -73,7 +73,7 @@ export function sanitizeDashboardData(
const updatedVariables = Object.entries(selectedData.variables).reduce(
(acc, [key, value]) => {
const { selectedValue, ...rest } = value;
const { selectedValue: _selectedValue, ...rest } = value;
acc[key] = rest;
return acc;
},

View File

@@ -9,10 +9,11 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
import { Atom, Play, Terminal } from 'lucide-react';
import { Atom, Terminal } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -165,9 +166,8 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={(): void => {
<RunQueryBtn
onStageRunQuery={(): void => {
runQuery();
logEvent('Alert: Stage and run query', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
@@ -176,11 +176,7 @@ function QuerySection({
queryType: queryCategory,
});
}}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
/>
</span>
}
items={tabs}
@@ -199,14 +195,7 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={runQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
<RunQueryBtn onStageRunQuery={runQuery} />
</span>
}
items={items}

View File

@@ -1,14 +1,6 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Tag,
Typography,
} from 'antd';
import { Button, Flex, Input, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';

View File

@@ -91,7 +91,7 @@ function Summary(): JSX.Element {
const queryFiltersWithoutId = useMemo(() => {
const filtersWithoutId = {
...queryFilters,
items: queryFilters.items.map(({ id, ...rest }) => rest),
items: queryFilters.items.map(({ id: _id, ...rest }) => rest),
};
return JSON.stringify(filtersWithoutId);
}, [queryFilters]);

View File

@@ -12,6 +12,7 @@ import {
getDefaultWidgetData,
PANEL_TYPE_TO_QUERY_TYPES,
} from 'container/NewWidget/utils';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
// import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
@@ -20,7 +21,7 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo, isUndefined } from 'lodash-es';
import { Atom, Play, Terminal } from 'lucide-react';
import { Atom, Terminal } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
getNextWidgets,
@@ -28,20 +29,14 @@ import {
getSelectedWidgetIndex,
} from 'providers/Dashboard/util';
import { useCallback, useEffect, useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
function QuerySection({
selectedGraph,
queryResponse,
}: QueryProps): JSX.Element {
function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
const {
currentQuery,
handleRunQuery: handleRunQueryFromQueryBuilder,
@@ -242,15 +237,7 @@ function QuerySection({
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<Button
loading={queryResponse.isFetching}
type="primary"
onClick={handleRunQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
<RunQueryBtn label="Stage & Run Query" onStageRunQuery={handleRunQuery} />
</span>
}
items={items}
@@ -261,10 +248,6 @@ function QuerySection({
interface QueryProps {
selectedGraph: PANEL_TYPES;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
}
export default QuerySection;

View File

@@ -64,7 +64,7 @@ function LeftContainer({
enableDrillDown={enableDrillDown}
/>
<QueryContainer className="query-section-left-container">
<QuerySection selectedGraph={selectedGraph} queryResponse={queryResponse} />
<QuerySection selectedGraph={selectedGraph} />
{selectedGraph === PANEL_TYPES.LIST && (
<ExplorerColumnsRenderer
selectedLogFields={selectedLogFields}

View File

@@ -166,7 +166,7 @@ function UpdateContextLinks({
onSave(newContextLink);
} catch (error) {
// Form validation failed, don't call onSave
console.log('Form validation failed:', error);
console.error('Form validation failed:', error);
}
};

View File

@@ -0,0 +1,37 @@
.run-query-btn {
display: flex;
min-width: 132px;
align-items: center;
gap: 6px;
.ant-btn-icon {
margin: 0 !important;
}
}
.cancel-query-btn {
display: flex;
min-width: 132px;
align-items: center;
gap: 6px;
}
.cmd-hint {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px 4px;
border-radius: 4px;
//not using var here to support opacity 60%. To be handled at design system level.
background: rgba(35, 38, 46, 0.6);
line-height: 1;
font-size: 10px;
}
.lightMode {
.cmd-hint {
color: var(--bg-ink-200);
//not using var here to support opacity 60%. To be handled at design system level.
background: rgba(231, 232, 236, 0.8);
}
}

View File

@@ -0,0 +1,53 @@
import './RunQueryBtn.scss';
import { Button } from 'antd';
import {
ChevronUp,
Command,
CornerDownLeft,
Loader2,
Play,
} from 'lucide-react';
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
interface RunQueryBtnProps {
label?: string;
isLoadingQueries?: boolean;
handleCancelQuery?: () => void;
onStageRunQuery?: () => void;
}
function RunQueryBtn({
label,
isLoadingQueries,
handleCancelQuery,
onStageRunQuery,
}: RunQueryBtnProps): JSX.Element {
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
return isLoadingQueries ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className="cancel-query-btn periscope-btn danger"
onClick={handleCancelQuery}
>
Cancel
</Button>
) : (
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled={isLoadingQueries || !onStageRunQuery}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
{label || 'Run Query'}
<div className="cmd-hint">
{isMac ? <Command size={12} /> : <ChevronUp size={12} />}
<CornerDownLeft size={12} />
</div>
</Button>
);
}
export default RunQueryBtn;

View File

@@ -0,0 +1,82 @@
// frontend/src/container/QueryBuilder/components/RunQueryBtn/__tests__/RunQueryBtn.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import RunQueryBtn from '../RunQueryBtn';
// Mock OS util
jest.mock('utils/getUserOS', () => ({
getUserOperatingSystem: jest.fn(),
UserOperatingSystem: { MACOS: 'mac', WINDOWS: 'win', LINUX: 'linux' },
}));
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
describe('RunQueryBtn', () => {
test('renders run state and triggers on click', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} />);
const btn = screen.getByRole('button', { name: /run query/i });
expect(btn).toBeEnabled();
fireEvent.click(btn);
expect(onRun).toHaveBeenCalledTimes(1);
});
test('disabled when onStageRunQuery is undefined', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
render(<RunQueryBtn />);
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
});
test('shows cancel state and calls handleCancelQuery', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onCancel = jest.fn();
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
const cancel = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancel);
expect(onCancel).toHaveBeenCalledTimes(1);
});
test('shows Command + CornerDownLeft on mac', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
expect(container.querySelector('.lucide-command')).toBeInTheDocument();
expect(
container.querySelector('.lucide-corner-down-left'),
).toBeInTheDocument();
});
test('shows ChevronUp + CornerDownLeft on non-mac', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.WINDOWS,
);
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
expect(container.querySelector('.lucide-chevron-up')).toBeInTheDocument();
expect(container.querySelector('.lucide-command')).not.toBeInTheDocument();
expect(
container.querySelector('.lucide-corner-down-left'),
).toBeInTheDocument();
});
test('renders custom label when provided', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
expect(
screen.getByRole('button', { name: /stage & run query/i }),
).toBeInTheDocument();
});
});

View File

@@ -1,12 +1,12 @@
import './ToolbarActions.styles.scss';
import { Button } from 'antd';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Loader2, Play } from 'lucide-react';
import { MutableRefObject, useEffect } from 'react';
import { useQueryClient } from 'react-query';
import RunQueryBtn from '../RunQueryBtn/RunQueryBtn';
interface RightToolbarActionsProps {
onStageRunQuery: () => void;
isLoadingQueries?: boolean;
@@ -42,14 +42,7 @@ export default function RightToolbarActions({
if (showLiveLogs) {
return (
<div className="right-toolbar-actions-container">
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled
icon={<Play size={14} />}
>
Run Query
</Button>
<RunQueryBtn />
</div>
);
}
@@ -65,26 +58,11 @@ export default function RightToolbarActions({
return (
<div className="right-toolbar-actions-container">
{isLoadingQueries ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className="cancel-query-btn periscope-btn danger"
onClick={handleCancelQuery}
>
Cancel
</Button>
) : (
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled={isLoadingQueries}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Run Query
</Button>
)}
<RunQueryBtn
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
onStageRunQuery={onStageRunQuery}
/>
</div>
);
}

View File

@@ -136,7 +136,6 @@ const useAggregateDrilldown = ({
query,
// panelType,
aggregateData: aggregateDataWithTimeRange,
widgetId,
onClose,
});

View File

@@ -129,7 +129,6 @@ const useBaseAggregateOptions = ({
const handleBaseDrilldown = useCallback(
(key: string): void => {
console.log('Base drilldown:', { key, aggregateData });
const route = getRoute(key);
const timeRange = aggregateData?.timeRange;
const filtersToAdd = aggregateData?.filters || [];

View File

@@ -17,7 +17,6 @@ interface UseBaseAggregateOptionsProps {
query?: Query;
// panelType?: PANEL_TYPES;
aggregateData?: AggregateData | null;
widgetId?: string;
onClose: () => void;
}
@@ -27,7 +26,6 @@ const useDashboardVarConfig = ({
query,
// panelType,
aggregateData,
widgetId,
onClose,
}: UseBaseAggregateOptionsProps): {
dashbaordVariablesConfig: {
@@ -83,11 +81,6 @@ const useDashboardVarConfig = ({
dashboardVar: [string, IDashboardVariable],
fieldValue: any,
) => {
console.log('Setting variable:', {
fieldName,
dashboardVarId: dashboardVar[0],
fieldValue,
});
onValueUpdate(fieldName, dashboardVar[1]?.id, fieldValue, false);
onClose();
},
@@ -96,10 +89,6 @@ const useDashboardVarConfig = ({
const handleUnsetVariable = useCallback(
(fieldName: string, dashboardVar: [string, IDashboardVariable]) => {
console.log('Unsetting variable:', {
fieldName,
dashboardVarId: dashboardVar[0],
});
onValueUpdate(fieldName, dashboardVar[0], null, false);
onClose();
},
@@ -109,12 +98,6 @@ const useDashboardVarConfig = ({
const handleCreateVariable = useCallback(
(fieldName: string, fieldValue: string | number | boolean) => {
const source = getSourceFromQuery();
console.log('Creating variable from drilldown:', {
fieldName,
fieldValue,
source,
widgetId,
});
createVariable(
fieldName,
fieldValue,
@@ -125,7 +108,7 @@ const useDashboardVarConfig = ({
);
onClose();
},
[createVariable, getSourceFromQuery, widgetId, onClose],
[createVariable, getSourceFromQuery, onClose],
);
const contextItems = useMemo(

View File

@@ -19,20 +19,6 @@
display: flex;
align-items: center;
gap: 8px;
.cancel-query-btn {
min-width: 96px;
display: flex;
align-items: center;
gap: 2px;
}
.run-query-btn {
min-width: 96px;
display: flex;
align-items: center;
gap: 2px;
}
}
}

View File

@@ -31,8 +31,8 @@ export const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
...step.filters,
items: step.filters.items.map((item) => {
const {
id: unusedId,
isIndexed,
id: _unusedId,
isIndexed: _isIndexed,
...keyObj
} = item.key as BaseAutocompleteData;
return {

View File

@@ -143,7 +143,7 @@ export const useValidateFunnelSteps = ({
selectedTime,
steps.map((step) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { latency_type, ...rest } = step;
const { latency_type: _latency_type, ...rest } = step;
return rest;
}),
],

View File

@@ -55,7 +55,7 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
const parsedDraggedColumns = await JSON.parse(localStorageColumns);
nextDraggedColumns = parsedDraggedColumns;
} catch (e) {
console.log('error while parsing json');
console.error('error while parsing json: ', e);
} finally {
redirectWithDraggedColumns(nextDraggedColumns);
}

View File

@@ -4,11 +4,6 @@ import parser from 'lib/logql/parser';
describe('lib/logql/parser', () => {
test('parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
try {
parser(queryObject.query);
} catch (e) {
console.log(e);
}
expect(parser(queryObject.query)).toEqual(queryObject.parsedQuery);
});
});

View File

@@ -4,11 +4,7 @@ import { reverseParser } from 'lib/logql/reverseParser';
describe('lib/logql/reverseParser', () => {
test('reverse parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
try {
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
} catch (e) {
console.log(e);
}
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
});
});
});

View File

@@ -11,8 +11,8 @@ describe('getYAxisScale', () => {
keyIndex: 1,
thresholdValue: 10,
thresholdUnit: 'percentunit',
moveThreshold(dragIndex, hoverIndex): void {
console.log(dragIndex, hoverIndex);
moveThreshold(): void {
// no-op
},
selectedGraph: PANEL_TYPES.TIME_SERIES,
},
@@ -21,8 +21,8 @@ describe('getYAxisScale', () => {
keyIndex: 2,
thresholdValue: 20,
thresholdUnit: 'percentunit',
moveThreshold(dragIndex, hoverIndex): void {
console.log(dragIndex, hoverIndex);
moveThreshold(): void {
// no-op
},
selectedGraph: PANEL_TYPES.TIME_SERIES,
},

View File

@@ -9,7 +9,6 @@ import {
MessagingQueuesPayloadProps,
} from 'api/messagingQueues/getConsumerLagDetails';
import axios from 'axios';
import { isNumber } from 'chart.js/helpers';
import cx from 'classnames';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';

View File

@@ -275,7 +275,7 @@ export function setConfigDetail(
},
): void {
// remove "key" and its value from the paramsToSet object
const { key, ...restParamsToSet } = paramsToSet || {};
const { key: _key, ...restParamsToSet } = paramsToSet || {};
if (!isEmpty(restParamsToSet)) {
const configDetail = {

View File

@@ -145,7 +145,7 @@ export const removeFilter = (
const updatedValues = prevValue.filter((item: any) => item !== value);
if (updatedValues.length === 0) {
const { [filterType]: item, ...remainingFilters } = prevFilters;
const { [filterType]: _item, ...remainingFilters } = prevFilters;
return Object.keys(remainingFilters).length > 0
? (remainingFilters as FilterType)
: undefined;
@@ -175,7 +175,7 @@ export const removeAllFilters = (
return prevFilters;
}
const { [filterType]: item, ...remainingFilters } = prevFilters;
const { [filterType]: _item, ...remainingFilters } = prevFilters;
return Object.keys(remainingFilters).length > 0
? (remainingFilters as Record<

View File

@@ -20,8 +20,7 @@ export const parseQueryIntoSpanKind = (
current = parsedValue;
}
} catch (error) {
console.log(error);
console.log('error while parsing json');
console.error('error while parsing json: ', error);
}
}

View File

@@ -19,7 +19,7 @@ export const parseQueryIntoCurrent = (
current = parseInt(parsedValue, 10);
}
} catch (error) {
console.log('error while parsing json');
console.error('error while parsing json: ', error);
}
}

View File

@@ -20,8 +20,7 @@ export const parseQueryIntoOrder = (
current = parsedValue;
}
} catch (error) {
console.log(error);
console.log('error while parsing json');
console.error('error while parsing json: ', error);
}
}

View File

@@ -20,8 +20,7 @@ export const parseAggregateOrderParams = (
current = parsedValue;
}
} catch (error) {
console.log(error);
console.log('error while parsing json');
console.error('error while parsing json: ', error);
}
}

View File

@@ -36,29 +36,6 @@ func NewBucketCache(settings factory.ProviderSettings, cache cache.Cache, cacheT
}
}
// cachedBucket represents a cached time bucket
type cachedBucket struct {
StartMs uint64 `json:"startMs"`
EndMs uint64 `json:"endMs"`
Type qbtypes.RequestType `json:"type"`
Value json.RawMessage `json:"value"`
Stats qbtypes.ExecStats `json:"stats"`
}
// cachedData represents the full cached data for a query
type cachedData struct {
Buckets []*cachedBucket `json:"buckets"`
Warnings []string `json:"warnings"`
}
func (c *cachedData) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
func (c *cachedData) MarshalBinary() ([]byte, error) {
return json.Marshal(c)
}
// GetMissRanges returns cached data and missing time ranges
func (bc *bucketCache) GetMissRanges(
ctx context.Context,
@@ -78,7 +55,7 @@ func (bc *bucketCache) GetMissRanges(
bc.logger.DebugContext(ctx, "cache key", "cache_key", cacheKey)
// Try to get cached data
var data cachedData
var data qbtypes.CachedData
err := bc.cache.Get(ctx, orgID, cacheKey, &data)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
@@ -147,9 +124,9 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que
cacheKey := bc.generateCacheKey(q)
// Get existing cached data
var existingData cachedData
var existingData qbtypes.CachedData
if err := bc.cache.Get(ctx, orgID, cacheKey, &existingData); err != nil {
existingData = cachedData{}
existingData = qbtypes.CachedData{}
}
// Trim the result to exclude data within flux interval
@@ -203,7 +180,7 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que
uniqueWarnings := bc.deduplicateWarnings(allWarnings)
// Create updated cached data
updatedData := cachedData{
updatedData := qbtypes.CachedData{
Buckets: mergedBuckets,
Warnings: uniqueWarnings,
}
@@ -222,7 +199,7 @@ func (bc *bucketCache) generateCacheKey(q qbtypes.Query) string {
}
// findMissingRangesWithStep identifies time ranges not covered by cached buckets with step alignment
func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startMs, endMs uint64, stepMs uint64) []*qbtypes.TimeRange {
func (bc *bucketCache) findMissingRangesWithStep(buckets []*qbtypes.CachedBucket, startMs, endMs uint64, stepMs uint64) []*qbtypes.TimeRange {
// When step is 0 or window is too small to be cached, use simple algorithm
if stepMs == 0 || (startMs+stepMs) > endMs {
return bc.findMissingRangesBasic(buckets, startMs, endMs)
@@ -265,7 +242,7 @@ func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startM
}
if needsSort {
slices.SortFunc(buckets, func(a, b *cachedBucket) int {
slices.SortFunc(buckets, func(a, b *qbtypes.CachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -339,7 +316,7 @@ func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startM
}
// findMissingRangesBasic is the simple algorithm without step alignment
func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs, endMs uint64) []*qbtypes.TimeRange {
func (bc *bucketCache) findMissingRangesBasic(buckets []*qbtypes.CachedBucket, startMs, endMs uint64) []*qbtypes.TimeRange {
// Check if already sorted before sorting
needsSort := false
for i := 1; i < len(buckets); i++ {
@@ -350,7 +327,7 @@ func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs,
}
if needsSort {
slices.SortFunc(buckets, func(a, b *cachedBucket) int {
slices.SortFunc(buckets, func(a, b *qbtypes.CachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -421,9 +398,9 @@ func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs,
}
// filterRelevantBuckets returns buckets that overlap with the requested time range
func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, endMs uint64) []*cachedBucket {
func (bc *bucketCache) filterRelevantBuckets(buckets []*qbtypes.CachedBucket, startMs, endMs uint64) []*qbtypes.CachedBucket {
// Pre-allocate with estimated capacity
relevant := make([]*cachedBucket, 0, len(buckets))
relevant := make([]*qbtypes.CachedBucket, 0, len(buckets))
for _, bucket := range buckets {
// Check if bucket overlaps with requested range
@@ -433,7 +410,7 @@ func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, e
}
// Sort by start time
slices.SortFunc(relevant, func(a, b *cachedBucket) int {
slices.SortFunc(relevant, func(a, b *qbtypes.CachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -447,7 +424,7 @@ func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, e
}
// mergeBuckets combines multiple cached buckets into a single result
func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*cachedBucket, warnings []string) *qbtypes.Result {
func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*qbtypes.CachedBucket, warnings []string) *qbtypes.Result {
if len(buckets) == 0 {
return &qbtypes.Result{}
}
@@ -480,7 +457,7 @@ func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*cachedBucket
}
// mergeTimeSeriesValues merges time series data from multiple buckets
func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cachedBucket) *qbtypes.TimeSeriesData {
func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*qbtypes.CachedBucket) *qbtypes.TimeSeriesData {
// Estimate capacity based on bucket count
estimatedSeries := len(buckets) * 10
@@ -631,7 +608,7 @@ func (bc *bucketCache) isEmptyResult(result *qbtypes.Result) (isEmpty bool, isFi
}
// resultToBuckets converts a query result into time-based buckets
func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Result, startMs, endMs uint64) []*cachedBucket {
func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Result, startMs, endMs uint64) []*qbtypes.CachedBucket {
// Check if result is empty
isEmpty, isFiltered := bc.isEmptyResult(result)
@@ -652,7 +629,7 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu
// Always create a bucket, even for empty filtered results
// This ensures we don't re-query for data that doesn't exist
return []*cachedBucket{
return []*qbtypes.CachedBucket{
{
StartMs: startMs,
EndMs: endMs,
@@ -664,9 +641,9 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu
}
// mergeAndDeduplicateBuckets combines and deduplicates bucket lists
func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*cachedBucket) []*cachedBucket {
func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*qbtypes.CachedBucket) []*qbtypes.CachedBucket {
// Create a map to deduplicate by time range
bucketMap := make(map[string]*cachedBucket)
bucketMap := make(map[string]*qbtypes.CachedBucket)
// Add existing buckets
for _, bucket := range existing {
@@ -681,13 +658,13 @@ func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*cachedBucke
}
// Convert back to slice with pre-allocated capacity
result := make([]*cachedBucket, 0, len(bucketMap))
result := make([]*qbtypes.CachedBucket, 0, len(bucketMap))
for _, bucket := range bucketMap {
result = append(result, bucket)
}
// Sort by start time
slices.SortFunc(result, func(a, b *cachedBucket) int {
slices.SortFunc(result, func(a, b *qbtypes.CachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}

View File

@@ -147,14 +147,14 @@ func BenchmarkBucketCache_MergeTimeSeriesValues(b *testing.B) {
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
// Create test buckets
buckets := make([]*cachedBucket, tc.numBuckets)
buckets := make([]*qbtypes.CachedBucket, tc.numBuckets)
for i := 0; i < tc.numBuckets; i++ {
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
result := createBenchmarkResultWithSeries(startMs, endMs, 1000, tc.numSeries, tc.numValues)
valueBytes, _ := json.Marshal(result.Value)
buckets[i] = &cachedBucket{
buckets[i] = &qbtypes.CachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: qbtypes.RequestTypeTimeSeries,
@@ -417,8 +417,8 @@ func createBenchmarkResultWithSeries(startMs, endMs uint64, _ uint64, numSeries,
}
// Helper function to create buckets with specific gap patterns
func createBucketsWithPattern(numBuckets int, pattern string) []*cachedBucket {
buckets := make([]*cachedBucket, 0, numBuckets)
func createBucketsWithPattern(numBuckets int, pattern string) []*qbtypes.CachedBucket {
buckets := make([]*qbtypes.CachedBucket, 0, numBuckets)
for i := 0; i < numBuckets; i++ {
// Skip some buckets based on pattern
@@ -432,7 +432,7 @@ func createBucketsWithPattern(numBuckets int, pattern string) []*cachedBucket {
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
buckets = append(buckets, &cachedBucket{
buckets = append(buckets, &qbtypes.CachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: qbtypes.RequestTypeTimeSeries,

View File

@@ -521,7 +521,7 @@ func TestBucketCache_FindMissingRanges_EdgeCases(t *testing.T) {
bc := NewBucketCache(instrumentationtest.New().ToProviderSettings(), memCache, cacheTTL, defaultFluxInterval).(*bucketCache)
// Test with buckets that have gaps and overlaps
buckets := []*cachedBucket{
buckets := []*qbtypes.CachedBucket{
{StartMs: 1000, EndMs: 2000},
{StartMs: 2500, EndMs: 3500},
{StartMs: 3000, EndMs: 4000}, // Overlaps with previous
@@ -1097,7 +1097,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
tests := []struct {
name string
buckets []*cachedBucket
buckets []*qbtypes.CachedBucket
startMs uint64
endMs uint64
stepMs uint64
@@ -1106,7 +1106,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
}{
{
name: "start_not_aligned_to_step",
buckets: []*cachedBucket{},
buckets: []*qbtypes.CachedBucket{},
startMs: 1500, // Not aligned to 1000ms step
endMs: 5000,
stepMs: 1000,
@@ -1118,7 +1118,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "end_not_aligned_to_step",
buckets: []*cachedBucket{},
buckets: []*qbtypes.CachedBucket{},
startMs: 1000,
endMs: 4500, // Not aligned to 1000ms step
stepMs: 1000,
@@ -1129,7 +1129,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "bucket_boundaries_not_aligned",
buckets: []*cachedBucket{
buckets: []*qbtypes.CachedBucket{
{StartMs: 1500, EndMs: 2500}, // Not aligned
},
startMs: 1000,
@@ -1143,7 +1143,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "small_window_less_than_step",
buckets: []*cachedBucket{},
buckets: []*qbtypes.CachedBucket{},
startMs: 1000,
endMs: 1500, // Less than one step
stepMs: 1000,
@@ -1154,7 +1154,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "zero_step_uses_basic_algorithm",
buckets: []*cachedBucket{},
buckets: []*qbtypes.CachedBucket{},
startMs: 1000,
endMs: 5000,
stepMs: 0,

View File

@@ -279,7 +279,7 @@ func (c *conditionBuilder) buildSpanScopeCondition(key *telemetrytypes.Telemetry
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "span scope field %s only supports '=' operator", key.Name)
}
isTrue := false
var isTrue bool
switch v := value.(type) {
case bool:
isTrue = v

View File

@@ -0,0 +1,61 @@
package querybuildertypesv5
import (
"bytes"
"encoding/json"
"maps"
"github.com/SigNoz/signoz/pkg/types/cachetypes"
)
var _ cachetypes.Cacheable = (*CachedData)(nil)
type CachedBucket struct {
StartMs uint64 `json:"startMs"`
EndMs uint64 `json:"endMs"`
Type RequestType `json:"type"`
Value json.RawMessage `json:"value"`
Stats ExecStats `json:"stats"`
}
func (c *CachedBucket) Clone() *CachedBucket {
return &CachedBucket{
StartMs: c.StartMs,
EndMs: c.EndMs,
Type: c.Type,
Value: bytes.Clone(c.Value),
Stats: ExecStats{
RowsScanned: c.Stats.RowsScanned,
BytesScanned: c.Stats.BytesScanned,
DurationMS: c.Stats.DurationMS,
StepIntervals: maps.Clone(c.Stats.StepIntervals),
},
}
}
// CachedData represents the full cached data for a query
type CachedData struct {
Buckets []*CachedBucket `json:"buckets"`
Warnings []string `json:"warnings"`
}
func (c *CachedData) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
func (c *CachedData) MarshalBinary() ([]byte, error) {
return json.Marshal(c)
}
func (c *CachedData) Clone() cachetypes.Cacheable {
clonedCachedData := new(CachedData)
clonedCachedData.Buckets = make([]*CachedBucket, len(c.Buckets))
for i := range c.Buckets {
clonedCachedData.Buckets[i] = c.Buckets[i].Clone()
}
clonedCachedData.Warnings = make([]string, len(c.Warnings))
copy(clonedCachedData.Warnings, c.Warnings)
return clonedCachedData
}

View File

@@ -0,0 +1,87 @@
package querybuildertypesv5
import (
"encoding/json"
"testing"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func createBuckets_TimeSeries(numBuckets int) []*CachedBucket {
buckets := make([]*CachedBucket, numBuckets)
for i := 0; i < numBuckets; i++ {
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
timeSeriesData := &TimeSeriesData{
QueryName: "A",
Aggregations: []*AggregationBucket{
{
Index: 0,
Series: []*TimeSeries{
{
Labels: []*Label{
{Key: telemetrytypes.TelemetryFieldKey{Name: "service"}, Value: "test"},
},
Values: []*TimeSeriesValue{
{Timestamp: 1672563720000, Value: 1, Partial: true}, // 12:02
{Timestamp: 1672563900000, Value: 2}, // 12:05
{Timestamp: 1672564200000, Value: 2.5}, // 12:10
{Timestamp: 1672564500000, Value: 2.6}, // 12:15
{Timestamp: 1672566600000, Value: 2.9}, // 12:50
{Timestamp: 1672566900000, Value: 3}, // 12:55
{Timestamp: 1672567080000, Value: 4, Partial: true}, // 12:58
},
},
},
},
},
}
value, err := json.Marshal(timeSeriesData)
if err != nil {
panic(err)
}
buckets[i] = &CachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: RequestTypeTimeSeries,
Value: json.RawMessage(value),
Stats: ExecStats{
RowsScanned: uint64(i * 500),
BytesScanned: uint64(i * 10000),
DurationMS: uint64(i * 1000),
},
}
}
return buckets
}
func BenchmarkCachedData_JSONMarshal_10kbuckets(b *testing.B) {
buckets := createBuckets_TimeSeries(10000)
data := &CachedData{Buckets: buckets}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := json.Marshal(data)
assert.NoError(b, err)
}
}
func BenchmarkCachedData_Clone_10kbuckets(b *testing.B) {
buckets := createBuckets_TimeSeries(10000)
data := &CachedData{Buckets: buckets}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = data.Clone()
}
}