> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tilebox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Query telemetry

> Query workflow logs and spans for a job from Python or Go.

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.

<CodeGroup>
  ```python Python theme={"system"}
  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 theme={"system"}
  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)
  	}
  }
  ```
</CodeGroup>

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 Python theme={"system"}
logs_df = client.jobs().query_logs(job).to_pandas()
logs_df[["time", "severity_text", "body"]]
```

<Frame>
  <img src="https://mintcdn.com/tilebox/f-tLpk_IhM06GmYl/assets/workflows/observability/job-logs-pandas-light.png?fit=max&auto=format&n=f-tLpk_IhM06GmYl&q=85&s=ff5fdea94d3c291cacf4d58f00de5de6" alt="Job Logs as Pandas DataFrame" className="dark:hidden" width="580" height="400" data-path="assets/workflows/observability/job-logs-pandas-light.png" />

  <img src="https://mintcdn.com/tilebox/f-tLpk_IhM06GmYl/assets/workflows/observability/job-logs-pandas-dark.png?fit=max&auto=format&n=f-tLpk_IhM06GmYl&q=85&s=779ce2f4304b74b0a38303bfe9fc32af" alt="Job Logs as Pandas DataFrame" className="hidden dark:block" width="519" height="393" data-path="assets/workflows/observability/job-logs-pandas-dark.png" />
</Frame>

## Query job spans

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

<CodeGroup>
  ```python Python theme={"system"}
  spans = client.jobs().query_spans(job.id)

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

  ```go Go theme={"system"}
  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)
  	}
  }
  ```
</CodeGroup>

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 Python theme={"system"}
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"]]
```

<Frame>
  <img src="https://mintcdn.com/tilebox/f-tLpk_IhM06GmYl/assets/workflows/observability/job-traces-pandas-light.png?fit=max&auto=format&n=f-tLpk_IhM06GmYl&q=85&s=901ed72bce41abe53b5af9f3c302ec9a" alt="Job Traces as Pandas DataFrame" className="dark:hidden" width="587" height="325" data-path="assets/workflows/observability/job-traces-pandas-light.png" />

  <img src="https://mintcdn.com/tilebox/f-tLpk_IhM06GmYl/assets/workflows/observability/job-traces-pandas-dark.png?fit=max&auto=format&n=f-tLpk_IhM06GmYl&q=85&s=e949963196156df328f7a389a4511323" alt="Job Traces as Pandas DataFrame" className="hidden dark:block" width="579" height="321" data-path="assets/workflows/observability/job-traces-pandas-dark.png" />
</Frame>

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