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
252 changes: 252 additions & 0 deletions compose/cluster-viz.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#!/bin/bash
# Visualize Dgraph cluster topology from docker-compose.yml

FILE="${1:-docker-compose.yml}"

if [[ ! -f ${FILE} ]]; then
echo "Usage: $0 [docker-compose.yml]"
exit 1
fi

# Flatten the file (join continuation lines) for easier parsing
FLAT=$(tr '\n' ' ' <"${FILE}" | sed 's/ */ /g' || true)

# Extract zero nodes (find --my=zeroN:port patterns near "zero")
zeros=$(echo "${FLAT}" | grep -oE '\-\-my=zero[a-zA-Z0-9_-]*:[0-9]+' | sed -E 's/--my=([^:]+):.*/\1/' | sort -u || true)

# Extract alpha nodes with their group assignments
TMPFILE=$(mktemp)

# Extract all alphas by their --my flag. Group assignment is optional in flags.
# If no group= is found near the alpha's command, put it under UNASSIGNED.
alphas=$(echo "${FLAT}" | grep -oE '\-\-my=alpha[a-zA-Z0-9_-]*:[0-9]+' | sed -E 's/--my=([^:]+):.*/\1/' | sort -u || true)
for alpha in ${alphas}; do
# Look for group= near this alpha occurrence in the flattened compose.
# Many compose files won't specify it; in that case group is assigned dynamically by Zero.
# Use awk (portable on macOS) instead of BSD sed regex intervals.
group=$(awk -v s="${FLAT}" -v a="--my=${alpha}:" '
BEGIN { pos = index(s, a); if (pos == 0) exit }
END {
win = substr(s, pos, 600)
if (match(win, /group=[0-9]+/)) {
g = substr(win, RSTART, RLENGTH)
split(g, parts, "=")
print parts[2]
}
}
' </dev/null || true)
if [[ -z ${group} ]]; then
group="UNASSIGNED"
fi
echo "${group} ${alpha}"
done >"${TMPFILE}"

# Get unique groups (numeric groups first, UNASSIGNED last, never duplicated)
numeric_groups=$(grep -E '^[0-9]+ ' "${TMPFILE}" | cut -d' ' -f1 | sort -n | uniq || true)
unique_groups="${numeric_groups}"
if grep -q '^UNASSIGNED ' "${TMPFILE}"; then
unique_groups="${unique_groups} UNASSIGNED"
fi

# Extract ratel if present
has_ratel=$(grep -q 'ratel' "${FILE}" && echo "yes" || echo "no")

# Get replica count
replicas=$(grep -oE '\-\-replicas=[0-9]+' "${FILE}" | head -1 | cut -d= -f2 || true)
replicas=${replicas:-1}

# Calculate max alphas per group for width calculation
max_per_group=0
for group in ${unique_groups}; do
cnt=$(grep -c "^${group} " "${TMPFILE}" 2>/dev/null || echo 0)
if [[ ${cnt} -gt ${max_per_group} ]]; then
max_per_group=${cnt}
fi
done

# Count zeros
zero_count=$(echo "${zeros}" | wc -w | tr -d ' ' || true)

# Box width: 14 chars per node (┌──────────┐ + 2 spaces) + margins
# Use the larger of zeros or max alphas per group
max_nodes=${zero_count}
if [[ ${max_per_group} -gt ${max_nodes} ]]; then
max_nodes=${max_per_group}
fi
[[ ${max_nodes} -lt 3 ]] && max_nodes=3

inner_width=$((max_nodes * 14 + 4))
[[ ${inner_width} -lt 60 ]] && inner_width=60
outer_width=$((inner_width + 4))

# Total line width (display columns between outer left and right border)
W=$((outer_width))

# Print N space characters
sp() {
local i
for ((i = 0; i < $1; i++)); do printf " "; done
}

# Print N copies of a character
rp() {
local i
for ((i = 0; i < $1; i++)); do printf "%s" "$2"; done
}

# Finish outer row: pad from current column to W, then print |
fin() {
sp "$((W - $1))"
printf "|\n"
}

# Finish inner row (inside group box): pad then print | |
# Total should be W+1 cols: content($1) + spaces + 3(| |) = W+1
fini() {
sp "$((W - $1 - 2))"
printf "| |\n"
}

# Separator using full width
sep() {
printf "%s" "$1"
rp "${W}" "$2"
printf "%s\n" "$3"
}

# Print a row that starts with '|' and does NOT yet include the final right border.
# Pads with spaces so the final '|' is always aligned.
outln() {
local s="$1"
local l=${#s}
# Total line length must be W + 2 (left border + interior + right border).
# If s already includes the left border '|', it must be padded to length W+1,
# then we print the final right border.
printf "%s" "${s}"
local pad=$((W + 1 - l))
if [[ ${pad} -lt 0 ]]; then
pad=0
fi
sp "${pad}"
printf "|\n"
}

# Render a row inside the group box.
# The group box top uses: "| +" + GW*"-" + "+".
# For interior rows we use: "| |" + <payload> + <pad-to-GW> + "|".
out_group() {
local payload="$1"
local plen=${#payload}
local pad=$((GW - plen))
if [[ ${pad} -lt 0 ]]; then
pad=0
fi
local s="| |${payload}"
# pad inside the group box up to width GW
for ((i = 0; i < pad; i++)); do
s+=" "
done
s+="|"
outln "${s}"
}

echo ""

# === HEADER ===
sep "+" "=" "+"
outln "| DGRAPH CLUSTER TOPOLOGY"
sep "+" "=" "+"
echo ""

# === ZERO LAYER ===
sep "+" "-" "+"
outln "| ZERO LAYER (Cluster Coordination)"
sep "+" "-" "+"

# Zero boxes: each is 14 cols (box 12 + 2 spaces)
line="| "
for z in ${zeros}; do line+="+----------+ "; done
outln "${line}"
line="| "
for z in ${zeros}; do line+=$(printf "| %-8s | " "${z}"); done
outln "${line}"
line="| "
for z in ${zeros}; do line+="| (zero) | "; done
outln "${line}"
line="| "
for z in ${zeros}; do line+="+----------+ "; done
outln "${line}"

sep "+" "-" "+"
echo ""

# === ALPHA LAYER ===
sep "+" "-" "+"
outln "| ALPHA LAYER (Data Storage) - Replicas: ${replicas}"
sep "+" "-" "+"

# Group box inner width
GW=$((inner_width - 2))

for group in ${unique_groups}; do
alphas=$(grep "^${group} " "${TMPFILE}" | cut -d' ' -f2 || true)
count=$(echo "${alphas}" | wc -l | tr -d ' ' || true)

# Empty row
outln "|"

# Group box top
line="| +"
for ((i = 0; i < GW; i++)); do line+="-"; done
line+="+"
outln "${line}"

# Group title row
if [[ ${group} == "UNASSIGNED" ]]; then
gtitle=" GROUP UNASSIGNED (Raft Replicas: ${count})"
else
gtitle=" GROUP ${group} (Raft Replicas: ${count})"
fi
out_group "${gtitle}"

# Alpha boxes
line=" "
for a in ${alphas}; do line+="+----------+ "; done
out_group "${line}"
line=" "
for a in ${alphas}; do line+=$(printf "| %-8s | " "${a}" || true); done
out_group "${line}"
line=" "
for a in ${alphas}; do line+="| (alpha) | "; done
out_group "${line}"
line=" "
for a in ${alphas}; do line+="+----------+ "; done
out_group "${line}"

# Group box bottom
line="| +"
for ((i = 0; i < GW; i++)); do line+="-"; done
line+="+"
outln "${line}"
done

outln "|"
sep "+" "-" "+"

# === RATEL ===
if [[ ${has_ratel} == "yes" ]]; then
echo ""
sep "+" "-" "+"
outln "| UI LAYER"
sep "+" "-" "+"
outln "| +----------+"
outln "| | ratel | (Web UI on :8000)"
outln "| +----------+"
sep "+" "-" "+"
fi

rm -f "${TMPFILE}"

echo ""
echo "Legend: Alphas in same GROUP replicate data via Raft consensus"
echo ""
72 changes: 55 additions & 17 deletions compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ type options struct {
DataVol bool
TmpFS bool
UserOwnership bool
Jaeger bool
Jaeger int
TraceRatio string
TraceService bool
Metrics bool
PortOffset int
Verbosity int
Expand Down Expand Up @@ -202,8 +204,15 @@ func initService(basename string, idx, grpcPort int) service {
svc.Command += fmt.Sprintf(" --cwd=/data/%s", svc.name)
}
svc.Command += " " + basename
if opts.Jaeger {
svc.Command += ` --trace "jaeger=http://jaeger:14268;"`
if opts.Jaeger > 0 {
traceFlag := "jaeger=http://jaeger:4318;"
if opts.TraceRatio != "" {
traceFlag += fmt.Sprintf(" ratio=%s;", opts.TraceRatio)
}
if opts.TraceService {
traceFlag += fmt.Sprintf(" service=%s;", svc.name)
}
svc.Command += fmt.Sprintf(` --trace "%s"`, traceFlag)
}
return svc
}
Expand Down Expand Up @@ -375,23 +384,44 @@ func getVolume(vol string) volume {

}

func getJaeger() service {
svc := service{
Image: "jaegertracing/all-in-one:1.18",
func getJaeger(version int) service {
if version == 2 {
// Jaeger v2.x uses OpenTelemetry Collector architecture with YAML config.
// OTLP receivers bind to localhost by default, so we need JAEGER_LISTEN_HOST=0.0.0.0
// for container environments.
return service{
Image: "jaegertracing/jaeger:latest",
ContainerName: containerName("jaeger"),
Ports: []string{
toPort(4318),
toPort(16686),
},
Environment: []string{
"JAEGER_LISTEN_HOST=0.0.0.0",
},
}
}
// Jaeger v1.60 with badger storage
return service{
Image: "jaegertracing/all-in-one:1.60",
ContainerName: containerName("jaeger"),
WorkingDir: "/working/jaeger",
User: "0",
Ports: []string{
toPort(14268),
toPort(4318),
toPort(16686),
},
Volumes: []volume{{
Type: "volume",
Source: "jaeger-volume",
Target: "/jaeger",
}},
Environment: []string{
"SPAN_STORAGE_TYPE=badger",
},
Command: "--badger.ephemeral=false" +
" --badger.directory-key /working/jaeger" +
" --badger.directory-value /working/jaeger",
" --badger.directory-key /jaeger" +
" --badger.directory-value /jaeger",
}
return svc
}

func getMinio(minioDataDir string) service {
Expand Down Expand Up @@ -421,12 +451,12 @@ func getRatel() service {
portFlag = fmt.Sprintf(" -port=%d", opts.RatelPort)
}
svc := service{
Image: opts.Image + ":" + opts.Tag,
Image: "dgraph/ratel:latest",
ContainerName: containerName("ratel"),
Ports: []string{
toPort(opts.RatelPort),
},
Command: "dgraph-ratel" + portFlag,
Command: portFlag,
}
return svc
}
Expand Down Expand Up @@ -543,8 +573,13 @@ func main() {
"run as the current user rather than root")
cmd.PersistentFlags().BoolVar(&opts.TmpFS, "tmpfs", false,
"store w and zw directories on a tmpfs filesystem")
cmd.PersistentFlags().BoolVarP(&opts.Jaeger, "jaeger", "j", false,
"include jaeger service")
cmd.PersistentFlags().IntVarP(&opts.Jaeger, "jaeger", "j", 0,
"include jaeger service (1 for v1.60, 2 for v2.x)")
cmd.PersistentFlags().Lookup("jaeger").NoOptDefVal = "1"
cmd.PersistentFlags().StringVar(&opts.TraceRatio, "trace_ratio", "",
"ratio of queries to trace (e.g., 0.01 for 1%)")
cmd.PersistentFlags().BoolVar(&opts.TraceService, "trace_service", false,
"use compose service name as trace service name (e.g., alpha1, zero1)")
cmd.PersistentFlags().BoolVarP(&opts.Metrics, "metrics", "m", false,
"include metrics (prometheus, grafana) services")
cmd.PersistentFlags().IntVarP(&opts.PortOffset, "port_offset", "o", 100,
Expand Down Expand Up @@ -689,8 +724,11 @@ func main() {
cfg.Volumes["data"] = stringMap{}
}

if opts.Jaeger {
services["jaeger"] = getJaeger()
if opts.Jaeger > 0 {
services["jaeger"] = getJaeger(opts.Jaeger)
if opts.Jaeger == 1 {
cfg.Volumes["jaeger-volume"] = stringMap{}
}
}

if opts.Ratel {
Expand Down
2 changes: 1 addition & 1 deletion contrib/jepsen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ var (
testCount = pflag.IntP("test-count", "c", 1, "Test count per Jepsen test.")
jaeger = pflag.StringP("jaeger", "j", "",
"Run with Jaeger collector. Set to empty string to disable collection to Jaeger."+
" Otherwise set to http://jaeger:14268.")
" Otherwise set to http://jaeger:4318.")
jaegerSaveTraces = pflag.Bool("jaeger-save-traces", true, "Save Jaeger traces on test error.")
deferDbTeardown = pflag.Bool("defer-db-teardown", false,
"Wait until user input to tear down DB nodes")
Expand Down
Loading
Loading