Remote debug (#564)

* Extend the trouble shooting with remote debug
This commit is contained in:
Zoltan Papp
2026-01-22 11:01:21 +01:00
committed by GitHub
parent 38eb2925c8
commit b5ce088750
3 changed files with 859 additions and 3 deletions

View File

@@ -187,6 +187,60 @@ issue that recovers when restarting the client.
</p>
Once the bundle generation is complete, you can click on `Copy Key` to get the uploaded key and share with NetBird\'s team.
### Remote debug bundle generation
Administrators can remotely request debug bundles from peer clients through the Management API or Dashboard. This is
particularly useful when troubleshooting issues on remote machines where local access is limited or when working with
end-users who may not be familiar with command-line tools.
When a remote debug bundle is requested:
1. The management server sends a job request to the target peer
2. The peer client receives the job and generates the debug bundle automatically
3. The generated bundle is uploaded to a centralized location
4. The administrator receives the upload key to access the bundle
#### Using the Management API
You can also trigger remote debug bundles programmatically via the Management API.
See the [Peers API documentation](/ipa/resources/peers#create-peer-debug-bundle-job) for complete API reference, including:
- Creating debug bundle jobs
- Listing all jobs for a peer
- Getting job status and upload keys
#### Using the Dashboard
You can trigger remote debug bundles directly from the NetBird Dashboard without requiring CLI access.
**To generate a remote debug bundle:**
1. Navigate to **Peers** in the dashboard
2. Click on the peer you want to troubleshoot
3. Click the **Run Remote Job** button (the peer must be online and connected)
4. Select **Debug Bundle** from the dropdown menu
5. Configure the debug bundle options:
- **Log File Count**: Number of log files to include (1-50, default: 10)
- **Enable Bundle Duration** (optional): Collect logs for a specific time period (1-5 minutes) before generating the bundle
- **Anonymize Log Data**: Remove sensitive information like IP addresses and domains
6. Click **Create Debug Bundle**
**Viewing job status and results:**
Once triggered, the job appears in the **Remote Jobs** section on the peer details page. The table shows:
- **Type**: The job type (Debug Bundle)
- **Status**: Pending (yellow), Completed (green), or Failed (red)
- **Created/Updated**: Timestamps for job lifecycle
- **Output**: Once completed, displays the upload key that you can copy and share with NetBird support
The upload key is automatically copyable by clicking on it. Share this key through GitHub Issues, Slack, or support channels.
#### Limitations
- The peer must be online and connected to the management server to receive the job
- Debug bundle generation may take a few seconds to a few minutes depending on log size and system information
- Bundles are automatically uploaded to NetBird's secure storage (or your configured upload endpoint for self-hosted deployments)
- Upload keys expire after 30 days for security
## Enabling debug logs on agent
Logs can be temporarily set using the following command.

View File

@@ -1597,8 +1597,807 @@ echo $response;
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Peer Debug Bundle Job {{ tag: 'POST' , label: '/api/peers/{peerId}/jobs' }}
<Row>
<Col>
Triggers a remote debug bundle generation on the specified peer. The peer must be online and connected to receive
the job. Once generated, the bundle is automatically uploaded to the configured storage location.
### Path Parameters
<Properties>
<Property name="peerId" type="string" required={true}>
The unique identifier of a peer
</Property>
</Properties>
### Request-Body Parameters
<Properties><Property name="workload" type="object" required={true}>
The job workload configuration
</Property>
</Properties>
#### Workload Object
<Properties>
<Property name="type" type="string" required={true}>
Job type. Use "bundle" for debug bundle generation
</Property>
<Property name="parameters" type="object" required={false}>
Job-specific parameters
</Property>
</Properties>
#### Parameters Object (for debug bundle)
<Properties>
<Property name="anonymize" type="boolean" required={false}>
Anonymize IP addresses and non-netbird.io domains in logs and status output (default: false)
</Property>
<Property name="bundle_for" type="boolean" required={false}>
Enable time-based log collection before generating bundle (default: false)
</Property>
<Property name="bundle_for_time" type="number" required={false}>
Duration in minutes for log collection (1-5 minutes). Only used if bundle_for is true
</Property>
<Property name="log_file_count" type="number" required={false}>
Number of log files to include (1-1000, default: 10)
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/peers/{peerId}/jobs">
```bash {{ title: 'cURL' }}
curl -X POST https://api.netbird.io/api/peers/{peerId}/jobs \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Authorization: Token <TOKEN>' \
--data-raw '{
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
}
}
}'
```
```js
const axios = require('axios');
let data = JSON.stringify({
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
}
}
});
let config = {
method: 'post',
maxBodyLength: Infinity,
url: '/api/peers/{peerId}/jobs',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Token <TOKEN>'
},
data : data
};
axios(config)
.then((response) => {
console.log(JSON.stringify(response.data));
})
.catch((error) => {
console.log(error);
});
```
```python
import requests
import json
url = "https://api.netbird.io/api/peers/{peerId}/jobs"
payload = json.dumps({
"workload": {
"type": "bundle",
"parameters": {
"anonymize": True,
"bundle_for": True,
"bundle_for_time": 2,
"log_file_count": 10
}
}
})
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Token <TOKEN>'
}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)
```
```go
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.netbird.io/api/peers/{peerId}/jobs"
method := "POST"
payload := strings.NewReader(`{
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
}
}
}`)
client := &http.Client {
}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
{
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Token <TOKEN>")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
```
```ruby
require "uri"
require "json"
require "net/http"
url = URI("https://api.netbird.io/api/peers/{peerId}/jobs")
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = "application/json"
request["Accept"] = "application/json"
request["Authorization"] = "Token <TOKEN>"
request.body = JSON.dump({
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
}
}
})
response = https.request(request)
puts response.read_body
```
```java
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, '{
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
}
}
}');
Request request = new Request.Builder()
.url("https://api.netbird.io/api/peers/{peerId}/jobs")
.method("POST", body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("Authorization: Token <TOKEN>")
.build();
Response response = client.newCall(request).execute();
```
```php
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://api.netbird.io/api/peers/{peerId}/jobs',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => '{
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
}
}
}',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json',
'Accept: application/json',
'Authorization: Token <TOKEN>'
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Example' }}
{
"id": "chacbco6lnnbn6cg5s91",
"peer_id": "chacbco6lnnbn6cg5s90",
"status": "pending",
"created_at": "2026-01-21T10:30:00.000Z",
"updated_at": "2026-01-21T10:30:00.000Z",
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
}
}
}
```
```json {{ title: 'Schema' }}
{
"id": "string",
"peer_id": "string",
"status": "string",
"created_at": "string",
"updated_at": "string",
"workload": {
"type": "string",
"parameters": {
"anonymize": "boolean",
"bundle_for": "boolean",
"bundle_for_time": "number",
"log_file_count": "number"
},
"result": {
"upload_key": "string"
}
}
}
```
</CodeGroup>
</Col>
</Row>
---
## List Peer Jobs {{ tag: 'GET' , label: '/api/peers/{peerId}/jobs' }}
<Row>
<Col>
Returns a list of all jobs for the specified peer, including debug bundle generation jobs.
### Path Parameters
<Properties>
<Property name="peerId" type="string" required={true}>
The unique identifier of a peer
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/peers/{peerId}/jobs">
```bash {{ title: 'cURL' }}
curl -X GET https://api.netbird.io/api/peers/{peerId}/jobs \
-H 'Accept: application/json' \
-H 'Authorization: Token <TOKEN>'
```
```js
const axios = require('axios');
let config = {
method: 'get',
maxBodyLength: Infinity,
url: '/api/peers/{peerId}/jobs',
headers: {
'Accept': 'application/json',
'Authorization': 'Token <TOKEN>'
}
};
axios(config)
.then((response) => {
console.log(JSON.stringify(response.data));
})
.catch((error) => {
console.log(error);
});
```
```python
import requests
import json
url = "https://api.netbird.io/api/peers/{peerId}/jobs"
headers = {
'Accept': 'application/json',
'Authorization': 'Token <TOKEN>'
}
response = requests.request("GET", url, headers=headers)
print(response.text)
```
```go
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.netbird.io/api/peers/{peerId}/jobs"
method := "GET"
client := &http.Client {
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
fmt.Println(err)
return
{
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Token <TOKEN>")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
```
```ruby
require "uri"
require "json"
require "net/http"
url = URI("https://api.netbird.io/api/peers/{peerId}/jobs")
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Accept"] = "application/json"
request["Authorization"] = "Token <TOKEN>"
response = https.request(request)
puts response.read_body
```
```java
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
Request request = new Request.Builder()
.url("https://api.netbird.io/api/peers/{peerId}/jobs")
.method("GET")
.addHeader("Accept", "application/json")
.addHeader("Authorization: Token <TOKEN>")
.build();
Response response = client.newCall(request).execute();
```
```php
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://api.netbird.io/api/peers/{peerId}/jobs',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => array(
'Accept: application/json',
'Authorization: Token <TOKEN>'
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Example' }}
[
{
"id": "chacbco6lnnbn6cg5s91",
"peer_id": "chacbco6lnnbn6cg5s90",
"status": "succeeded",
"created_at": "2026-01-21T10:30:00.000Z",
"updated_at": "2026-01-21T10:31:15.000Z",
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
},
"result": {
"upload_key": "1234567890ab27fb37c88b3b4be7011e22aa2e5ca6f38ffa9c4481884941f726/12345678-90ab-cdef-1234-567890abcdef"
}
}
},
{
"id": "chacbco6lnnbn6cg5s92",
"peer_id": "chacbco6lnnbn6cg5s90",
"status": "pending",
"created_at": "2026-01-21T11:00:00.000Z",
"updated_at": "2026-01-21T11:00:00.000Z",
"workload": {
"type": "bundle",
"parameters": {
"anonymize": false,
"log_file_count": 10
}
}
}
]
```
```json {{ title: 'Schema' }}
[
{
"id": "string",
"peer_id": "string",
"status": "string",
"created_at": "string",
"updated_at": "string",
"workload": {
"type": "string",
"parameters": {
"anonymize": "boolean",
"bundle_for": "boolean",
"bundle_for_time": "number",
"log_file_count": "number"
},
"result": {
"upload_key": "string"
}
}
}
]
```
</CodeGroup>
</Col>
</Row>
---
## Get Peer Job {{ tag: 'GET' , label: '/api/peers/{peerId}/jobs/{jobId}' }}
<Row>
<Col>
Returns details of a specific job for the peer, including the upload key if the debug bundle generation is complete.
### Path Parameters
<Properties>
<Property name="peerId" type="string" required={true}>
The unique identifier of a peer
</Property>
<Property name="jobId" type="string" required={true}>
The unique identifier of a job
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/peers/{peerId}/jobs/{jobId}">
```bash {{ title: 'cURL' }}
curl -X GET https://api.netbird.io/api/peers/{peerId}/jobs/{jobId} \
-H 'Accept: application/json' \
-H 'Authorization: Token <TOKEN>'
```
```js
const axios = require('axios');
let config = {
method: 'get',
maxBodyLength: Infinity,
url: '/api/peers/{peerId}/jobs/{jobId}',
headers: {
'Accept': 'application/json',
'Authorization': 'Token <TOKEN>'
}
};
axios(config)
.then((response) => {
console.log(JSON.stringify(response.data));
})
.catch((error) => {
console.log(error);
});
```
```python
import requests
import json
url = "https://api.netbird.io/api/peers/{peerId}/jobs/{jobId}"
headers = {
'Accept': 'application/json',
'Authorization': 'Token <TOKEN>'
}
response = requests.request("GET", url, headers=headers)
print(response.text)
```
```go
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.netbird.io/api/peers/{peerId}/jobs/{jobId}"
method := "GET"
client := &http.Client {
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
fmt.Println(err)
return
{
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Token <TOKEN>")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
```
```ruby
require "uri"
require "json"
require "net/http"
url = URI("https://api.netbird.io/api/peers/{peerId}/jobs/{jobId}")
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Accept"] = "application/json"
request["Authorization"] = "Token <TOKEN>"
response = https.request(request)
puts response.read_body
```
```java
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
Request request = new Request.Builder()
.url("https://api.netbird.io/api/peers/{peerId}/jobs/{jobId}")
.method("GET")
.addHeader("Accept", "application/json")
.addHeader("Authorization: Token <TOKEN>")
.build();
Response response = client.newCall(request).execute();
```
```php
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://api.netbird.io/api/peers/{peerId}/jobs/{jobId}',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => array(
'Accept: application/json',
'Authorization: Token <TOKEN>'
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Example' }}
{
"id": "chacbco6lnnbn6cg5s91",
"peer_id": "chacbco6lnnbn6cg5s90",
"status": "succeeded",
"created_at": "2026-01-21T10:30:00.000Z",
"updated_at": "2026-01-21T10:31:15.000Z",
"workload": {
"type": "bundle",
"parameters": {
"anonymize": true,
"bundle_for": true,
"bundle_for_time": 2,
"log_file_count": 10
},
"result": {
"upload_key": "1234567890ab27fb37c88b3b4be7011e22aa2e5ca6f38ffa9c4481884941f726/12345678-90ab-cdef-1234-567890abcdef"
}
}
}
```
```json {{ title: 'Schema' }}
{
"id": "string",
"peer_id": "string",
"status": "string",
"created_at": "string",
"updated_at": "string",
"workload": {
"type": "string",
"parameters": {
"anonymize": "boolean",
"bundle_for": "boolean",
"bundle_for_time": "number",
"log_file_count": "number"
},
"result": {
"upload_key": "string"
}
}
}
```
</CodeGroup>
</Col>
</Row>

View File

@@ -23,7 +23,7 @@ The current version of NetBird tracks a wide range of network changes that occur
<details>
<summary>Click here to view the full list of tracked events</summary>
- **Peer Management:**
- **Peer Management:**
- Peer added by user
- Peer added with setup key
- Peer removed by user
@@ -33,6 +33,9 @@ The current version of NetBird tracks a wide range of network changes that occur
- Peer login expiration enabled
- Peer login expiration disabled
- **Remote Job Management:**
- Remote job created for peer
- **User Management:**
- User joined
- User invited