# Query telemetry

Tilebox stores logs and spans for each workflow job. Use the jobs client to query that telemetry from notebooks, scripts, or automated diagnostics.

## Query job logs

`query_logs()` returns a `LogRecords` list. Pagination is handled automatically.

**Python**

```python title="Python"
from tilebox.workflows import Client

client = Client()
job = client.jobs().find("019e07b1-916b-0630-f3ba-f1c33235d174")

logs = client.jobs().query_logs(job)

for record in logs:
    print(record.time, record.severity_text, record.body)
    print(record.attributes)
```

**Go**

```go title="Go"
package main

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/google/uuid"
	"github.com/tilebox/tilebox-go/workflows/v1"
)

func main() {
	ctx := context.Background()
	client := workflows.NewClient()
	jobID := uuid.MustParse("019e07b1-916b-0630-f3ba-f1c33235d174")

	for record, err := range client.Jobs.QueryLogs(
		ctx,
		jobID,
		workflows.WithSortDirection(workflows.Ascending),
	) {
		if err != nil {
			slog.ErrorContext(ctx, "failed to query job logs", slog.Any("error", err))
			return
		}

		fmt.Println(record.Time.Format(time.RFC3339Nano), record.Level, record.Body)
		fmt.Println(record.Attributes)
	}
}
```

Each log record includes:

* `time`
* `severity_number` and `severity_text`
* `body`
* `trace_id` and `span_id`
* `attributes`
* `runner_attributes`

### As pandas DataFrame

Use `to_pandas()` to convert log records to a pandas DataFrame.

```python title="Python"
logs_df = client.jobs().query_logs(job).to_pandas()
logs_df[["time", "severity_text", "body"]]
```

![Job Logs as Pandas DataFrame](/docs/assets/workflows/observability/job-logs-pandas-light.png)

![Job Logs as Pandas DataFrame](/docs/assets/workflows/observability/job-logs-pandas-dark.png)

## Query job spans

`query_spans()` returns a `Spans` list. Pagination is handled automatically.

**Python**

```python title="Python"
spans = client.jobs().query_spans(job.id)

for span in spans:
    print(span.name, span.status_code, span.duration)
    print(span.attributes)
```

**Go**

```go title="Go"
package main

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/google/uuid"
	"github.com/tilebox/tilebox-go/workflows/v1"
)

func main() {
	ctx := context.Background()
	client := workflows.NewClient()
	jobID := uuid.MustParse("019e07b1-916b-0630-f3ba-f1c33235d174")

	for span, err := range client.Jobs.QuerySpans(
		ctx,
		jobID,
		workflows.WithSortDirection(workflows.Ascending),
	) {
		if err != nil {
			slog.ErrorContext(ctx, "failed to query job spans", slog.Any("error", err))
			return
		}

		fmt.Println(span.Name, span.StatusCode, span.Duration())
		fmt.Println(span.Attributes)
	}
}
```

Each span includes:

* `start_time` and `end_time`
* `duration`
* `trace_id`, `span_id`, and `parent_span_id`
* `name`
* `status_code` and `status_message`
* `attributes`
* `runner_attributes`
* `events`

### As pandas DataFrame

Use `to_pandas()` to convert spans to a pandas DataFrame.

```python title="Python"
spans_df = client.jobs().query_spans(job).to_pandas()

slow_spans = spans_df.sort_values("duration", ascending=False).head(10)
slow_spans[["name", "duration", "start_time"]]
```

![Job Traces as Pandas DataFrame](/docs/assets/workflows/observability/job-traces-pandas-light.png)

![Job Traces as Pandas DataFrame](/docs/assets/workflows/observability/job-traces-pandas-dark.png)

Nested `attributes`, `runner_attributes`, and `events` stay as Python objects in DataFrame columns. Span DataFrames include a computed `duration` column.
