Add option to cosume connect events rather than polling to restart deployments

This commit is contained in:
jillianwilson
2021-10-06 14:50:47 -03:00
parent b35c668959
commit a5f4a7a0c1
733 changed files with 86908 additions and 24010 deletions

201
vendor/github.com/suborbital/grav/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@@ -0,0 +1,84 @@
package local
import (
"encoding/json"
"fmt"
"time"
"github.com/schollz/peerdiscovery"
"github.com/suborbital/grav/grav"
"github.com/suborbital/vektor/vlog"
)
// Discovery is a grav Discovery plugin using local network multicast
type Discovery struct {
opts *grav.DiscoveryOpts
log *vlog.Logger
discoveryFunc grav.DiscoveryFunc
}
// payload is a discovery payload
type payload struct {
UUID string `json:"uuid"`
Port string `json:"port"`
Path string `json:"path"`
}
// New creates a new local discovery plugin
func New() *Discovery {
g := &Discovery{}
return g
}
// Start starts discovery
func (d *Discovery) Start(opts *grav.DiscoveryOpts, discoveryFunc grav.DiscoveryFunc) error {
d.opts = opts
d.log = opts.Logger
d.discoveryFunc = discoveryFunc
d.log.Info("[discovery-local] starting discovery, advertising endpoint", opts.TransportPort, opts.TransportURI)
payloadFunc := func() []byte {
payload := payload{
UUID: d.opts.NodeUUID,
Port: opts.TransportPort,
Path: opts.TransportURI,
}
payloadBytes, _ := json.Marshal(payload)
return payloadBytes
}
notifyFunc := func(peer peerdiscovery.Discovered) {
d.log.Debug("[discovery-local] potential peer found:", peer.Address)
payload := payload{}
if err := json.Unmarshal(peer.Payload, &payload); err != nil {
d.log.Debug("[discovery-local] peer did not offer correct payload, discarding")
return
}
endpoint := fmt.Sprintf("%s:%s%s", peer.Address, payload.Port, payload.Path)
// send the discovery to Grav. Grav is responsible for ensuring uniqueness of the connections.
d.discoveryFunc(endpoint, payload.UUID)
}
_, err := peerdiscovery.Discover(peerdiscovery.Settings{
Limit: -1,
PayloadFunc: payloadFunc,
Delay: 10 * time.Second,
TimeLimit: -1,
Notify: notifyFunc,
AllowSelf: true,
})
return err
}
// UseDiscoveryFunc sets the function to be used when a new peer is discovered
func (d *Discovery) UseDiscoveryFunc(dFunc func(endpoint string, uuid string)) {
d.discoveryFunc = dFunc
}

83
vendor/github.com/suborbital/grav/grav/bus.go generated vendored Normal file
View File

@@ -0,0 +1,83 @@
package grav
const (
defaultBusChanSize = 256
)
// messageBus is responsible for emitting messages among the connected pods
// and managing the failure cases for those pods
type messageBus struct {
busChan MsgChan
pool *connectionPool
buffer *MsgBuffer
}
// newMessageBus creates a new messageBus
func newMessageBus() *messageBus {
b := &messageBus{
busChan: make(chan Message, defaultBusChanSize),
pool: newConnectionPool(),
buffer: NewMsgBuffer(defaultBufferSize),
}
b.start()
return b
}
// addPod adds a pod to the connection pool
func (b *messageBus) addPod(pod *Pod) {
b.pool.insert(pod)
}
func (b *messageBus) start() {
go func() {
// continually take new messages and for each,
// grab the next active connection from the ring and then
// start traversing around the ring to emit the message to
// each connection until landing back at the beginning of the
// ring, and repeat forever when each new message arrives
for msg := range b.busChan {
for {
// make sure the next pod is ready for messages
if err := b.pool.prepareNext(b.buffer); err == nil {
break
}
}
startingConn := b.pool.next()
b.traverse(msg, startingConn)
b.buffer.Push(msg)
}
}()
}
func (b *messageBus) traverse(msg Message, start *podConnection) {
startID := start.ID
conn := start
for {
// send the message to the pod
conn.send(msg)
// run checks on the next podConnection to see if
// anything needs to be done (including potentially deleting it)
next := b.pool.peek()
if err := b.pool.prepareNext(b.buffer); err != nil {
if startID == next.ID {
startID = next.next.ID
}
}
// now advance the ring
conn = b.pool.next()
if startID == conn.ID {
// if we have arrived back at the starting point on the ring
// we have done our job and are ready for the next message
break
}
}
}

21
vendor/github.com/suborbital/grav/grav/discovery.go generated vendored Normal file
View File

@@ -0,0 +1,21 @@
package grav
import "github.com/suborbital/vektor/vlog"
// DiscoveryFunc is a function that allows a plugin to report a newly discovered node
type DiscoveryFunc func(endpoint string, uuid string)
// Discovery represents a discovery plugin
type Discovery interface {
// Start is called to start the Discovery plugin
Start(*DiscoveryOpts, DiscoveryFunc) error
}
// DiscoveryOpts is a set of options for transports
type DiscoveryOpts struct {
NodeUUID string
TransportPort string
TransportURI string
Logger *vlog.Logger
Custom interface{}
}

71
vendor/github.com/suborbital/grav/grav/filter.go generated vendored Normal file
View File

@@ -0,0 +1,71 @@
package grav
import (
"sync"
)
// messageFilter is a series of maps that associate things about a message (its UUID, type, etc) with a boolean value to say if
// it should be allowed or denied. For each of the maps, if an entry is included, the value of the boolean is respected (true = allow, false = deny)
// Maps are either inclusive (meaning that a missing entry defaults to allow), or exclusive (meaning that a missing entry defaults to deny)
// This can be configured per map by modifiying the UUIDInclusive, TypeInclusive (etc) fields.
type messageFilter struct {
UUIDMap map[string]bool
UUIDInclusive bool
TypeMap map[string]bool
TypeInclusive bool
lock sync.RWMutex
}
func newMessageFilter() *messageFilter {
mf := &messageFilter{
UUIDMap: map[string]bool{},
UUIDInclusive: true,
TypeMap: map[string]bool{},
TypeInclusive: true,
lock: sync.RWMutex{},
}
return mf
}
func (mf *messageFilter) allow(msg Message) bool {
mf.lock.RLock()
defer mf.lock.RUnlock()
// for each map, deny the message if:
// - a filter entry exists and it's value is false
// - a filter entry doesn't exist and its inclusive rule is false
allowType, typeExists := mf.TypeMap[msg.Type()]
if typeExists && !allowType {
return false
} else if !typeExists && !mf.TypeInclusive {
return false
}
allowUUID, uuidExists := mf.UUIDMap[msg.UUID()]
if uuidExists && !allowUUID {
return false
} else if !uuidExists && !mf.UUIDInclusive {
return false
}
return true
}
// FilterUUID likely should not be used in normal cases, it adds a message UUID to the pod's filter.
func (mf *messageFilter) FilterUUID(uuid string, allow bool) {
mf.lock.Lock()
defer mf.lock.Unlock()
mf.UUIDMap[uuid] = allow
}
func (mf *messageFilter) FilterType(msgType string, allow bool) {
mf.lock.Lock()
defer mf.lock.Unlock()
mf.TypeMap[msgType] = allow
}

72
vendor/github.com/suborbital/grav/grav/grav.go generated vendored Normal file
View File

@@ -0,0 +1,72 @@
package grav
import (
"github.com/pkg/errors"
"github.com/google/uuid"
"github.com/suborbital/vektor/vlog"
)
// ErrTransportNotConfigured represent package-level vars
var (
ErrTransportNotConfigured = errors.New("transport plugin not configured")
)
// Grav represents a Grav message bus instance
type Grav struct {
NodeUUID string
bus *messageBus
logger *vlog.Logger
hub *hub
}
// New creates a new Grav with the provided options
func New(opts ...OptionsModifier) *Grav {
nodeUUID := uuid.New().String()
options := newOptionsWithModifiers(opts...)
g := &Grav{
NodeUUID: nodeUUID,
bus: newMessageBus(),
logger: options.Logger,
}
// the hub handles coordinating the transport and discovery plugins
g.hub = initHub(nodeUUID, options, options.Transport, options.Discovery, g.Connect)
return g
}
// Connect creates a new connection (pod) to the bus
func (g *Grav) Connect() *Pod {
opts := &podOpts{WantsReplay: false}
return g.connectWithOpts(opts)
}
// ConnectWithReplay creates a new connection (pod) to the bus
// and replays recent messages when the pod sets its onFunc
func (g *Grav) ConnectWithReplay() *Pod {
opts := &podOpts{WantsReplay: true}
return g.connectWithOpts(opts)
}
// ConnectEndpoint uses the configured transport to connect the bus to an external endpoint
func (g *Grav) ConnectEndpoint(endpoint string) error {
return g.hub.connectEndpoint(endpoint, "")
}
// ConnectBridgeTopic connects the Grav instance to a particular topic on the connected bridge
func (g *Grav) ConnectBridgeTopic(topic string) error {
return g.hub.connectBridgeTopic(topic)
}
func (g *Grav) connectWithOpts(opts *podOpts) *Pod {
pod := newPod(g.bus.busChan, opts)
g.bus.addPod(pod)
return pod
}

290
vendor/github.com/suborbital/grav/grav/hub.go generated vendored Normal file
View File

@@ -0,0 +1,290 @@
package grav
import (
"sync"
"github.com/pkg/errors"
"github.com/suborbital/vektor/vlog"
)
// hub is responsible for coordinating the transport and discovery plugins
type hub struct {
nodeUUID string
transport Transport
discovery Discovery
log *vlog.Logger
pod *Pod
connectFunc func() *Pod
connections map[string]Connection
topicConnections map[string]TopicConnection
lock sync.RWMutex
}
func initHub(nodeUUID string, options *Options, tspt Transport, dscv Discovery, connectFunc func() *Pod) *hub {
h := &hub{
nodeUUID: nodeUUID,
transport: tspt,
discovery: dscv,
log: options.Logger,
pod: connectFunc(),
connectFunc: connectFunc,
connections: map[string]Connection{},
topicConnections: map[string]TopicConnection{},
lock: sync.RWMutex{},
}
// start transport, then discovery if each have been configured (can have transport but no discovery)
if h.transport != nil {
transportOpts := &TransportOpts{
NodeUUID: nodeUUID,
Port: options.Port,
URI: options.URI,
Logger: options.Logger,
}
// setup messages to be sent to all active connections
h.pod.On(h.outgoingMessageHandler())
go func() {
if err := h.transport.Setup(transportOpts, h.handleIncomingConnection, h.findConnection); err != nil {
options.Logger.Error(errors.Wrap(err, "failed to Setup transport"))
}
}()
if h.discovery != nil {
discoveryOpts := &DiscoveryOpts{
NodeUUID: nodeUUID,
TransportPort: transportOpts.Port,
TransportURI: transportOpts.URI,
Logger: options.Logger,
}
go func() {
if err := h.discovery.Start(discoveryOpts, h.discoveryHandler()); err != nil {
options.Logger.Error(errors.Wrap(err, "failed to Start discovery"))
}
}()
}
}
return h
}
func (h *hub) discoveryHandler() func(endpoint string, uuid string) {
return func(endpoint string, uuid string) {
if uuid == h.nodeUUID {
h.log.Debug("discovered self, discarding")
return
}
// connectEndpoint does this check as well, but it's better to do it here as well
// as it reduces the number of extraneous outgoing handshakes that get attempted.
if existing, exists := h.findConnection(uuid); exists {
if !existing.CanReplace() {
h.log.Debug("encountered duplicate connection, discarding")
return
}
}
if err := h.connectEndpoint(endpoint, uuid); err != nil {
h.log.Error(errors.Wrap(err, "failed to connectEndpoint for discovered peer"))
}
}
}
// connectEndpoint creates a new outgoing connection
func (h *hub) connectEndpoint(endpoint, uuid string) error {
if h.transport == nil {
return ErrTransportNotConfigured
}
if h.transport.Type() == TransportTypeBridge {
return ErrBridgeOnlyTransport
}
h.log.Debug("connecting to endpoint", endpoint)
conn, err := h.transport.CreateConnection(endpoint)
if err != nil {
return errors.Wrap(err, "failed to transport.CreateConnection")
}
h.setupOutgoingConnection(conn, uuid)
return nil
}
// connectBridgeTopic creates a new outgoing connection
func (h *hub) connectBridgeTopic(topic string) error {
if h.transport == nil {
return ErrTransportNotConfigured
}
if h.transport.Type() != TransportTypeBridge {
return ErrNotBridgeTransport
}
h.log.Debug("connecting to topic", topic)
conn, err := h.transport.ConnectBridgeTopic(topic)
if err != nil {
return errors.Wrap(err, "failed to transport.CreateConnection")
}
h.addTopicConnection(conn, topic)
return nil
}
func (h *hub) setupOutgoingConnection(connection Connection, uuid string) {
handshake := &TransportHandshake{h.nodeUUID}
ack, err := connection.DoOutgoingHandshake(handshake)
if err != nil {
h.log.Error(errors.Wrap(err, "failed to connection.DoOutgoingHandshake"))
connection.Close()
return
}
if uuid == "" {
if ack.UUID == "" {
h.log.ErrorString("connection handshake returned empty UUID, terminating connection")
connection.Close()
return
}
uuid = ack.UUID
} else if ack.UUID != uuid {
h.log.ErrorString("connection handshake Ack did not match Discovery Ack, terminating connection")
connection.Close()
return
}
h.setupNewConnection(connection, uuid)
}
func (h *hub) handleIncomingConnection(connection Connection) {
ack := &TransportHandshakeAck{h.nodeUUID}
handshake, err := connection.DoIncomingHandshake(ack)
if err != nil {
h.log.Error(errors.Wrap(err, "failed to connection.DoIncomingHandshake"))
connection.Close()
return
}
if handshake.UUID == "" {
h.log.ErrorString("connection handshake returned empty UUID, terminating connection")
connection.Close()
return
}
h.setupNewConnection(connection, handshake.UUID)
}
func (h *hub) setupNewConnection(connection Connection, uuid string) {
// if an existing connection is found, check if it can be replaced and do so if possible
if existing, exists := h.findConnection(uuid); exists {
if !existing.CanReplace() {
connection.Close()
h.log.Debug("encountered duplicate connection, discarding")
} else {
existing.Close()
h.replaceConnection(connection, uuid)
}
} else {
h.addConnection(connection, uuid)
}
}
func (h *hub) outgoingMessageHandler() MsgFunc {
return func(msg Message) error {
// read-lock while dispatching all of the goroutines to prevent concurrent read/write
h.lock.RLock()
defer h.lock.RUnlock()
for u := range h.connections {
uuid := u
conn := h.connections[uuid]
go func() {
h.log.Debug("sending message", msg.UUID(), "to", uuid)
if err := conn.Send(msg); err != nil {
if errors.Is(err, ErrConnectionClosed) {
h.log.Debug("attempted to send on closed connection, will remove")
} else {
h.log.Warn("error sending to connection", uuid, ":", err.Error())
}
h.removeConnection(uuid)
}
}()
}
return nil
}
}
func (h *hub) incomingMessageHandler(uuid string) ReceiveFunc {
return func(msg Message) {
h.log.Debug("received message ", msg.UUID(), "from node", uuid)
h.pod.Send(msg)
}
}
func (h *hub) addConnection(connection Connection, uuid string) {
h.lock.Lock()
defer h.lock.Unlock()
h.log.Debug("adding connection for", uuid)
connection.Start(h.incomingMessageHandler(uuid))
h.connections[uuid] = connection
}
func (h *hub) addTopicConnection(connection TopicConnection, topic string) {
h.lock.Lock()
defer h.lock.Unlock()
h.log.Debug("adding bridge connection for", topic)
connection.Start(h.connectFunc())
h.topicConnections[topic] = connection
}
func (h *hub) replaceConnection(newConnection Connection, uuid string) {
h.lock.Lock()
defer h.lock.Unlock()
h.log.Debug("replacing connection for", uuid)
delete(h.connections, uuid)
newConnection.Start(h.incomingMessageHandler(uuid))
h.connections[uuid] = newConnection
}
func (h *hub) removeConnection(uuid string) {
h.lock.Lock()
defer h.lock.Unlock()
h.log.Debug("removing connection for", uuid)
delete(h.connections, uuid)
}
func (h *hub) findConnection(uuid string) (Connection, bool) {
h.lock.RLock()
defer h.lock.RUnlock()
conn, exists := h.connections[uuid]
return conn, exists
}

170
vendor/github.com/suborbital/grav/grav/message.go generated vendored Normal file
View File

@@ -0,0 +1,170 @@
package grav
import (
"encoding/json"
"io/ioutil"
"net/http"
"time"
"github.com/google/uuid"
)
// MsgTypeDefault and other represent message consts
const (
MsgTypeDefault string = "grav.default"
msgTypePodFeedback string = "grav.feedback"
)
// MsgFunc is a callback function that accepts a message and returns an error
type MsgFunc func(Message) error
// MsgChan is a channel that accepts a message
type MsgChan chan Message
// Message represents a message
type Message interface {
// Unique ID for this message
UUID() string
// ID of the parent event or request, such as HTTP request
ParentID() string
// The UUID of the message being replied to, if any
ReplyTo() string
// Allow setting a message UUID that this message is a response to
SetReplyTo(string)
// Type of message (application-specific)
Type() string
// Time the message was sent
Timestamp() time.Time
// Raw data of message
Data() []byte
// Unmarshal the message's data into a struct
UnmarshalData(interface{}) error
// Marshal the message itself to encoded bytes (JSON or otherwise)
Marshal() ([]byte, error)
// Unmarshal encoded Message into object
Unmarshal([]byte) error
}
// NewMsg creates a new Message with the built-in `_message` type
func NewMsg(msgType string, data []byte) Message {
return new(msgType, "", data)
}
// NewMsgWithParentID returns a new message with the provided parent ID
func NewMsgWithParentID(msgType, parentID string, data []byte) Message {
return new(msgType, parentID, data)
}
// NewMsgReplyTo creates a new message in response to a previous message
func NewMsgReplyTo(ticket MsgReceipt, msgType string, data []byte) Message {
m := new(msgType, "", data)
m.SetReplyTo(ticket.UUID)
return m
}
// MsgFromBytes returns a default _message that has been unmarshalled from bytes.
// Should only be used if the default _message type is being used.
func MsgFromBytes(bytes []byte) (Message, error) {
m := &_message{}
if err := m.Unmarshal(bytes); err != nil {
return nil, err
}
return m, nil
}
// MsgFromRequest extracts an encoded Message from an HTTP request
func MsgFromRequest(r *http.Request) (Message, error) {
defer r.Body.Close()
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
return MsgFromBytes(bytes)
}
func new(msgType, parentID string, data []byte) Message {
uuid := uuid.New()
m := &_message{
Meta: _meta{
UUID: uuid.String(),
ParentID: parentID,
ReplyTo: "",
MsgType: msgType,
Timestamp: time.Now(),
},
Payload: _payload{
Data: data,
},
}
return m
}
// _message is a basic built-in implementation of Message
// most applications should define their own data structure
// that implements the interface
type _message struct {
Meta _meta `json:"meta"`
Payload _payload `json:"payload"`
}
type _meta struct {
UUID string `json:"uuid"`
ParentID string `json:"parent_id"`
ReplyTo string `json:"response_to"`
MsgType string `json:"msg_type"`
Timestamp time.Time `json:"timestamp"`
}
type _payload struct {
Data []byte `json:"data"`
}
func (m *_message) UUID() string {
return m.Meta.UUID
}
func (m *_message) ParentID() string {
return m.Meta.ParentID
}
func (m *_message) ReplyTo() string {
return m.Meta.ReplyTo
}
func (m *_message) SetReplyTo(uuid string) {
m.Meta.ReplyTo = uuid
}
func (m *_message) Type() string {
return m.Meta.MsgType
}
func (m *_message) Timestamp() time.Time {
return m.Meta.Timestamp
}
func (m *_message) Data() []byte {
return m.Payload.Data
}
func (m *_message) UnmarshalData(target interface{}) error {
return json.Unmarshal(m.Payload.Data, target)
}
func (m *_message) Marshal() ([]byte, error) {
bytes, err := json.Marshal(m)
if err != nil {
return nil, err
}
return bytes, nil
}
func (m *_message) Unmarshal(bytes []byte) error {
return json.Unmarshal(bytes, m)
}

90
vendor/github.com/suborbital/grav/grav/msgbuffer.go generated vendored Normal file
View File

@@ -0,0 +1,90 @@
package grav
import (
"sync"
)
const (
defaultBufferSize = 128
)
// MsgBuffer is a buffer of messages with a particular size limit.
// Oldest messages are automatically evicted as new ones are added
// past said limit. Push() and Iter() are thread-safe.
type MsgBuffer struct {
msgs map[string]Message
order []string
limit int
startIndex int
lock sync.RWMutex
}
func NewMsgBuffer(limit int) *MsgBuffer {
m := &MsgBuffer{
msgs: map[string]Message{},
order: []string{},
limit: limit,
startIndex: 0,
lock: sync.RWMutex{},
}
return m
}
// Push pushes a new message onto the end of the buffer and evicts the oldest, if needed (based on limit)
func (m *MsgBuffer) Push(msg Message) {
m.lock.Lock()
defer m.lock.Unlock()
m.msgs[msg.UUID()] = msg
lastIndex := len(m.order) - 1
if len(m.order) == m.limit {
delete(m.msgs, m.order[m.startIndex]) // delete the current "first"
m.order[m.startIndex] = msg.UUID()
if m.startIndex == lastIndex {
m.startIndex = 0
} else {
m.startIndex++
}
} else {
m.order = append(m.order, msg.UUID())
}
}
// Iter calls msgFunc once per message in the buffer
func (m *MsgBuffer) Iter(msgFunc MsgFunc) {
m.lock.RLock()
defer m.lock.RUnlock()
if len(m.order) == 0 {
return
}
index := m.startIndex
lastIndex := len(m.order) - 1
more := true
for more {
uuid := m.order[index]
msg := m.msgs[uuid]
msgFunc(msg)
newIndex := index
if newIndex == lastIndex {
newIndex = 0
} else {
newIndex++
}
if newIndex == m.startIndex {
more = false
}
index = newIndex
}
}

72
vendor/github.com/suborbital/grav/grav/options.go generated vendored Normal file
View File

@@ -0,0 +1,72 @@
package grav
import "github.com/suborbital/vektor/vlog"
// Options represent Grav options
type Options struct {
Logger *vlog.Logger
Transport Transport
Discovery Discovery
Port string
URI string
}
// OptionsModifier is function that modifies an option
type OptionsModifier func(*Options)
func newOptionsWithModifiers(mods ...OptionsModifier) *Options {
opts := defaultOptions()
for _, m := range mods {
m(opts)
}
return opts
}
// UseLogger allows a custom logger to be used
func UseLogger(logger *vlog.Logger) OptionsModifier {
return func(o *Options) {
o.Logger = logger
}
}
// UseTransport sets the transport plugin to be used.
func UseTransport(transport Transport) OptionsModifier {
return func(o *Options) {
o.Transport = transport
}
}
// UseEndpoint sets the endpoint settings for the instance to broadcast for discovery
// Pass empty strings for either if you would like to keep the defaults (8080 and /meta/message)
func UseEndpoint(port, uri string) OptionsModifier {
return func(o *Options) {
if port != "" {
o.Port = port
}
if uri != "" {
o.URI = uri
}
}
}
// UseDiscovery sets the discovery plugin to be used
func UseDiscovery(discovery Discovery) OptionsModifier {
return func(o *Options) {
o.Discovery = discovery
}
}
func defaultOptions() *Options {
o := &Options{
Logger: vlog.Default(),
Port: "8080",
URI: "/meta/message",
Transport: nil,
Discovery: nil,
}
return o
}

