Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove unused cloudwatch annotations from deployment objects
- Fix: log queries on k8s scopes now return the time range that was selected, instead of the most recent lines whatever range was chosen
- Fix: paging through logs on k8s scopes no longer repeats lines already shown, and now reaches the end of the selected range
- Fix: diagnose on k8s scopes no longer fails to publish its results when a check collects application logs

## [1.15.1] - 2026-08-12
- Fix: gRPC additional ports on k8s scopes now leave the declared port free for the application, so a gRPC server can bind the port configured in the scope instead of failing to start with "address already in use". gRPC ports now work the same way HTTP ones already did
Expand Down
56 changes: 56 additions & 0 deletions k8s/diagnose/tests/diagnose_utils.bats
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,59 @@ strip_ansi() {
local clean=$(strip_ansi "$output")
assert_contains "$clean" "⚠ No JSON result files found in $NP_OUTPUT_DIR"
}

@test "notify_results: sends payloads larger than the argv limit" {
# 2 MB of log text in one check: past the argv limit on both Linux and macOS.
local line
line=$(printf 'x%.0s' {1..1000})
jq -nc --arg line "$line" \
'{category: "logs", status: "success", evidence: {}, logs: [range(2000) | $line]}' \
> "$NP_OUTPUT_DIR/big.json"

local body_file="$(mktemp)"
export BODY_CAPTURE="$body_file"
# Mirror np: --body is read from disk only when the value ends in .json,
# otherwise it is sent verbatim. Resolved here because the caller cleans up.
np() {
local prev=""
for arg in "$@"; do
if [[ "$prev" == "--body" ]]; then
if [[ "$arg" == *.json ]]; then cat "$arg" > "$BODY_CAPTURE"; else printf '%s' "$arg" > "$BODY_CAPTURE"; fi
fi
prev="$arg"
done
return 0
}
export -f np

run notify_results
[ "$status" -eq 0 ]

assert_equal "$(jq -r '.results.categories[0].category' "$body_file")" "logs"
assert_equal "$(jq -r '.results.categories[0].checks[0].logs | length' "$body_file")" "2000"

rm -f "$body_file"
unset BODY_CAPTURE
}

# =============================================================================
# lines_to_json_array
# =============================================================================
@test "lines_to_json_array: passes short lines through untouched" {
local out
out=$(printf 'alpha\n\nbeta\n' | lines_to_json_array)

assert_equal "$(echo "$out" | jq -r 'length')" "2"
assert_equal "$(echo "$out" | jq -r '.[0]')" "alpha"
assert_equal "$(echo "$out" | jq -r '.[1]')" "beta"
}

@test "lines_to_json_array: truncates a line past the character cap" {
local out
out=$(printf 'x%.0s' {1..5000} | EVIDENCE_LOG_LINE_MAX_CHARS=100 lines_to_json_array)

assert_equal "$(echo "$out" | jq -r 'length')" "1"
assert_contains "$(echo "$out" | jq -r '.[0]')" "[truncated]"
# 100 kept characters plus the marker, nowhere near the original 5000.
[ "$(echo "$out" | jq -r '.[0] | length')" -lt 200 ]
}
54 changes: 41 additions & 13 deletions k8s/diagnose/utils/diagnose_utils
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,16 @@ read_log_tail() {
# Convert newline-delimited stdin into a JSON array of non-empty strings.
# Used by read_log_tail and update_check_result to share one canonical
# tail-text-to-JSON pipeline.
#
# Lines are truncated at $EVIDENCE_LOG_LINE_MAX_CHARS (default 2000): kubectl's
# --tail bounds line count, not line length.
lines_to_json_array() {
jq -R -s 'split("\n") | map(select(length > 0))'
local max="${EVIDENCE_LOG_LINE_MAX_CHARS:-2000}"
jq -R -s --argjson max "$max" '
split("\n")
| map(select(length > 0))
| map(if length > $max then .[:$max] + "…[truncated]" else . end)
'
}

# Append a JSON object to a bash indexed array (passed by name). Avoids the
Expand Down Expand Up @@ -333,10 +341,18 @@ notify_results() {
exit 1
fi

# Generate grouped results directly in memory
# Use xargs to avoid "Argument list too long" error
local grouped_results
grouped_results=$(echo "$json_files" | xargs jq -s '
# The payload must never travel through argv: Linux caps a single execve
# argument at 128 KiB, and ulimit does not lift it. Hence xargs, then
# --slurpfile, then a file path for np --body.
#
# The .json suffix is required: np reads --body from disk only when the
# value ends in .json, otherwise it sends the string itself.
local work_dir results_file body_file rc
work_dir="$(mktemp -d)"
results_file="$work_dir/results.json"
body_file="$work_dir/body.json"

if ! echo "$json_files" | xargs jq -s '
# category helper
def cat: .category // "unknown";

Expand All @@ -355,19 +371,31 @@ notify_results() {
},
checks: .
})
')
' > "$results_file"; then
print_error "Failed to group check results from $NP_OUTPUT_DIR"
rm -rf "$work_dir"
return 1
fi

# Extract action and service IDs
ACTION_ID=$(echo "$NP_ACTION_CONTEXT" | jq -r '.notification.id')
SERVICE_ID=$(echo "$NP_ACTION_CONTEXT" | jq -r '.notification.service.id')

# Build action body with grouped results
ACTION_BODY=$(jq -nc \
--argjson result "$grouped_results" \
'{
results: { categories: $result }
}')
if ! jq -nc \
--slurpfile result "$results_file" \
'{
results: { categories: $result[0] }
}' > "$body_file"; then
print_error "Failed to build the action body"
rm -rf "$work_dir"
return 1
fi

# Send to np service
np service action patch --id "$ACTION_ID" --serviceId "$SERVICE_ID" --body "$ACTION_BODY" --no-output
}
np service action patch --id "$ACTION_ID" --serviceId "$SERVICE_ID" --body "$body_file" --no-output
rc=$?

rm -rf "$work_dir"
return $rc
}
Loading