> ## 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.

# Logging

> Emit structured task logs and tune how workflow clients export log records.

Tilebox collects workflow logs automatically. When a task runner is created from a `Client`, logs emitted through the task execution context are exported to Tilebox and correlated with the active job, task, and trace.

<Frame>
  <img src="https://mintcdn.com/tilebox/TYquvc9froFIydg1/assets/console/job-logs-light.png?fit=max&auto=format&n=TYquvc9froFIydg1&q=85&s=7a86605d83daa3101e46d88fdb4cac6a" alt="Job Logs View" className="dark:hidden" width="1536" height="970" data-path="assets/console/job-logs-light.png" />

  <img src="https://mintcdn.com/tilebox/TYquvc9froFIydg1/assets/console/job-logs-dark.png?fit=max&auto=format&n=TYquvc9froFIydg1&q=85&s=04f1281ed80c6e79d993b5de0f1b6563" alt="Job Logs View" className="hidden dark:block" width="1536" height="970" data-path="assets/console/job-logs-dark.png" />
</Frame>

## Write task logs

Use `context.logger` inside `Task.execute`. It supports standard log levels and structured attributes.

<CodeGroup>
  ```python Python theme={"system"}
  from tilebox.workflows import ExecutionContext, Task

  class ProcessScene(Task):
      scene_id: str

      def execute(self, context: ExecutionContext) -> None:
          context.logger.info("Started scene processing", scene_id=self.scene_id)

          log = context.logger.bind(component="atmospheric-correction")
          log.debug("Fetching auxiliary data")

          try:
              # process the scene
              log.info("Scene processed", output_format="cog")
          except Exception:
              context.logger.exception("Scene processing failed", scene_id=self.scene_id)
              raise
  ```

  ```go Go theme={"system"}
  package tasks

  import (
  	"context"
  	"log/slog"
  )

  type ProcessScene struct{}

  func (t *ProcessScene) Execute(ctx context.Context) error {
  	slog.InfoContext(ctx, "started scene processing", slog.String("scene_id", "S2A_001"))
  	slog.DebugContext(ctx, "fetching auxiliary data", slog.String("component", "atmospheric-correction"))
  	return nil
  }
  ```
</CodeGroup>

Structured attributes become searchable log attributes. Bind shared attributes to a logger when records need the same context.

## What Tilebox adds automatically

Logs emitted from a task include workflow metadata without extra code. Tilebox attaches job ID, job name, task ID, task display name, parent task data, task identifier name and version, runner service data, SDK version, host, OS, and process ID. When a log is emitted inside an active span, Tilebox also attaches `trace_id` and `span_id`.

Logs are also added as events on the active trace span, so a trace view can show important log messages inline with task execution.

## Configure local console logging

Built-in Tilebox export does not require configuration. For local development, add a console handler to print Tilebox workflow logs to standard output.

<CodeGroup>
  ```python Python theme={"system"}
  import logging

  from tilebox.workflows import Client
  from tilebox.workflows.observability.logging import configure_console_logging

  from my_workflow import ProcessScene

  configure_console_logging(level=logging.DEBUG)

  client = Client()
  runner = client.runner(tasks=[ProcessScene])
  runner.run_forever()
  ```

  ```go Go theme={"system"}
  package main

  import (
  	"context"
  	"log/slog"

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

  type ProcessScene struct{}

  func (t *ProcessScene) Execute(ctx context.Context) error {
  	slog.InfoContext(ctx, "processing scene")
  	return nil
  }

  func main() {
  	ctx := context.Background()
  	workflows.ConfigureConsoleLogging(slog.LevelDebug)

  	client := workflows.NewClient()
  	runner, err := client.NewTaskRunner(ctx)
  	if err != nil {
  		slog.ErrorContext(ctx, "failed to create task runner", slog.Any("error", err))
  		return
  	}

  	if err := runner.RegisterTasks(&ProcessScene{}); err != nil {
  		slog.ErrorContext(ctx, "failed to register tasks", slog.Any("error", err))
  		return
  	}

  	runner.Run(ctx)
  }
  ```
</CodeGroup>

`configure_console_logging()` and `workflows.ConfigureConsoleLogging()` are process-wide for Tilebox workflow loggers. Use them for local runs and debugging distributed runners.

## Configure the client log level

Use `Client.configure_logging()` to choose which task and runner logs a client exports to Tilebox.

<CodeGroup>
  ```python Python theme={"system"}
  import logging

  from tilebox.workflows import Client

  client = Client(name="sentinel-2-runner")

  # Export task logs at DEBUG and internal runner logs at INFO.
  client.configure_logging(level=logging.DEBUG, runner_level=logging.INFO)
  ```

  ```go Go theme={"system"}
  package main

  import (
  	"log/slog"

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

  func main() {
  	workflows.ConfigureConsoleLogging(slog.LevelDebug)
  	client := workflows.NewClient()
  	_ = client
  }
  ```
</CodeGroup>

The Python `level` argument applies to logs emitted with `context.logger`. The optional `runner_level` argument applies to internal task runner logs. If `runner_level` is omitted, it uses the same value as `level`. In Go, `workflows.ConfigureConsoleLogging()` sets the local console log level, and `workflows.NewClient()` configures Tilebox workflow log export.

## Query logs

You can retrieve logs for a job through the jobs client. Python results can also be converted to a pandas DataFrame.

<CodeGroup>
  ```python Python theme={"system"}
  from tilebox.workflows import Client

  client = Client()
  job = client.jobs().submit("process-scene", ProcessScene(scene_id="S2A_001"))

  logs = client.jobs().query_logs(job)
  for record in logs:
      print(record.time, record.severity_text, record.body, record.attributes)

  df = logs.to_pandas()
  ```

  ```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")

  	logs, err := workflows.Collect(
  		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
  	}

  	for _, record := range logs {
  		fmt.Printf("%s %-5s %s\n",
  			record.Time.Format(time.RFC3339Nano),
  			record.Level,
  			record.Body,
  		)
  	}
  }
  ```
</CodeGroup>

See [Query telemetry](/workflows/observability/query) for the log and span query APIs.

## Export to another backend

Tilebox stores logs by default. To export logs to your own observability backend as well, configure an [OpenTelemetry](/workflows/observability/integrations/open-telemetry) or [Axiom](/workflows/observability/integrations/axiom) integration when the runner process starts.
