Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5998871
PrometheusFileReporter.js
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
9 KB
Referenced Files
None
Subscribers
None
PrometheusFileReporter.js
View Options
import
{
readdirSync
,
readFileSync
,
writeFileSync
,
rmSync
}
from
'fs'
;
import
path
from
'path'
;
import
WDIOReporter
from
'@wdio/reporter'
;
/**
* Clean a string into a valid Prometheus tag name.
* Replaces all non-word characters with underscores and lower-cases the result.
*
* @param {string} name - Original tag name.
* @return {string} A cleaned, lowercase string for Prometheus labels.
*/
const
getValidPrometheusTagName
=
(
name
)
=>
name
.
replace
(
/\W+/g
,
'_'
).
toLowerCase
();
/**
* Formats a single Prometheus text format line.
*
* @param {string} name - Metric name (must already follow Prometheus naming conventions).
* @param {number|string} value - The metric value.
* @param {Object.<string, string>} [labels={}] - Optional key/value map of label names/values.
* @return {string} A line in the Prometheus text‐based text format, e.g.
* wdio_test_duration_seconds{project="core",name="test_name"} 12
*/
function
formatMetric
(
name
,
value
,
labels
=
{}
)
{
const
keys
=
Object
.
keys
(
labels
);
let
labelsAsString
=
''
;
if
(
keys
.
length
>
0
)
{
const
parts
=
keys
.
map
(
(
key
)
=>
`
${
key
}
="
${
labels
[
key
]
}
"`
);
labelsAsString
=
`{
${
parts
.
join
(
', '
)
}
}`
;
}
return
`
${
name
}${
labelsAsString
}
${
value
}
`
;
}
/**
* Sums up the durations of all suites to produce a spec-level total.
*
* @param {Array.<{duration: number}>} suiteMetrics - Array of suites with duration.
* @return {number} Total duration in seconds across all suites.
*/
function
getSpecDuration
(
suiteMetrics
)
{
let
specDuration
=
0
;
for
(
const
{
duration
}
of
suiteMetrics
)
{
specDuration
+=
duration
;
}
return
specDuration
.
toFixed
(
3
);
}
/**
* The PrometheusFileReporter writes webdriver.io test data
* to a file following the Prometheus text based format:
* https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md
*
* The reporter collects data on a project and individual tests level.
* Metrics are reported as Gauges to be consumed by a Prometheus PushGateway.
* We use Gauges since counters will just be replaced in the Pushgateway.
*
* The Reporter is a custom reporter built upon WDIOReporter.
*
*/
class
PrometheusFileReporter
extends
WDIOReporter
{
constructor
(
options
=
{}
)
{
super
(
{
...
options
}
);
this
.
labels
=
{};
// Make sure the tags is ok for Prometheus.
for
(
const
[
tagName
,
tagValue
]
of
Object
.
entries
(
options
.
tags
||
{}
)
)
{
const
cleanKey
=
getValidPrometheusTagName
(
tagName
);
const
cleanValue
=
tagValue
!==
undefined
?
getValidPrometheusTagName
(
tagValue
)
:
''
;
this
.
labels
[
cleanKey
]
=
cleanValue
;
}
this
.
testMetrics
=
{};
this
.
suiteMetrics
=
[];
this
.
outputDir
=
options
.
outputDir
;
this
.
getFileName
=
options
.
outputFileName
;
this
.
spec
=
{
passed
:
0
,
failed
:
0
,
skipped
:
0
,
retries
:
0
,
totalTests
:
0
};
}
onSuiteEnd
(
suite
)
{
// We collect total suite time to make it easier to get
// correct project run time.
if
(
suite
.
start
&&
suite
.
end
)
{
const
durationSec
=
(
suite
.
end
-
suite
.
start
)
/
1000
;
const
suiteName
=
getValidPrometheusTagName
(
suite
.
title
);
this
.
suiteMetrics
.
push
(
{
name
:
suiteName
,
duration
:
durationSec
}
);
}
}
onTestStart
(
test
)
{
if
(
!
this
.
testMetrics
[
test
.
uid
]
)
{
this
.
testMetrics
[
test
.
uid
]
=
{
name
:
getValidPrometheusTagName
(
test
.
title
),
suite
:
test
.
parent
?
getValidPrometheusTagName
(
test
.
parent
)
:
'unknown'
,
passed
:
0
,
failed
:
0
,
skipped
:
0
,
retries
:
0
,
maxDuration
:
0
};
this
.
spec
.
totalTests
++
;
}
}
onTestPass
(
test
)
{
const
testDurationInSeconds
=
(
test
.
end
-
test
.
start
)
/
1000
;
const
myTest
=
this
.
testMetrics
[
test
.
uid
];
myTest
.
passed
++
;
myTest
.
maxDuration
=
Math
.
max
(
myTest
.
maxDuration
,
testDurationInSeconds
);
this
.
spec
.
passed
++
;
}
onTestFail
(
test
)
{
const
testDurationInSeconds
=
(
test
.
end
-
test
.
start
)
/
1000
;
const
myTest
=
this
.
testMetrics
[
test
.
uid
];
myTest
.
failed
++
;
myTest
.
maxDuration
=
Math
.
max
(
myTest
.
maxDuration
,
testDurationInSeconds
);
this
.
spec
.
failed
++
;
}
onTestSkip
(
test
)
{
if
(
!
this
.
testMetrics
[
test
.
uid
]
)
{
this
.
testMetrics
[
test
.
uid
]
=
{
name
:
getValidPrometheusTagName
(
test
.
title
),
suite
:
test
.
parent
?
getValidPrometheusTagName
(
test
.
parent
)
:
'unknown'
,
passed
:
0
,
failed
:
0
,
skipped
:
1
,
retries
:
0
,
maxDuration
:
0
};
this
.
spec
.
totalTests
++
;
}
this
.
spec
.
skipped
++
;
}
onTestRetry
(
test
)
{
this
.
testMetrics
[
test
.
uid
].
retries
++
;
this
.
spec
.
retries
++
;
}
onRunnerEnd
(
runnerStats
)
{
const
labels
=
this
.
labels
;
const
specMetrics
=
this
.
spec
;
const
workerId
=
runnerStats
.
cid
;
// Depending on how we configure webdriver.io we have two kind of retries:
// either per test or for a spec file (where the runner is restarted)
// and we want the number of retries from both
specMetrics
.
retries
+=
runnerStats
.
retries
;
specMetrics
.
duration
=
getSpecDuration
(
this
.
suiteMetrics
);
specMetrics
.
labels
=
labels
;
specMetrics
.
tests
=
Object
.
values
(
this
.
testMetrics
);
const
outputPath
=
path
.
join
(
this
.
outputDir
,
'specs-'
+
workerId
+
'.json'
);
writeFileSync
(
outputPath
,
JSON
.
stringify
(
specMetrics
),
{
encoding
:
'utf-8'
}
);
}
}
function
writeAllProjectMetrics
(
metricsDir
,
fileName
)
{
const
projectMetrics
=
{
passed
:
0
,
failed
:
0
,
skipped
:
0
,
retries
:
0
,
totalTests
:
0
,
duration
:
0
};
const
tests
=
[];
for
(
const
file
of
readdirSync
(
metricsDir
)
)
{
if
(
!
file
.
startsWith
(
'specs-'
)
||
!
file
.
endsWith
(
'.json'
)
)
{
continue
;
}
const
filePath
=
path
.
join
(
metricsDir
,
file
);
const
data
=
JSON
.
parse
(
readFileSync
(
filePath
,
'utf-8'
)
);
// We have read the raw data, renmove it since we only need the .prom
rmSync
(
filePath
);
projectMetrics
.
passed
+=
data
.
passed
;
projectMetrics
.
failed
+=
data
.
failed
;
projectMetrics
.
skipped
+=
data
.
skipped
;
projectMetrics
.
retries
+=
data
.
retries
;
projectMetrics
.
totalTests
+=
data
.
totalTests
;
projectMetrics
.
duration
+=
Number
(
data
.
duration
);
projectMetrics
.
labels
=
data
.
labels
;
tests
.
push
(
...
data
.
tests
);
}
projectMetrics
.
duration
.
toFixed
(
3
);
const
lines
=
[];
const
labels
=
projectMetrics
.
labels
;
// Add Project metrics
lines
.
push
(
'# HELP wdio_project_duration_seconds Total duration of all test suites per project'
);
lines
.
push
(
'# TYPE wdio_project_duration_seconds gauge'
);
lines
.
push
(
formatMetric
(
'wdio_project_duration_seconds'
,
projectMetrics
.
duration
,
{
...
labels
}
)
);
lines
.
push
(
'# HELP wdio_project_passed Number of tests passed per project'
);
lines
.
push
(
'# TYPE wdio_project_passed gauge'
);
lines
.
push
(
formatMetric
(
'wdio_project_passed'
,
projectMetrics
.
passed
,
{
...
labels
}
)
);
lines
.
push
(
'# HELP wdio_project_failed Number of tests failed per project'
);
lines
.
push
(
'# TYPE wdio_project_failed gauge'
);
lines
.
push
(
formatMetric
(
'wdio_project_failed'
,
projectMetrics
.
failed
,
{
...
labels
}
)
);
lines
.
push
(
'# HELP wdio_project_retries Number of test retries per project'
);
lines
.
push
(
'# TYPE wdio_project_retries gauge'
);
lines
.
push
(
formatMetric
(
'wdio_project_retries'
,
projectMetrics
.
retries
,
{
...
labels
}
)
);
lines
.
push
(
'# HELP wdio_project_skipped Number of tests skipped per project'
);
lines
.
push
(
'# TYPE wdio_project_skipped gauge'
);
lines
.
push
(
formatMetric
(
'wdio_project_skipped'
,
projectMetrics
.
skipped
,
{
...
labels
}
)
);
lines
.
push
(
'# HELP wdio_project_tests Total number of tests per project'
);
lines
.
push
(
'# TYPE wdio_project_tests gauge'
);
lines
.
push
(
formatMetric
(
'wdio_project_tests'
,
projectMetrics
.
totalTests
,
{
...
labels
}
)
);
// Add test metrics
let
addMetaData
=
true
;
for
(
const
test
of
tests
)
{
const
testLabels
=
{
...
labels
,
test
:
test
.
name
};
if
(
addMetaData
)
{
lines
.
push
(
'# HELP wdio_test_passed Number of tests passed (per test)'
);
lines
.
push
(
'# TYPE wdio_test_passed gauge'
);
lines
.
push
(
formatMetric
(
'wdio_test_passed'
,
test
.
passed
,
testLabels
)
);
lines
.
push
(
'# HELP wdio_test_failed Number of tests failed (per test)'
);
lines
.
push
(
'# TYPE wdio_test_failed gauge'
);
lines
.
push
(
formatMetric
(
'wdio_test_failed'
,
test
.
failed
,
testLabels
)
);
lines
.
push
(
'# HELP wdio_test_skipped Number of tests skipped (per test)'
);
lines
.
push
(
'# TYPE wdio_test_skipped gauge'
);
lines
.
push
(
formatMetric
(
'wdio_test_skipped'
,
test
.
skipped
,
testLabels
)
);
lines
.
push
(
'# HELP wdio_test_retries Number of test retries (per test)'
);
lines
.
push
(
'# TYPE wdio_test_retries gauge'
);
lines
.
push
(
formatMetric
(
'wdio_test_retries'
,
test
.
retries
,
testLabels
)
);
lines
.
push
(
'# HELP wdio_test_duration_max_seconds Max observed test duration (seconds per test)'
);
lines
.
push
(
'# TYPE wdio_test_duration_max_seconds gauge'
);
lines
.
push
(
formatMetric
(
'wdio_test_duration_max_seconds'
,
test
.
maxDuration
.
toFixed
(
3
),
{
...
testLabels
}
)
);
addMetaData
=
false
;
}
else
{
lines
.
push
(
formatMetric
(
'wdio_test_passed'
,
test
.
passed
,
testLabels
)
);
lines
.
push
(
formatMetric
(
'wdio_test_failed'
,
test
.
failed
,
testLabels
)
);
lines
.
push
(
formatMetric
(
'wdio_test_skipped'
,
test
.
skipped
,
testLabels
)
);
lines
.
push
(
formatMetric
(
'wdio_test_retries'
,
test
.
retries
,
testLabels
)
);
lines
.
push
(
formatMetric
(
'wdio_test_duration_max_seconds'
,
test
.
maxDuration
.
toFixed
(
3
),
{
...
testLabels
}
)
);
}
}
const
projectName
=
projectMetrics
.
labels
.
project
;
writeFileSync
(
path
.
join
(
metricsDir
,
`
${
projectName
}
-
${
fileName
}
.prom`
),
`
${
lines
.
join
(
'\n'
)
}
\n`
,
'utf-8'
);
}
export
{
PrometheusFileReporter
,
writeAllProjectMetrics
};
File Metadata
Details
Attached
Mime Type
text/x-java
Expires
Oct 4 2026, 05:23 (5 w, 16 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
db/0f/471f6f444a2f04e9eeddc8321d07
Default Alt Text
PrometheusFileReporter.js (9 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment