Skip to content

Commit

Permalink
Change NewBulkIndexer signature to accept BulkIndexerConfig with a ne…
Browse files Browse the repository at this point in the history
…w Pipeline option; Release v2 (#155)

Changes NewBulkIndexer signature to accept a config. The config includes a new Pipeline option, which will then be included in the bulk request.

Release v2 as NewBulkIndexer signature changed.
  • Loading branch information
carsonip authored Apr 18, 2024
1 parent b1a51d4 commit c45c6aa
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 54 deletions.
15 changes: 14 additions & 1 deletion appender.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ type Appender struct {
// New returns a new Appender that indexes documents into Elasticsearch.
// It is only tested with v8 go-elasticsearch client. Use other clients at your own risk.
func New(client esapi.Transport, cfg Config) (*Appender, error) {
if client == nil {
return nil, errors.New("client is nil")
}

if cfg.CompressionLevel < -1 || cfg.CompressionLevel > 9 {
return nil, fmt.Errorf(
"expected CompressionLevel in range [-1,9], got %d",
Expand Down Expand Up @@ -142,7 +146,16 @@ func New(client esapi.Transport, cfg Config) (*Appender, error) {
}
available := make(chan *BulkIndexer, cfg.MaxRequests)
for i := 0; i < cfg.MaxRequests; i++ {
available <- NewBulkIndexer(client, cfg.CompressionLevel, cfg.MaxDocumentRetries)
bi, err := NewBulkIndexer(BulkIndexerConfig{
client: client,
MaxDocumentRetries: cfg.MaxDocumentRetries,
CompressionLevel: cfg.CompressionLevel,
Pipeline: cfg.Pipeline,
})
if err != nil {
return nil, fmt.Errorf("error creating bulk indexer: %w", err)
}
available <- bi
}
if cfg.Logger == nil {
cfg.Logger = zap.NewNop()
Expand Down
4 changes: 2 additions & 2 deletions appender_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import (
"go.elastic.co/fastjson"
"go.uber.org/zap"

"github.com/elastic/go-docappender"
"github.com/elastic/go-docappender/docappendertest"
"github.com/elastic/go-docappender/v2"
"github.com/elastic/go-docappender/v2/docappendertest"
)

func BenchmarkAppender(b *testing.B) {
Expand Down
34 changes: 32 additions & 2 deletions appender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ import (
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"

"github.com/elastic/go-docappender"
"github.com/elastic/go-docappender/docappendertest"
"github.com/elastic/go-docappender/v2"
"github.com/elastic/go-docappender/v2/docappendertest"
"github.com/elastic/go-elasticsearch/v8/esutil"
)

Expand Down Expand Up @@ -1038,6 +1038,36 @@ func TestAppenderCloseBusyIndexer(t *testing.T) {
IndexersActive: 0}, indexer.Stats())
}

func TestAppenderPipeline(t *testing.T) {
const expected = "my_pipeline"
var actual string
client := docappendertest.NewMockElasticsearchClient(t, func(w http.ResponseWriter, r *http.Request) {
actual = r.URL.Query().Get("pipeline")
_, result := docappendertest.DecodeBulkRequest(r)
json.NewEncoder(w).Encode(result)
})
indexer, err := docappender.New(client, docappender.Config{
FlushInterval: time.Minute,
Pipeline: expected,
})
require.NoError(t, err)
defer indexer.Close(context.Background())

err = indexer.Add(context.Background(), "logs-foo-testing", newJSONReader(map[string]any{
"@timestamp": time.Unix(123, 456789111).UTC().Format(docappendertest.TimestampFormat),
"data_stream.type": "logs",
"data_stream.dataset": "foo",
"data_stream.namespace": "testing",
}))
require.NoError(t, err)

// Closing the indexer flushes enqueued documents.
err = indexer.Close(context.Background())
require.NoError(t, err)

assert.Equal(t, expected, actual)
}

func TestAppenderScaling(t *testing.T) {
newIndexer := func(t *testing.T, cfg docappender.Config) *docappender.Appender {
t.Helper()
Expand Down
77 changes: 54 additions & 23 deletions bulk_indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package docappender
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -47,17 +48,36 @@ import (
// of concurrent bulk requests. This way we can ensure bulk requests have the
// maximum possible size, based on configuration and throughput.

// BulkIndexerConfig holds configuration for BulkIndexer.
type BulkIndexerConfig struct {
// client holds the Elasticsearch client.
client esapi.Transport

// MaxDocumentRetries holds the maximum number of document retries
MaxDocumentRetries int

// CompressionLevel holds the gzip compression level, from 0 (gzip.NoCompression)
// to 9 (gzip.BestCompression). Higher values provide greater compression, at a
// greater cost of CPU. The special value -1 (gzip.DefaultCompression) selects the
// default compression level.
CompressionLevel int

// Pipeline holds the ingest pipeline ID.
//
// If Pipeline is empty, no ingest pipeline will be specified in the Bulk request.
Pipeline string
}

type BulkIndexer struct {
client esapi.Transport
maxDocumentRetry int
itemsAdded int
bytesFlushed int
jsonw fastjson.Writer
writer io.Writer
gzipw *gzip.Writer
copyBuf []byte
buf bytes.Buffer
retryCounts map[int]int
config BulkIndexerConfig
itemsAdded int
bytesFlushed int
jsonw fastjson.Writer
writer io.Writer
gzipw *gzip.Writer
copyBuf []byte
buf bytes.Buffer
retryCounts map[int]int
}

type BulkIndexerResponseStat struct {
Expand Down Expand Up @@ -138,22 +158,32 @@ func init() {

// NewBulkIndexer returns a bulk indexer that issues bulk requests to Elasticsearch.
// It is only tested with v8 go-elasticsearch client. Use other clients at your own risk.
func NewBulkIndexer(client esapi.Transport, compressionLevel int, maxDocRetry int) *BulkIndexer {
func NewBulkIndexer(cfg BulkIndexerConfig) (*BulkIndexer, error) {
if cfg.client == nil {
return nil, errors.New("client is nil")
}

if cfg.CompressionLevel < -1 || cfg.CompressionLevel > 9 {
return nil, fmt.Errorf(
"expected CompressionLevel in range [-1,9], got %d",
cfg.CompressionLevel,
)
}

b := &BulkIndexer{
client: client,
maxDocumentRetry: maxDocRetry,
retryCounts: make(map[int]int),
config: cfg,
retryCounts: make(map[int]int),
}
if compressionLevel != gzip.NoCompression {
b.gzipw, _ = gzip.NewWriterLevel(&b.buf, compressionLevel)
if cfg.CompressionLevel != gzip.NoCompression {
b.gzipw, _ = gzip.NewWriterLevel(&b.buf, cfg.CompressionLevel)
b.writer = b.gzipw
} else {
b.writer = &b.buf
}
return b
return b, nil
}

// BulkIndexer resets b, ready for a new request.
// Reset resets bulk indexer, ready for a new request.
func (b *BulkIndexer) Reset() {
b.bytesFlushed = 0
}
Expand All @@ -166,7 +196,7 @@ func (b *BulkIndexer) resetBuf() {
}
}

// Added returns the number of buffered items.
// Items returns the number of buffered items.
func (b *BulkIndexer) Items() int {
return b.itemsAdded
}
Expand Down Expand Up @@ -230,7 +260,7 @@ func (b *BulkIndexer) Flush(ctx context.Context) (BulkIndexerResponseStat, error
}
}

if b.maxDocumentRetry > 0 {
if b.config.MaxDocumentRetries > 0 {
if cap(b.copyBuf) < b.buf.Len() {
b.copyBuf = slices.Grow(b.copyBuf, b.buf.Len()-cap(b.copyBuf))
b.copyBuf = b.copyBuf[:cap(b.copyBuf)]
Expand All @@ -243,13 +273,14 @@ func (b *BulkIndexer) Flush(ctx context.Context) (BulkIndexerResponseStat, error
Body: &b.buf,
Header: make(http.Header),
FilterPath: []string{"items.*._index", "items.*.status", "items.*.error.type", "items.*.error.reason"},
Pipeline: b.config.Pipeline,
}
if b.gzipw != nil {
req.Header.Set("Content-Encoding", "gzip")
}

bytesFlushed := b.buf.Len()
res, err := req.Do(ctx, b.client)
res, err := req.Do(ctx, b.config.client)
if err != nil {
b.resetBuf()
return BulkIndexerResponseStat{}, fmt.Errorf("failed to execute the request: %w", err)
Expand All @@ -276,7 +307,7 @@ func (b *BulkIndexer) Flush(ctx context.Context) (BulkIndexerResponseStat, error
}

// Only run the retry logic if document retries are enabled
if b.maxDocumentRetry > 0 {
if b.config.MaxDocumentRetries > 0 {
buf := make([]byte, 0, 4096)

// Eliminate previous retry counts that aren't present in the bulk
Expand Down Expand Up @@ -327,7 +358,7 @@ func (b *BulkIndexer) Flush(ctx context.Context) (BulkIndexerResponseStat, error
// Increment 429 count for the positions found.
count := b.retryCounts[res.Position] + 1
// check if we are above the maxDocumentRetry setting
if count > b.maxDocumentRetry {
if count > b.config.MaxDocumentRetries {
// do not retry, return the document as failed
tmp = append(tmp, res)
continue
Expand Down
5 changes: 5 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ type Config struct {
// If DocumentBufferSize is zero, the default 1024 will be used.
DocumentBufferSize int

// Pipeline holds the ingest pipeline ID.
//
// If Pipeline is empty, no ingest pipeline will be specified in the Bulk request.
Pipeline string

// Scaling configuration for the docappender.
//
// If unspecified, scaling is enabled by default.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/elastic/go-docappender
module github.com/elastic/go-docappender/v2

go 1.21

Expand Down
4 changes: 2 additions & 2 deletions integrationtest/appender_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/elastic/go-docappender"
"github.com/elastic/go-docappender/docappendertest"
"github.com/elastic/go-docappender/v2"
"github.com/elastic/go-docappender/v2/docappendertest"
elasticsearch7 "github.com/elastic/go-elasticsearch/v7"
esapi7 "github.com/elastic/go-elasticsearch/v7/esapi"
elasticsearch8 "github.com/elastic/go-elasticsearch/v8"
Expand Down
5 changes: 3 additions & 2 deletions integrationtest/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module integrationtest
go 1.22.0

require (
github.com/elastic/go-docappender v1.1.0
github.com/elastic/go-docappender/v2 v2.0.0
github.com/elastic/go-elasticsearch/v7 v7.17.10
github.com/elastic/go-elasticsearch/v8 v8.13.1
github.com/stretchr/testify v1.9.0
Expand All @@ -18,6 +18,7 @@ require (
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
Expand All @@ -35,4 +36,4 @@ require (
howett.net/plist v1.0.0 // indirect
)

replace github.com/elastic/go-docappender => ../
replace github.com/elastic/go-docappender/v2 => ../
33 changes: 12 additions & 21 deletions integrationtest/go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elastic/elastic-transport-go/v8 v8.5.0 h1:v5membAl7lvQgBTexPRDBO/RdnlQX+FM9fUVDyXxvH0=
github.com/elastic/elastic-transport-go/v8 v8.5.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
github.com/elastic/go-docappender v1.1.0 h1:wuA3Im+Y0PuQQ/FzLZUb0+6eT64oLhGCqQV49OvR9EU=
github.com/elastic/go-docappender v1.1.0/go.mod h1:u0hkrzDr9w81uNFWUxeOyM0IX9aZUag/gHlOnHyCrzA=
github.com/elastic/go-elasticsearch/v7 v7.17.10 h1:TCQ8i4PmIJuBunvBS6bwT2ybzVFxxUhhltAs3Gyu1yo=
github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
github.com/elastic/go-elasticsearch/v8 v8.13.1 h1:du5F8IzUUyCkzxyHdrO9AtopcG95I/qwi2WK8Kf1xlg=
Expand All @@ -30,8 +29,9 @@ github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand All @@ -44,39 +44,30 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.elastic.co/apm/module/apmelasticsearch/v2 v2.5.0 h1:0S5Vj5/L4EkXQS7YUr+1ylTuB3njTuBNzdmn3mjXAFI=
go.elastic.co/apm/module/apmelasticsearch/v2 v2.5.0/go.mod h1:zNEXwAPoThH/bAb3TWKD5Og0Zyk0OWURsEHAja1kra4=
go.elastic.co/apm/module/apmelasticsearch/v2 v2.6.0 h1:ukMcwyMaDXsS1dRK2qRYXT2AsfwaUy74TOOYCqkWJow=
go.elastic.co/apm/module/apmelasticsearch/v2 v2.6.0/go.mod h1:YpfiTTrqX5LB/CKBwX89oDCBAxuLJTFv40gcfxJyehM=
go.elastic.co/apm/module/apmhttp/v2 v2.5.0 h1:4AWlw8giL7hRYBQiwF1/Thm0GDsbQH/Ofe4eySAnURo=
go.elastic.co/apm/module/apmhttp/v2 v2.5.0/go.mod h1:ZP7gLEzY/OAPTqNZjp8AzA06HF82zfwXEpKI2sSVTgk=
go.elastic.co/apm/module/apmhttp/v2 v2.6.0 h1:s8UeNFQmVBCNd4eoz7KDD9rEFhQC0HeUFXz3z9gpAmQ=
go.elastic.co/apm/module/apmhttp/v2 v2.6.0/go.mod h1:D0GLppLuI0Ddwvtl595GUxRgn6Z8L5KaDFVMv2H3GK0=
go.elastic.co/apm/v2 v2.5.0 h1:UYqdu/bjcubcP9BIy5+os2ExRzw03yOQFG+sRGGhVlQ=
go.elastic.co/apm/v2 v2.5.0/go.mod h1:+CiBUdrrAGnGCL9TNx7tQz3BrfYV23L8Ljvotoc87so=
go.elastic.co/apm/v2 v2.6.0 h1:VieBMLQFtXua2YxpYxaSdYGnmmxhLT46gosI5yErJgY=
go.elastic.co/apm/v2 v2.6.0/go.mod h1:33rOXgtHwbgZcDgi6I/GtCSMZQqgxkHC0IQT3gudKvo=
go.elastic.co/fastjson v1.3.0 h1:hJO3OsYIhiqiT4Fgu0ZxAECnKASbwgiS+LMW5oCopKs=
go.elastic.co/fastjson v1.3.0/go.mod h1:K9vDh7O0ODsVKV2B5e2XYLY277QZaCbB3tS1SnARvko=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw=
go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8=
go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0=
go.opentelemetry.io/otel/sdk/metric v1.25.0 h1:7CiHOy08LbrxMAp4vWpbiPcklunUshVpAvGBrdDRlGw=
go.opentelemetry.io/otel/sdk/metric v1.25.0/go.mod h1:LzwoKptdbBBdYfvtGCzGwk6GWMA3aUzBOwtQpR6Nz7o=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down

0 comments on commit c45c6aa

Please sign in to comment.