277
vendor/github.com/suborbital/grav/grav/pod.go generated vendored Normal file
View File

@@ -0,0 +1,277 @@
package grav
import (
"errors"
"sync"
"sync/atomic"
)
const (
// defaultPodChanSize is the default size of the channels used for pod - bus communication
defaultPodChanSize = 128
)
// podFeedbackMsgReplay and others are the messages sent via feedback channel when the pod needs to communicate its state to the bus
var (
podFeedbackMsgReplay = NewMsg(msgTypePodFeedback, []byte{})
podFeedbackMsgSuccess = NewMsg(msgTypePodFeedback, []byte{})
podFeedbackMsgDisconnect = NewMsg(msgTypePodFeedback, []byte{})
)
/**
┌─────────────────────┐
│ │
──messageChan─────▶─────────────────────▶─────On────▶
┌────────┐ │ │ ┌───────────────┐
│ Bus │ │ Pod │ │ Pod Owner │
└────────┘ │ │ └───────────────┘
◀───BusChan------─◀─────────────────────◀────Send────
│ │
└─────────────────────┘
Created with Monodraw
**/
// Pod is a connection to Grav
// Pods are bi-directional. Messages can be sent to them from the bus, and they can be used to send messages
// to the bus. Pods are meant to be extremely lightweight with no persistence they are meant to quickly
// and immediately route a message between its owner and the Bus. The Bus is responsible for any "smarts".
// Messages coming from the bus are filtered using the pod's messageFilter, which is configurable by the caller.
type Pod struct {
onFunc MsgFunc // the onFunc is called whenever a message is recieved
onFuncLock sync.RWMutex
messageChan MsgChan // messageChan is used to recieve messages coming from the bus
feedbackChan MsgChan // feedbackChan is used to send "feedback" to the bus about the pod's status
busChan MsgChan // busChan is used to emit messages to the bus
*messageFilter // the embedded messageFilter controls which messages reach the onFunc
opts *podOpts
dead *atomic.Value
}
type podOpts struct {
WantsReplay bool
replayOnce sync.Once
}
// newPod creates a new Pod
func newPod(busChan MsgChan, opts *podOpts) *Pod {
p := &Pod{
onFuncLock: sync.RWMutex{},
messageChan: make(chan Message, defaultPodChanSize),
feedbackChan: make(chan Message, defaultPodChanSize),
busChan: busChan,
messageFilter: newMessageFilter(),
opts: opts,
dead: &atomic.Value{},
}
// do some "delayed setup"
p.opts.replayOnce = sync.Once{}
p.dead.Store(false)
p.start()
return p
}
// Send emits a message to be routed to the bus
// If the returned ticket is nil, it means the pod was unable to send
// It is safe to call methods on a nil ticket, they will error with ErrNoTicket
// This means error checking can be done on a chained call such as err := p.Send(msg).Wait(...)
func (p *Pod) Send(msg Message) *MsgReceipt {
// check to see if the pod has died (aka disconnected)
if p.dead.Load().(bool) == true {
return nil
}
p.FilterUUID(msg.UUID(), false) // don't allow the same message to bounce back through this pod
p.busChan <- msg
t := &MsgReceipt{
UUID: msg.UUID(),
pod: p,
}
return t
}
// ReplyTo sends a response to a message. The reply message's ticket is returned.
func (p *Pod) ReplyTo(inReplyTo Message, msg Message) *MsgReceipt {
msg.SetReplyTo(inReplyTo.UUID())
return p.Send(msg)
}
// On sets the function to be called whenever this pod recieves a message from the bus. If nil is passed, the pod will ignore all messages.
// Calling On multiple times causes the function to be overwritten. To recieve using two different functions, create two pods.
// Errors returned from the onFunc are interpreted as problems handling messages. Too many errors will result in the pod being disconnected.
// Failed messages will be replayed when messages begin to succeed. Returning an error is inadvisable unless there is a real problem handling messages.
func (p *Pod) On(onFunc MsgFunc) {
p.onFuncLock.Lock()
defer p.onFuncLock.Unlock()
p.setOnFunc(onFunc)
}
// OnType sets the function to be called whenever this pod recieves a message and sets the pod's filter to only receive certain message types.
// The same rules as `On` about error handling apply to OnType.
func (p *Pod) OnType(msgType string, onFunc MsgFunc) {
p.onFuncLock.Lock()
defer p.onFuncLock.Unlock()
p.setOnFunc(onFunc)
p.FilterType(msgType, true)
p.TypeInclusive = false // only allow the listed types
}
// Disconnect indicates to the bus that this pod is no longer needed and should be disconnected.
// Sending will immediately become unavailable, and the pod will soon stop recieving messages.
func (p *Pod) Disconnect() {
// stop future messages from being sent and then indicate to the bus that disconnection is desired
// The bus will close the busChan, which will cause the onFunc listener to quit.
p.dead.Store(true)
p.feedbackChan <- podFeedbackMsgDisconnect
}
// ErrMsgNotWanted is used by WaitOn to determine if the current message is what's being waited on
var ErrMsgNotWanted = errors.New("message not wanted")
// ErrWaitTimeout is returned if a timeout is exceeded
var ErrWaitTimeout = errors.New("waited past timeout")
// WaitOn takes a function to be called whenever this pod recieves a message and blocks until that function returns
// something other than ErrMsgNotWanted. WaitOn should be used if there is a need to wait for a particular message.
// When the onFunc returns something other than ErrMsgNotWanted (such as nil or a different error), WaitOn will return and set
// the onFunc to nil. If an error other than ErrMsgNotWanted is returned from the onFunc, it will be propogated to the caller.
// WaitOn will block forever if the desired message is never found. Use WaitUntil if a timeout is desired.
func (p *Pod) WaitOn(onFunc MsgFunc) error {
return p.WaitUntil(nil, onFunc)
}
// WaitUntil takes a function to be called whenever this pod recieves a message and blocks until that function returns
// something other than ErrMsgNotWanted. WaitOn should be used if there is a need to wait for a particular message.
// When the onFunc returns something other than ErrMsgNotWanted (such as nil or a different error), WaitUntil will return and set
// the onFunc to nil. If an error other than ErrMsgNotWanted is returned from the onFunc, it will be propogated to the caller.
// A timeout can be provided. If the timeout is non-nil and greater than 0, ErrWaitTimeout is returned if the time is exceeded.
func (p *Pod) WaitUntil(timeout TimeoutFunc, onFunc MsgFunc) error {
p.onFuncLock.Lock()
errChan := make(chan error)
p.setOnFunc(func(msg Message) error {
if err := onFunc(msg); err != nil {
if err == ErrMsgNotWanted {
return nil // don't do anything
}
errChan <- err
} else {
errChan <- nil
}
return nil
})
p.onFuncLock.Unlock() // can't stay locked here or the onFunc will never be called
var onFuncErr error
if timeout == nil {
timeout = Timeout(-1)
}
select {
case err := <-errChan:
onFuncErr = err
case <-timeout():
onFuncErr = ErrWaitTimeout
}
p.onFuncLock.Lock()
defer p.onFuncLock.Unlock()
p.setOnFunc(nil)
return onFuncErr
}
// waitOnReply waits on a reply message to arrive at the pod and then calls onFunc with that message.
// If the onFunc produces an error, it will be propogated to the caller.
// If a non-nil timeout greater than 0 is passed, the function will return ErrWaitTimeout if the timeout elapses.
func (p *Pod) waitOnReply(ticket *MsgReceipt, timeout TimeoutFunc, onFunc MsgFunc) error {
var reply Message
if err := p.WaitUntil(timeout, func(msg Message) error {
if msg.ReplyTo() != ticket.UUID {
return ErrMsgNotWanted
}
reply = msg
return nil
}); err != nil {
return err
}
return onFunc(reply)
}
// setOnFunc sets the OnFunc. THIS DOES NOT LOCK. THE CALLER MUST LOCK.
func (p *Pod) setOnFunc(on MsgFunc) {
// reset the message filter when the onFunc is changed
p.messageFilter = newMessageFilter()
p.onFunc = on
// request replay from the bus if needed
if on != nil {
p.opts.replayOnce.Do(func() {
if p.opts.WantsReplay {
p.feedbackChan <- podFeedbackMsgReplay
}
})
}
}
// busChans returns the messageChan and feedbackChan to be used by the bus
func (p *Pod) busChans() (MsgChan, MsgChan) {
return p.messageChan, p.feedbackChan
}
func (p *Pod) start() {
go func() {
// this loop ends when the bus closes the messageChan
for {
msg, ok := <-p.messageChan
if !ok {
break
}
go func() {
p.onFuncLock.RLock() // in case the onFunc gets replaced
defer p.onFuncLock.RUnlock()
if p.onFunc == nil {
return
}
if p.allow(msg) {
if err := p.onFunc(msg); err != nil {
// if the onFunc failed, send it back to the bus to be re-sent later
p.feedbackChan <- msg
} else {
// if it was successful, a success message on the channel lets the conn know all is well
p.feedbackChan <- podFeedbackMsgSuccess
}
}
}()
}
// if we've gotten this far, it means the pod has been killed and should not be allowed to send
p.dead.Store(true)
}()
}

260
vendor/github.com/suborbital/grav/grav/pool.go generated vendored Normal file
View File

