Skip to content

Commit 2d5df0b

Browse files
author
Youen Péron
authored
Merge pull request #197 from CGI-FR/196-proposal-add-a-command-to-generate-json-dump-file-with-execution-stats
feat(stats): adds a command to generate a stat file
2 parents b8aefc9 + b3c0227 commit 2d5df0b

File tree

5 files changed

+160
-19
lines changed

5 files changed

+160
-19
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ This takes the `data.json` file, masks the data contained inside it and put the
171171
* `--mask` Declare a simple masking definition in command line (minified YAML format: `--mask "value={fluxUri: 'pimo://nameFR'}"`, or `--mask "value=[{add: ''},{fluxUri: 'pimo://nameFR'}]"` for multiple masks). For advanced use case (e.g. if caches needed) `masking.yml` file definition will be preferred.
172172
* `--repeat-until <condition>` This flag will make PIMO keep masking every input until the condition is met. Condition format is using [Template](https://pkg.go.dev/text/template). Last output verifies the condition.
173173
* `--repeat-while <condition>` This flag will make PIMO keep masking every input while the condition is met. Condition format is using [Template](https://pkg.go.dev/text/template).
174+
* `--stats <filename | url>` This flag either outputs run statistics to the specified file or send them to specified url (has to start with `http` or `https`).
175+
* `--statsTemplate <string>` This flag will have PIMO use the value as a template to generate statistics. Please use go templating format to include statistics. To include them you have to specify them as `{{ .Stats }}`. (i.e. `{"software":"PIMO","stats":{{ .Stats }}}`)
174176

175177
### PIMO Play
176178

cmd/pimo/main.go

100755100644
Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,20 @@
1818
package main
1919

2020
import (
21+
"bytes"
2122
"fmt"
23+
netHttp "net/http"
2224
"os"
2325
"runtime"
2426
"strings"
27+
"text/template"
2528
"time"
2629

2730
over "github.com/adrienaury/zeromdc"
2831
"github.com/cgi-fr/pimo/internal/app/pimo"
2932
"github.com/cgi-fr/pimo/pkg/flow"
3033
"github.com/cgi-fr/pimo/pkg/model"
34+
"github.com/cgi-fr/pimo/pkg/statistics"
3135
"github.com/mattn/go-isatty"
3236
"github.com/rs/zerolog"
3337
"github.com/rs/zerolog/log"
@@ -42,21 +46,23 @@ var (
4246
buildDate string
4347
builtBy string
4448

45-
verbosity string
46-
debug bool
47-
jsonlog bool
48-
colormode string
49-
iteration int
50-
emptyInput bool
51-
maskingFile string
52-
cachesToDump map[string]string
53-
cachesToLoad map[string]string
54-
skipLineOnError bool
55-
skipFieldOnError bool
56-
seedValue int64
57-
maskingOneLiner []string
58-
repeatUntil string
59-
repeatWhile string
49+
verbosity string
50+
debug bool
51+
jsonlog bool
52+
colormode string
53+
iteration int
54+
emptyInput bool
55+
maskingFile string
56+
cachesToDump map[string]string
57+
cachesToLoad map[string]string
58+
skipLineOnError bool
59+
skipFieldOnError bool
60+
seedValue int64
61+
maskingOneLiner []string
62+
repeatUntil string
63+
repeatWhile string
64+
statisticsDestination string
65+
statsTemplate string
6066
)
6167

6268
func main() {
@@ -89,6 +95,8 @@ There is NO WARRANTY, to the extent permitted by law.`, version, commit, buildDa
8995
rootCmd.PersistentFlags().StringArrayVarP(&maskingOneLiner, "mask", "m", []string{}, "one liner masking")
9096
rootCmd.PersistentFlags().StringVar(&repeatUntil, "repeat-until", "", "mask each input repeatedly until the given condition is met")
9197
rootCmd.PersistentFlags().StringVar(&repeatWhile, "repeat-while", "", "mask each input repeatedly while the given condition is met")
98+
rootCmd.PersistentFlags().StringVar(&statisticsDestination, "stats", "", "generate execution statistics in the specified dump file")
99+
rootCmd.PersistentFlags().StringVar(&statsTemplate, "statsTemplate", "", "template string to format stats (to include them you have to specify them as `{{ .Stats }}` like `{\"software\":\"PIMO\",\"stats\":{{ .Stats }}}`)")
92100

93101
rootCmd.AddCommand(&cobra.Command{
94102
Use: "jsonschema",
@@ -186,14 +194,18 @@ func run() {
186194
os.Exit(1)
187195
}
188196

197+
startTime := time.Now()
198+
189199
stats, err := ctx.Execute(os.Stdout)
190200
if err != nil {
191201
log.Err(err).Msg("Cannot execute pipeline")
192202
log.Warn().Int("return", stats.GetErrorCode()).Msg("End PIMO")
193203
os.Exit(stats.GetErrorCode())
194204
}
195205

196-
log.Info().RawJSON("stats", stats.ToJSON()).Int("return", 0).Msg("End PIMO")
206+
duration := time.Since(startTime)
207+
statistics.SetDuration(duration)
208+
dumpStats(stats)
197209
os.Exit(0)
198210
}
199211

@@ -240,3 +252,58 @@ func initLog() {
240252
over.MDC().Set("config", maskingFile)
241253
over.SetGlobalFields([]string{"config"})
242254
}
255+
256+
func dumpStats(stats statistics.ExecutionStats) {
257+
statsToWrite := stats.ToJSON()
258+
if statsTemplate != "" {
259+
tmpl, err := template.New("statsTemplate").Parse(statsTemplate)
260+
if err != nil {
261+
log.Error().Err(err).Msg(("Error parsing statistics template"))
262+
os.Exit(1)
263+
}
264+
var output bytes.Buffer
265+
err = tmpl.ExecuteTemplate(&output, "statsTemplate", Stats{Stats: string(stats.ToJSON())})
266+
if err != nil {
267+
log.Error().Err(err).Msg("Error adding stats to template")
268+
os.Exit(1)
269+
}
270+
statsToWrite = output.Bytes()
271+
}
272+
if statisticsDestination != "" {
273+
if strings.HasPrefix(statisticsDestination, "http") {
274+
sendMetrics(statisticsDestination, statsToWrite)
275+
} else {
276+
writeMetricsToFile(statisticsDestination, statsToWrite)
277+
}
278+
}
279+
280+
log.Info().RawJSON("stats", stats.ToJSON()).Int("return", 0).Msg("End PIMO")
281+
}
282+
283+
func writeMetricsToFile(statsFile string, statsByte []byte) {
284+
file, err := os.Create(statsFile)
285+
if err != nil {
286+
log.Error().Err(err).Msg("Error generating statistics dump file")
287+
}
288+
defer file.Close()
289+
290+
_, err = file.Write(statsByte)
291+
if err != nil {
292+
log.Error().Err(err).Msg("Error writing statistics to dump file")
293+
}
294+
log.Info().Msgf("Statistics exported to file %s", file.Name())
295+
}
296+
297+
func sendMetrics(statsDestination string, statsByte []byte) {
298+
requestBody := bytes.NewBuffer(statsByte)
299+
// nolint: gosec
300+
_, err := netHttp.Post(statsDestination, "application/json", requestBody)
301+
if err != nil {
302+
log.Error().Err(err).Msgf("An error occurred trying to send metrics to %s", statsDestination)
303+
}
304+
log.Info().Msgf("Statistics sent to %s", statsDestination)
305+
}
306+
307+
type Stats struct {
308+
Stats interface{} `json:"stats"`
309+
}

pkg/statistics/statistics.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package statistics
22

33
import (
44
"encoding/json"
5+
"time"
56

67
over "github.com/adrienaury/zeromdc"
78
"github.com/rs/zerolog/log"
@@ -14,15 +15,17 @@ type ExecutionStats interface {
1415
GetIgnoredPathsCount() int // counter for path not found in data
1516
GetIgnoredLinesCount() int // counter for line skipped (flag --skip-line-on-error)
1617
GetIgnoredFieldsCount() int // counter for field skipped (flag --skip-field-on-error)
18+
GetDuration() time.Duration
1719

1820
ToJSON() []byte
1921
}
2022

2123
type stats struct {
2224
errorCode int
23-
IgnoredPathsCounter int `json:"ignoredPaths"`
24-
IgnoredLinesCounter int `json:"skippedLines"`
25-
IgnoredFieldsCounter int `json:"skippedFields"`
25+
IgnoredPathsCounter int `json:"ignoredPaths"`
26+
IgnoredLinesCounter int `json:"skippedLines"`
27+
IgnoredFieldsCounter int `json:"skippedFields"`
28+
Duration time.Duration `json:"duration"`
2629
}
2730

2831
// Reset all statistics to zero
@@ -101,3 +104,12 @@ func getStats() *stats {
101104
log.Warn().Msg("Statistics uncorrectly initialized")
102105
return &stats{}
103106
}
107+
108+
func SetDuration(duration time.Duration) {
109+
stats := getStats()
110+
stats.Duration = duration
111+
}
112+
113+
func (s *stats) GetDuration() time.Duration {
114+
return s.Duration
115+
}

test/httpmock/default.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"httpRequest": {
4+
"path": "/api/v1/stats"
5+
},
6+
"httpResponse": {
7+
"statusCode": 200
8+
}
9+
}
10+
]

test/suites/dump-statistics.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: dump statistics file features
2+
testcases:
3+
- name: dump file
4+
steps:
5+
- script: rm -f masking.yml
6+
- script: rm -f pimo-stats.json
7+
- script: |-
8+
cat > masking.yml <<EOF
9+
version: "1"
10+
masking:
11+
- selector:
12+
jsonpath: "name"
13+
mask:
14+
add: "Dorothy"
15+
EOF
16+
- script: |-
17+
echo '{ "name": "John" }' | pimo -v 3 --stats pimo-stats.json
18+
assertions:
19+
- result.code ShouldEqual 0
20+
- result.systemerr ShouldContainSubstring Statistics exported to file
21+
- script: cat pimo-stats.json
22+
assertions:
23+
- result.code ShouldEqual 0
24+
- result.systemout ShouldContainSubstring "ignoredPaths":0
25+
- result.systemout ShouldContainSubstring duration
26+
27+
- name: dump file with template
28+
steps:
29+
- script: rm -f masking.yml
30+
- script: rm -f pimo-stats-template.json
31+
- script: |-
32+
cat > masking.yml <<EOF
33+
version: "1"
34+
masking:
35+
- selector:
36+
jsonpath: "name"
37+
mask:
38+
add: "Dorothy"
39+
EOF
40+
- script: |-
41+
echo '{ "name": "John" }' | pimo -v 3 --log-json --stats pimo-stats-template.json --statsTemplate '{"software":"PIMO","stats":{{ .Stats }}}'
42+
assertions:
43+
- result.code ShouldEqual 0
44+
- result.systemerr ShouldContainSubstring Statistics exported to file
45+
- script: cat pimo-stats-template.json
46+
assertions:
47+
- result.code ShouldEqual 0
48+
- result.systemout ShouldContainSubstring {"software":"PIMO","stats"
49+
- result.systemout ShouldContainSubstring ignoredPaths":0
50+
- result.systemout ShouldContainSubstring duration

0 commit comments

Comments
 (0)