Skip to content

Commit 3ac5104

Browse files
authored
debug: Adding debugger SDK (#6877)
This is an experimental feature, subject to change. Fixes: #6876 Signed-off-by: Johan Fylling <[email protected]>
1 parent b0f417f commit 3ac5104

18 files changed

+4406
-9
lines changed

debug/README.md

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# OPA Debug API
2+
3+
This directory contains the OPA Debug API.
4+
The Debug API facilitates programmatic debugging of Rego policies, on top of which 3rd parties can build tools for debugging.
5+
6+
This API takes inspiration from the [Debug Adapter Protocol (DAP)](https://microsoft.github.io/debug-adapter-protocol/),
7+
and follows the conventions established therein for managing threads, breakpoints, and variable scopes.
8+
9+
> **Note**: The Debug API is experimental and subject to change.
10+
11+
## Creating a Debug Session
12+
13+
```go
14+
debugger := debug.NewDebugger()
15+
16+
ctx := context.Background()
17+
evalProps := debug.EvalProperties{
18+
Query: "data.example.allow = x",
19+
InputPath: "/path/to/input.json",
20+
LaunchProperties: LaunchProperties{
21+
DataPaths: []string{"/path/to/data.json", "/path/to/policy.rego"},
22+
},
23+
}
24+
session, err := s.debugger.LaunchEval(ctx, evalProps)
25+
if err != nil {
26+
// handle error
27+
}
28+
29+
// The session is launched in a paused state.
30+
// Before resuming the session, here is the opportunity to set breakpoints
31+
32+
// Resume execution of all threads associated with the session
33+
err = session.ResumeAll()
34+
if err != nil {
35+
// handle error
36+
}
37+
```
38+
39+
## Managing Breakpoints
40+
41+
Breakpoints can be added, removed, and enumerated.
42+
43+
Breakpoints are added to file-and-row locations in a module, and are triggered when the policy evaluation reaches that location.
44+
Breakpoints can be added at any time during policy evaluation.
45+
46+
```go
47+
// Add a breakpoint
48+
br, err := session.AddBreakpoint(location.Location{
49+
File: "/path/to/policy.rego",
50+
Row: 10,
51+
})
52+
if err != nil {
53+
// handle error
54+
}
55+
56+
// ...
57+
58+
// Remove the breakpoint
59+
_, err = session.RemoveBreakpoint(br.ID)
60+
if err != nil {
61+
// handle error
62+
}
63+
```
64+
65+
## Stepping Through Policy Evaluation
66+
67+
When evaluation execution is paused, either immidiately after launching a session or when a breakpoint is hit, the session can be stepped through.
68+
69+
### Step Over
70+
71+
`StepOver()` executes the next expression in the current scope and then stops on the next expression in the same scope,
72+
not stopping on expressions in sub-scopes; e.g. execution of referenced rule, called function, comprehension, or every expression.
73+
74+
```go
75+
threads, err := session.Threads()
76+
if err != nil {
77+
// handle error
78+
}
79+
80+
if err := session.StepOver(threads[0].ID); err != nil {
81+
// handle error
82+
}
83+
```
84+
85+
#### Example 1
86+
87+
```
88+
allow if {
89+
x := f(input) >-+
90+
x == 1 |
91+
} |
92+
|
93+
f(x) := y if { <-+
94+
y := x + 1
95+
}
96+
```
97+
98+
### Example 2
99+
100+
```
101+
allow if {
102+
every x in l { >-+
103+
x < 10 <-+
104+
}
105+
input.x == 1
106+
```
107+
108+
### Step In
109+
110+
`StepIn()` executes the next expression in the current scope and then stops on the next expression in the same scope or sub-scope;
111+
stepping into any referenced rule, called function, comprehension, or every expression.
112+
113+
```go
114+
if err := session.StepIn(threads[0].ID); err != nil {
115+
// handle error
116+
}
117+
```
118+
119+
### Example 1
120+
121+
```
122+
allow if {
123+
x := f(input) >-+
124+
x == 1 |
125+
} |
126+
|
127+
f(x) := y if { <-+
128+
y := x + 1
129+
}
130+
```
131+
132+
### Example 2
133+
134+
```
135+
allow if {
136+
every x in l { >-+
137+
x < 10 <-+
138+
}
139+
input.x == 1
140+
}
141+
```
142+
143+
### Step Out
144+
145+
`StepOut()` steps out of the current scope (rule, function, comprehension, every expression) and stops on the next expression in the parent scope.
146+
147+
```go
148+
if err := session.StepOut(threads[0].ID); err != nil {
149+
// handle error
150+
}
151+
```
152+
153+
#### Example 1
154+
155+
```
156+
allow if {
157+
x := f(input) <-+
158+
x == 1 |
159+
} |
160+
|
161+
f(x) := y if { |
162+
y := x + 1 >-+
163+
}
164+
```
165+
166+
### Example 2
167+
168+
```
169+
allow if {
170+
every x in l {
171+
x < 10 >-+
172+
} |
173+
input.x == 1 <-+
174+
}
175+
```
176+
177+
## Fetching Variable Values
178+
179+
The current values of local and global variables are organized into scopes:
180+
181+
* `Local`: contains variables defined in the current rule, function, comprehension, or every expression.
182+
* `Virtual Cache`: contains the state of the global Virtual Cache, where calculated return values for rules and functions are stored.
183+
* `Input`: contains the input document.
184+
* `Data`: contains the data document.
185+
* `Result Set`: contains the result set of the current query. This scope is only available on the final expression of the query evaluation.
186+
187+
```go
188+
scopes, err := session.Scopes(thread.ID)
189+
if err != nil {
190+
// handle error
191+
}
192+
193+
var localScope debug.Scope
194+
for _, scope := range scopes {
195+
if scope.Name == "Local" {
196+
localScope = scope
197+
break
198+
}
199+
}
200+
201+
variables, err := session.Variables(localScope.VariablesReference())
202+
if err != nil {
203+
// handle error
204+
}
205+
206+
// Enumerate and process variables
207+
```

debug/breakpoint.go

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright 2024 The OPA Authors. All rights reserved.
2+
// Use of this source code is governed by an Apache2
3+
// license that can be found in the LICENSE file.
4+
5+
package debug
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"sync"
11+
12+
"github.com/open-policy-agent/opa/ast/location"
13+
)
14+
15+
type BreakpointID int
16+
17+
type Breakpoint interface {
18+
ID() BreakpointID
19+
Location() location.Location
20+
}
21+
22+
type breakpoint struct {
23+
id BreakpointID
24+
location location.Location
25+
}
26+
27+
func (b breakpoint) ID() BreakpointID {
28+
return b.id
29+
}
30+
31+
func (b breakpoint) Location() location.Location {
32+
return b.location
33+
}
34+
35+
func (b breakpoint) String() string {
36+
return fmt.Sprintf("<%d> %s:%d", b.id, b.location.File, b.location.Row)
37+
}
38+
39+
type breakpointList []Breakpoint
40+
41+
func (b breakpointList) String() string {
42+
if b == nil {
43+
return "[]"
44+
}
45+
46+
buf := new(bytes.Buffer)
47+
buf.WriteString("[")
48+
for i, bp := range b {
49+
if i > 0 {
50+
buf.WriteString(", ")
51+
}
52+
_, _ = fmt.Fprint(buf, bp)
53+
}
54+
buf.WriteString("]")
55+
return buf.String()
56+
}
57+
58+
type breakpointCollection struct {
59+
breakpoints map[string]breakpointList
60+
idCounter BreakpointID
61+
mtx sync.Mutex
62+
}
63+
64+
func newBreakpointCollection() *breakpointCollection {
65+
return &breakpointCollection{
66+
breakpoints: map[string]breakpointList{},
67+
}
68+
}
69+
70+
func (bc *breakpointCollection) newID() BreakpointID {
71+
bc.idCounter++
72+
return bc.idCounter
73+
}
74+
75+
func (bc *breakpointCollection) add(location location.Location) Breakpoint {
76+
bc.mtx.Lock()
77+
defer bc.mtx.Unlock()
78+
79+
bp := breakpoint{
80+
id: bc.newID(),
81+
location: location,
82+
}
83+
bps := bc.breakpoints[bp.location.File]
84+
bps = append(bps, bp)
85+
bc.breakpoints[bp.location.File] = bps
86+
return bp
87+
}
88+
89+
func (bc *breakpointCollection) all() breakpointList {
90+
bc.mtx.Lock()
91+
defer bc.mtx.Unlock()
92+
93+
var bps breakpointList
94+
for _, list := range bc.breakpoints {
95+
bps = append(bps, list...)
96+
}
97+
return bps
98+
}
99+
100+
func (bc *breakpointCollection) allForFilePath(path string) breakpointList {
101+
bc.mtx.Lock()
102+
defer bc.mtx.Unlock()
103+
104+
return bc.breakpoints[path]
105+
}
106+
107+
func (bc *breakpointCollection) remove(id BreakpointID) Breakpoint {
108+
bc.mtx.Lock()
109+
defer bc.mtx.Unlock()
110+
111+
var removed Breakpoint
112+
for path, bps := range bc.breakpoints {
113+
var newBps breakpointList
114+
for _, bp := range bps {
115+
if bp.ID() != id {
116+
newBps = append(newBps, bp)
117+
} else {
118+
removed = bp
119+
}
120+
}
121+
bc.breakpoints[path] = newBps
122+
}
123+
124+
return removed
125+
}
126+
127+
func (bc *breakpointCollection) clear() {
128+
bc.mtx.Lock()
129+
defer bc.mtx.Unlock()
130+
131+
bc.breakpoints = map[string]breakpointList{}
132+
}
133+
134+
func (bc *breakpointCollection) String() string {
135+
if bc == nil {
136+
return "[]"
137+
}
138+
139+
buf := new(bytes.Buffer)
140+
buf.WriteString("[")
141+
for _, bps := range bc.breakpoints {
142+
for i, bp := range bps {
143+
if i > 0 {
144+
buf.WriteString(", ")
145+
}
146+
_, _ = fmt.Fprint(buf, bp)
147+
}
148+
}
149+
buf.WriteString("]")
150+
return buf.String()
151+
}

0 commit comments

Comments
 (0)