@@ -0,0 +1,260 @@
package grav
import (
"errors"
"sync"
"sync/atomic"
)
const (
highWaterMark = 64
)
var (
errFailedMessage = errors.New("pod reports failed message")
errFailedMessageMax = errors.New("pod reports max number of failed messages, will terminate connection")
)
// connectionPool is a ring of connections to pods
// which will be iterated over constantly in order to send
// incoming messages to them
type connectionPool struct {
current *podConnection
maxID int64
lock sync.Mutex
}
func newConnectionPool() *connectionPool {
p := &connectionPool{
current: nil,
maxID: 0,
lock: sync.Mutex{},
}
return p
}
// insert inserts a new connection into the ring
func (c *connectionPool) insert(pod *Pod) {
c.lock.Lock()
defer c.lock.Unlock()
c.maxID++
id := c.maxID
conn := newPodConnection(id, pod)
// if there's nothing in the ring, create a "ring of one"
if c.current == nil {
conn.next = conn
c.current = conn
} else {
c.current.insertAfter(conn)
}
}
// peek returns a peek at the next connection in the ring wihout advancing the ring's current location
func (c *connectionPool) peek() *podConnection {
c.lock.Lock()
defer c.lock.Unlock()
return c.current.next
}
// next returns the next connection in the ring
func (c *connectionPool) next() *podConnection {
c.lock.Lock()
defer c.lock.Unlock()
c.current = c.current.next
return c.current
}
// prepareNext ensures that the next pod connection in the ring is ready to recieve
// new messages by checking its status, deleting it if unhealthy or disconnected, replaying the message
// buffer if needed, or flushing failed messages back onto its channel if needeed.
func (c *connectionPool) prepareNext(buffer *MsgBuffer) error {
// peek gives us the next conn without advancing the ring
// this makes it easy to delete the next conn if it's unhealthy
next := c.peek()
// check the state of the next connection
status := next.checkStatus()
if status.Error != nil {
// if the connection has an issue, handle it
if status.Error == errFailedMessageMax {
c.deleteNext()
return errors.New("removing next podConnection")
}
} else if status.WantsDisconnect {
// if the pod has requested disconnection, grant its wish
c.deleteNext()
return errors.New("next pod requested disconnection, removing podConnection")
} else if status.WantsReplay {
// if the pod has indicated that it wants a replay of recent messages, do so
c.replayNext(buffer)
}
if status.HadSuccess {
// if the most recent status check indicates there was a success,
// then tell the connection to flush any failed messages
// this is a no-op if there are no failed messages queued
next.flushFailed()
}
return nil
}
// replayNext replays the current message buffer into the next connection
func (c *connectionPool) replayNext(buffer *MsgBuffer) {
next := c.peek()
// iterate over the buffer and send each message to the pod
buffer.Iter(func(msg Message) error {
next.send(msg)
return nil
})
}
// deleteNext deletes the next connection in the ring
// this is useful after having checkError'd the next conn
// and seeing that it's unhealthy
func (c *connectionPool) deleteNext() {
c.lock.Lock()
defer c.lock.Unlock()
next := c.current.next
// indicate the conn is dead so future attempts to send are abandonded
next.dead.Store(true)
// close the messageChan so the pod can know it's been cut off
close(next.messageChan)
if next == c.current {
// if there's only one thing in the ring, empty the ring
c.current = nil
} else {
// cut out `next` and link `current` to `next-next`
c.current.next = next.next
}
}
// podConnection is a connection to a pod via its messageChan
// podConnection is also a circular linked list/ring of connections
// that is meant to be iterated around and inserted into/removed from
// forever as the bus sends events to the registered pods
type podConnection struct {
ID int64
next *podConnection
messageChan MsgChan
feedbackChan MsgChan
failed []Message
dead *atomic.Value
}
// connStatus is used to communicate the status of a podConnection back to the bus
type connStatus struct {
HadSuccess bool
WantsReplay bool
WantsDisconnect bool
Error error
}
func newPodConnection(id int64, pod *Pod) *podConnection {
msgChan, feedbackChan := pod.busChans()
p := &podConnection{
ID: id,
messageChan: msgChan,
feedbackChan: feedbackChan,
failed: []Message{},
dead: &atomic.Value{},
next: nil,
}
p.dead.Store(false)
return p
}
// send asynchronously writes a message to a connection's messageChan
// ordering to the messageChan if it becomes full is not guaranteed, this
// is sacrificed to ensure that the bus does not block because of a delinquient pod
func (p *podConnection) send(msg Message) {
go func() {
// if the conn is dead, abandon the attempt
if p.dead.Load().(bool) == true {
return
}
p.messageChan <- msg
}()
}
// checkStatus checks the pod's feedback for any information or failed messages and drains the failures into the failed Message buffer
func (p *podConnection) checkStatus() *connStatus {
status := &connStatus{
HadSuccess: false,
WantsReplay: false,
WantsDisconnect: false,
Error: nil,
}
done := false
for !done {
select {
case feedbackMsg := <-p.feedbackChan:
if feedbackMsg == podFeedbackMsgSuccess {
status.HadSuccess = true
} else if feedbackMsg == podFeedbackMsgReplay {
status.WantsReplay = true
} else if feedbackMsg == podFeedbackMsgDisconnect {
status.WantsDisconnect = true
} else {
p.failed = append(p.failed, feedbackMsg)
status.Error = errFailedMessage
}
default:
done = true
}
}
if len(p.failed) >= highWaterMark {
status.Error = errFailedMessageMax
}
return status
}
// flushFailed takes all of the failed messages in the failed queue
// and pushes them back out onto the pod's channel
func (p *podConnection) flushFailed() {
for i := range p.failed {
failedMsg := p.failed[i]
p.send(failedMsg)
}
if len(p.failed) > 0 {
p.failed = []Message{}
}
}
// insertAfter inserts a new connection into the ring
func (p *podConnection) insertAfter(conn *podConnection) {
next := p
if p.next != nil {
next = p.next
}
p.next = conn
conn.next = next
}

44
vendor/github.com/suborbital/grav/grav/receipt.go generated vendored Normal file
View File

@@ -0,0 +1,44 @@
package grav
import "github.com/pkg/errors"
// ErrNoReceipt is returned when a method is called on a nil ticket
var ErrNoReceipt = errors.New("message receipt is nil")
// MsgReceipt represents a "ticket" that references a message that was sent with the hopes of getting a response
// The embedded pod is a pointer to the pod that sent the original message, and therefore any ticket methods used
// will replace the OnFunc of the pod.
type MsgReceipt struct {
UUID string
pod *Pod
}
// WaitOn will block until a response to the message is recieved and passes it to the provided onFunc.
// onFunc errors are propogated to the caller.
func (m *MsgReceipt) WaitOn(onFunc MsgFunc) error {
return m.WaitUntil(nil, onFunc)
}
// WaitUntil will block until a response to the message is recieved and passes it to the provided onFunc.
// ErrWaitTimeout is returned if the timeout elapses, onFunc errors are propogated to the caller.
func (m *MsgReceipt) WaitUntil(timeout TimeoutFunc, onFunc MsgFunc) error {
if m == nil {
return ErrNoReceipt
}
return m.pod.waitOnReply(m, timeout, onFunc)
}
// OnReply will set the pod's OnFunc to the provided MsgFunc and set it to run asynchronously when a reply is received
// onFunc errors are discarded.
func (m *MsgReceipt) OnReply(mfn MsgFunc) error {
if m == nil {
return ErrNoReceipt
}
go func() {
m.pod.waitOnReply(m, nil, mfn)
}()
return nil
}

28
vendor/github.com/suborbital/grav/grav/timeout.go generated vendored Normal file
View File

@@ -0,0 +1,28 @@
package grav
import "time"
// TimeoutFunc is a function that takes a value (a number of seconds) and returns a channel that fires after that given amount of time
type TimeoutFunc func() chan time.Time
// Timeout returns a function that returns a channel that fires after the provided number of seconds have elapsed
// if the value passed is less than or equal to 0, the timeout will never fire
func Timeout(seconds int) TimeoutFunc {
return func() chan time.Time {
tChan := make(chan time.Time)
if seconds > 0 {
go func() {
duration := time.Second * time.Duration(seconds)
tChan <- <-time.After(duration)
}()
}
return tChan
}
}
// TO is a shorthand for Timeout
func TO(seconds int) TimeoutFunc {
return Timeout(seconds)
}

92
vendor/github.com/suborbital/grav/grav/transport.go generated vendored Normal file
View File

@@ -0,0 +1,92 @@
package grav
import (
"github.com/pkg/errors"
"github.com/suborbital/vektor/vlog"
)
// TransportMsgTypeHandshake and others represent internal Transport message types used for handshakes and metadata transfer
const (
TransportMsgTypeHandshake = 1
TransportMsgTypeUser = 2
)
// ErrConnectionClosed and others are transport and connection related errors
var (
ErrConnectionClosed = errors.New("connection was closed")
ErrNodeUUIDMismatch = errors.New("handshake UUID did not match node UUID")
ErrNotBridgeTransport = errors.New("transport is not a bridge")
ErrBridgeOnlyTransport = errors.New("transport only supports bridge connection")
)
var (
TransportTypeMesh = TransportType("transport.mesh")
TransportTypeBridge = TransportType("transport.bridge")
)
type (
// ReceiveFunc is a function that allows passing along a received message
ReceiveFunc func(msg Message)
// ConnectFunc is a function that provides a new Connection
ConnectFunc func(Connection)
// FindFunc allows a Transport to query Grav for an active connection for the given UUID
FindFunc func(uuid string) (Connection, bool)
// TransportType defines the type of Transport (mesh or bridge)
TransportType string
)
// TransportOpts is a set of options for transports
type TransportOpts struct {
NodeUUID string
Port string
URI string
Logger *vlog.Logger
Custom interface{}
}
// Transport represents a Grav transport plugin
type Transport interface {
// Type returns the transport's type (mesh or bridge)
Type() TransportType
// Setup is a transport-specific function that allows bootstrapping
// Setup can block forever if needed; for example if a webserver is bring run
Setup(opts *TransportOpts, connFunc ConnectFunc, findFunc FindFunc) error
// CreateConnection connects to an endpoint and returns the Connection
CreateConnection(endpoint string) (Connection, error)
// ConnectBridgeTopic connects to a topic and returns a TopicConnection
ConnectBridgeTopic(topic string) (TopicConnection, error)
}
// Connection represents a connection to another node
type Connection interface {
// Called when the connection handshake is complete and the connection can actively start exchanging messages
Start(recvFunc ReceiveFunc)
// Send a message from the local instance to the connected node
Send(msg Message) error
// CanReplace returns true if the connection can be replaced (i.e. is not a persistent connection like a websocket)
CanReplace() bool
// Initiate a handshake for an outgoing connection and return the remote Ack
DoOutgoingHandshake(handshake *TransportHandshake) (*TransportHandshakeAck, error)
// Wait for an incoming handshake and return the provided Ack to the remote connection
DoIncomingHandshake(handshakeAck *TransportHandshakeAck) (*TransportHandshake, error)
// Close requests that the Connection close itself
Close()
}
// TopicConnection is a connection to something via a bridge such as a topic
type TopicConnection interface {
// Called when the connection can actively start exchanging messages
Start(pod *Pod)
// Close requests that the Connection close itself
Close()
}
// TransportHandshake represents a handshake sent to a node that you're trying to connect to
type TransportHandshake struct {
UUID string `json:"uuid"`
}
// TransportHandshakeAck represents a handshake response
type TransportHandshakeAck struct {
UUID string `json:"uuid"`
}

View File

@@ -0,0 +1,5 @@
# Grav Transport: Websocket
This is a streaming transport plugin for Grav that uses standard websockets.
Handler functions are made available for http.Server. Connections are managed by the `Transport` object.

View File

