Skip to content

Commit 020d3de

Browse files
radulucutmr-karan
authored andcommitted
feat: integrate Globalping API for global network diagnostics
1 parent 44818dd commit 020d3de

File tree

8 files changed

+234
-37
lines changed

8 files changed

+234
-37
lines changed

cmd/doggo/cli.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"sync"
1010
"time"
1111

12+
"github.com/jsdelivr/globalping-cli/globalping"
1213
"github.com/knadh/koanf/providers/posflag"
1314
"github.com/knadh/koanf/v2"
1415
"github.com/mr-karan/doggo/internal/app"
@@ -43,6 +44,20 @@ func main() {
4344
logger := utils.InitLogger(cfg.debug)
4445
app := initializeApp(logger, cfg)
4546

47+
if app.QueryFlags.From != "" {
48+
res, err := app.GlobalpingMeasurement()
49+
if err != nil {
50+
logger.Error("Error fetching globalping measurement", "error", err)
51+
os.Exit(2)
52+
}
53+
err = app.OutputGlobalping(res)
54+
if err != nil {
55+
logger.Error("Error outputting globalping measurement", "error", err)
56+
os.Exit(2)
57+
}
58+
return
59+
}
60+
4661
if cfg.reverseLookup {
4762
app.ReverseLookup()
4863
}
@@ -121,6 +136,9 @@ func setupFlags() *flag.FlagSet {
121136
f.StringSliceP("nameserver", "n", []string{}, "Address of the nameserver to send packets to")
122137
f.BoolP("reverse", "x", false, "Performs a DNS Lookup for an IPv4 or IPv6 address")
123138

139+
f.String("from", "", "Probe locations as a comma-separated list")
140+
f.Int("limit", 1, "Limit the number of responses")
141+
124142
f.DurationP("timeout", "T", 5*time.Second, "Sets the timeout for a query")
125143
f.Bool("search", true, "Use the search list provided in resolv.conf")
126144
f.Int("ndots", -1, "Specify the ndots parameter")
@@ -162,7 +180,12 @@ func parseAndLoadFlags(f *flag.FlagSet) error {
162180
}
163181

164182
func initializeApp(logger *slog.Logger, cfg *config) *app.App {
165-
app := app.New(logger, buildVersion)
183+
globlpingClient := globalping.NewClient(globalping.Config{
184+
APIURL: "https://api.globalping.io/v1",
185+
APIToken: os.Getenv("GLOBALPING_TOKEN"),
186+
})
187+
188+
app := app.New(logger, globlpingClient, buildVersion)
166189

167190
if err := k.Unmarshal("", &app.QueryFlags); err != nil {
168191
logger.Error("Error loading args", "error", err)

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/ameshkov/dnsstamps v1.0.3
88
github.com/fatih/color v1.17.0
99
github.com/go-chi/chi/v5 v5.1.0
10+
github.com/jsdelivr/globalping-cli v1.3.1-0.20240717104136-2edb7127957b
1011
github.com/knadh/koanf/parsers/toml v0.1.0
1112
github.com/knadh/koanf/providers/env v0.1.0
1213
github.com/knadh/koanf/providers/file v1.0.0
@@ -23,6 +24,7 @@ require (
2324
github.com/AdguardTeam/golibs v0.24.1 // indirect
2425
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
2526
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
27+
github.com/andybalholm/brotli v1.1.0 // indirect
2628
github.com/fsnotify/fsnotify v1.7.0 // indirect
2729
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
2830
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP
88
github.com/ameshkov/dnscrypt/v2 v2.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE=
99
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
1010
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
11+
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
12+
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
1113
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1214
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1315
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
@@ -26,6 +28,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
2628
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2729
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg=
2830
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
31+
github.com/jsdelivr/globalping-cli v1.3.1-0.20240717104136-2edb7127957b h1:ZL7LfEaU+P2r6/Lxo99Qt6qw4Mb3BXt5UB4r+RoI6LA=
32+
github.com/jsdelivr/globalping-cli v1.3.1-0.20240717104136-2edb7127957b/go.mod h1:2+lO4/xYSauKsf+pZ62bro1c4StxDO3cYcrLx4jsYmI=
2933
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
3034
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
3135
github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=

internal/app/app.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package app
33
import (
44
"log/slog"
55

6+
"github.com/jsdelivr/globalping-cli/globalping"
67
"github.com/miekg/dns"
78
"github.com/mr-karan/doggo/pkg/models"
89
"github.com/mr-karan/doggo/pkg/resolvers"
@@ -17,10 +18,16 @@ type App struct {
1718
Resolvers []resolvers.Resolver
1819
ResolverOpts resolvers.Options
1920
Nameservers []models.Nameserver
21+
22+
globalping globalping.Client
2023
}
2124

2225
// NewApp initializes an instance of App which holds app wide configuration.
23-
func New(logger *slog.Logger, buildVersion string) App {
26+
func New(
27+
logger *slog.Logger,
28+
globalping globalping.Client,
29+
buildVersion string,
30+
) App {
2431
app := App{
2532
Logger: logger,
2633
Version: buildVersion,
@@ -31,6 +38,7 @@ func New(logger *slog.Logger, buildVersion string) App {
3138
Nameservers: []string{},
3239
},
3340
Nameservers: []models.Nameserver{},
41+
globalping: globalping,
3442
}
3543
return app
3644
}

internal/app/globalping.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package app
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"strings"
8+
"time"
9+
10+
"github.com/fatih/color"
11+
"github.com/jsdelivr/globalping-cli/globalping"
12+
"github.com/olekukonko/tablewriter"
13+
)
14+
15+
var (
16+
ErrTargetIPVersionNotAllowed = errors.New("ipVersion is not allowed when target is not a domain")
17+
ErrResolverIPVersionNotAllowed = errors.New("ipVersion is not allowed when resolver is not a domain")
18+
)
19+
20+
func (app *App) GlobalpingMeasurement() (*globalping.Measurement, error) {
21+
target := app.QueryFlags.QNames[0]
22+
resolver := ""
23+
if len(app.QueryFlags.Nameservers) > 0 {
24+
resolver = app.QueryFlags.Nameservers[0]
25+
}
26+
27+
if app.QueryFlags.UseIPv4 || app.QueryFlags.UseIPv6 {
28+
if net.ParseIP(target) != nil {
29+
return nil, ErrTargetIPVersionNotAllowed
30+
}
31+
if resolver != "" && net.ParseIP(resolver) != nil {
32+
return nil, ErrResolverIPVersionNotAllowed
33+
}
34+
}
35+
36+
o := &globalping.MeasurementCreate{
37+
Type: "dns",
38+
Target: target,
39+
Limit: app.QueryFlags.Limit,
40+
Locations: parseGlobalpingLocations(app.QueryFlags.From),
41+
Options: &globalping.MeasurementOptions{
42+
// TODO: Add support for these flags.
43+
// Protocol: opts.Protocol,
44+
// Port: opts.Port,
45+
},
46+
}
47+
if app.QueryFlags.UseIPv4 {
48+
o.Options.IPVersion = globalping.IPVersion4
49+
} else if app.QueryFlags.UseIPv6 {
50+
o.Options.IPVersion = globalping.IPVersion6
51+
}
52+
if len(app.QueryFlags.Nameservers) > 0 {
53+
o.Options.Resolver = app.QueryFlags.Nameservers[0]
54+
}
55+
if len(app.QueryFlags.QTypes) > 0 {
56+
o.Options.Query = &globalping.QueryOptions{
57+
Type: app.QueryFlags.QTypes[0],
58+
}
59+
}
60+
res, err := app.globalping.CreateMeasurement(o)
61+
if err != nil {
62+
return nil, err
63+
}
64+
measurement, err := app.globalping.GetMeasurement(res.ID)
65+
if err != nil {
66+
return nil, err
67+
}
68+
for measurement.Status == globalping.StatusInProgress {
69+
time.Sleep(500 * time.Millisecond)
70+
measurement, err = app.globalping.GetMeasurement(res.ID)
71+
if err != nil {
72+
return nil, err
73+
}
74+
}
75+
76+
if measurement.Status != globalping.StatusFinished {
77+
return nil, &globalping.MeasurementError{
78+
Message: "measurement did not complete successfully",
79+
}
80+
}
81+
return measurement, nil
82+
}
83+
84+
// TODO: Add support for json output && short output
85+
func (app *App) OutputGlobalping(m *globalping.Measurement) error {
86+
// Disables colorized output if user specified.
87+
if !app.QueryFlags.Color {
88+
color.NoColor = true
89+
}
90+
91+
table := tablewriter.NewWriter(color.Output)
92+
header := []string{"Location", "Name", "Type", "Class", "TTL", "Address", "Nameserver"}
93+
94+
// Formatting options for the table.
95+
table.SetHeader(header)
96+
table.SetAutoWrapText(true)
97+
table.SetAutoFormatHeaders(true)
98+
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
99+
table.SetAlignment(tablewriter.ALIGN_LEFT)
100+
table.SetCenterSeparator("")
101+
table.SetColumnSeparator("")
102+
table.SetRowSeparator("")
103+
table.SetHeaderLine(false)
104+
table.SetBorder(false)
105+
table.SetTablePadding("\t") // pad with tabs
106+
table.SetNoWhiteSpace(true)
107+
108+
for i := range m.Results {
109+
table.Append([]string{getGlobalPingLocationText(&m.Results[i]), "", "", "", "", "", ""})
110+
answers, err := globalping.DecodeDNSAnswers(m.Results[i].Result.AnswersRaw)
111+
if err != nil {
112+
return err
113+
}
114+
resolver := m.Results[i].Result.Resolver
115+
for _, ans := range answers {
116+
typOut := getColoredType(ans.Type)
117+
output := []string{"", TerminalColorGreen(ans.Name), typOut, ans.Class, fmt.Sprintf("%ds", ans.TTL), ans.Value, resolver}
118+
table.Append(output)
119+
}
120+
}
121+
table.Render()
122+
return nil
123+
}
124+
125+
func parseGlobalpingLocations(from string) []globalping.Locations {
126+
if from == "" {
127+
return []globalping.Locations{
128+
{
129+
Magic: "world",
130+
},
131+
}
132+
}
133+
fromArr := strings.Split(from, ",")
134+
locations := make([]globalping.Locations, len(fromArr))
135+
for i, v := range fromArr {
136+
locations[i] = globalping.Locations{
137+
Magic: strings.TrimSpace(v),
138+
}
139+
}
140+
return locations
141+
}
142+
143+
func getGlobalPingLocationText(m *globalping.ProbeMeasurement) string {
144+
state := ""
145+
if m.Probe.State != "" {
146+
state = " (" + m.Probe.State + ")"
147+
}
148+
return m.Probe.City + state + ", " +
149+
m.Probe.Country + ", " +
150+
m.Probe.Continent + ", " +
151+
m.Probe.Network + " " +
152+
"(AS" + fmt.Sprint(m.Probe.ASN) + ")"
153+
}

internal/app/output.go

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import (
1111
"github.com/olekukonko/tablewriter"
1212
)
1313

14+
var (
15+
TerminalColorGreen = color.New(color.FgGreen, color.Bold).SprintFunc()
16+
TerminalColorBlue = color.New(color.FgBlue, color.Bold).SprintFunc()
17+
TerminalColorYellow = color.New(color.FgYellow, color.Bold).SprintFunc()
18+
TerminalColorCyan = color.New(color.FgCyan, color.Bold).SprintFunc()
19+
TerminalColorRed = color.New(color.FgRed, color.Bold).SprintFunc()
20+
TerminalColorMagenta = color.New(color.FgMagenta, color.Bold).SprintFunc()
21+
)
22+
1423
func (app *App) outputJSON(rsp []resolvers.Response) {
1524
jsonOutput := struct {
1625
Responses []resolvers.Response `json:"responses"`
@@ -36,15 +45,6 @@ func (app *App) outputShort(rsp []resolvers.Response) {
3645
}
3746

3847
func (app *App) outputTerminal(rsp []resolvers.Response) {
39-
var (
40-
green = color.New(color.FgGreen, color.Bold).SprintFunc()
41-
blue = color.New(color.FgBlue, color.Bold).SprintFunc()
42-
yellow = color.New(color.FgYellow, color.Bold).SprintFunc()
43-
cyan = color.New(color.FgCyan, color.Bold).SprintFunc()
44-
red = color.New(color.FgRed, color.Bold).SprintFunc()
45-
magenta = color.New(color.FgMagenta, color.Bold).SprintFunc()
46-
)
47-
4848
// Disables colorized output if user specified.
4949
if !app.QueryFlags.Color {
5050
color.NoColor = true
@@ -92,57 +92,60 @@ func (app *App) outputTerminal(rsp []resolvers.Response) {
9292

9393
for _, r := range rsp {
9494
for _, ans := range r.Answers {
95-
var typOut string
96-
switch typ := ans.Type; typ {
97-
case "A":
98-
typOut = blue(ans.Type)
99-
case "AAAA":
100-
typOut = blue(ans.Type)
101-
case "MX":
102-
typOut = magenta(ans.Type)
103-
case "NS":
104-
typOut = cyan(ans.Type)
105-
case "CNAME":
106-
typOut = yellow(ans.Type)
107-
case "TXT":
108-
typOut = yellow(ans.Type)
109-
case "SOA":
110-
typOut = red(ans.Type)
111-
default:
112-
typOut = blue(ans.Type)
113-
}
114-
output := []string{green(ans.Name), typOut, ans.Class, ans.TTL, ans.Address, ans.Nameserver}
95+
typOut := getColoredType(ans.Type)
96+
output := []string{TerminalColorGreen(ans.Name), typOut, ans.Class, ans.TTL, ans.Address, ans.Nameserver}
11597
// Print how long it took
11698
if app.QueryFlags.DisplayTimeTaken {
11799
output = append(output, ans.RTT)
118100
}
119101
if outputStatus {
120-
output = append(output, red(ans.Status))
102+
output = append(output, TerminalColorRed(ans.Status))
121103
}
122104
table.Append(output)
123105
}
124106
for _, auth := range r.Authorities {
125107
var typOut string
126108
switch typ := auth.Type; typ {
127109
case "SOA":
128-
typOut = red(auth.Type)
110+
typOut = TerminalColorRed(auth.Type)
129111
default:
130-
typOut = blue(auth.Type)
112+
typOut = TerminalColorBlue(auth.Type)
131113
}
132-
output := []string{green(auth.Name), typOut, auth.Class, auth.TTL, auth.MName, auth.Nameserver}
114+
output := []string{TerminalColorGreen(auth.Name), typOut, auth.Class, auth.TTL, auth.MName, auth.Nameserver}
133115
// Print how long it took
134116
if app.QueryFlags.DisplayTimeTaken {
135117
output = append(output, auth.RTT)
136118
}
137119
if outputStatus {
138-
output = append(output, red(auth.Status))
120+
output = append(output, TerminalColorRed(auth.Status))
139121
}
140122
table.Append(output)
141123
}
142124
}
143125
table.Render()
144126
}
145127

128+
func getColoredType(t string) string {
129+
switch t {
130+
case "A":
131+
return TerminalColorBlue(t)
132+
case "AAAA":
133+
return TerminalColorBlue(t)
134+
case "MX":
135+
return TerminalColorMagenta(t)
136+
case "NS":
137+
return TerminalColorCyan(t)
138+
case "CNAME":
139+
return TerminalColorYellow(t)
140+
case "TXT":
141+
return TerminalColorYellow(t)
142+
case "SOA":
143+
return TerminalColorRed(t)
144+
default:
145+
return TerminalColorBlue(t)
146+
}
147+
}
148+
146149
// Output takes a list of `dns.Answers` and based
147150
// on the output format specified displays the information.
148151
func (app *App) Output(responses []resolvers.Response) {

0 commit comments

Comments
 (0)