Skip to main content

Debugging

Pipeline failures can be both frustrating and lead to wasted resources as the team seeks to understand what went wrong and why. Dagger provides various tools and features that can help greatly when trying to debug a pipeline failure.

Rerun commands with --interactive

Run dagger call with the --interactive (-i for short) flag to open a terminal in the context of a pipeline failure. No changes are required to your Dagger Function code.

tip

Interactive mode defaults executing /bin/sh when opening a terminal. Change the command to execute with the --interactive-command flag.

Use the Terminal() method to inspect the pipeline live

As an alternative to the --interactive flag, use the Terminal() method to set one or more explicit breakpoints in your Dagger pipeline. Dagger then starts an interactive terminal session at each breakpoint. This lets you inspect a Directory or a Container at any point in your pipeline run, with all the necessary context available to you.

Here is an example of a Dagger Function which opens an interactive terminal at two different points in the Dagger pipeline to inspect the built container:

package main

import (
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

func (m *MyModule) Container() *dagger.Container {
return dag.Container().
From("alpine:latest").
Terminal().
WithExec([]string{"sh", "-c", "echo hello world > /foo && cat /foo"}).
Terminal()
}

The Terminal() method can be chained. It returns a Container, so it can be injected at any point in a pipeline (in this example, between From() and WithExec() methods).

tip

Multiple terminals are supported in the same Dagger Function; they will open in sequence.

It's also possible to inspect a directory using the Terminal() method. Here is an example of a Dagger Function which opens an interactive terminal on a directory:

package main

import (
"context"
)

type MyModule struct{}

func (m *MyModule) SimpleDirectory(ctx context.Context) (string, error) {
return dag.
Git("https://github.com/dagger/dagger.git").
Head().
Tree().
Terminal().
File("README.md").
Contents(ctx)
}

Under the hood, this creates a new container (defaults to alpine) and starts a shell, mounting the directory inside. This container can be customized using additional options. Here is a more complex example, which produces the same result as the previous one but this time using an ubuntu container image and bash shell instead of the default alpine container image and sh shell:

package main

import (
"context"
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

func (m *MyModule) AdvancedDirectory(ctx context.Context) (string, error) {
return dag.
Git("https://github.com/dagger/dagger.git").
Head().
Tree().
Terminal(dagger.DirectoryTerminalOpts{
Container: dag.Container().From("ubuntu"),
Cmd: []string{"/bin/bash"},
}).
File("README.md").
Contents(ctx)
}

Rerun commands with --debug

The Dagger CLI tries to keep its output concise by default. If you're running into issues and want to debug with more detailed output, you can run any dagger subcommand with the --debug flag to have it reveal all available information.

Access the Dagger Engine logs

The Dagger Engine runs in a dedicated Docker container and emits log messages as it works. Here's how to access these logs:

DAGGER_ENGINE_DOCKER_CONTAINER="$(docker container list --all --filter 'name=^dagger-engine-*' --format '{{.Names}}')"
docker logs $DAGGER_ENGINE_DOCKER_CONTAINER

Enable SDK debug logs

important

The information in this section is only applicable to the Python SDK. Debug logs are not currently available for the Go and TypeScript SDKs.

The Python SDK prints debugging information at various steps of the execution flow of a module, which can be very useful in understanding what's being received from and sent to the API.

This is done using standard Python logging, so it's highly configurable (for example, saving to a file or sending to an external system like Sentry). But for a simple lowering of the default logging level to logging.DEBUG, there's a convenience function for that:

import logging

from dagger import function, object_type
from dagger.log import configure_logging

configure_logging(logging.DEBUG)


@object_type
class MyModule:
@function
def echo(self, msg: str) -> str:
return msg
note

With the TUI, you need to use a progress output that doesn't collapse on success like --progress=plain or --debug, otherwise it won't show in the terminal.

Using the command dagger call --debug echo --msg="hello", you should see a large number of debug messages, eventually ending with output similar to the below:

  ✔ connect 0.1s
✔ Debug.echo(msg: "hello"): String! 0.9s
┃ [DEBUG] dagger.mod._resolver: func => <Signature (msg: str) -> str>
┃ [DEBUG] dagger.mod._resolver: input args => {'msg': 'hello'}
┃ [DEBUG] dagger.mod._resolver: structured args => {'msg': 'hello'}
┃ [DEBUG] dagger.mod._module: result => 'hello'
┃ [DEBUG] dagger.mod._module: output => '"hello"'
┃ [DEBUG] dagger.client._session: Closing client session to GraphQL server

hello

The above gives a lot of useful information:

  • The function and parent object that the API wants to execute
  • The parent object's state
  • The function's signature
  • The user inputs before and after deserialization
  • The user inputs after being converted to more complex types (structuring)
  • The function's result before and after serialization