@@ -0,0 +1,261 @@
package websocket
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/suborbital/grav/grav"
"github.com/suborbital/vektor/vlog"
)
var upgrader = websocket.Upgrader{}
// Transport is a transport that connects Grav nodes via standard websockets
type Transport struct {
opts *grav.TransportOpts
log *vlog.Logger
connectionFunc func(grav.Connection)
}
// Conn implements transport.Connection and represents a websocket connection
type Conn struct {
nodeUUID string
log *vlog.Logger
conn *websocket.Conn
cLock sync.Mutex
recvFunc grav.ReceiveFunc
}
// New creates a new websocket transport
func New() *Transport {
t := &Transport{}
return t
}
// Type returns the transport's type
func (t *Transport) Type() grav.TransportType {
return grav.TransportTypeMesh
}
// Setup sets up the transport
func (t *Transport) Setup(opts *grav.TransportOpts, connFunc grav.ConnectFunc, findFunc grav.FindFunc) error {
// independent serving is not yet implemented, use the HTTP handler
t.opts = opts
t.log = opts.Logger
t.connectionFunc = connFunc
return nil
}
// CreateConnection adds a websocket endpoint to emit messages to
func (t *Transport) CreateConnection(endpoint string) (grav.Connection, error) {
if !strings.HasPrefix(endpoint, "ws") {
endpoint = fmt.Sprintf("ws://%s", endpoint)
}
endpointURL, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
c, _, err := websocket.DefaultDialer.Dial(endpointURL.String(), nil)
if err != nil {
return nil, errors.Wrapf(err, "[transport-websocket] failed to Dial endpoint")
}
conn := &Conn{
log: t.log,
conn: c,
cLock: sync.Mutex{},
}
return conn, nil
}
// ConnectBridgeTopic connects to a topic if the transport is a bridge
func (t *Transport) ConnectBridgeTopic(topic string) (grav.TopicConnection, error) {
return nil, grav.ErrNotBridgeTransport
}
// HTTPHandlerFunc returns an http.HandlerFunc for incoming connections
func (t *Transport) HTTPHandlerFunc() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if t.connectionFunc == nil {
t.log.ErrorString("[transport-websocket] incoming connection received, but no connFunc configured")
w.WriteHeader(http.StatusInternalServerError)
return
}
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.log.Error(errors.Wrap(err, "[transport-websocket] failed to upgrade connection"))
return
}
t.log.Debug("[transport-websocket] upgraded connection:", r.URL.String())
conn := &Conn{
conn: c,
log: t.log,
}
t.connectionFunc(conn)
}
}
// Start begins the receiving of messages
func (c *Conn) Start(recvFunc grav.ReceiveFunc) {
c.recvFunc = recvFunc
c.conn.SetCloseHandler(func(code int, text string) error {
c.log.Warn(fmt.Sprintf("[transport-websocket] connection closing with code: %d", code))
return nil
})
go func() {
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
c.log.Error(errors.Wrap(err, "[transport-websocket] failed to ReadMessage, terminating connection"))
break
}
c.log.Debug("[transport-websocket] recieved message via", c.nodeUUID)
msg, err := grav.MsgFromBytes(message)
if err != nil {
c.log.Error(errors.Wrap(err, "[transport-websocket] failed to MsgFromBytes"))
continue
}
// send to the Grav instance
c.recvFunc(msg)
}
}()
}
// Send sends a message to the connection
func (c *Conn) Send(msg grav.Message) error {
msgBytes, err := msg.Marshal()
if err != nil {
// not exactly sure what to do here (we don't want this going into the dead letter queue)
c.log.Error(errors.Wrap(err, "[transport-websocket] failed to Marshal message"))
return nil
}
c.log.Debug("[transport-websocket] sending message to connection", c.nodeUUID)
if err := c.WriteMessage(grav.TransportMsgTypeUser, msgBytes); err != nil {
if errors.Is(err, websocket.ErrCloseSent) {
return grav.ErrConnectionClosed
}
return errors.Wrap(err, "[transport-websocket] failed to WriteMessage")
}
c.log.Debug("[transport-websocket] sent message to connection", c.nodeUUID)
return nil
}
// CanReplace returns true if the connection can be replaced
func (c *Conn) CanReplace() bool {
return false
}
// DoOutgoingHandshake performs a connection handshake and returns the UUID of the node that we're connected to
// so that it can be validated against the UUID that was provided in discovery (or if none was provided)
func (c *Conn) DoOutgoingHandshake(handshake *grav.TransportHandshake) (*grav.TransportHandshakeAck, error) {
handshakeJSON, err := json.Marshal(handshake)
if err != nil {
return nil, errors.Wrap(err, "failed to Marshal handshake JSON")
}
c.log.Debug("[transport-websocket] sending handshake")
if err := c.WriteMessage(grav.TransportMsgTypeHandshake, handshakeJSON); err != nil {
return nil, errors.Wrap(err, "failed to WriteMessage handshake")
}
mt, message, err := c.conn.ReadMessage()
if err != nil {
return nil, errors.Wrap(err, "failed to ReadMessage for handshake ack, terminating connection")
}
if mt != grav.TransportMsgTypeHandshake {
return nil, errors.New("first message recieved was not handshake ack")
}
c.log.Debug("[transport-websocket] recieved handshake ack")
ack := grav.TransportHandshakeAck{}
if err := json.Unmarshal(message, &ack); err != nil {
return nil, errors.Wrap(err, "failed to Unmarshal handshake ack")
}
c.nodeUUID = ack.UUID
return &ack, nil
}
// DoIncomingHandshake performs a connection handshake and returns the UUID of the node that we're connected to
// so that it can be validated against the UUID that was provided in discovery (or if none was provided)
func (c *Conn) DoIncomingHandshake(handshakeAck *grav.TransportHandshakeAck) (*grav.TransportHandshake, error) {
mt, message, err := c.conn.ReadMessage()
if err != nil {
return nil, errors.Wrap(err, "failed to ReadMessage for handshake, terminating connection")
}
if mt != grav.TransportMsgTypeHandshake {
return nil, errors.New("first message recieved was not handshake")
}
c.log.Debug("[transport-websocket] recieved handshake")
handshake := grav.TransportHandshake{}
if err := json.Unmarshal(message, &handshake); err != nil {
return nil, errors.Wrap(err, "failed to Unmarshal handshake")
}
ackJSON, err := json.Marshal(handshakeAck)
if err != nil {
return nil, errors.Wrap(err, "failed to Marshal handshake JSON")
}
c.log.Debug("[transport-websocket] sending handshake ack")
if err := c.WriteMessage(grav.TransportMsgTypeHandshake, ackJSON); err != nil {
return nil, errors.Wrap(err, "failed to WriteMessage handshake ack")
}
c.log.Debug("[transport-websocket] sent handshake ack")
c.nodeUUID = handshake.UUID
return &handshake, nil
}
// Close closes the underlying connection
func (c *Conn) Close() {
c.log.Debug("[transport-websocket] connection for", c.nodeUUID, "is closing")
c.conn.Close()
}
// WriteMessage is a concurrent-safe wrapper around the websocket WriteMessage
func (c *Conn) WriteMessage(messageType int, data []byte) error {
c.cLock.Lock()
defer c.cLock.Unlock()
return c.conn.WriteMessage(messageType, data)
}

201
vendor/github.com/suborbital/vektor/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

14
vendor/github.com/suborbital/vektor/vk/README.md generated vendored Normal file
View File

@@ -0,0 +1,14 @@
# vektor API
`vk` is the vektor component that allows for easy development of API servers in Go.
Features:
- HTTPS by default using LetsEncrypt
- Easy configuration of CORS
- Built in logging
- Authentication plug-in point
- Fast HTTP router built in
Planned:
- Rate limiter

76
vendor/github.com/suborbital/vektor/vk/context.go generated vendored Normal file
View File

@@ -0,0 +1,76 @@
package vk
import (
"context"
"net/http"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
"github.com/suborbital/vektor/vlog"
)
// ctxKey is a type to represent a key in the Ctx context.
type ctxKey string
// Ctx serves a similar purpose to context.Context, but has some typed fields
type Ctx struct {
Context context.Context
Log *vlog.Logger
Params httprouter.Params
RespHeaders http.Header
requestID string
scope interface{}
}
// NewCtx creates a new Ctx
func NewCtx(log *vlog.Logger, params httprouter.Params, headers http.Header) *Ctx {
ctx := &Ctx{
Context: context.Background(),
Log: log,
Params: params,
RespHeaders: headers,
}
return ctx
}
// Set sets a value on the Ctx's embedded Context (a la key/value store)
func (c *Ctx) Set(key string, val interface{}) {
realKey := ctxKey(key)
c.Context = context.WithValue(c.Context, realKey, val)
}
// Get gets a value from the Ctx's embedded Context (a la key/value store)
func (c *Ctx) Get(key string) interface{} {
realKey := ctxKey(key)
val := c.Context.Value(realKey)
return val
}
// UseScope sets an object to be the scope of the request, including setting the logger's scope
// the scope can be retrieved later with the Scope() method
func (c *Ctx) UseScope(scope interface{}) {
c.Log = c.Log.CreateScoped(scope)
c.scope = scope
}
// Scope retrieves the context's scope
func (c *Ctx) Scope() interface{} {
return c.scope
}
// UseRequestID is a setter for the request ID
func (c *Ctx) UseRequestID(id string) {
c.requestID = id
}
// RequestID returns the request ID of the current request, generating one if none exists.
func (c *Ctx) RequestID() string {
if c.requestID == "" {
c.requestID = uuid.New().String()
}
return c.requestID
}

91
vendor/github.com/suborbital/vektor/vk/error.go generated vendored Normal file
View File

@@ -0,0 +1,91 @@
package vk
import (
"encoding/json"
"fmt"
"github.com/suborbital/vektor/vlog"
)
// Error is an interface representing a failed request
type Error interface {
Error() string // this ensures all Errors will also conform to the normal error interface
Message() string
Status() int
}
// ErrorResponse is a concrete implementation of Error,
// representing a failed HTTP request
type ErrorResponse struct {
StatusCode int `json:"status"`
MessageText string `json:"message"`
}
// Error returns a full error string
func (e *ErrorResponse) Error() string {
return fmt.Sprintf("%d: %s", e.StatusCode, e.MessageText)
}
// Status returns the error status code
func (e *ErrorResponse) Status() int {
return e.StatusCode
}
// Message returns the error's message
func (e *ErrorResponse) Message() string {
return e.MessageText
}
// Err returns an error with status and message
func Err(status int, message string) Error {
e := &ErrorResponse{
StatusCode: status,
MessageText: message,
}
return e
}
// E is Err for those who like terse code
func E(status int, message string) Error {
return Err(status, message)
}
// Wrap wraps an error in vk.Error
func Wrap(status int, err error) Error {
return Err(status, err.Error())
}
var (
genericErrorResponseBytes = []byte("Internal Server Error")
genericErrorResponseCode = 500
)
// converts _something_ into bytes, best it can:
// if data is Error type, returns (status, {status: status, message: message})
// if other error, returns (500, []byte(err.Error()))
func errorOrOtherToBytes(l *vlog.Logger, err error) (int, []byte, contentType) {
statusCode := genericErrorResponseCode
// first, check if it's vk.Error interface type, and unpack it for further processing
if e, ok := err.(Error); ok {
statusCode = e.Status() // grab this in case anything fails
errResp := Err(e.Status(), e.Message()) // create a concrete instance that can be marshalled
errJSON, marshalErr := json.Marshal(errResp)
if marshalErr != nil {
// any failure results in the generic response body being used
l.ErrorString("failed to marshal vk.Error:", marshalErr.Error(), "original error:", err.Error())
return statusCode, genericErrorResponseBytes, contentTypeTextPlain
}
return statusCode, errJSON, contentTypeJSON
}
l.Warn("redacting potential unsafe error response, original error:", err.Error())
return statusCode, genericErrorResponseBytes, contentTypeTextPlain
}

140
vendor/github.com/suborbital/vektor/vk/group.go generated vendored Normal file
View File

@@ -0,0 +1,140 @@
package vk
import (
"fmt"
"net/http"
"strings"
)
// RouteGroup represents a group of routes
type RouteGroup struct {
prefix string
routes []routeHandler
middleware []Middleware
afterware []Afterware
}
type routeHandler struct {
Method string
Path string
Handler HandlerFunc
}
// Group creates a group of routes with a common prefix and middlewares
func Group(prefix string) *RouteGroup {
rg := &RouteGroup{
prefix: prefix,
routes: []routeHandler{},
middleware: []Middleware{},
afterware: []Afterware{},
}
return rg
}
// GET is a shortcut for server.Handle(http.MethodGet, path, handler)
func (g *RouteGroup) GET(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodGet, path, handler)
}
// HEAD is a shortcut for server.Handle(http.MethodHead, path, handler)
func (g *RouteGroup) HEAD(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodHead, path, handler)
}
// OPTIONS is a shortcut for server.Handle(http.MethodOptions, path, handler)
func (g *RouteGroup) OPTIONS(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodOptions, path, handler)
}
// POST is a shortcut for server.Handle(http.MethodPost, path, handler)
func (g *RouteGroup) POST(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodPost, path, handler)
}
// PUT is a shortcut for server.Handle(http.MethodPut, path, handler)
func (g *RouteGroup) PUT(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodPut, path, handler)
}
// PATCH is a shortcut for server.Handle(http.MethodPatch, path, handler)
func (g *RouteGroup) PATCH(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodPatch, path, handler)
}
// DELETE is a shortcut for server.Handle(http.MethodDelete, path, handler)
func (g *RouteGroup) DELETE(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodDelete, path, handler)
}
// Handle adds a route to be handled
func (g *RouteGroup) Handle(method, path string, handler HandlerFunc) {
g.addRouteHandler(method, path, handler)
}
// AddGroup adds a group of routes to this group as a subgroup.
// the subgroup's prefix is added to all of the routes it contains,
// with the resulting path being "/group.prefix/subgroup.prefix/route/path/here"
func (g *RouteGroup) AddGroup(group *RouteGroup) {
g.routes = append(g.routes, group.routeHandlers()...)
}
// Before adds middleware to the group, which are applied to every handler in the group (called before the handler)
func (g *RouteGroup) Before(middleware ...Middleware) *RouteGroup {
g.middleware = append(g.middleware, middleware...)
return g
}
// After adds afterware to the group, which are applied to every handler in the group (called after the handler)
func (g *RouteGroup) After(afterware ...Afterware) *RouteGroup {
g.afterware = append(g.afterware, afterware...)
return g
}
// routeHandlers computes the "full" path for each handler, and creates
// a HandlerFunc that chains together the group's middlewares
// before calling the inner HandlerFunc. It can be called 'recursively'
// since groups can be added to groups
func (g *RouteGroup) routeHandlers() []routeHandler {
routes := make([]routeHandler, len(g.routes))
for i, r := range g.routes {
fullPath := fmt.Sprintf("%s%s", ensureLeadingSlash(g.prefix), ensureLeadingSlash(r.Path))
augR := routeHandler{
Method: r.Method,
Path: fullPath,
Handler: augmentHandler(r.Handler, g.middleware, g.afterware),
}
routes[i] = augR
}
return routes
}
func (g *RouteGroup) addRouteHandler(method string, path string, handler HandlerFunc) {
rh := routeHandler{
Method: method,
Path: path,
Handler: handler,
}
g.routes = append(g.routes, rh)
}
func (g *RouteGroup) routePrefix() string {
return g.prefix
}
func ensureLeadingSlash(path string) string {
if path == "" {
// handle the "root group" case
return ""
} else if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return path
}

