Commit 1a3e2c11 authored by Svetomir Smiljkovic's avatar Svetomir Smiljkovic

Pull from upstream

parents 948ea22f 5ec4f25e
......@@ -25,10 +25,10 @@ Use one of the multiaddresses as bootnode for `node 2` in order to connect them:
bee start --api-addr :8502 --p2p-addr :30402 --data-dir data2 --bootnode /ip4/127.0.0.1/tcp/30401/p2p/QmT4TNB4cKYanUjdYodw1Cns8cuVaRVo24hHNYcT7JjkTB
```
Take the address of the connected peer to `node 1` from log line `peer "4932309428148935717" connected` and make an HTTP request to `localhost:{PORT1}/pingpong/{ADDRESS}` like:
Take the address of the connected peer to `node 1` from log line `peer "4932309428148935717" connected` and make an HTTP POST request to `localhost:{PORT1}/pingpong/{ADDRESS}` like:
```sh
curl localhost:8502/pingpong/4932309428148935717
curl -XPOST localhost:8502/pingpong/4932309428148935717
```
## Structure
......
......@@ -6,7 +6,6 @@ package cmd
import (
"errors"
"os"
"path/filepath"
"strings"
......@@ -120,16 +119,3 @@ func (c *command) setHomeDir() (err error) {
c.homeDir = dir
return nil
}
// baseDir is the directory where the executable is located.
var baseDir = func() string {
path, err := os.Executable()
if err != nil {
panic(err)
}
path, err = filepath.EvalSymlinks(path)
if err != nil {
panic(err)
}
return filepath.Dir(path)
}()
......@@ -32,6 +32,7 @@ func (c *command) initStartCmd() (err error) {
optionNameP2PAddr = "p2p-addr"
optionNameP2PDisableWS = "p2p-disable-ws"
optionNameP2PDisableQUIC = "p2p-disable-quic"
optionNameEnableDebugAPI = "enable-debug-api"
optionNameDebugAPIAddr = "debug-api-addr"
optionNameBootnodes = "bootnode"
optionNameNetworkID = "network-id"
......@@ -49,7 +50,7 @@ func (c *command) initStartCmd() (err error) {
return cmd.Help()
}
logger := logging.New(cmd.OutOrStdout())
logger := logging.New(cmd.OutOrStdout()).(*logrus.Logger)
switch v := strings.ToLower(c.config.GetString(optionNameVerbosity)); v {
case "0", "silent":
logger.SetOutput(ioutil.Discard)
......@@ -80,9 +81,14 @@ func (c *command) initStartCmd() (err error) {
libp2pPrivateKey = f
}
debugAPIAddr := c.config.GetString(optionNameDebugAPIAddr)
if !c.config.GetBool(optionNameEnableDebugAPI) {
debugAPIAddr = ""
}
b, err := node.NewBee(node.Options{
APIAddr: c.config.GetString(optionNameAPIAddr),
DebugAPIAddr: c.config.GetString(optionNameDebugAPIAddr),
DebugAPIAddr: debugAPIAddr,
LibP2POptions: libp2p.Options{
PrivateKey: libp2pPrivateKey,
Addr: c.config.GetString(optionNameP2PAddr),
......@@ -142,6 +148,7 @@ func (c *command) initStartCmd() (err error) {
cmd.Flags().Bool(optionNameP2PDisableWS, false, "disable P2P WebSocket protocol")
cmd.Flags().Bool(optionNameP2PDisableQUIC, false, "disable P2P QUIC protocol")
cmd.Flags().StringSlice(optionNameBootnodes, nil, "initial nodes to connect to")
cmd.Flags().Bool(optionNameEnableDebugAPI, false, "enable debug HTTP API")
cmd.Flags().String(optionNameDebugAPIAddr, ":6060", "debug HTTP API listen address")
cmd.Flags().Int32(optionNameNetworkID, 1, "ID of the Swarm network")
cmd.Flags().Int(optionNameConnectionsLow, 200, "low watermark governing the number of connections that'll be maintained")
......
......@@ -7,7 +7,7 @@ package api
import (
"net/http"
"github.com/ethersphere/bee/pkg/p2p"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/pingpong"
"github.com/prometheus/client_golang/prometheus"
)
......@@ -24,8 +24,8 @@ type server struct {
}
type Options struct {
P2P p2p.Service
Pingpong *pingpong.Service
Pingpong pingpong.Interface
Logger logging.Logger
}
func New(o Options) Service {
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package api_test
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/pingpong"
"resenje.org/web"
)
type testServerOptions struct {
Pingpong pingpong.Interface
}
func newTestServer(t *testing.T, o testServerOptions) (client *http.Client, cleanup func()) {
s := api.New(api.Options{
Pingpong: o.Pingpong,
Logger: logging.New(ioutil.Discard),
})
ts := httptest.NewServer(s)
cleanup = ts.Close
client = &http.Client{
Transport: web.RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
u, err := url.Parse(ts.URL + r.URL.String())
if err != nil {
return nil, err
}
r.URL = u
return ts.Client().Transport.RoundTrip(r)
}),
}
return client, cleanup
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package api
type PingpongResponse = pingpongResponse
......@@ -5,10 +5,12 @@
package api
import (
"errors"
"net/http"
"time"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/p2p"
"github.com/gorilla/mux"
)
......@@ -22,7 +24,13 @@ func (s *server) pingpongHandler(w http.ResponseWriter, r *http.Request) {
rtt, err := s.Pingpong.Ping(ctx, peerID, "hey", "there", ",", "how are", "you", "?")
if err != nil {
jsonhttp.InternalServerError(w, err.Error())
if errors.Is(err, p2p.ErrPeerNotFound) {
s.Logger.Debugf("pingpong: ping %s: %w", peerID, err)
jsonhttp.NotFound(w, "peer not found")
return
}
s.Logger.Errorf("pingpong: ping %s: %w", peerID, err)
jsonhttp.InternalServerError(w, err)
return
}
s.metrics.PingRequestCount.Inc()
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package api_test
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/p2p"
pingpongmock "github.com/ethersphere/bee/pkg/pingpong/mock"
)
func TestPingpong(t *testing.T) {
rtt := time.Minute
peerID := "124762324"
unknownPeerID := "55555555"
errorPeerID := "77777777"
testErr := errors.New("test error")
pingpongService := pingpongmock.New(func(ctx context.Context, address string, msgs ...string) (time.Duration, error) {
if address == errorPeerID {
return 0, testErr
}
if address != peerID {
return 0, p2p.ErrPeerNotFound
}
return rtt, nil
})
client, cleanup := newTestServer(t, testServerOptions{
Pingpong: pingpongService,
})
defer cleanup()
t.Run("ok", func(t *testing.T) {
jsonhttptest.ResponseDirect(t, client, http.MethodPost, "/pingpong/"+peerID, nil, http.StatusOK, api.PingpongResponse{
RTT: rtt,
})
})
t.Run("peer not found", func(t *testing.T) {
jsonhttptest.ResponseDirect(t, client, http.MethodPost, "/pingpong/"+unknownPeerID, nil, http.StatusNotFound, jsonhttp.StatusResponse{
Code: http.StatusNotFound,
Message: "peer not found",
})
})
t.Run("error", func(t *testing.T) {
jsonhttptest.ResponseDirect(t, client, http.MethodPost, "/pingpong/"+errorPeerID, nil, http.StatusInternalServerError, jsonhttp.StatusResponse{
Code: http.StatusInternalServerError,
Message: testErr.Error(),
})
})
t.Run("get method not allowed", func(t *testing.T) {
jsonhttptest.ResponseDirect(t, client, http.MethodGet, "/pingpong/"+peerID, nil, http.StatusMethodNotAllowed, jsonhttp.StatusResponse{
Code: http.StatusMethodNotAllowed,
Message: http.StatusText(http.StatusMethodNotAllowed),
})
})
}
......@@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"resenje.org/web"
......@@ -20,7 +21,9 @@ func (s *server) setupRouting() {
fmt.Fprintln(w, "User-agent: *\nDisallow: /")
})
baseRouter.HandleFunc("/pingpong/{peer-id}", s.pingpongHandler)
baseRouter.Handle("/pingpong/{peer-id}", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.pingpongHandler),
})
s.Handler = web.ChainHandlers(
handlers.CompressHandler,
......
......@@ -8,6 +8,7 @@ import (
"net/http"
"github.com/ethersphere/bee/pkg/p2p"
"github.com/ethersphere/bee/pkg/logging"
"github.com/prometheus/client_golang/prometheus"
)
......@@ -25,6 +26,7 @@ type server struct {
type Options struct {
P2P p2p.Service
Logger logging.Logger
}
func New(o Options) Service {
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package debugapi_test
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/ethersphere/bee/pkg/debugapi"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/p2p"
"resenje.org/web"
)
type testServerOptions struct {
P2P p2p.Service
}
func newTestServer(t *testing.T, o testServerOptions) (client *http.Client, cleanup func()) {
s := debugapi.New(debugapi.Options{
P2P: o.P2P,
Logger: logging.New(ioutil.Discard),
})
ts := httptest.NewServer(s)
cleanup = ts.Close
client = &http.Client{
Transport: web.RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
u, err := url.Parse(ts.URL + r.URL.String())
if err != nil {
return nil, err
}
r.URL = u
return ts.Client().Transport.RoundTrip(r)
}),
}
return client, cleanup
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package debugapi
type (
StatusResponse = statusResponse
PeerConnectResponse = peerConnectResponse
)
......@@ -19,13 +19,15 @@ type peerConnectResponse struct {
func (s *server) peerConnectHandler(w http.ResponseWriter, r *http.Request) {
addr, err := multiaddr.NewMultiaddr("/" + mux.Vars(r)["multi-address"])
if err != nil {
jsonhttp.BadRequest(w, err.Error())
s.Logger.Debugf("debug api: peer connect: parse multiaddress: %w", err)
jsonhttp.BadRequest(w, err)
return
}
address, err := s.P2P.Connect(r.Context(), addr)
if err != nil {
jsonhttp.InternalServerError(w, err.Error())
s.Logger.Errorf("debug api: peer connect: %w", err)
jsonhttp.InternalServerError(w, err)
return
}
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package debugapi_test
import (
"context"
"errors"
"net/http"
"testing"
"github.com/ethersphere/bee/pkg/debugapi"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/p2p/mock"
ma "github.com/multiformats/go-multiaddr"
)
func TestConnect(t *testing.T) {
underlay := "/ip4/127.0.0.1/tcp/7070/p2p/16Uiu2HAkx8ULY8cTXhdVAcMmLcH9AsTKz6uBQ7DPLKRjMLgBVYkS"
errorUnderlay := "/ip4/127.0.0.1/tcp/7070/p2p/16Uiu2HAkw88cjH2orYrB6fDui4eUNdmgkwnDM8W681UbfsPgM9QY"
overlay := "985732527402"
testErr := errors.New("test error")
client, cleanup := newTestServer(t, testServerOptions{
P2P: mock.NewService(func(ctx context.Context, addr ma.Multiaddr) (string, error) {
if addr.String() == errorUnderlay {
return "", testErr
}
return overlay, nil
}),
})
defer cleanup()
t.Run("ok", func(t *testing.T) {
jsonhttptest.ResponseDirect(t, client, http.MethodPost, "/connect"+underlay, nil, http.StatusOK, debugapi.PeerConnectResponse{
Address: overlay,
})
})
t.Run("error", func(t *testing.T) {
jsonhttptest.ResponseDirect(t, client, http.MethodPost, "/connect"+errorUnderlay, nil, http.StatusInternalServerError, jsonhttp.StatusResponse{
Code: http.StatusInternalServerError,
Message: testErr.Error(),
})
})
t.Run("get method not allowed", func(t *testing.T) {
jsonhttptest.ResponseDirect(t, client, http.MethodGet, "/connect"+underlay, nil, http.StatusMethodNotAllowed, jsonhttp.StatusResponse{
Code: http.StatusMethodNotAllowed,
Message: http.StatusText(http.StatusMethodNotAllowed),
})
})
}
......@@ -9,6 +9,7 @@ import (
"net/http"
"net/http/pprof"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
......@@ -42,7 +43,9 @@ func (s *server) setupRouting() {
internalRouter.HandleFunc("/health", s.statusHandler)
internalRouter.HandleFunc("/readiness", s.statusHandler)
internalRouter.HandleFunc("/connect/{multi-address:.+}", s.peerConnectHandler)
internalRouter.Handle("/connect/{multi-address:.+}", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.peerConnectHandler),
})
s.Handler = internalBaseRouter
}
......@@ -5,10 +5,17 @@
package debugapi
import (
"fmt"
"net/http"
"github.com/ethersphere/bee/pkg/jsonhttp"
)
type statusResponse struct {
Status string `json:"status"`
}
func (s *server) statusHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{"status":"ok"}`)
jsonhttp.OK(w, statusResponse{
Status: "ok",
})
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package debugapi_test
import (
"net/http"
"testing"
"github.com/ethersphere/bee/pkg/debugapi"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
)
func TestHealth(t *testing.T) {
client, cleanup := newTestServer(t, testServerOptions{})
defer cleanup()
jsonhttptest.ResponseDirect(t, client, http.MethodGet, "/health", nil, http.StatusOK, debugapi.StatusResponse{
Status: "ok",
})
}
func TestReadiness(t *testing.T) {
client, cleanup := newTestServer(t, testServerOptions{})
defer cleanup()
jsonhttptest.ResponseDirect(t, client, http.MethodGet, "/readiness", nil, http.StatusOK, debugapi.StatusResponse{
Status: "ok",
})
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jsonhttp
import (
"net/http"
"resenje.org/web"
)
type MethodHandler map[string]http.Handler
func (h MethodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
web.HandleMethods(h, `{"message":"Method Not Allowed","code":405}`, DefaultContentTypeHeader, w, r)
}
......@@ -22,23 +22,41 @@ var (
EscapeHTML = false
)
// StatusResponse is a standardized error format for specific HTTP responses.
// Code field corresponds with HTTP status code, and Message field is a short
// description of that code or provides more context about the reason for such
// response.
type StatusResponse struct {
Message string `json:"message,omitempty"`
Code int `json:"code,omitempty"`
}
// Respond writes a JSON-encoded body to http.ResponseWriter.
func Respond(w http.ResponseWriter, statusCode int, response interface{}) {
type statusResponse struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
if response == nil {
response = &statusResponse{
Code: statusCode,
response = &StatusResponse{
Message: http.StatusText(statusCode),
}
} else if message, ok := response.(string); ok {
response = &statusResponse{
Code: statusCode,
Message: message,
}
} else {
switch message := response.(type) {
case string:
response = &StatusResponse{
Message: message,
Code: statusCode,
}
case error:
response = &StatusResponse{
Message: message.Error(),
Code: statusCode,
}
case interface {
String() string
}:
response = &StatusResponse{
Message: message.String(),
Code: statusCode,
}
}
}
var b bytes.Buffer
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jsonhttptest
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"testing"
)
func ResponseDirect(t *testing.T, client *http.Client, method, url string, body io.Reader, responseCode int, response interface{}) {
t.Helper()
resp := request(t, client, method, url, body, responseCode)
defer resp.Body.Close()
got, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
got = bytes.TrimSpace(got)
want, err := json.Marshal(response)
if err != nil {
t.Error(err)
}
if !bytes.Equal(got, want) {
t.Errorf("got response %s, want %s", string(got), string(want))
}
}
func ResponseUnmarshal(t *testing.T, client *http.Client, method, url string, body io.Reader, responseCode int, response interface{}) {
t.Helper()
resp := request(t, client, method, url, body, responseCode)
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
t.Fatal(err)
}
}
func request(t *testing.T, client *http.Client, method, url string, body io.Reader, responseCode int) *http.Response {
t.Helper()
req, err := http.NewRequest(method, url, body)
if err != nil {
t.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != responseCode {
t.Errorf("got response status %s, want %v %s", resp.Status, responseCode, http.StatusText(responseCode))
}
return resp
}
......@@ -10,11 +10,25 @@ import (
"github.com/sirupsen/logrus"
)
func New(w io.Writer) *logrus.Logger {
logger := logrus.New()
logger.SetOutput(w)
logger.Formatter = &logrus.TextFormatter{
type Logger interface {
Tracef(format string, args ...interface{})
Trace(args ...interface{})
Debugf(format string, args ...interface{})
Debug(args ...interface{})
Infof(format string, args ...interface{})
Info(args ...interface{})
Warningf(format string, args ...interface{})
Warning(args ...interface{})
Errorf(format string, args ...interface{})
Error(args ...interface{})
SetOutput(io.Writer)
}
func New(w io.Writer) Logger {
l := logrus.New()
l.SetOutput(w)
l.Formatter = &logrus.TextFormatter{
FullTimestamp: true,
}
return logger
return l
}
......@@ -77,7 +77,6 @@ func NewBee(o Options) (*Bee, error) {
if o.APIAddr != "" {
// API server
apiService = api.New(api.Options{
P2P: p2ps,
Pingpong: pingPong,
})
apiListener, err := net.Listen("tcp", o.APIAddr)
......
......@@ -7,6 +7,7 @@ package handshake
import (
"fmt"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/p2p"
"github.com/ethersphere/bee/pkg/p2p/libp2p/internal/handshake/pb"
"github.com/ethersphere/bee/pkg/p2p/protobuf"
......@@ -21,10 +22,10 @@ const (
type Service struct {
overlay string
networkID int32
logger Logger
logger logging.Logger
}
func New(overlay string, networkID int32, logger Logger) *Service {
func New(overlay string, networkID int32, logger logging.Logger) *Service {
return &Service{
overlay: overlay,
networkID: networkID,
......@@ -32,10 +33,6 @@ func New(overlay string, networkID int32, logger Logger) *Service {
}
}
type Logger interface {
Tracef(format string, args ...interface{})
}
func (s *Service) Handshake(stream p2p.Stream) (i *Info, err error) {
w, r := protobuf.NewWriterAndReader(stream)
var resp pb.ShakeHand
......
......@@ -17,8 +17,8 @@ import (
"strconv"
"time"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/p2p"
handshake "github.com/ethersphere/bee/pkg/p2p/libp2p/internal/handshake"
"github.com/libp2p/go-libp2p"
autonat "github.com/libp2p/go-libp2p-autonat-svc"
......@@ -49,7 +49,7 @@ type Service struct {
networkID int32
handshakeService *handshake.Service
peers *peerRegistry
logger Logger
logger logging.Logger
}
type Options struct {
......@@ -62,13 +62,7 @@ type Options struct {
ConnectionsLow int
ConnectionsHigh int
ConnectionsGrace time.Duration
Logger Logger
}
type Logger interface {
Tracef(format string, args ...interface{})
Infof(format string, args ...interface{})
Errorf(format string, args ...interface{})
Logger logging.Logger
}
func New(ctx context.Context, o Options) (*Service, error) {
......
......@@ -6,13 +6,31 @@ package mock
import (
"context"
"errors"
"fmt"
"io"
"sync"
"github.com/ethersphere/bee/pkg/p2p"
ma "github.com/multiformats/go-multiaddr"
)
type Service struct {
connectFunc func(ctx context.Context, addr ma.Multiaddr) (overlay string, err error)
}
func NewService(connectFunc func(ctx context.Context, addr ma.Multiaddr) (overlay string, err error)) *Service {
return &Service{connectFunc: connectFunc}
}
func (s *Service) AddProtocol(_ p2p.ProtocolSpec) error {
return errors.New("not implemented")
}
func (s *Service) Connect(ctx context.Context, addr ma.Multiaddr) (overlay string, err error) {
return s.connectFunc(ctx, addr)
}
type Recorder struct {
records map[string][]Record
recordsMu sync.Mutex
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mock
import (
"context"
"time"
)
type Service struct {
pingFunc func(ctx context.Context, address string, msgs ...string) (rtt time.Duration, err error)
}
func New(pingFunc func(ctx context.Context, address string, msgs ...string) (rtt time.Duration, err error)) *Service {
return &Service{pingFunc: pingFunc}
}
func (s *Service) Ping(ctx context.Context, address string, msgs ...string) (rtt time.Duration, err error) {
return s.pingFunc(ctx, address, msgs...)
}
......@@ -10,6 +10,7 @@ import (
"io"
"time"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/p2p"
"github.com/ethersphere/bee/pkg/p2p/protobuf"
"github.com/ethersphere/bee/pkg/pingpong/pb"
......@@ -21,19 +22,19 @@ const (
streamVersion = "1.0.0"
)
type Interface interface {
Ping(ctx context.Context, address string, msgs ...string) (rtt time.Duration, err error)
}
type Service struct {
streamer p2p.Streamer
logger Logger
logger logging.Logger
metrics metrics
}
type Options struct {
Streamer p2p.Streamer
Logger Logger
}
type Logger interface {
Debugf(format string, args ...interface{})
Logger logging.Logger
}
func New(o Options) *Service {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment