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
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,114 @@ mping batch --count 5 192.168.1.0/29
mping batch -f hosts.txt --count 5
```

## Target Expansion

mping supports powerful target expansion syntax for monitoring large numbers of hosts efficiently:

### Bracket Expansion
Use square brackets `[]` to specify multiple targets with patterns:

#### Numeric Ranges
```bash
# Basic numeric range
mping server[1-5].example.com
# Expands to: server1.example.com, server2.example.com, ..., server5.example.com

# Zero-padded ranges (auto-detected from start format)
mping web[01-10].example.com
# Expands to: web01.example.com, web02.example.com, ..., web10.example.com

# Three-digit padding
mping host[001-100].datacenter.com
# Expands to: host001.datacenter.com, host002.datacenter.com, ..., host100.datacenter.com
```

#### Character Ranges
```bash
# Lowercase letters
mping server[a-d].example.com
# Expands to: servera.example.com, serverb.example.com, serverc.example.com, serverd.example.com

# Uppercase letters
mping db[A-C].cluster.internal
# Expands to: dbA.cluster.internal, dbB.cluster.internal, dbC.cluster.internal
```

#### List Expansion
```bash
# Comma-separated lists
mping service[web,api,db].example.com
# Expands to: serviceweb.example.com, serviceapi.example.com, servicedb.example.com

# Mixed environments
mping app[dev,staging,prod].company.com
# Expands to: appdev.company.com, appstaging.company.com, appprod.company.com
```

#### Multiple Brackets (Combinations)
```bash
# Multiple expansion points
mping server[1-2].[dev,prod].example.com
# Expands to: server1.dev.example.com, server1.prod.example.com,
# server2.dev.example.com, server2.prod.example.com

# Protocol-specific monitoring
mping https://api[1-3].[us,eu].service.com/health
```

#### Advanced Use Cases
```bash
# Infrastructure monitoring
mping db[01-05].cluster[a-c].datacenter.com

# Multi-region web services
mping https://[web,api,cdn][1-3].[us-west,eu-central].example.com

# Network equipment
mping switch[01-24].rack[a-f].datacenter.internal
```

### File-based Expansion
Bracket expansion also works in target files (`-f` option):

```bash
# Create targets.txt
cat > targets.txt << EOF
# Web tier
web[01-05].prod.example.com
api[1-3].prod.example.com

# Database tier
db[01-02].[master,slave].prod.example.com

# Monitoring endpoints
https://health[1-5].prod.example.com/status
EOF

# Monitor all expanded targets
mping -f targets.txt
```

### Combination with CIDR Ranges
Bracket expansion works alongside CIDR notation:

```bash
# Mix bracket expansion with subnet ranges
mping server[1-3].example.com 192.168.1.0/29

# In files
echo "web[01-03].prod.example.com" >> targets.txt
echo "10.0.1.0/28" >> targets.txt
mping -f targets.txt
```

### Key Features
- **Cross-platform**: Works on Windows, macOS, and Linux
- **Shell-independent**: Unlike bash `{}` expansion, works in files and non-bash environments
- **Zero-padding intelligence**: Automatically detects and maintains padding format
- **Error handling**: Invalid patterns are preserved as-is (e.g., `[5-1]` stays `[5-1]`)
- **Protocol support**: Works with all protocol prefixes (`https://`, `tcp://`, `dns://`, etc.)

## DNS Monitoring Details

### DNS Target Format
Expand Down
14 changes: 7 additions & 7 deletions internal/command/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ mping batch dns://8.8.8.8/google.com`,
return err
}

hosts := parseHostnames(args, filename)
hosts := ExpandTargets(args, filename)
if len(hosts) == 0 {
cmd.Println("Please set hostname or ip.")
cmd.Help()
Expand All @@ -68,34 +68,34 @@ mping batch dns://8.8.8.8/google.com`,
// Create ProbeManager and MetricsManager
probeManager := prober.NewProbeManager(cfg.Prober, cfg.Default)
metricsManager := stats.NewMetricsManager()

// Add targets
err = probeManager.AddTargets(hosts...)
if err != nil {
return fmt.Errorf("failed to add targets: %w", err)
}

// Subscribe to events for metrics collection
metricsManager.Subscribe(probeManager.Events())

// Start probing with timeout context
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(counter)*_interval)
defer cancel()

cmd.Print("probe")
go func() {
if err := probeManager.Run(ctx, _interval, _timeout); err != nil {
fmt.Printf("ProbeManager error: %v\n", err)
}
}()

// Wait for specified duration
for counter > 0 {
counter--
cmd.Print(".")
time.Sleep(_interval)
}