80
vendor/github.com/suborbital/vektor/vk/middleware.go generated vendored Normal file
View File

@@ -0,0 +1,80 @@
package vk
import (
"net/http"
)
// Middleware represents a handler that runs on a request before reaching its handler
type Middleware func(*http.Request, *Ctx) error
// Afterware represents a handler that runs on a request after the handler has dealt with the request
type Afterware func(*http.Request, *Ctx)
// ContentTypeMiddleware allows the content-type to be set
func ContentTypeMiddleware(contentType string) Middleware {
return func(r *http.Request, ctx *Ctx) error {
ctx.RespHeaders.Set(contentTypeHeaderKey, contentType)
return nil
}
}
// CORSMiddleware enables CORS with the given domain for a route
// pass "*" to allow all domains, or empty string to allow none
func CORSMiddleware(domain string) Middleware {
return func(r *http.Request, ctx *Ctx) error {
enableCors(ctx, domain)
return nil
}
}
// CORSHandler enables CORS for a route
// pass "*" to allow all domains, or empty string to allow none
func CORSHandler(domain string) HandlerFunc {
return func(r *http.Request, ctx *Ctx) (interface{}, error) {
enableCors(ctx, domain)
return nil, nil
}
}
func enableCors(ctx *Ctx, domain string) {
if domain != "" {
ctx.RespHeaders.Set("Access-Control-Allow-Origin", domain)
ctx.RespHeaders.Set("X-Requested-With", "XMLHttpRequest")
ctx.RespHeaders.Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, cache-control")
}
}
func loggerMiddleware() Middleware {
return func(r *http.Request, ctx *Ctx) error {
ctx.Log.Info(r.Method, r.URL.String())
return nil
}
}
// generate a HandlerFunc that passes the request through a set of Middleware first and Afterware after
func augmentHandler(inner HandlerFunc, middleware []Middleware, afterware []Afterware) HandlerFunc {
return func(r *http.Request, ctx *Ctx) (interface{}, error) {
defer func() {
// run the afterware (which cannot affect the response)
// even if something in the request chain fails
for _, a := range afterware {
a(r, ctx)
}
}()
// run the middleware (which can error to stop progression)
for _, m := range middleware {
if err := m(r, ctx); err != nil {
return nil, err
}
}
resp, err := inner(r, ctx)
return resp, err
}
}

View File

@@ -0,0 +1,73 @@
package vk
import (
"crypto/tls"
"net/http"
"github.com/suborbital/vektor/vlog"
)
// OptionsModifier takes an options struct and returns a modified Options struct
type OptionsModifier func(*Options)
// UseDomain sets the server to use a particular domain for TLS
func UseDomain(domain string) OptionsModifier {
return func(o *Options) {
o.Domain = domain
}
}
// UseTLSConfig sets a TLS config that will be used for HTTPS
// This will take precedence over the Domain option in all cases
func UseTLSConfig(config *tls.Config) OptionsModifier {
return func(o *Options) {
o.TLSConfig = config
}
}
// UseTLSPort sets the HTTPS port to be used:
func UseTLSPort(port int) OptionsModifier {
return func(o *Options) {
o.TLSPort = port
}
}
// UseHTTPPort sets the HTTP port to be used:
// If domain is set, HTTP port will be used for LetsEncrypt challenge server
// If domain is NOT set, this option will put VK in insecure HTTP mode
func UseHTTPPort(port int) OptionsModifier {
return func(o *Options) {
o.HTTPPort = port
}
}
// UseLogger allows a custom logger to be used
func UseLogger(logger *vlog.Logger) OptionsModifier {
return func(o *Options) {
o.Logger = logger
}
}
// UseAppName allows an app name to be set (for vanity only, really....)
func UseAppName(name string) OptionsModifier {
return func(o *Options) {
o.AppName = name
}
}
// UseEnvPrefix uses the provided env prefix (default VK) when looking up other options such as `VK_HTTP_PORT`
func UseEnvPrefix(prefix string) OptionsModifier {
return func(o *Options) {
o.EnvPrefix = prefix
}
}
// UseInspector sets a function that will be allowed to inspect every HTTP request
// before it reaches VK's internal router, but cannot modify said request or affect
// the handling of said request in any way. Use at your own risk, as it may introduce
// performance issues if not used correctly.
func UseInspector(isp func(http.Request)) OptionsModifier {
return func(o *Options) {
o.PreRouterInspector = isp
}
}

95
vendor/github.com/suborbital/vektor/vk/options.go generated vendored Normal file
View File

@@ -0,0 +1,95 @@
package vk
import (
"context"
"crypto/tls"
"net/http"
"github.com/pkg/errors"
"github.com/sethvargo/go-envconfig"
"github.com/suborbital/vektor/vlog"
)
// Options are the available options for Server
type Options struct {
AppName string `env:"_APP_NAME"`
Domain string `env:"_DOMAIN"`
HTTPPort int `env:"_HTTP_PORT"`
TLSPort int `env:"_TLS_PORT"`
TLSConfig *tls.Config `env:"-"`
EnvPrefix string `env:"-"`
Logger *vlog.Logger `env:"-"`
PreRouterInspector func(http.Request) `env:"-"`
}
func newOptsWithModifiers(mods ...OptionsModifier) *Options {
options := &Options{}
// loop through the provided options and apply the
// modifier function to the options object
for _, mod := range mods {
mod(options)
}
envPrefix := defaultEnvPrefix
if options.EnvPrefix != "" {
envPrefix = options.EnvPrefix
}
options.finalize(envPrefix)
return options
}
// ShouldUseTLS returns true if domain is set and/or TLS is configured
func (o *Options) ShouldUseTLS() bool {
return o.Domain != "" || o.TLSConfig != nil
}
// HTTPPortSet returns true if the HTTP port is set
func (o *Options) HTTPPortSet() bool {
return o.HTTPPort != 0
}
// ShouldUseHTTP returns true if insecure HTTP should be used
func (o *Options) ShouldUseHTTP() bool {
return !o.ShouldUseTLS() && o.HTTPPortSet()
}
// finalize "locks in" the options by overriding any existing options with the version from the environment, and setting the default logger if needed
func (o *Options) finalize(prefix string) {
if o.Logger == nil {
o.Logger = vlog.Default(vlog.EnvPrefix(prefix))
}
// if no inspector was set, create an empty one
if o.PreRouterInspector == nil {
o.PreRouterInspector = func(_ http.Request) {}
}
envOpts := Options{}
if err := envconfig.ProcessWith(context.Background(), &envOpts, envconfig.PrefixLookuper(prefix, envconfig.OsLookuper())); err != nil {
o.Logger.Error(errors.Wrap(err, "[vk] failed to ProcessWith environment config"))
return
}
o.replaceFieldsIfNeeded(&envOpts)
}
func (o *Options) replaceFieldsIfNeeded(replacement *Options) {
if replacement.AppName != "" {
o.AppName = replacement.AppName
}
if replacement.Domain != "" {
o.Domain = replacement.Domain
}
if replacement.HTTPPort != 0 {
o.HTTPPort = replacement.HTTPPort
}
if replacement.TLSPort != 0 {
o.TLSPort = replacement.TLSPort
}
}

77
vendor/github.com/suborbital/vektor/vk/response.go generated vendored Normal file
View File

@@ -0,0 +1,77 @@
package vk
import (
"encoding/json"
"net/http"
"github.com/pkg/errors"
"github.com/suborbital/vektor/vlog"
)
// Response represents a non-error HTTP response
type Response struct {
status int
body interface{}
}
// Respond returns a filled-in response
func Respond(status int, body interface{}) Response {
r := Response{
status: status,
body: body,
}
return r
}
// R is `Respond` for those who prefer terse code
func R(status int, body interface{}) Response {
return Respond(status, body)
}
// TODO: add convenience helpers for status codes
const (
contentTypeJSON contentType = "application/json"
contentTypeTextPlain contentType = "text/plain"
contentTypeOctetStream contentType = "application/octet-stream"
)
// converts _something_ into bytes, best it can:
// if data is Response type, returns (status, body processed as below)
// if bytes, return (200, bytes)
// if string, return (200, []byte(string))
// if struct, return (200, json(struct))
// otherwise, return (500, nil)
func responseOrOtherToBytes(l *vlog.Logger, data interface{}) (int, []byte, contentType) {
if data == nil {
return http.StatusNoContent, []byte{}, contentTypeTextPlain
}
statusCode := http.StatusOK
realData := data
// first, check if it's response type, and unpack it for further processing
if r, ok := data.(Response); ok {
statusCode = r.status
realData = r.body
}
// if data is []byte or string, return it as-is
if b, ok := realData.([]byte); ok {
return statusCode, b, contentTypeOctetStream
} else if s, ok := realData.(string); ok {
return statusCode, []byte(s), contentTypeTextPlain
}
// otherwise, assume it's a struct of some kind,
// so JSON marshal it and return it
json, err := json.Marshal(realData)
if err != nil {
l.Error(errors.Wrap(err, "failed to Marshal response struct"))
return genericErrorResponseCode, []byte(genericErrorResponseBytes), contentTypeTextPlain
}
return statusCode, json, contentTypeJSON
}

144
vendor/github.com/suborbital/vektor/vk/router.go generated vendored Normal file
View File

