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.
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:
- Go
- Python
- TypeScript
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()
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def container(self) -> dagger.Container:
return (
dag.container()
.from_("alpine:latest")
.terminal()
.with_exec(["sh", "-c", "echo hello world > /foo && cat /foo"])
.terminal()
)
import { dag, Container, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
container(): Container {
return dag
.container()
.from("alpine:latest")
.terminal()
.withExec(["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).
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:
- Go
- Python
- TypeScript
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)
}
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def simple_directory(self) -> str:
return await (
dag.git("https://github.com/dagger/dagger.git")
.head()
.tree()
.terminal()
.file("README.md")
.contents()
)
import { dag, Container, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async simpleDirectory(): Promise<string> {
return await dag
.git("https://github.com/dagger/dagger.git")
.head()
.tree()
.terminal()
.file("README.md")
.contents()
}
}
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:
- Go
- Python
- TypeScript
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)
}
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def advanced_directory(self) -> str:
return await (
dag.git("https://github.com/dagger/dagger.git")
.head()
.tree()
.terminal(
container=dag.container().from_("ubuntu"),
cmd=["/bin/bash"],
)
.file("README.md")
.contents()
)
import { dag, Container, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async advancedDirectory(): Promise<string> {
return await dag
.git("https://github.com/dagger/dagger.git")
.head()
.tree()
.terminal({
container: dag.container().from("ubuntu"),
cmd: ["/bin/bash"],
})
.file("README.md")
.contents()
}
}
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
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
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