Skip to content

Commit 4ef15e2

Browse files
committed
Adding OpenMetrics report support
- added OpenMetrics reporter - added unit tests for OpenMetrics reporter - added `openmetrics` to usage help - updated usage and added description of OpenMetrics to `README.md` - extracted common language summary aggregation from `toCSVSummary` and `toJSON` into separate method `aggregateLanguageSummary` - renamed `golang.org/x/text/language` import to avoid warnings about naming conflicts with local variables
1 parent 9f2bb6b commit 4ef15e2

File tree

4 files changed

+358
-101
lines changed

4 files changed

+358
-101
lines changed

README.md

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ Flags:
207207
--debug enable debug output
208208
--exclude-dir strings directories to exclude (default [.git,.hg,.svn])
209209
--file-gc-count int number of files to parse before turning the GC on (default 10000)
210-
-f, --format string set output format [tabular, wide, json, csv, csv-stream, cloc-yaml, html, html-table, sql, sql-insert] (default "tabular")
210+
-f, --format string set output format [tabular, wide, json, csv, csv-stream, cloc-yaml, html, html-table, sql, sql-insert, openmetrics] (default "tabular")
211211
--format-multi string have multiple format output overriding --format [e.g. tabular:stdout,csv:file.csv,json:file.json]
212212
--gen identify generated files
213213
--generated-markers strings string markers in head of generated files (default [do not edit,<auto-generated />])
@@ -440,7 +440,7 @@ Note that in all cases if the remap rule does not apply normal #! rules will app
440440

441441
By default `scc` will output to the console. However you can produce output in other formats if you require.
442442

443-
The different options are `tabular, wide, json, csv, csv-stream, cloc-yaml, html, html-table, sql, sql-insert`.
443+
The different options are `tabular, wide, json, csv, csv-stream, cloc-yaml, html, html-table, sql, sql-insert, openmetrics`.
444444

445445
Note that you can write `scc` output to disk using the `-o, --output` option. This allows you to specify a file to
446446
write your output to. For example `scc -f html -o output.html` will run `scc` against the current directory, and output
@@ -589,6 +589,58 @@ sqlite3 code.db 'select project,file,max(nCode) as nL from t
589589
See the cloc documentation for more examples.
590590

591591

