Repository: cloudprober/cloudprober

Issue:
[singlerun] Add support for JSON format results.


Discussion:
@manugarg: #1124 will implement it.

Solution patch:
diff --git a/cmd/cloudprober/main.go b/cmd/cloudprober/main.go
index aa05eb9c..138d0fcb 100644
--- a/cmd/cloudprober/main.go
+++ b/cmd/cloudprober/main.go
@@ -49,8 +49,8 @@ var (
 	dumpConfig       = flag.Bool("dumpconfig", false, "Dump processed config to stdout")
 	dumpConfigFormat = flag.String("dumpconfig_fmt", "textpb", "Dump config format (textpb, json, yaml)")
 	runOnce          = flag.Bool("run_once", false, "Run a single probe and exit")
-	runOnceOutFormat = flag.String("run_once_out_fmt", "text", "Run once output format (text, json)")
-	runOnceOutIndent = flag.String("run_once_out_indent", "  ", "Run once output indent")
+	runOnceOutFormat = flag.String("run_once_output_format", "text", "Run once output format (text, json)")
+	runOnceOutIndent = flag.String("run_once_output_indent", "  ", "Run once output indent")
 )
 
 // These variables get overwritten by using -ldflags="-X main.<var>=<value?" at
diff --git a/metrics/singlerun/singlerun.go b/metrics/singlerun/singlerun.go
index 08e290c3..6849d5b5 100644
--- a/metrics/singlerun/singlerun.go
+++ b/metrics/singlerun/singlerun.go
@@ -16,6 +16,7 @@
 package singlerun
 
 import (
+	"encoding/json"
 	"fmt"
 	"sort"
 	"strings"
@@ -82,8 +83,51 @@ func textFormatProbeRunResults(probeResults map[string][]*ProbeRunResult, indent
 	return strings.Join(out, "\n")
 }
 
-func jsonFormatProbeRunResults(_ map[string][]*ProbeRunResult) string {
-	return "json format not supported yet"
+func jsonFormatProbeRunResults(probeResults map[string][]*ProbeRunResult, indent string) string {
+	type probeTargetResult struct {
+		Success bool     `json:"success"`
+		Latency string   `json:"latency,omitempty"`
+		Error   string   `json:"error,omitempty"`
+		Metrics []string `json:"metrics,omitempty"`
+	}
+
+	results := make(map[string]map[string]probeTargetResult)
+
+	for name := range probeResults {
+		results[name] = make(map[string]probeTargetResult)
+
+		for _, prr := range probeResults[name] {
+			tr := probeTargetResult{
+				Success: prr.Success,
+			}
+
+			if prr.Latency > 0 {
+				tr.Latency = prr.Latency.String()
+			}
+
+			if prr.Error != nil {
+				tr.Error = prr.Error.Error()
+			}
+
+			if len(prr.Metrics) > 0 {
+				tr.Metrics = make([]string, 0, len(prr.Metrics))
+				for _, m := range prr.Metrics {
+					tr.Metrics = append(tr.Metrics, m.String())
+				}
+			}
+
+			results[name][prr.Target.Dst()] = tr
+		}
+	}
+
+	// TODO(manugarg): Migrate to encoding/json/v2 once it's available.
+	// v2's marshaler provides an option to make output deterministic.
+	jsonData, err := json.MarshalIndent(results, "", indent)
+	if err != nil {
+		return fmt.Sprintf("{\"error\": \"%v\"}", err)
+	}
+
+	return string(jsonData)
 }
 
 func FormatProbeRunResults(probeResults map[string][]*ProbeRunResult, format Format, indent string) string {
@@ -91,7 +135,7 @@ func FormatProbeRunResults(probeResults map[string][]*ProbeRunResult, format For
 	case FormatText:
 		return textFormatProbeRunResults(probeResults, indent)
 	case FormatJSON:
-		return jsonFormatProbeRunResults(probeResults)
+		return jsonFormatProbeRunResults(probeResults, indent)
 	}
 	return ""
 }



Test patch:
diff --git a/metrics/singlerun/singlerun_test.go b/metrics/singlerun/singlerun_test.go
index 75c64274..a5549d82 100644
--- a/metrics/singlerun/singlerun_test.go
+++ b/metrics/singlerun/singlerun_test.go
@@ -61,13 +61,31 @@ Probe: ping
 
 `
 
-func TestTextFormatProbeRunResults(t *testing.T) {
-	t.Helper()
+var expectedJsonFormatOutput = `{
+  "http": {
+    "1.2.3.4": {
+      "success": false,
+      "latency": "123ms",
+      "error": "connection timeout"
+    }
+  },
+  "ping": {
+    "1.2.3.4": {
+      "success": true,
+      "latency": "123ms",
+      "metrics": [
+        "%s"
+      ]
+    }
+  }
+}`
 
+func TestTextFormatProbeRunResults(t *testing.T) {
 	em := metrics.NewEventMetrics(time.Now()).AddMetric("rtt", metrics.NewFloat(123))
 	assert.Equal(t, fmt.Sprintf(expectedTextFormatOutput, em.String()), textFormatProbeRunResults(testPrrs(em), "  "))
 }
 
 func TestJsonFormatProbeRunResults(t *testing.T) {
-	assert.Equal(t, jsonFormatProbeRunResults(nil), "json format not supported yet")
+	em := metrics.NewEventMetrics(time.Now()).AddMetric("rtt", metrics.NewFloat(123))
+	assert.Equal(t, fmt.Sprintf(expectedJsonFormatOutput, em.String()), jsonFormatProbeRunResults(testPrrs(em), "  "))
 }