@@ -0,0 +1,144 @@
package vk
import (
"fmt"
"net/http"
"sync"
"github.com/julienschmidt/httprouter"
"github.com/suborbital/vektor/vlog"
)
const contentTypeHeaderKey = "Content-Type"
// used internally to convey content types
type contentType string
// HandlerFunc is the vk version of http.HandlerFunc
// instead of exposing the ResponseWriter, the function instead returns
// an object and an error, which are handled as described in `With` below
type HandlerFunc func(*http.Request, *Ctx) (interface{}, error)
// Router handles the responses on behalf of the server
type Router struct {
*RouteGroup // the "root" RouteGroup that is mounted at server start
hrouter *httprouter.Router // the internal 'actual' router
finalizeOnce sync.Once // ensure that the root only gets mounted once
log *vlog.Logger
}
type defaultScope struct {
RequestID string `json:"request_id"`
}
// NewRouter creates a new Router
func NewRouter(logger *vlog.Logger) *Router {
// add the logger middleware
middleware := []Middleware{loggerMiddleware()}
r := &Router{
RouteGroup: Group("").Before(middleware...),
hrouter: httprouter.New(),
finalizeOnce: sync.Once{},
log: logger,
}
return r
}
// HandleHTTP handles a classic Go HTTP handlerFunc
func (rt *Router) HandleHTTP(method, path string, handler http.HandlerFunc) {
rt.hrouter.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
handler(w, r)
})
}
// Finalize mounts the root group to prepare the Router to handle requests
func (rt *Router) Finalize() {
rt.finalizeOnce.Do(func() {
rt.mountGroup(rt.RouteGroup)
})
}
//ServeHTTP serves HTTP requests
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// check to see if the router has a handler for this path
handler, params, _ := rt.hrouter.Lookup(r.Method, r.URL.Path)
if handler != nil {
handler(w, r, params)
} else {
rt.log.Debug("not handled:", r.Method, r.URL.String())
// let httprouter handle the fallthrough cases
rt.hrouter.ServeHTTP(w, r)
}
}
// mountGroup adds a group of handlers to the httprouter
func (rt *Router) mountGroup(group *RouteGroup) {
for _, r := range group.routeHandlers() {
rt.log.Debug("mounting route", r.Method, r.Path)
rt.hrouter.Handle(r.Method, r.Path, rt.handleWrap(r.Handler))
}
}
// handleWrap returns an httprouter.Handle that uses the `inner` vk.HandleFunc to handle the request
//
// inner returns a body and an error;
// the body can can be:
// - a vk.Response object (status and body are written to w)
// - []byte (written directly to w, status 200)
// - a struct (marshalled to JSON and written to w, status 200)
//
// the error can be:
// - a vk.Error type (status and message are written to w)
// - any other error object (status 500 and error.Error() are written to w)
//
func (rt *Router) handleWrap(inner HandlerFunc) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
var status int
var body []byte
var detectedCType contentType
// create a context handleWrap the configured logger
// (and use the ctx.Log for all remaining logging
// in case a scope was set on it)
ctx := NewCtx(rt.log, params, w.Header())
ctx.UseScope(defaultScope{ctx.RequestID()})
resp, err := inner(r, ctx)
if err != nil {
status, body, detectedCType = errorOrOtherToBytes(ctx.Log, err)
} else {
status, body, detectedCType = responseOrOtherToBytes(ctx.Log, resp)
}
// check if anything in the handler chain set the content type
// header, and only use the auto-detected value if it wasn't
headerCType := w.Header().Get(contentTypeHeaderKey)
shouldSetCType := headerCType == ""
ctx.Log.Debug("post-handler contenttype:", string(headerCType))
// if no contentType was set in the middleware chain,
// then set it here based on the type detected
if shouldSetCType {
ctx.Log.Debug("setting auto-detected contenttype:", string(detectedCType))
w.Header().Set(contentTypeHeaderKey, string(detectedCType))
}
w.WriteHeader(status)
w.Write(body)
ctx.Log.Info(r.Method, r.URL.String(), fmt.Sprintf("completed (%d: %s)", status, http.StatusText(status)))
}
}
// canHandle returns true if there's a registered handler that can
// handle the method and path provided or not
func (rt *Router) canHandle(method, path string) bool {
handler, _, _ := rt.hrouter.Lookup(method, path)
return handler != nil
}

289
vendor/github.com/suborbital/vektor/vk/server.go generated vendored Normal file
View File

@@ -0,0 +1,289 @@
package vk
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"sync"
"sync/atomic"
"golang.org/x/crypto/acme/autocert"
)
const defaultEnvPrefix = "VK"
// Server represents a vektor API server
type Server struct {
router *Router
lock sync.RWMutex
started atomic.Value
server *http.Server
options *Options
}
// New creates a new vektor API server
func New(opts ...OptionsModifier) *Server {
options := newOptsWithModifiers(opts...)
router := NewRouter(options.Logger)
s := &Server{
router: router,
lock: sync.RWMutex{},
started: atomic.Value{},
options: options,
}
s.started.Store(false)
// yes this creates a circular reference,
// but the VK server and HTTP server are
// extremely tightly wound together so
// we have to make this compromise
s.server = createGoServer(options, s)
return s
}
// Start starts the server listening
func (s *Server) Start() error {
if s.started.Load().(bool) {
err := errors.New("server already started")
s.options.Logger.Error(err)
return err
}
// lock the router modifiers (GET, POST etc.)
s.started.Store(true)
// mount the root set of routes before starting
s.router.Finalize()
if s.options.AppName != "" {
s.options.Logger.Info("starting", s.options.AppName, "...")
}
s.options.Logger.Info("serving on", s.server.Addr)
if !s.options.HTTPPortSet() && !s.options.ShouldUseTLS() {
s.options.Logger.ErrorString("domain and HTTP port options are both unset, server will start up but fail to acquire a certificate. reconfigure and restart")
} else if s.options.ShouldUseHTTP() {
return s.server.ListenAndServe()
}
return s.server.ListenAndServeTLS("", "")
}
// TestStart "starts" the server for automated testing with vtest
func (s *Server) TestStart() error {
if s.started.Load().(bool) {
err := errors.New("server already started")
s.options.Logger.Error(err)
return err
}
// lock the router modifiers (GET, POST etc.)
s.started.Store(true)
// mount the root set of routes before starting
s.router.Finalize()
if s.options.AppName != "" {
s.options.Logger.Info("starting", s.options.AppName, "in Test Mode...")
}
return nil
}
// ServeHTTP serves HTTP requests using the internal router while allowing
// said router to be swapped out underneath at any time in a thread-safe way
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// run the inspector with a dereferenced pointer
// so that it can view but not change said request
//
// we intentionally run this before the lock as it's
// possible the inspector may trigger a router-swap
// and that would cause a nasty deadlock
s.options.PreRouterInspector(*r)
// now lock to ensure the router isn't being swapped
// out from underneath us while we're serving this req
s.lock.RLock()
defer s.lock.RUnlock()
s.router.ServeHTTP(w, r)
}
// SwapRouter allows swapping VK's router out in realtime while
// continuing to serve requests in the background
func (s *Server) SwapRouter(router *Router) {
router.Finalize()
// lock after Finalizing the router so
// the lock is released as quickly as possible
s.lock.Lock()
defer s.lock.Unlock()
s.router = router
}
// CanHandle returns true if the server can handle a given method and path
func (s *Server) CanHandle(method, path string) bool {
s.lock.RLock()
defer s.lock.RUnlock()
return s.router.canHandle(method, path)
}
// GET is a shortcut for router.Handle(http.MethodGet, path, handle)
func (s *Server) GET(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.GET(path, handler)
}
// HEAD is a shortcut for router.Handle(http.MethodHead, path, handle)
func (s *Server) HEAD(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.HEAD(path, handler)
}
// OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle)
func (s *Server) OPTIONS(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.OPTIONS(path, handler)
}
// POST is a shortcut for router.Handle(http.MethodPost, path, handle)
func (s *Server) POST(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.POST(path, handler)
}
// PUT is a shortcut for router.Handle(http.MethodPut, path, handle)
func (s *Server) PUT(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.PUT(path, handler)
}
// PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle)
func (s *Server) PATCH(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.PATCH(path, handler)
}
// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle)
func (s *Server) DELETE(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.DELETE(path, handler)
}
// Handle adds a route to be handled
func (s *Server) Handle(method, path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.Handle(method, path, handler)
}
// AddGroup adds a RouteGroup to be handled
func (s *Server) AddGroup(group *RouteGroup) {
if s.started.Load().(bool) {
return
}
s.router.AddGroup(group)
}
// HandleHTTP allows vk to handle a standard http.HandlerFunc
func (s *Server) HandleHTTP(method, path string, handler http.HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.HandleHTTP(method, path, handler)
}
func createGoServer(options *Options, handler http.Handler) *http.Server {
if useHTTP := options.ShouldUseHTTP(); useHTTP {
return goHTTPServerWithPort(options, handler)
}
return goTLSServerWithDomain(options, handler)
}
func goTLSServerWithDomain(options *Options, handler http.Handler) *http.Server {
if options.TLSConfig != nil {
options.Logger.Info("configured for HTTPS with custom configuration")
} else if options.Domain != "" {
options.Logger.Info("configured for HTTPS using domain", options.Domain)
}
tlsConfig := options.TLSConfig
if tlsConfig == nil {
m := &autocert.Manager{
Cache: autocert.DirCache("~/.autocert"),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(options.Domain),
}
addr := fmt.Sprintf(":%d", options.HTTPPort)
if options.HTTPPort == 0 {
addr = ":8080"
}
options.Logger.Info("serving TLS challenges on", addr)
go http.ListenAndServe(addr, m.HTTPHandler(nil))
tlsConfig = &tls.Config{GetCertificate: m.GetCertificate}
}
addr := fmt.Sprintf(":%d", options.TLSPort)
if options.TLSPort == 0 {
addr = ":443"
}
s := &http.Server{
Addr: addr,
TLSConfig: tlsConfig,
Handler: handler,
}
return s
}
func goHTTPServerWithPort(options *Options, handler http.Handler) *http.Server {
options.Logger.Warn("configured to use HTTP with no TLS")
s := &http.Server{
Addr: fmt.Sprintf(":%d", options.HTTPPort),
Handler: handler,
}
return s
}

110
vendor/github.com/suborbital/vektor/vlog/README.md generated vendored Normal file
View File

@@ -0,0 +1,110 @@
# VLog: Simple and Safe logging package
`vlog` is the logging package for the Suborbital Development Platform. It is designed to have a minimal performance impact and promote logging safety.
## The default instance
For most users, using `vlog.Default()` is enough. This creates a `Logger` that logs to stdout, uses the `info` log level, and redacts non-string inputs. If you want to gain finer control over the logger, read on!
## Using the logger
The logger uses a simple API to get out of your way:
```golang
// ErrorString logs the input as an error
func (v *Logger) ErrorString(msgs ...interface{}) {}
// Error logs an error object
func (v *Logger) Error(err error) {}
// Warn logs the input as an warning
func (v *Logger) Warn(msgs ...interface{}) {}
// Info logs the input as an info message
func (v *Logger) Info(msgs ...interface{}) {}
// Debug logs the input as debug output
func (v *Logger) Debug(msgs ...interface{}) {}
// Trace logs a function name and returns a function to be deferred, logging the completion of a function
func (v *Logger) Trace(fnName string) func() {}
```
Each method takes in a list of `interface{}` which are appended when logging. For example:
```golang
log.Info("user", user.Email, "completed signin")
```
Will print `(I) user info@example.com completed signup`. The `(I)` indicates the log level (info). How the logger processes the passed in objects is determined by the producer, which is discussed below.
## Log levels
The logger will automatically filter out anything higher than the configured level. For example, if the logger is configured for `LogLevelError`, then the higher levels such as Info, Debug, and Trace will not be logged. The available log level are as follows:
```golang
// LogLevelTrace and others represent log levels
const (
LogLevelTrace = "trace" // 5
LogLevelDebug = "debug" // 4
LogLevelInfo = "info" // 3
LogLevelWarn = "warn" // 2
LogLevelError = "error" // 1
)
```
### The trace level
The `Trace` log method is special, in that it returns a function. This allows for easy function tracing:
```golang
func SomethingAwesome() {
defer log.Trace("SomethingAwesome")
}
```
This will print something like:
```
(T) SomethingAwesome
[...]
(T) SomethingAwesome completed
```
## Logger options
The default constructor and `vlog.New()` both take a set of `OptionModifier` parameters, which are functions that set the various available options. For example:
```golang
log := vlog.Default(
vlog.Level(vlog.LogLevelTrace)
)
```
Passing in options will allow you to tweak the behaviour of the logger. The available options are:
```golang
// Level sets the logging level to one of error, warn, info, debug, or trace (VLOG_LOG_LEVEL env var)
func Level(level string)
// ToFile sets the logger to open the file specified and write logs to it (VLOG_LOG_FILE env var)
func ToFile(filepath string)
// LogPrefix sets a prefix on all of the log messages (VLOG_LOG_PREFIX env var)
func LogPrefix(prefix string)
// EnvPrefix sets the prefix to be used for environment variable settings (replaces VLOG with prefix in env var keys above)
func EnvPrefix(prefix string)
// AppMeta sets the meta object to be included with structured logs (not configurable from env vars)
func AppMeta(meta interface{})
// PreLogHook sets a function that will be called every time something
// is logged. The value will be the structured JSON for the log line
// LogHookFunc has the signature `func([]byte)`
func PreLogHook(hook LogHookFunc)
```
> Note if `ToFile` is used, structured logs are written to the file and plain text logs are duplicated to stdout.
## The Producer
`vlog` uses an object called the `Producer` to process all log lines. `Producer` is an interface type, and its implementation is responsible for taking the input passed into each log method and converting it into a string for logging. The `Producer` that ships with `vlog` is called `defaultProducer`; it logs all strings, but redacts all other types it is given for safety. If logging of structs or other types is needed, it is reccomended that a custom `Producer` is created. Simply copy `defaultproducer.go`, add your own functionality, and pass it in to `vlog.New(producer, opts...)` to create your logger.
## Structured logging
Structured logs are core to vlog, and there are a number of features that make it useful. Things like the log level and timestamp are included by default, and `AppMeta` and `Scope` are two ways to make structured logs even more useful.
An example of a structured log is as follows:
```json
{"log_message":"(I) serving on :443","timestamp":"2020-10-12T20:55:00.644217-04:00","level":3,"app":{"version":"v0.1.1"}}
```
### AppMeta
`AppMeta` (configured using the `Meta()` OptionModifier when instantiating the logger) represents metadata about the running application. The configured meta will be included with every log message. This can be used to indicate the version of the currently running application, for example. The `AppMeta` is included in structured logs under the `app` JSON key. If the object set as AppMeta cannot be JSON marshalled, an error will occur.
### Scope
A `Logger` instance can create a "scoped" instance of itself, which is essentially a clone with a specific scope object attached. Scope can be useful to add a specific request ID to logs related to it, for instance. Calling `logger.CreateScoped(scope interface{})` on a `Logger` will return a new `Logger` that includes the provided object under the `scope` JSON key. If the object set as Scope cannot be JSON marshalled, an error will occur.
A shortcut for setting scope on the logger with `vk` is the `ctx.UseScope()` method on the `vk.Ctx` type. This will automatically create a scoped logger, set it as the logger for that request, and make the scope object available for later use via the `ctx.Scope()` method.