// Stop probing
probeManager.Stop()
cmd.Print("\r")
Expand Down
2 changes: 1 addition & 1 deletion internal/command/mping.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ mping dns://8.8.8.8/google.com`,
return err
}

hosts := parseHostnames(args, filename)
hosts := ExpandTargets(args, filename)
if len(hosts) == 0 {
cmd.Println("Please set hostname or ip.")
cmd.Help()
Expand Down
158 changes: 148 additions & 10 deletions internal/command/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,150 @@ package command
import (
"bufio"
"errors"
"fmt"
"io"
"net"
"os"
"regexp"
"strconv"
"strings"
)

func parseCidr(_hosts []string) []string {
// ExpandTargets processes target specifications through a pipeline of expansions
func ExpandTargets(args []string, filePath string) []string {
targets := collectTargets(args, filePath)
targets = parseBrackets(targets)
targets = parseCIDR(targets)
return targets
}

func parseBrackets(_hosts []string) []string {
hosts := []string{}
bracketRegex := regexp.MustCompile(`\[([^\]]+)\]`)

for _, h := range _hosts {
matches := bracketRegex.FindAllStringSubmatch(h, -1)
if len(matches) == 0 {
hosts = append(hosts, h)
continue
}

// Generate all combinations for this host
expanded := expandHost(h, matches)
hosts = append(hosts, expanded...)
}

return hosts
}

func expandHost(host string, matches [][]string) []string {
results := []string{host}

for _, match := range matches {
fullMatch := match[0] // [1-10] or [web,db,cache]
content := match[1] // 1-10 or web,db,cache

var expansions []string
var isValidExpansion bool

if strings.Contains(content, "-") && !strings.Contains(content, ",") {
// Range expansion: [1-10], [01-05]
expansions = expandRange(content)
// Check if expansion was successful (not just returning original content)
isValidExpansion = len(expansions) != 1 || expansions[0] != content
} else if strings.Contains(content, ",") {
// List expansion: [web,db,cache]
expansions = expandList(content)
isValidExpansion = true
} else {
// Single value: [5]
expansions = []string{content}
isValidExpansion = true
}

// If expansion failed, skip this match and keep original brackets
if !isValidExpansion {
continue
}

// Apply expansions to all current results
var newResults []string
for _, result := range results {
for _, expansion := range expansions {
newResults = append(newResults, strings.Replace(result, fullMatch, expansion, 1))
}
}
results = newResults
}

return results
}

func expandRange(content string) []string {
parts := strings.Split(content, "-")
if len(parts) != 2 {
return []string{content} // Invalid range format
}

startStr, endStr := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])

// Try to parse as integers
start, startErr := strconv.Atoi(startStr)
end, endErr := strconv.Atoi(endStr)

if startErr != nil || endErr != nil {
// Try character range (a-z, A-Z)
if len(startStr) == 1 && len(endStr) == 1 {
return expandCharRange(startStr[0], endStr[0])
}
return []string{content} // Invalid range
}

if start > end {
return []string{content} // Invalid range
}

// Determine zero-padding from the start string
padding := len(startStr)
if startStr[0] != '0' && start != 0 {
padding = 0 // No padding unless starts with 0
}

var result []string
for i := start; i <= end; i++ {
if padding > 0 {
result = append(result, fmt.Sprintf("%0*d", padding, i))
} else {
result = append(result, strconv.Itoa(i))
}
}

return result
}

func expandCharRange(start, end byte) []string {
if start > end {
return []string{string(start) + "-" + string(end)} // Invalid range
}

var result []string
for i := start; i <= end; i++ {
result = append(result, string(i))
}

return result
}

func expandList(content string) []string {
parts := strings.Split(content, ",")
var result []string
for _, part := range parts {
result = append(result, strings.TrimSpace(part))
}
return result
}

func parseCIDR(_hosts []string) []string {
hosts := []string{}
for _, h := range _hosts {
ip, ipnet, err := net.ParseCIDR(h)
Expand Down Expand Up @@ -60,18 +196,20 @@ func file2hostnames(fp *os.File) []string {
return hosts
}

func parseHostnames(args []string, fpath string) []string {
hosts := []string{}

// Only attempt to open file if path is not empty
if fpath != "" {
fp, err := os.Open(fpath)
// collectTargets gathers targets from command-line arguments and optional file
func collectTargets(args []string, filePath string) []string {
targets := []string{}

// Load targets from file if specified
if filePath != "" {
fp, err := os.Open(filePath)
if err == nil {
hosts = file2hostnames(fp)
targets = file2hostnames(fp)
fp.Close() // Critical fix: close file to prevent resource leak
}
}

hosts = append(hosts, args...)
return parseCidr(hosts)
// Append command-line arguments
targets = append(targets, args...)
return targets
}
Loading
Loading