592+
#### OpenMetrics
593+
594+
[OpenMetrics](https://openmetrics.io/) is a metric reporting format specification extending the Prometheus exposition text format.
595+
596+
The produced output is natively supported by [Prometheus](https://prometheus.io/) and [GitLab CI](https://docs.gitlab.com/ee/ci/metrics_reports.html)
597+
598+
Note that OpenMetrics respects `--by-file` and as such will return a summary by default.
599+
600+
The output includes a metadata header containing definitions of the returned metrics:
601+
```text
602+
# TYPE scc_files count
603+
# HELP scc_files Number of sourcecode files.
604+
# TYPE scc_lines count
605+
# UNIT scc_lines lines
606+
# HELP scc_lines Number of lines.
607+
# TYPE scc_code count
608+
# UNIT scc_code lines
609+
# HELP scc_code Number of lines of actual code.
610+
# TYPE scc_comments count
611+
# HELP scc_comments Number of comments.
612+
# TYPE scc_blanks count
613+
# UNIT scc_blanks lines
614+
# HELP scc_blanks Number of blank lines.
615+
# TYPE scc_complexity count
616+
# UNIT scc_complexity lines
617+
# HELP scc_complexity Code complexity.
618+
# TYPE scc_bytes count
619+
# UNIT scc_bytes bytes
620+
# HELP scc_bytes Size in bytes.
621+
```
622+
623+
The header is followed by the metric data in either language summary form:
624+
```text
625+
scc_files{language="Go"} 1
626+
scc_lines{language="Go"} 1000
627+
scc_code{language="Go"} 1000
628+
scc_comments{language="Go"} 1000
629+
scc_blanks{language="Go"} 1000
630+
scc_complexity{language="Go"} 1000
631+
scc_bytes{language="Go"} 1000
632+
```
633+
634+
or, if `--by-file` is present, in per file form:
635+
```text
636+
scc_lines{language="Go", file="./bbbb.go"} 1000
637+
scc_code{language="Go", file="./bbbb.go"} 1000
638+
scc_comments{language="Go", file="./bbbb.go"} 1000
639+
scc_blanks{language="Go", file="./bbbb.go"} 1000
640+
scc_complexity{language="Go", file="./bbbb.go"} 1000
641+
scc_bytes{language="Go", file="./bbbb.go"} 1000
642+
```
643+
592644
### Performance
593645

594646
Generally `scc` will the fastest code counter compared to any I am aware of and have compared against. The below comparisons are taken from the fastest alternative counters. See `Other similar projects` above to see all of the other code counters compared against. It is designed to scale to as many CPU's cores as you can provide.

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func main() {
8787
"format",
8888
"f",
8989
"tabular",
90-
"set output format [tabular, wide, json, csv, csv-stream, cloc-yaml, html, html-table, sql, sql-insert]",
90+
"set output format [tabular, wide, json, csv, csv-stream, cloc-yaml, html, html-table, sql, sql-insert, openmetrics]",
9191
)
9292
flags.StringSliceVarP(
9393
&processor.AllowListExtensions,

processor/formatters.go

Lines changed: 125 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
"github.com/mattn/go-runewidth"
1919

20-
"golang.org/x/text/language"
20+
glanguage "golang.org/x/text/language"
2121
gmessage "golang.org/x/text/message"
2222
"gopkg.in/yaml.v2"
2323
)
@@ -42,6 +42,29 @@ var tabularWideFormatBody = "%-33s %9d %9d %8d %9d %8d %10d %16.2f\n"
4242
var tabularWideFormatFile = "%s %9d %8d %9d %8d %10d %16.2f\n"
4343
var wideFormatFileTruncate = 42
4444

45+
var openMetricsMetadata = `# TYPE scc_files count
46+
# HELP scc_files Number of sourcecode files.
47+
# TYPE scc_lines count
48+
# UNIT scc_lines lines
49+
# HELP scc_lines Number of lines.
50+
# TYPE scc_code count
51+
# UNIT scc_code lines
52+
# HELP scc_code Number of lines of actual code.
53+
# TYPE scc_comments count
54+
# HELP scc_comments Number of comments.
55+
# TYPE scc_blanks count
56+
# UNIT scc_blanks lines
57+
# HELP scc_blanks Number of blank lines.
58+
# TYPE scc_complexity count
59+
# UNIT scc_complexity lines
60+
# HELP scc_complexity Code complexity.
61+
# TYPE scc_bytes count
62+
# UNIT scc_bytes bytes
63+
# HELP scc_bytes Size in bytes.
64+
`
65+
var openMetricsSummaryRecordFormat = "scc_%s{language=\"%s\"} %d\n"
66+
var openMetricsFileRecordFormat = "scc_%s{language=\"%s\", file=\"%s\"} %d\n"
67+
4568
func sortSummaryFiles(summary *LanguageSummary) {
4669
switch {
4770
case SortBy == "name" || SortBy == "names" || SortBy == "language" || SortBy == "languages":
@@ -200,54 +223,7 @@ func toClocYAML(input chan *FileJob) string {
200223

201224
func toJSON(input chan *FileJob) string {
202225
startTime := makeTimestampMilli()
203-
languages := map[string]LanguageSummary{}
204-
205-
for res := range input {
206-
_, ok := languages[res.Language]
207-
208-
if !ok {
209-
files := []*FileJob{}
210-
if Files {
211-
files = append(files, res)
212-
}
213-
214-
languages[res.Language] = LanguageSummary{
215-
Name: res.Language,
216-
Lines: res.Lines,
217-
Code: res.Code,
218-
Comment: res.Comment,
219-
Blank: res.Blank,
220-
Complexity: res.Complexity,
221-
Count: 1,
222-
Files: files,
223-
Bytes: res.Bytes,
224-
}
225-
} else {
226-
tmp := languages[res.Language]
227-
files := tmp.Files
228-
if Files {
229-
files = append(files, res)
230-
}
231-
232-
languages[res.Language] = LanguageSummary{
233-
Name: res.Language,
234-
Lines: tmp.Lines + res.Lines,
235-
Code: tmp.Code + res.Code,
236-
Comment: tmp.Comment + res.Comment,
237-
Blank: tmp.Blank + res.Blank,
238-
Complexity: tmp.Complexity + res.Complexity,
239-
Count: tmp.Count + 1,
240-
Files: files,
241-
Bytes: res.Bytes + tmp.Bytes,
242-
}
243-
}
244-
}
245-
246-
language := []LanguageSummary{}
247-
for _, summary := range languages {
248-
language = append(language, summary)
249-
}
250-
226+
language := aggregateLanguageSummary(input)
251227
language = sortLanguageSummary(language)
252228

253229
jsonString, _ := json.Marshal(language)
@@ -268,53 +244,7 @@ func toCSV(input chan *FileJob) string {
268244
}
269245

270246
func toCSVSummary(input chan *FileJob) string {
271-
languages := map[string]LanguageSummary{}
272-
273-
for res := range input {
274-
_, ok := languages[res.Language]
275-
276-
if !ok {
277-
files := []*FileJob{}
278-
if Files {
279-
files = append(files, res)
280-
}
281-
282-
languages[res.Language] = LanguageSummary{
283-
Name: res.Language,
284-
Lines: res.Lines,
285-
Code: res.Code,
286-
Comment: res.Comment,
287-
Blank: res.Blank,
288-
Complexity: res.Complexity,
289-
Count: 1,
290-
Files: files,
291-
Bytes: res.Bytes,
292-
}
293-
} else {
294-
tmp := languages[res.Language]
295-
files := tmp.Files
296-
if Files {
297-
files = append(files, res)
298-
}
299-
300-
languages[res.Language] = LanguageSummary{
301-
Name: res.Language,
302-
Lines: tmp.Lines + res.Lines,
303-
Code: tmp.Code + res.Code,
304-
Comment: tmp.Comment + res.Comment,
305-
Blank: tmp.Blank + res.Blank,
306-
Complexity: tmp.Complexity + res.Complexity,
307-
Count: tmp.Count + 1,
308-
Files: files,
309-
Bytes: res.Bytes + tmp.Bytes,
310-
}
311-
}
312-
}
313-
314-
language := []LanguageSummary{}
315-
for _, summary := range languages {
316-
language = append(language, summary)
317-
}
247+
language := aggregateLanguageSummary(input)
318248
language = sortLanguageSummary(language)
319249

320250
records := [][]string{{
@@ -380,6 +310,47 @@ func toCSVFiles(input chan *FileJob) string {
380310
return b.String()
381311
}
382312

313+
func toOpenMetrics(input chan *FileJob) string {
314+
if Files {
315+
return toOpenMetricsFiles(input)
316+
}
317+
318+
return toOpenMetricsSummary(input)
319+
}
320+
321+
func toOpenMetricsSummary(input chan *FileJob) string {
322+
language := aggregateLanguageSummary(input)
323+
language = sortLanguageSummary(language)
324+
325+
var sb strings.Builder
326+
sb.WriteString(openMetricsMetadata)
327+
for _, result := range language {
328+
sb.WriteString(fmt.Sprintf(openMetricsSummaryRecordFormat, "files", result.Name, result.Count))
329+
sb.WriteString(fmt.Sprintf(openMetricsSummaryRecordFormat, "lines", result.Name, result.Lines))
330+
sb.WriteString(fmt.Sprintf(openMetricsSummaryRecordFormat, "code", result.Name, result.Code))
331+
sb.WriteString(fmt.Sprintf(openMetricsSummaryRecordFormat, "comments", result.Name, result.Comment))
332+
sb.WriteString(fmt.Sprintf(openMetricsSummaryRecordFormat, "blanks", result.Name, result.Blank))
333+
sb.WriteString(fmt.Sprintf(openMetricsSummaryRecordFormat, "complexity", result.Name, result.Complexity))
334+
sb.WriteString(fmt.Sprintf(openMetricsSummaryRecordFormat, "bytes", result.Name, result.Bytes))
335+
}
336+
return sb.String()
337+
}
338+
339+
func toOpenMetricsFiles(input chan *FileJob) string {
340+
var sb strings.Builder
341+
sb.WriteString(openMetricsMetadata)
342+
for file := range input {
343+
var filename = strings.ReplaceAll(file.Location, "\\", "\\\\")
344+
sb.WriteString(fmt.Sprintf(openMetricsFileRecordFormat, "lines", file.Language, filename, file.Lines))
345+
sb.WriteString(fmt.Sprintf(openMetricsFileRecordFormat, "code", file.Language, filename, file.Code))
346+
sb.WriteString(fmt.Sprintf(openMetricsFileRecordFormat, "comments", file.Language, filename, file.Comment))
347+
sb.WriteString(fmt.Sprintf(openMetricsFileRecordFormat, "blanks", file.Language, filename, file.Blank))
348+
sb.WriteString(fmt.Sprintf(openMetricsFileRecordFormat, "complexity", file.Language, filename, file.Complexity))
349+
sb.WriteString(fmt.Sprintf(openMetricsFileRecordFormat, "bytes", file.Language, filename, file.Bytes))
350+
}
351+
return sb.String()
352+
}
353+
383354
// For very large repositories CSV stream can be used which prints results out as they come in
384355
// with the express idea of lowering memory usage, see https://github.com/boyter/scc/issues/210 for
385356
// the background on why this might be needed
@@ -610,6 +581,8 @@ func fileSummarize(input chan *FileJob) string {
610581
return toSql(input)
611582
case strings.ToLower(Format) == "sql-insert":
612583
return toSqlInsert(input)
584+
case strings.ToLower(Format) == "openmetrics":
585+
return toOpenMetrics(input)
613586
}
614587

615588
return fileSummarizeShort(input)
@@ -665,6 +638,8 @@ func fileSummarizeMulti(input chan *FileJob) string {
665638
val = toSql(i)
666639
case "sql-insert":
667640
val = toSqlInsert(i)
641+
case "openmetrics":
642+
val = toOpenMetrics(i)
668643
}
669644

670645
if t[1] == "stdout" {
@@ -1000,7 +975,7 @@ func calculateCocomoSLOCCount(sumCode int64, str *strings.Builder) {
1000975
estimatedPeopleRequired := estimatedEffort / estimatedScheduleMonths
1001976
estimatedCost := EstimateCost(estimatedEffort, AverageWage, Overhead)
1002977

1003-
p := gmessage.NewPrinter(language.Make(os.Getenv("LANG")))
978+
p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))
1004979

1005980
str.WriteString(p.Sprintf("Total Physical Source Lines of Code (SLOC) = %d\n", sumCode))
1006981
str.WriteString(p.Sprintf("Development Effort Estimate, Person-Years (Person-Months) = %.2f (%.2f)\n", estimatedEffort/12, estimatedEffort))
@@ -1018,7 +993,7 @@ func calculateCocomo(sumCode int64, str *strings.Builder) {
1018993
estimatedScheduleMonths := EstimateScheduleMonths(estimatedEffort)
1019994
estimatedPeopleRequired := estimatedEffort / estimatedScheduleMonths
1020995

1021-
p := gmessage.NewPrinter(language.Make(os.Getenv("LANG")))
996+
p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))
1022997

1023998
str.WriteString(p.Sprintf("Estimated Cost to Develop (%s) %s%d\n", CocomoProjectType, CurrencySymbol, int64(estimatedCost)))
1024999
str.WriteString(p.Sprintf("Estimated Schedule Effort (%s) %.2f months\n", CocomoProjectType, estimatedScheduleMonths))
@@ -1093,6 +1068,58 @@ func isLeapYear(year int) bool {
10931068
return leapFlag
10941069
}
10951070

1071+
func aggregateLanguageSummary(input chan *FileJob) []LanguageSummary {
1072+
languages := map[string]LanguageSummary{}
1073+
1074+
for res := range input {
1075+
_, ok := languages[res.Language]
1076+
1077+
if !ok {
1078+
var files []*FileJob
1079+
if Files {
1080+
files = append(files, res)
1081+
}
1082+
1083+
languages[res.Language] = LanguageSummary{
1084+
Name: res.Language,
1085+
Lines: res.Lines,
1086+
Code: res.Code,
1087+
Comment: res.Comment,
1088+
Blank: res.Blank,
1089+
Complexity: res.Complexity,
1090+
Count: 1,
1091+
Files: files,
1092+
Bytes: res.Bytes,
1093+
}
1094+
} else {
1095+
tmp := languages[res.Language]
1096+
files := tmp.Files
1097+
if Files {
1098+
files = append(files, res)
1099+
}
1100+
1101+
languages[res.Language] = LanguageSummary{
1102+
Name: res.Language,
1103+
Lines: tmp.Lines + res.Lines,
1104+
Code: tmp.Code + res.Code,
1105+
Comment: tmp.Comment + res.Comment,
1106+
Blank: tmp.Blank + res.Blank,
1107+
Complexity: tmp.Complexity + res.Complexity,
1108+
Count: tmp.Count + 1,
1109+
Files: files,
1110+
Bytes: res.Bytes + tmp.Bytes,
1111+
}
1112+
}
1113+
}
1114+
1115+
var language []LanguageSummary
1116+
for _, summary := range languages {
1117+
language = append(language, summary)
1118+
}
1119+
1120+
return language
1121+
}
1122+
10961123
func sortLanguageSummary(language []LanguageSummary) []LanguageSummary {
10971124
// Cater for the common case of adding plural even for those options that don't make sense
10981125
// as its quite common for those who English is not a first language to make a simple mistake

0 commit comments

Comments
 (0)