View File

@@ -0,0 +1,64 @@
package vlog
import (
"bytes"
"fmt"
)
type defaultProducer struct{}
// ErrorString prints a string as an error
func (d *defaultProducer) ErrorString(msgs ...interface{}) string {
return fmt.Sprintf("(E) %s", redactAndJoinInterfaces(msgs...))
}
// Error prints a string as an error
func (d *defaultProducer) Error(err error) string {
return fmt.Sprintf("(E) %s", err.Error())
}
// Warn prints a string as an warning
func (d *defaultProducer) Warn(msgs ...interface{}) string {
return fmt.Sprintf("(W) %s", redactAndJoinInterfaces(msgs...))
}
// Info prints a string as an info message
func (d *defaultProducer) Info(msgs ...interface{}) string {
return fmt.Sprintf("(I) %s", redactAndJoinInterfaces(msgs...))
}
// Debug prints a string as debug output
func (d *defaultProducer) Debug(msgs ...interface{}) string {
return fmt.Sprintf("(D) %s", redactAndJoinInterfaces(msgs...))
}
// Trace prints a function name and returns a function to be deferred, logging the completion of a function
func (d *defaultProducer) Trace(fnName string) (string, func() string) {
traceFunc := func() string {
return (fmt.Sprintf("(T) %s completed", fnName))
}
return (fmt.Sprintf("(T) %s", fnName)), traceFunc
}
func redactAndJoinInterfaces(msgs ...interface{}) string {
msg := ""
for _, m := range msgs {
switch elem := m.(type) {
case string:
msg += fmt.Sprintf(" %s", elem)
case uint, uint8, uint16, uint32, int, int8, int16, int32, int64, float32, float64, complex64, complex128:
buf := &bytes.Buffer{}
fmt.Fprint(buf, elem)
msg += " " + buf.String()
case SafeStringer:
msg += " " + elem.SafeString()
default:
msg += fmt.Sprintf(" [redacted %T]", elem)
}
}
// get rid of that first space
return msg[1:]
}

149
vendor/github.com/suborbital/vektor/vlog/options.go generated vendored Normal file
View File

@@ -0,0 +1,149 @@
package vlog
import (
"context"
"fmt"
"strings"
"github.com/sethvargo/go-envconfig"
)
const defaultEnvPrefix = "VLOG"
// LogLevelTrace and others represent log levels
const (
LogLevelTrace = "trace" // 5
LogLevelDebug = "debug" // 4
LogLevelInfo = "info" // 3
LogLevelWarn = "warn" // 2
LogLevelError = "error" // 1
)
var levelStringMap = map[string]int{
LogLevelTrace: 5,
LogLevelDebug: 4,
LogLevelInfo: 3,
LogLevelWarn: 2,
LogLevelError: 1,
}
// Options represents the options for a VLogger
type Options struct {
Level int `env:"-"`
LevelString string `env:"_LOG_LEVEL"`
Filepath string `env:"_LOG_FILE"`
LogPrefix string `env:"_LOG_PREFIX"`
EnvPrefix string `env:"-"`
AppMeta interface{} `env:"-"`
PreLogHook LogHookFunc `env:"-"`
}
type LogHookFunc func([]byte)
// OptionsModifier is a options modifier function
type OptionsModifier func(*Options)
func newOptions(mods ...OptionsModifier) *Options {
opts := defaultOptions()
for _, mod := range mods {
mod(opts)
}
envPrefix := defaultEnvPrefix
if opts.EnvPrefix != "" {
envPrefix = opts.EnvPrefix
}
opts.finalize(envPrefix)
return opts
}
// Level sets the logging level to one of error, warn, info, debug, or trace
func Level(level string) OptionsModifier {
return func(opt *Options) {
opt.Level = logLevelValFromString(level)
}
}
// ToFile sets the logger to open the file specified and write logs to it
func ToFile(filepath string) OptionsModifier {
return func(opt *Options) {
opt.Filepath = filepath
}
}
// LogPrefix sets a prefix on all of the log messages
func LogPrefix(logPrefix string) OptionsModifier {
return func(opt *Options) {
opt.LogPrefix = logPrefix
}
}
// EnvPrefix sets a prefix for evaluating logger settings from env
func EnvPrefix(envPrefix string) OptionsModifier {
return func(opt *Options) {
opt.EnvPrefix = envPrefix
}
}
// AppMeta sets the AppMeta object to be included with structured logs
func AppMeta(meta interface{}) OptionsModifier {
return func(opt *Options) {
opt.AppMeta = meta
}
}
// PreLogHook sets a function to be run before each logged value
func PreLogHook(hook LogHookFunc) OptionsModifier {
return func(opt *Options) {
opt.PreLogHook = hook
}
}
func defaultOptions() *Options {
o := &Options{
Level: logLevelValFromString(LogLevelInfo),
LevelString: "",
Filepath: "",
LogPrefix: "",
EnvPrefix: "",
AppMeta: nil,
}
return o
}
// finalize "locks in" the options by overriding any existing options with the version from the environment, and setting the default logger if needed
func (o *Options) finalize(envPrefix string) {
envOpts := Options{}
if err := envconfig.ProcessWith(context.Background(), &envOpts, envconfig.PrefixLookuper(envPrefix, envconfig.OsLookuper())); err != nil {
fmt.Printf("[vlog] failed to ProcessWith environment config:" + err.Error())
return
}
o.replaceFieldsIfNeeded(&envOpts)
}
func (o *Options) replaceFieldsIfNeeded(replacement *Options) {
if replacement.LevelString != "" {
o.Level = logLevelValFromString(replacement.LevelString)
}
if replacement.Filepath != "" {
o.Filepath = replacement.Filepath
}
if replacement.LogPrefix != "" {
o.LogPrefix = replacement.LogPrefix
}
}
func logLevelValFromString(level string) int {
if level, ok := levelStringMap[strings.ToLower(level)]; ok {
return level
}
return 3
}

View File

@@ -0,0 +1,11 @@
package vlog
import "time"
type structuredLog struct {
LogMessage string `json:"log_message"`
Timestamp time.Time `json:"timestamp"`
Level int `json:"level"`
AppMeta interface{} `json:"app,omitempty"`
ScopeMeta interface{} `json:"scope,omitempty"`
}

188
vendor/github.com/suborbital/vektor/vlog/vlog.go generated vendored Normal file
View File

@@ -0,0 +1,188 @@
package vlog
import (
"encoding/json"
"io"
"os"
"sync"
"time"
)
// Producer represents an object that is considered a producer of messages
type Producer interface {
ErrorString(...interface{}) string // Logs an error string
Error(error) string // Logs an error obj
Warn(...interface{}) string // Logs a warning
Info(...interface{}) string // Logs information
Debug(...interface{}) string // Logs debug information
Trace(string) (string, func() string) // Logs a function name and returns a function to be deferred, indicating the end of the function
}
// Logger is the main logger object, responsible for taking input from the
// producer and managing scoped loggers
type Logger struct {
producer Producer
scope interface{}
opts *Options
output io.Writer
lock *sync.Mutex
}
// SafeStringer allows a struct to produse a "safe" string representation for logging
// the intention is avoiding accidentally including sensitive information in struct fields.
type SafeStringer interface {
SafeString() string
}
// Default returns a Logger using the default producer
func Default(opts ...OptionsModifier) *Logger {
prod := &defaultProducer{}
return New(prod, opts...)
}
// New returns a Logger with the provided producer and options
func New(producer Producer, opts ...OptionsModifier) *Logger {
options := newOptions(opts...)
v := &Logger{
producer: producer,
scope: nil,
opts: options,
lock: &sync.Mutex{},
}
output, err := outputForOptions(options)
if err != nil {
v.output = os.Stdout
os.Stderr.Write([]byte("[vlog] failed to set output: " + err.Error() + "\n"))
} else {
v.output = output
}
return v
}
// CreateScoped creates a duplicate logger which has a particular scope
func (v *Logger) CreateScoped(scope interface{}) *Logger {
sl := &Logger{
producer: v.producer,
scope: scope,
opts: v.opts,
output: v.output,
lock: v.lock,
}
return sl
}
// ErrorString logs a string as an error
func (v *Logger) ErrorString(msgs ...interface{}) {
msg := v.producer.ErrorString(msgs...)
v.log(msg, v.scope, 1)
}
// Error logs an error as an error
func (v *Logger) Error(err error) {
msg := v.producer.Error(err)
v.log(msg, v.scope, 1)
}
// Warn logs a string as an warning
func (v *Logger) Warn(msgs ...interface{}) {
msg := v.producer.Warn(msgs...)
v.log(msg, v.scope, 2)
}
// Info logs a string as an info message
func (v *Logger) Info(msgs ...interface{}) {
msg := v.producer.Info(msgs...)
v.log(msg, v.scope, 3)
}
// Debug logs a string as debug output
func (v *Logger) Debug(msgs ...interface{}) {
msg := v.producer.Debug(msgs...)
v.log(msg, v.scope, 4)
}
// Trace logs a function name and returns a function to be deferred, logging the completion of a function
func (v *Logger) Trace(fnName string) func() {
msg, traceFunc := v.producer.Trace(fnName)
v.log(msg, v.scope, 5)
return func() {
msg := traceFunc()
v.log(msg, v.scope, 5)
}
}
func (v *Logger) log(message string, scope interface{}, level int) {
if level > v.opts.Level {
return
}
if v.opts.LogPrefix != "" {
message = v.opts.LogPrefix + " " + message
}
// send the raw message to the console
if v.output != os.Stdout {
// acquire a lock as the output may be a file
v.lock.Lock()
defer v.lock.Unlock()
// throwing away the error here since there's nothing much we can do
os.Stdout.Write([]byte(message))
os.Stdout.Write([]byte("\n"))
}
structured := structuredLog{
LogMessage: message,
Timestamp: time.Now(),
Level: level,
AppMeta: v.opts.AppMeta,
ScopeMeta: scope,
}
structuredJSON, err := json.Marshal(structured)
if err != nil {
os.Stderr.Write([]byte("[vlog] failed to marshal structured log"))
}
if v.opts.PreLogHook != nil {
v.opts.PreLogHook(structuredJSON)
}
_, err = v.output.Write(structuredJSON)
if err != nil {
os.Stderr.Write([]byte("[vlog] failed to write to configured output: " + err.Error() + "\n"))
} else {
v.output.Write([]byte("\n"))
}
}
func outputForOptions(opts *Options) (io.Writer, error) {
var output io.Writer
if opts.Filepath != "" {
file, err := os.OpenFile(opts.Filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm)
if err != nil {
return nil, err
}
output = file
} else {
output = os.Stdout
}
return output, nil
}