Files
openobserve/tests/api-testing
ac60e181dd feat: dashboard join support for auto sql (#9343)
# Nested Functions and Joins - User Guide

## Overview

This guide covers two powerful features that enable sophisticated data
analysis in OpenObserve:

- **Nested Functions**: Combine multiple functions together to create
complex calculations
- **Joins**: Merge data from multiple streams (data sources) based on
common fields

These features work in the **Auto SQL** mode of dashboard panels,
allowing you to write advanced queries without deep SQL knowledge.

---

## What's New

### Why These Features?

**Before:**
- Limited function composition - you could only use basic, single-level
functions
- No way to combine data from multiple streams in one query
- Complex analytics required exporting data and manual processing

**Now:**
- Compose unlimited nested functions with full DataFusion support
- Join multiple streams directly in the dashboard panel editor
- Create sophisticated analytics queries with a visual interface

### Key Benefits

✓ Calculate complex metrics (error rates, percentiles, conditional
aggregations)
✓ Build advanced dashboards without custom code
✓ Support for all DataFusion aggregate and scalar functions

---

## Nested Functions

### What Are Nested Functions?

Nested functions let you combine multiple functions together - using the
output of one function as input to another. Think of it like a cooking
recipe where each step builds on the previous one.

**Simple Example:**
- Basic: `AVG(response_time)` - calculates average response time
- Nested: `ROUND(AVG(response_time), 2)` - calculates average AND rounds
it to 2 decimal places

### Supported Functions

OpenObserve supports **all DataFusion functions**, giving you powerful
analytics capabilities:

**Aggregate Functions** (work across multiple rows):
- `count`, `sum`, `avg`, `min`, `max` - basic aggregations
- `median`, `stddev`, `variance` - statistical functions
- `percentile_cont` - calculate percentiles (e.g., p95, p99)
- `string_agg` - combine text values

**Mathematical Functions**:
- `round`, `ceil`, `floor` - rounding operations
- `abs`, `power`, `sqrt` - mathematical operations
- `log`, `log10`, `log2` - logarithmic functions
- `sin`, `cos`, `tan` - trigonometric functions

**String Functions**:
- `concat`, `upper`, `lower`, `trim`
- `substring`, `length`, `replace`

**Conditional Functions**:
- `CASE WHEN ... THEN ... ELSE ... END` - conditional logic
- `COALESCE` - return first non-null value

📖 **Full Reference**: [DataFusion Aggregate
Functions](https://datafusion.apache.org/user-guide/sql/aggregate_functions.html)
| [DataFusion Scalar
Functions](https://datafusion.apache.org/user-guide/sql/scalar_functions.html)

---

### Using Nested Functions (Step-by-Step)

#### Step 1: Create or Edit a Dashboard Panel

Open your dashboard and create a new panel or edit an existing one. Make
sure you're in **Auto SQL** mode (not Custom SQL).

#### Step 2: Select a Function

In the Y-Axis or column configuration, click **Select Function** to
choose your base function.

<img width="795" alt="Nested Function - Adding Layers"
src="https://github.com/user-attachments/assets/0b6b134e-59b8-48b1-a4ae-52fb4800d772"
/>

In this example, we're using the `Concat` function to combine multiple
fields (namespace and pod name).

#### Step 3: Add Nested Layers

Click the function icon to add another function layer. You can nest
functions multiple times.

<img width="793" alt="Nested Function - Select Base Function"
src="https://github.com/user-attachments/assets/4bd9e03e-1104-4e82-b2ec-a34e6bb64870"
/>


Here we're building a nested function:
- Inner layer: `Avg(severity)` - calculates average severity
- Outer layer: `Ceil()` - rounds up the average to the nearest integer

#### Step 4: Build Complex Expressions

For advanced analytics, you can combine nested functions with
conditional logic using `CASE WHEN` statements.

<img width="1340" alt="Complex Nested Function with CASE WHEN"
src="https://github.com/user-attachments/assets/4c3e3623-bfad-44c4-a760-e10cc94d4a95"
/>

This example shows a sophisticated calculation:
```sql
ROUND(100.0 * SUM(CASE WHEN COALESCE(severity, 0) >= 4 THEN 1 ELSE 0 END) / COUNT(_timestamp), 2)
```

**What this does:**
1. `COALESCE(severity, 0)` - replaces null severity with 0
2. `CASE WHEN severity >= 4 THEN 1 ELSE 0` - returns 1 for high
severity, 0 otherwise
3. `SUM(...)` - counts high severity events
4. Divide by `COUNT(_timestamp)` - calculates percentage
5. Multiply by `100.0` - converts to percentage
6. `ROUND(..., 2)` - rounds to 2 decimal places

**Result**: Percentage of high-severity events, rounded to 2 decimals

#### Step 5: View the Generated SQL

Switch to the **Raw** tab to see the complete SQL query that was
generated from your nested function configuration.

<img width="1388" alt="View Generated SQL"
src="https://github.com/user-attachments/assets/d7f6111a-26c6-4dd0-9e9f-e26092fe6368"
/>

You can review the auto-generated SQL in the Query section at the bottom
of the panel editor. The chart visualization updates in real-time as you
build your nested functions.

---

## Joins

### What Are Joins?

Joins allow you to combine data from two or more different streams (data
sources) based on common fields between them.

**Example:**

**employee_data_join** stream:
```json
[
    {"employee_id": 1, "name": "Alice", "department_id": 1},
    {"employee_id": 2, "name": "Bob", "department_id": 2},
    {"employee_id": 3, "name": "Charlie", "department_id": null},
    {"employee_id": 4, "name": "David", "department_id": 1},
    {"employee_id": 5, "name": "Eve", "department_id": 3}
]
```

**dept_data_join** stream:
```json
[
    {"department_id": 1, "department_name": "HR"},
    {"department_id": 2, "department_name": "Engineering"},
    {"department_id": 3, "department_name": "Sales"},
    {"department_id": 4, "department_name": "Marketing"}
]
```

A join lets you combine these streams to see the department name
alongside each employee, matched by `department_id`.

### Types of Joins

OpenObserve supports three types of joins:

| Join Type | Description | When to Use |
|-----------|-------------|-------------|
| **INNER JOIN** | Returns only records with matches in both streams |
When you need data that exists in both sources |
| **LEFT JOIN** | Returns all records from left stream + matching
records from right | When you want all data from your primary source,
with supplemental info where available |
| **RIGHT JOIN** | Returns all records from right stream + matching
records from left | When you want all data from your secondary source,
with supplemental info where available |

> **Note:** UNION and CROSS joins are not currently supported.

---

### Using Joins (Step-by-Step)

#### Step 1: Access the Joins Section

In your dashboard panel editor, locate the **Joins** section. Click the
**+** button to add a join.

<img width="1725" alt="Access Joins Interface"
src="https://github.com/user-attachments/assets/35db4f28-c526-43a2-bdd1-470e188f53d7"
/>

In this example:
- **Primary stream**: `employee_data_join` (contains employee names)
- We'll join it with `dept_data_join` (contains department information)
- The interface shows available fields from both streams in the left
panel

#### Step 2: Configure the Join

Click on the **Joins** dropdown and select the stream you want to join.
Then configure:

1. **Joining Stream**: Select the stream to join (e.g.,
`employee_data_join`)
2. **Join Type**: Choose from LEFT, INNER, or RIGHT using the visual
toggle
3. **On Stream**: Select the target stream (e.g., `dept_data_join`)

<img width="1725" alt="Configure Join Type"
src="https://github.com/user-attachments/assets/35db4f28-c526-43a2-bdd1-470e188f53d7"
/>

The interface displays: **"Performing a Left Join between
employee_data_join and dept_data_join"**

Notice the visual toggle showing three join types:
- **Left** (selected in this example)
- **Inner**
- **Right**

#### Step 3: Define the Joining Clause

Set up the join condition by specifying which fields to match between
the two streams:

**Clause 1:**
- **Select Field** (Left): Choose a field from the first stream (e.g.,
`department_id`)
- **Select Operator**: Choose comparison operator (usually `=`)
- **Select Field** (Right): Choose the matching field from the second
stream (e.g., `department_id`)

You can add multiple clauses using the **+** button for complex join
conditions (e.g., joining on multiple fields or adding additional
filters).

#### Step 4: View the Results

After configuring the join, the data table will show combined results
from both streams.

<img width="1724" alt="Join Results"
src="https://github.com/user-attachments/assets/94ca9f33-6cfa-4ad1-9b84-bee9db34f912"
/>

In this example:
- **Name** column from `employee_data_join` (Alice, Bob, Charlie, David,
Eve)
- **Department Name** column from `dept_data_join` (HR, Engineering, HR,
Sales)
- Data successfully joined on `department_id`

#### Step 5: Review the Generated SQL

Check the **Query** section at the bottom to see the generated SQL:

```sql
SELECT employee_data_join.name as "x_axis_1",
       stream_0.department_name as "x_axis_2"
FROM "employee_data_join"
LEFT JOIN "dept_data_join" AS stream_0
ON employee_data_join.department_id = stream_0.department_id
```

The SQL query shows exactly how OpenObserve is combining your data
streams.

---

---

## Examples

### Example 1: String Concatenation with Nested Functions

**Use Case**: Combine multiple fields into a single display value

```sql
Concat(k8s_namespace_name, '-', k8s_pod_name)
```

This joins namespace and pod names with a hyphen separator.

### Example 2: Statistical Analysis with Nested Math Functions

**Use Case**: Calculate rounded average of a metric

```sql
Ceil(Avg(severity))
```

This calculates the average severity level and rounds up to the nearest
whole number.

### Example 3: Conditional Percentage Calculation

**Use Case**: Calculate percentage of high-severity events

```sql
ROUND(100.0 * SUM(CASE WHEN COALESCE(severity, 0) >= 4 THEN 1 ELSE 0 END) / COUNT(_timestamp), 2)
```

**Breakdown:**
- Handles null values with `COALESCE`
- Counts events where severity >= 4
- Calculates percentage of total events
- Rounds to 2 decimal places

### Example 4: Employee-Department Join

**Use Case**: Show employee names with their department information

```sql
SELECT employee_data_join.name as "Name",
       dept_data_join.department_name as "Department Name"
FROM employee_data_join
LEFT JOIN dept_data_join
ON employee_data_join.department_id = dept_data_join.department_id
```

This displays all employees with their department names, showing null
for employees without a department assignment.

### Example 5: Complex Analytics - Joins + Nested Functions

**Use Case**: Calculate error rate per service with deployment
information

```sql
SELECT
    logs.service_name,
    deployments.version,
    COUNT(*) as total_requests,
    ROUND((COUNT(CASE WHEN logs.status_code >= 500 THEN 1 END) * 100.0 / COUNT(*)), 2) as error_rate_percent
FROM service_logs as logs
LEFT JOIN deployments ON logs.service_name = deployments.service_name
GROUP BY logs.service_name, deployments.version
ORDER BY error_rate_percent DESC
```

This combines:
- Join between service logs and deployment data
- Nested functions for conditional counting and percentage calculation
- Grouping and sorting for analysis

---

## Tips and Best Practices

### Nested Functions Best Practices

✅ **Start Simple**: Build your function one layer at a time
- Begin with inner function: `AVG(response_time)`
- Add outer layers: `ROUND(AVG(response_time), 2)`

✅ **Test Each Layer**: Verify results before adding complexity
- Test the inner function first
- Add one layer at a time and check output

✅ **Handle Null Values**: Use `COALESCE` to avoid unexpected nulls
```sql
COALESCE(AVG(response_time), 0)
```

✅ **Use Clear Aliases**: Name your calculated fields descriptively
```sql
ROUND(AVG(response_time), 2) as avg_response_ms
```

✅ **Check for Division by Zero**: Protect against zero denominators
```sql
CASE
    WHEN COUNT(*) > 0 THEN (SUM(errors) * 100.0 / COUNT(*))
    ELSE 0
END as error_rate
```

### Joins Best Practices

✅ **Choose the Right Join Type**:
- **INNER JOIN**: Both streams must have matching data
- **LEFT JOIN**: Keep all records from primary stream
- **RIGHT JOIN**: Keep all records from secondary stream

✅ **Verify Field Compatibility**:
- Match data types (don't join string to number)
- Ensure field names are correct
- Check that values actually match between streams

✅ **Consider Performance**:
- Join on indexed fields when possible
- Test with limited data first


---


## FAQ

### Q: Can I nest more than 2 functions?

**A**: Yes! You can nest as many functions as needed. Example:
```sql
ROUND(CEIL(AVG(COALESCE(response_time, 0))), 2)
```

### Q: What's the difference between LEFT and RIGHT joins?

**A**:
- **LEFT JOIN**: Keeps all records from the first (left) stream
- **RIGHT JOIN**: Keeps all records from the second (right) stream
- Choose based on which dataset you want to preserve completely

### Q: Can I join more than 2 streams?

**A**: Yes! Click the **+** button in the Joins section to add
additional joins. You can chain multiple joins together.

### Q: Do nested functions impact performance?

**A**: Complex nested functions may take longer to compute, especially
on large datasets. Test with small time ranges first and optimize your
expressions.

### Q: Can I use Custom SQL instead of Auto SQL?

**A**: Yes! If you prefer writing raw SQL, switch to **Custom SQL**
mode. However, Auto SQL provides a visual interface that's easier for
building queries.


---

## Additional Resources

📚 **DataFusion Documentation**:
- [Aggregate
Functions](https://datafusion.apache.org/user-guide/sql/aggregate_functions.html)
- COUNT, SUM, AVG, etc.
- [Scalar
Functions](https://datafusion.apache.org/user-guide/sql/scalar_functions.html)
- ROUND, CONCAT, CASE, etc.

🔗 **OpenObserve Resources**:
- [GitHub Repository](https://github.com/openobserve/openobserve)
- [PR #9343 - Feature
Implementation](https://github.com/openobserve/openobserve/pull/9343)
- [OpenObserve Documentation](https://openobserve.ai/docs/)

💬 **Get Help**:
- [GitHub Issues](https://github.com/openobserve/openobserve/issues) -
Report bugs or request features
- [Community
Discussions](https://github.com/openobserve/openobserve/discussions) -
Ask questions and share ideas

---

Design at: https://github.com/openobserve/designs/tree/main/joins

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2025-12-18 14:17:23 +05:30
..
2025-07-31 14:31:33 +08:00
2025-07-31 14:31:33 +08:00

pytest-openobserve

This is a repo to test openobserve API endpoints.

Introduction:

The tests are running using pytest against alpha1 for now. Idea is to parametrize it for the other endpoints like production as well.

Getting Started

  • We use rye to manage the python version and environment - https://github.com/mitsuhiko/rye
  • Once installed, simply run the following steps:
    • rye sync
    • rye run pytest

TODOS:

  • Parametrize tests to run against different environments
  • Run the tests as cron-job every few hours or daily etc. for different environments
  • Send slack-notification ( use github-actions ) in case something fails on alpha1 or production endpoints.