forked from mirror/client_golang
Add HTTP multiplexor wrapper for automatic telemetry.
This commit is contained in:
parent
6c08aa2e95
commit
c5b4952f97
|
@ -20,3 +20,6 @@ _cgo_export.*
|
||||||
_testmain.go
|
_testmain.go
|
||||||
|
|
||||||
*.exe
|
*.exe
|
||||||
|
|
||||||
|
*~
|
||||||
|
*#
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Copyright 2013 Prometheus Team
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
MAKE_ARTIFACTS = search_index
|
||||||
|
|
||||||
|
all: test
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build ./...
|
||||||
|
|
||||||
|
test: build
|
||||||
|
go test ./... $(GO_TEST_FLAGS)
|
||||||
|
|
||||||
|
format:
|
||||||
|
find . -iname '*.go' -exec gofmt -w -s=true '{}' ';'
|
||||||
|
|
||||||
|
advice:
|
||||||
|
go tool vet .
|
||||||
|
|
||||||
|
search_index:
|
||||||
|
godoc -index -write_index -index_files='search_index'
|
||||||
|
|
||||||
|
documentation: search_index
|
||||||
|
godoc -http=:6060 -index -index_files='search_index'
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(MAKE_ARTIFACTS)
|
||||||
|
|
||||||
|
.PHONY: advice build clean documentation format test
|
|
@ -1,9 +0,0 @@
|
||||||
// Copyright (c) 2013, Matt T. Proud
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
|
|
||||||
// A repository of various contributed Prometheus client components that may
|
|
||||||
// assist in your use of the library.
|
|
||||||
package contributor
|
|
|
@ -1,36 +0,0 @@
|
||||||
// Copyright (c) 2013, Matt T. Proud
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
|
|
||||||
package contributor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
unknownStatusCode = "unknown"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResponseWriterDelegator is a means of wrapping http.ResponseWriter to divine
|
|
||||||
// the response code from a given answer, especially in systems where the
|
|
||||||
// response is treated as a blackbox.
|
|
||||||
type ResponseWriterDelegator struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
Status *string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ResponseWriterDelegator) WriteHeader(code int) {
|
|
||||||
*r.Status = strconv.Itoa(code)
|
|
||||||
|
|
||||||
r.ResponseWriter.WriteHeader(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewResponseWriterDelegator(delegate http.ResponseWriter) ResponseWriterDelegator {
|
|
||||||
defaultStatusCode := unknownStatusCode
|
|
||||||
|
|
||||||
return ResponseWriterDelegator{delegate, &defaultStatusCode}
|
|
||||||
}
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright (c) 2012, Matt T. Proud
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// This skeletal example of the telemetry library is provided to demonstrate the
|
||||||
|
// use of boilerplate HTTP delegation telemetry methods.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"github.com/prometheus/client_golang"
|
||||||
|
"github.com/prometheus/client_golang/exp"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// helloHandler demonstrates the DefaultCoarseMux's ability to sniff a
|
||||||
|
// http.ResponseWriter (specifically http.response) implicit setting of
|
||||||
|
// a response code.
|
||||||
|
func helloHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("Hello, hello, hello..."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// goodbyeHandler demonstrates the DefaultCoarseMux's ability to sniff an
|
||||||
|
// http.ResponseWriter (specifically http.response) explicit setting of
|
||||||
|
// a response code.
|
||||||
|
func goodbyeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusGone)
|
||||||
|
w.Write([]byte("... and now for the big goodbye!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// teapotHandler demonstrates the DefaultCoarseMux's ability to sniff an
|
||||||
|
// http.ResponseWriter (specifically http.response) explicit setting of
|
||||||
|
// a response code for pure comedic value.
|
||||||
|
func teapotHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
w.Write([]byte("Short and stout..."))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
exp.HandleFunc("/hello", helloHandler)
|
||||||
|
exp.HandleFunc("/goodbye", goodbyeHandler)
|
||||||
|
exp.HandleFunc("/teapot", teapotHandler)
|
||||||
|
exp.Handle(registry.ExpositionResource, registry.DefaultHandler)
|
||||||
|
|
||||||
|
http.ListenAndServe(*listeningAddress, exp.DefaultCoarseMux)
|
||||||
|
}
|
|
@ -22,15 +22,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
listeningAddress string
|
barDomain = flag.Float64("random.fooDomain", 200, "The domain for the random parameter foo.")
|
||||||
|
barMean = flag.Float64("random.barDomain", 10, "The domain for the random parameter bar.")
|
||||||
barDomain float64
|
fooDomain = flag.Float64("random.barMean", 100, "The mean for the random parameter bar.")
|
||||||
barMean float64
|
|
||||||
fooDomain float64
|
|
||||||
|
|
||||||
// Create a histogram to track fictitious interservice RPC latency for three
|
// Create a histogram to track fictitious interservice RPC latency for three
|
||||||
// distinct services.
|
// distinct services.
|
||||||
rpc_latency = metrics.NewHistogram(&metrics.HistogramSpecification{
|
rpcLatency = metrics.NewHistogram(&metrics.HistogramSpecification{
|
||||||
// Four distinct histogram buckets for values:
|
// Four distinct histogram buckets for values:
|
||||||
// - equally-sized,
|
// - equally-sized,
|
||||||
// - 0 to 50, 50 to 100, 100 to 150, and 150 to 200.
|
// - 0 to 50, 50 to 100, 100 to 150, and 150 to 200.
|
||||||
|
@ -45,7 +43,7 @@ var (
|
||||||
ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99},
|
ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99},
|
||||||
})
|
})
|
||||||
|
|
||||||
rpc_calls = metrics.NewCounter()
|
rpcCalls = metrics.NewCounter()
|
||||||
|
|
||||||
// If for whatever reason you are resistant to the idea of having a static
|
// If for whatever reason you are resistant to the idea of having a static
|
||||||
// registry for metrics, which is a really bad idea when using Prometheus-
|
// registry for metrics, which is a really bad idea when using Prometheus-
|
||||||
|
@ -53,36 +51,33 @@ var (
|
||||||
customRegistry = registry.NewRegistry()
|
customRegistry = registry.NewRegistry()
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
flag.StringVar(&listeningAddress, "listeningAddress", ":8080", "The address to listen to requests on.")
|
|
||||||
flag.Float64Var(&fooDomain, "random.fooDomain", 200, "The domain for the random parameter foo.")
|
|
||||||
flag.Float64Var(&barDomain, "random.barDomain", 10, "The domain for the random parameter bar.")
|
|
||||||
flag.Float64Var(&barMean, "random.barMean", 100, "The mean for the random parameter bar.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
rpc_latency.Add(map[string]string{"service": "foo"}, rand.Float64()*fooDomain)
|
rpcLatency.Add(map[string]string{"service": "foo"}, rand.Float64()**fooDomain)
|
||||||
rpc_calls.Increment(map[string]string{"service": "foo"})
|
rpcCalls.Increment(map[string]string{"service": "foo"})
|
||||||
|
|
||||||
rpc_latency.Add(map[string]string{"service": "bar"}, (rand.NormFloat64()*barDomain)+barMean)
|
rpcLatency.Add(map[string]string{"service": "bar"}, (rand.NormFloat64()**barDomain)+*barMean)
|
||||||
rpc_calls.Increment(map[string]string{"service": "bar"})
|
rpcCalls.Increment(map[string]string{"service": "bar"})
|
||||||
|
|
||||||
rpc_latency.Add(map[string]string{"service": "zed"}, rand.ExpFloat64())
|
rpcLatency.Add(map[string]string{"service": "zed"}, rand.ExpFloat64())
|
||||||
rpc_calls.Increment(map[string]string{"service": "zed"})
|
rpcCalls.Increment(map[string]string{"service": "zed"})
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
http.Handle(registry.ExpositionResource, customRegistry.Handler())
|
http.Handle(registry.ExpositionResource, customRegistry.Handler())
|
||||||
http.ListenAndServe(listeningAddress, nil)
|
http.ListenAndServe(*listeningAddress, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
customRegistry.Register("rpc_latency_microseconds", "RPC latency.", registry.NilLabels, rpc_latency)
|
customRegistry.Register("rpc_latency_microseconds", "RPC latency.", registry.NilLabels, rpcLatency)
|
||||||
customRegistry.Register("rpc_calls_total", "RPC calls.", registry.NilLabels, rpc_calls)
|
customRegistry.Register("rpc_calls_total", "RPC calls.", registry.NilLabels, rpcCalls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
||||||
|
)
|
||||||
|
|
|
@ -14,17 +14,13 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
listeningAddress string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
flag.StringVar(&listeningAddress, "listeningAddress", ":8080", "The address to listen to requests on.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
http.Handle(registry.ExpositionResource, registry.DefaultHandler)
|
http.Handle(registry.ExpositionResource, registry.DefaultHandler)
|
||||||
http.ListenAndServe(listeningAddress, nil)
|
http.ListenAndServe(*listeningAddress, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
// Copyright (c) 2013, Matt T. Proud
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package exp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/prometheus/client_golang"
|
||||||
|
"github.com/prometheus/client_golang/metrics"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
handler = "handler"
|
||||||
|
code = "code"
|
||||||
|
method = "method"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
coarseMux struct {
|
||||||
|
*http.ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerDelegator struct {
|
||||||
|
delegate http.Handler
|
||||||
|
pattern string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
requestCounts = metrics.NewCounter()
|
||||||
|
requestDuration = metrics.NewCounter()
|
||||||
|
requestDurations = metrics.NewDefaultHistogram()
|
||||||
|
requestBytes = metrics.NewCounter()
|
||||||
|
responseBytes = metrics.NewCounter()
|
||||||
|
|
||||||
|
// DefaultCoarseMux is a drop-in replacement for http.DefaultServeMux that
|
||||||
|
// provides standardized telemetry for Go's standard HTTP handler registration
|
||||||
|
// and dispatch API.
|
||||||
|
//
|
||||||
|
// The name is due to the coarse grouping of telemetry by (HTTP Method, HTTP Response Code,
|
||||||
|
// and handler match pattern) triples.
|
||||||
|
DefaultCoarseMux = newCoarseMux()
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h handlerDelegator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
rwd := NewResponseWriterDelegator(w)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
duration := float64(time.Since(start) / time.Microsecond)
|
||||||
|
status := rwd.Status()
|
||||||
|
labels := map[string]string{handler: h.pattern, code: status, method: strings.ToLower(r.Method)}
|
||||||
|
requestCounts.Increment(labels)
|
||||||
|
requestDuration.IncrementBy(labels, duration)
|
||||||
|
requestDurations.Add(labels, duration)
|
||||||
|
requestBytes.IncrementBy(labels, float64(computeApproximateRequestSize(*r)))
|
||||||
|
responseBytes.IncrementBy(labels, float64(rwd.BytesWritten))
|
||||||
|
}()
|
||||||
|
|
||||||
|
h.delegate.ServeHTTP(rwd, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h handlerDelegator) String() string {
|
||||||
|
return fmt.Sprintf("handlerDelegator wrapping %s for %s", h.delegate, h.pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle registers a http.Handler to this CoarseMux. See http.ServeMux.Handle.
|
||||||
|
func (m *coarseMux) handle(pattern string, handler http.Handler) {
|
||||||
|
m.ServeMux.Handle(pattern, handlerDelegator{
|
||||||
|
delegate: handler,
|
||||||
|
pattern: pattern,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle registers a handler to this CoarseMux. See http.ServeMux.HandleFunc.
|
||||||
|
func (m *coarseMux) handleFunc(pattern string, handler http.HandlerFunc) {
|
||||||
|
m.ServeMux.Handle(pattern, handlerDelegator{
|
||||||
|
delegate: handler,
|
||||||
|
pattern: pattern,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCoarseMux() *coarseMux {
|
||||||
|
return &coarseMux{
|
||||||
|
ServeMux: http.NewServeMux(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle registers a http.Handler to DefaultCoarseMux. See http.Handle.
|
||||||
|
func Handle(pattern string, handler http.Handler) {
|
||||||
|
DefaultCoarseMux.handle(pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFunc registers a handler to DefaultCoarseMux. See http.HandleFunc.
|
||||||
|
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
||||||
|
DefaultCoarseMux.handleFunc(pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.Register("http_requests_total", "A counter of the total number of HTTP requests made against the default multiplexor.", registry.NilLabels, requestCounts)
|
||||||
|
registry.Register("http_request_durations_total_microseconds", "The total amount of time the default multiplexor has spent answering HTTP requests (microseconds).", registry.NilLabels, requestDuration)
|
||||||
|
registry.Register("http_request_durations_microseconds", "The amounts of time the default multiplexor has spent answering HTTP requests (microseconds).", registry.NilLabels, requestDurations)
|
||||||
|
registry.Register("http_request_bytes_total", "The total volume of content body sizes received (bytes).", registry.NilLabels, requestBytes)
|
||||||
|
registry.Register("http_response_bytes_total", "The total volume of response payloads emitted (bytes).", registry.NilLabels, responseBytes)
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) 2013, Matt T. Proud
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
// A repository of various immature Prometheus client components that may
|
||||||
|
// assist in your use of the library. Items contained herein are regarded as
|
||||||
|
// especially interface unstable and may change without warning. Upon
|
||||||
|
// maturation, they should be migrated into a formal package for users.
|
||||||
|
package exp
|
|
@ -0,0 +1,100 @@
|
||||||
|
// Copyright (c) 2013, Matt T. Proud
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package exp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
unknownStatusCode = "unknown"
|
||||||
|
statusFieldName = "status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type status string
|
||||||
|
|
||||||
|
func (s status) unknown() bool {
|
||||||
|
return len(s) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s status) String() string {
|
||||||
|
if s.unknown() {
|
||||||
|
return unknownStatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeApproximateRequestSize(r http.Request) (s int) {
|
||||||
|
s += len(r.Method)
|
||||||
|
if r.URL != nil {
|
||||||
|
s += len(r.URL.String())
|
||||||
|
}
|
||||||
|
s += len(r.Proto)
|
||||||
|
for name, values := range r.Header {
|
||||||
|
s += len(name)
|
||||||
|
for _, value := range values {
|
||||||
|
s += len(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s += len(r.Host)
|
||||||
|
|
||||||
|
// N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
|
||||||
|
|
||||||
|
if r.ContentLength != -1 {
|
||||||
|
s += int(r.ContentLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseWriterDelegator is a means of wrapping http.ResponseWriter to divine
|
||||||
|
// the response code from a given answer, especially in systems where the
|
||||||
|
// response is treated as a blackbox.
|
||||||
|
type ResponseWriterDelegator struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status status
|
||||||
|
BytesWritten int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ResponseWriterDelegator) String() string {
|
||||||
|
return fmt.Sprintf("ResponseWriterDelegator decorating %s with status %s and %d bytes written.", r.ResponseWriter, r.status, r.BytesWritten)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResponseWriterDelegator) WriteHeader(code int) {
|
||||||
|
r.status = status(strconv.Itoa(code))
|
||||||
|
|
||||||
|
r.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResponseWriterDelegator) Status() string {
|
||||||
|
if r.status.unknown() {
|
||||||
|
delegate := reflect.ValueOf(r.ResponseWriter).Elem()
|
||||||
|
statusField := delegate.FieldByName(statusFieldName)
|
||||||
|
if statusField.IsValid() {
|
||||||
|
r.status = status(strconv.Itoa(int(statusField.Int())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.status.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResponseWriterDelegator) Write(b []byte) (n int, err error) {
|
||||||
|
n, err = r.ResponseWriter.Write(b)
|
||||||
|
r.BytesWritten += n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponseWriterDelegator(delegate http.ResponseWriter) *ResponseWriterDelegator {
|
||||||
|
return &ResponseWriterDelegator{
|
||||||
|
ResponseWriter: delegate,
|
||||||
|
}
|
||||||
|
}
|
|
@ -137,7 +137,7 @@ func testRegister(t test.Tester) {
|
||||||
|
|
||||||
for i, scenario := range scenarios {
|
for i, scenario := range scenarios {
|
||||||
if len(scenario.inputs) != len(scenario.outputs) {
|
if len(scenario.inputs) != len(scenario.outputs) {
|
||||||
t.Fatalf("%d. len(scenario.inputs) != len(scenario.outputs)")
|
t.Fatalf("%d. expected scenario output length %d, got %d", i, len(scenario.inputs), len(scenario.outputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
abortOnMisuse = false
|
abortOnMisuse = false
|
||||||
|
|
Loading…
Reference in New Issue