Definição
Um
context
é simplesmente uma instância que atende à interface deContext
definida no pacote decontext
. Go é idiomático e incentiva a passagem explícita dos dados por meio dos parâmetros da função. O mesmo é verdade paracontext
. É apenas mais um parâmetro para sua função. Assim como Go tem uma convenção de que o último valor de retorno de uma função é umerror
, há outra convenção em Go de que ocontext
é explicitamente passado por seu programa como o primeiro parâmetro da função.
O nome usual para o parâmetro de context
é ctx
:
func logic(ctx context.Context, info string) (string, error) {
// do some interesting stuff here
return "", nil
}
Além de definir a interface de Context
, o pacote de context
também contém várias funções de fábrica para criar e agrupar contexts. Quando você não tem um context
existente, como no entry point para um programa CLI, crie um context
inicial vazio com a função context.Background
. Isso retorna uma variável do tipo context.Context
.
Um context vazio é um ponto de partida; cada vez que você adiciona metadados ao context, você o faz envolvendo o context
existente usando uma das funções de fábrica do pacote context
:
ctx := context.Background()
result, err := logic(ctx, "a string")
Ao manipular um servidor HTTP, você usa um padrão ligeiramente diferente para adquirir e passar o context por meio de camadas de middleware para o top-level http.Handler
. Infelizmente, o context
foi adicionado às APIs do Go muito depois que o pacote net/http
foi criado. Devido à promessa de compatibilidade, não havia como alterar a interface http.Handler
para adicionar um parâmetro context.Context
.
A promessa de compatibilidade permite que novos métodos sejam adicionados aos tipos existentes, e foi isso que a equipe do Go fez. Existem dois métodos relacionados ao context em http.Request
:
-
Context
retorna ocontext.Context
associado à solicitação; -
WithContext
recebe umcontext.Context
e retorna um novohttp.Request
com o estado da solicitação anterior combinado com ocontext.Context
fornecido;
Aqui está o padrão geral:
func Middleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
// wrap the context with stuff -- we'll see how soon!
req = req.WithContext(ctx)
handler.ServeHTTP(rw, req)
})
}
A primeira coisa que fazemos em nosso middleware é extrair o context existente da solicitação usando o método Context
. Depois de colocar os valores no context, criamos uma nova solicitação com base na solicitação antiga e no context agora preenchido, usando o método WithContext
. Por fim, chamamos o manipulador e passamos nossa nova solicitação e o http.ResponseWriter
existente.
Quando o fluxo chega no método handler
, é extraído o context
da solicitação usando o método Context
e chama a lógica de negócios no método logic
com o context
como o primeiro parâmetro, assim como vimos anteriormente:
func handler(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
err := req.ParseForm()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(err.Error()))
return
}
data := req.FormValue("data")
result, err := logic(ctx, data)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(err.Error()))
return
}
rw.Write([]byte(result))
}
Há mais uma situação em que você usa o método WithContext
: ao fazer uma chamada HTTP de seu aplicativo para outro serviço HTTP. Assim como fizemos ao passar um context
por meio de middleware, você define o context
na solicitação de saída usando WithContext
:
type ServiceCaller struct {
client *http.Client
}
func (sc ServiceCaller) callAnotherService(ctx context.Context, data string) (string, error) {
req, err := http.NewRequest(http.MethodGet, "http://example.com?data=" + data, nil)
if err != nil {
return "", err
}
req = req.WithContext(ctx)
resp, err := sc.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Unexpected status code %d", resp.StatusCode)
}
// do the rest of the stuff to process the response
id, err := processResponse(resp.Body)
return id, err
}
Agora que sabemos como adquirir e passar um context
, é hora de torná-los úteis. Começando com cancelamento.
Cancelamento do Contexto
Imagine que você tenha uma solicitação que gera vários goroutines, cada uma chamando um serviço HTTP diferente. Se um serviço retorna um erro que o impede de retornar um resultado válido, não há nenhum ponto/porque em continuar processando as outras goroutines. Em Go, isso é chamado de cancellation e o context
fornece o mecanismo para implementação.
Para criar um context
cancelável, use a função context.WithCancel
. Ele recebe um context.Context
como parâmetro e retorna um context.Context
e um context.CancelFunc
. O context.Context
devolvido não é o mesmo context que foi passado para a função. Em vez disso, é um context filho que envolve o context pai context.Context
passado. Um context.CancelFunc
é uma função que cancela o context
, informando a todo o código que está ouvindo o cancelamento em potencial que é hora de interromper o processamento.
Vamos dar uma olhada em como isso funciona. Como esse código configura um servidor. Primeiro, vamos configurar dois servidores em um arquivo chamado servers.go
:
package main
import (
"net/http"
"net/http/httptest"
"time"
)
func slowServer() *httptest.Server {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
w.Write([]byte("slow response"))
}))
return s
}
func fastServer() *httptest.Server {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("error") == "true" {
w.Write([]byte("error"))
return
}
w.Write([]byte("ok"))
}))
return s
}
Essas funções iniciam servidores HTTP quando são chamadas. Um servidor dorme/aguarda por dez segundos e, em seguida, retorna a mensagem slow response
. O outro verifica se há um parâmetro na query string da URL de error
definido como true
. Se houver, ele retornará a mensagem de error
. Caso contrário, retorna a mensagem ok
.
É usando o httptest.Server
, o que torna mais fácil escrever testes unitários para o código que se comunica com servidores HTTP remotos.
A seguir, vamos escrever a parte do código do client em um arquivo chamado client.go
:
package main
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"sync"
)
var client = http.Client{}
func callBoth(ctx context.Context, errVal string, slowURL string, fastURL string) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
err := callServer(ctx, "slow", slowURL)
if err != nil {
cancel()
}
}()
go func() {
defer wg.Done()
err := callServer(ctx, "fast", fastURL+"?error="+errVal)
if err != nil {
cancel()
}
}()
wg.Wait()
fmt.Println("done with both")
}
func callServer(ctx context.Context, label string, url string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
fmt.Println(label, "request err:", err)
return err
}
resp, err := client.Do(req)
if err != nil {
fmt.Println(label, "response err:", err)
return err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(label, "read err:", err)
return err
}
result := string(data)
if result != "" {
fmt.Println(label, "result:", result)
}
if result == "error" {
fmt.Println("cancelling from", label)
return errors.New("error happened")
}
return nil
}
Todas as coisas interessantes estão neste arquivo. Primeiro, a função callBoth
cria um context
cancelável e uma função de cancelamento a partir do context
passado. Por convenção, essa variável de função é chamada de cancel
. É importante lembrar que sempre que você criar um context
cancelável, deverá chamar a função cancel
. Não tem problema chamá-lo mais de uma vez; cada invocação após a primeira é ignorada. Usamos defer
para garantir que ele seja eventualmente chamado. Em seguida, configuramos duas goroutines e passamos o context
cancelável, uma label e a URL para callServer
e esperamos que ambos sejam concluídos. Se qualquer chamada para callServer
retornar um erro, chamamos a função de cancel
.
A função callServer
é um simples client. Criamos as requests com o context
cancelável e fazemos uma chamada. Se ocorrer um erro, ou se obtivermos o error
da query string, retornamos o erro.
Por fim, temos a função main
, que dá início ao programa, no arquivo main.go
:
package main
import (
"context"
"os"
)
func main() {
ss := slowServer()
defer ss.Close()
fs := fastServer()
defer fs.Close()
ctx := context.Background()
callBoth(ctx, os.Args[1], ss.URL, fs.URL)
}
Na função main
, é iniciado os servidores HTTP, criado um context
, e depois chamar os clients com o context como primeiro argumento para o programa, e as URLs para os servidores.
Veja o que acontece se você executar sem erros:
make run-ok
# fast result: ok
# slow result: slow response
# done with both
E aqui está o que acontece se um erro for acionado:
make run-cancel
# fast result: error
# cancelling from fast
# slow response err: Get "http://127.0.0.1:53053": context canceled
# done with both
Sempre que você criar um context que tenha uma função de cancelamento associada, deverá chamar essa função de cancelamento ao terminar o processamento, quer o processamento termine ou não em erro. Se você não fizer isso, seu programa vazará(leak) recursos (memória e goroutines) e, eventualmente, ficará lento ou travará(crash). Não haverá erro se você chamar a função cancel
mais de uma vez; qualquer invocação após a primeira não faz nada. A maneira mais fácil de ter certeza de chamar a função cancel
é usar defer
.
Embora o cancelamento manual seja útil, não é sua única opção. No próxima tópico, veremos como automatizar o cancelamento com tempos limite.
Timers
Uma das tarefas mais importantes para um servidor HTTP é gerenciar as requests. Um programador novato frequentemente pensa que um servidor deve receber tantas requests quanto possível e manipular elas pelo tempo que puder até retornar um resultado para cada client.
O problema é que essa abordagem não é escalável. Um server HTTP é um recurso compartilhado. Como todos os recursos compartilhados, cada usuário deseja obter o máximo possível deles e não tem nenhuma preocupação com as necessidades dos outros usuários. É responsabilidade do recurso compartilhado gerenciar a si mesmo de forma que forneça uma quantidade razoável de tempo a todos os seus usuários.
Geralmente, há quatro coisas que um servidor HTTP pode fazer para gerenciar sua carga:
- Limitar requests simultâneas;
- Limitar o número de request que estão na fila à espera de execução;
- Limite por quanto tempo uma request pode ser executada;
- Limitar os recursos que uma request pode usar (como memória ou espaço em disco);
Go fornece ferramentas para lidar com os três primeiros. Ao limitar o número de goroutines, um servidor gerencia a carga simultânea. O tamanho da fila de espera é controlado por meio de canais buffered.
O context
fornece uma maneira de controlar por quanto tempo uma solicitação é executada. Ao construir um aplicativo, você deve ter uma ideia de seu desempenho: quanto tempo você tem para que sua solicitação seja concluída antes que o usuário tenha uma experiência insatisfatória. Se você souber a quantidade máxima de tempo que uma solicitação pode ser executada, poderá aplicá-la usando o context
.
Você pode usar uma das duas funções diferentes para criar um context
de tempo limitado. O primeiro é context.WithTimeout
. Leva dois parâmetros, um context
existente e time.Duration
que especifica a duração até que o context
seja cancelado automaticamente. Ele retorna um context
que dispara automaticamente um cancelamento após a duração especificada, bem como uma função de cancelamento que é chamada para cancelar o context
imediatamente.
A segunda função é context.WithDeadline
. Esta função considera um context
existente e um time.Time
que especifica a hora em que o context
é cancelado automaticamente. Como context.WithTimeout
, ele retorna um context
que dispara automaticamente um cancelamento após o tempo especificado ter decorrido, bem como uma função de cancelamento.
Se você quiser saber quando um context
será cancelado automaticamente, use o método Deadline
em context.Context
. Ele retorna um time.Time
que indica o tempo e um bool
que indica se houve um tempo limite definido. Isso reflete o comma ok idiom que é usado ao ler mapas ou canais.
Ao definir um limite de tempo para a duração da solicitação, você pode querer subdividir esse tempo. E se você chamar outro serviço a partir do seu serviço, pode limitar o tempo de execução da chamada de rede, reservando algum tempo para o restante do processamento ou para outras chamadas de rede. Você controla quanto tempo uma chamada individual leva criando um context
filho que envolve um context
pai usando context.WithTimeout
ou context.WithDeadline
.
Qualquer tempo limite definido no context
filho é limitado pelo tempo limite definido no context
pai; se um context
pai expira em dois segundos, você pode declarar que um context
filho expira em três segundos, mas quando o context
pai expira depois de dois segundos, o filho também expirará.
Podemos ver isso com um simples programa:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
parent, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
child, cancel2 := context.WithTimeout(parent, 3*time.Second)
defer cancel2()
start := time.Now()
<-child.Done()
end := time.Now()
fmt.Println(end.Sub(start)) // 2s
}
Neste exemplo, especificamos um tempo limite de dois segundos no context
pai e um tempo limite de três segundos no context
filho. Em seguida, esperamos que o context
filho seja concluído, aguardando no canal retornado do método Done
no context
filho de context.Context
.
Você pode executar este código e verá o seguinte resultado: 2s
Cancelamento de context
Na maioria das vezes, você não precisa se preocupar com tempos limite ou cancelamento dentro do seu próprio código; ele simplesmente não funciona por tempo suficiente. Sempre que você chamar outro serviço HTTP ou o banco de dados, deve transmitir o context
; essas bibliotecas tratam adequadamente o cancelamento por meio do context
.
Se você escrever um código que deve ser interrompido por um cancelamento de context
, você implementa as verificações de cancelamento usando os recursos de simultaneidade. O tipo context.Context
possui dois métodos que são usados ao gerenciar o cancelamento.
O método Done
retorna um canal de struct{}
. (O motivo pelo qual este é o tipo de retorno escolhido é que uma estrutura vazia não usa memória). O canal é fechado quando o context
é cancelado devido a um tempo limite especificado ou a função de cancel
que está sendo chamada. Lembre-se de que um canal fechado sempre retorna imediatamente seu zero value quando você tenta lê-lo.
Se você chamar Done
em um context
que não pode ser cancelado, ele retornará nil
. Como sabemos, uma leitura de um canal nil
nunca retorna. Se isso não for feito dentro de um case
em uma instrução select
, seu programa irá travar.
O método Err
retorna nil
se o context
ainda estiver ativo, ou retorna um dos dois erros se o context
foi cancelado: context.Canceled
e context.DeadlineExceeded
. O primeiro é retornado após o cancelamento explícito e o segundo é retornado quando um cancelamento acionado por tempo limite.
Este é o padrão para suportar o cancelamento de um context
em seu código:
func longRunningThingManager(ctx context.Context, data string) (string, error) {
type wrapper struct {
result string
err error
}
ch := make(chan wrapper, 1)
go func() {
// do the long running thing
result, err := longRunningThing(ctx, data)
ch <- wrapper{result, err}
}()
select {
case data := <-ch:
return data.result, data.err
case <-ctx.Done():
return "", ctx.Err()
}
}
No código acima, precisamos colocar os dados retornados da função de longa duração em uma estrutura, para que possamos passá-los em um canal. Em seguida, criamos um canal do tipo wrapper
com tamanho de buffer 1
. Armazenando o canal em buffer, permitimos que a goroutine se encerre, mesmo se o valor buffered nunca for lido devido ao cancelamento.
Na goroutine, recuperamos a saída da função de longa duração e a colocamos no canal buffered. Temos então um select
com dois case
. Em nosso primeiro case
do select
, lemos os dados da função de longa duração e os retornamos. Este é o caso que é acionado se o context
não for cancelado devido ao tempo limite ou invocação da função cancel
. O segundo case
do select
é acionado se o context
for cancelado. Retornamos o zero value para os dados e o erro do context
para nos dizer por que ele foi cancelado.
Values
Existe mais um uso para o context
. Ele também fornece uma maneira de passar metadados pela solicitação por meio do programa.
Por padrão, você deve preferir passar os dados por meio de parâmetros explícitos. Como foi mencionado antes, o Go é idiomático a favorece o explícito sobre o implícito, e isso inclui a passagem de dados explícita. Se uma função depende de alguns dados, deve ficar claro de onde ele veio.
Assim como há métodos de fábrica no pacote de context
para criar contexts timed e cancellable, há um método de fábrica para colocar valores no context, context.WithValue
. Ele possui três parâmetros: um context
para ser manipulado, uma chave para pesquisar o valor e o próprio valor. Ele retorna um context
filho que contém os dados de chave-valor. O tipo da chave e os parâmetros de valor são declarados como interfaces vazias (interface{}
).
Para verificar se um valor está em um context
ou em algum de seus parents, use o método Value
em context.Context
. Este método recebe uma chave e retorna o valor associado à chave. Novamente, o parâmetro da chave e o resultado do valor são declarados do tipo interface{}
. Se nenhum valor for encontrado para a chave fornecida, nil
será retornado. Use comma ok idiom com type assert do valor retornado para o tipo correto.
Embora o valor armazenado no context
possa ser de qualquer tipo, existe um padrão idiomático usado para garantir a exclusividade da chave. Da mesma forma que a chave para um mapa, a chave para o valor do context
deve ser comparável. Crie um novo tipo não exportado para a chave, com base em um int
:
type userKey int
Se você usar uma string ou outro tipo público para o tipo da chave, diferentes pacotes podem criar chaves idênticas, resultando em colisões. Isso causa problemas difíceis de depurar, como um pacote gravando dados no context
que mascara os dados gravados por outro pacote ou lendo dados do context
gravado por outro pacote.
Depois de declarar seu tipo de chave não exportada, você declara uma constante não exportada desse tipo:
const key userKey = 1
Com o tipo e a constante da chave não sendo exportados, nenhum código de fora de seu pacote pode colocar dados no context
que causaria uma colisão. Se o seu pacote precisar colocar vários valores no context
, defina uma chave diferente do mesmo tipo para cada valor, usando o padrão iota
. Como nos preocupamos apenas com o valor da constante como forma de diferenciar entre várias chaves, esse é um uso perfeito para iota
.
Em seguida, construa uma API para colocar e ler um valor no context
. Torne essas funções públicas apenas se o código fora de seu pacote puder ler e gravar seus valores no context
. O nome da função que cria um context
com o valor deve começar com ContextWith
. A função que retorna o valor do context
deve ter um nome que termina com FromContext
. Aqui estão as implementações de nossas funções para obter e ler o usuário a partir do context
:
func ContextWithUser(ctx context.Context, user string) context.Context {
return context.WithValue(ctx, key, user)
}
func UserFromContext(ctx context.Context) (string, bool) {
user, ok := ctx.Value(key).(string)
return user, ok
}
Agora que escrevemos nosso código de gerenciamento de usuário, vamos ver como usá-lo. Vamos escrever um middleware que extraia o ID do usuário de um cookie:
// a real implementation would be signed to make sure
// the user didn't spoof their identity
func extractUser(req *http.Request) (string, error) {
userCookie, err := req.Cookie("user")
if err != nil {
return "", err
}
return userCookie.Value, nil
}
func Middleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
user, err := extractUser(req)
if err != nil {
rw.WriteHeader(http.StatusUnauthorized)
return
}
ctx := req.Context()
ctx = ContextWithUser(ctx, user)
req = req.WithContext(ctx)
h.ServeHTTP(rw, req)
})
}
No middleware, primeiro obtemos o valor do usuário. Depois, extraímos o context
da request com o método Context
e criamos um novo context
que contém o usuário com a função ContextWithUser
. Em seguida, criamos uma nova request a partir da request antiga e do novo context
usando o método WithContext
. Finalmente, chamamos a próxima função em nossa cadeia de manipuladores com a nova request e o http.ResponseWriter
do método.
Na maioria dos casos, você deseja extrair o valor do context
em seu manipulador da request e passá-lo para sua lógica de negócios explicitamente. As funções Go têm parâmetros explícitos e você não deve usar o context
como uma forma de passar os valores pela API:
func (c Controller) handleRequest(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
user, ok := identity.UserFromContext(ctx)
if !ok {
rw.WriteHeader(http.StatusInternalServerError)
return
}
data := req.URL.Query().Get("data")
result, err := c.Logic.businessLogic(ctx, user, data)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(err.Error()))
return
}
rw.Write([]byte(result))
}
Nosso manipulador obtém o context
usando o método Context
na request, extrai o usuário do context
usando a função UserFromContext
e chama a lógica de negócios.
Existem algumas situações em que é melhor manter um valor no context
. O GUID de rastreamento é uma delas. Essas informações são destinadas ao gerenciamento do aplicativo; não faz parte do estado de negócios. Passá-lo explicitamente por meio do código adiciona parâmetros adicionais e impede a integração com bibliotecas de terceiros que não sabem sobre sua metainformação. Ao deixar um GUID de rastreamento no context
, ele passa invisivelmente pela lógica de negócios que não precisa saber sobre rastreamento e está disponível quando seu programa grava uma mensagem de log ou se conecta a outro servidor.
Aqui está uma implementação GUID simples com base no context
que rastreia de serviço a serviço e cria logs com o GUID incluído:
package tracker
import (
"context"
"fmt"
"net/http"
"github.com/google/uuid"
)
type guidKey int
const key guidKey = 1
func contextWithGUID(ctx context.Context, guid string) context.Context {
return context.WithValue(ctx, key, guid)
}
func guidFromContext(ctx context.Context) (string, bool) {
g, ok := ctx.Value(key).(string)
return g, ok
}
func Middleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
if guid := req.Header.Get("X-GUID"); guid != "" {
ctx = contextWithGUID(ctx, guid)
} else {
ctx = contextWithGUID(ctx, uuid.New().String())
}
req = req.WithContext(ctx)
h.ServeHTTP(rw, req)
})
}
type Logger struct{}
func (Logger) Log(ctx context.Context, message string) {
if guid, ok := guidFromContext(ctx); ok {
message = fmt.Sprintf("GUID: %s - %s", guid, message)
}
// do logging
fmt.Println(message)
}
func Request(req *http.Request) *http.Request {
ctx := req.Context()
if guid, ok := guidFromContext(ctx); ok {
req.Header.Add("X-GUID", guid)
}
return req
}
A função Middleware
extrai o GUID da request de entrada ou gera um novo GUID. Em ambos os casos, ele coloca o GUID no context
, cria uma nova request com o context
atualizado e continua a cadeia de chamadas.
A seguir, veremos como esse GUID é usado. A estrutura Logger
fornece um método de registro Log
que tem como parâmetro um context
e uma string. Se houver um GUID no context
, ele o anexará ao início da mensagem de log. A função Request
é usada quando este serviço faz uma chamada para outro serviço. Ele recebe um *http.Request
, adiciona um cabeçalho com o GUID se ele existir no context
e retorna o *http.Request
.
Assim que tivermos esse pacote, podemos usar as técnicas de injeção de dependência para criar uma lógica de negócios que não tem conhecimento de nenhuma informação de rastreamento. Primeiro, declaramos uma interface para representar o logger
, um tipo de função para representar um Decorator
da request e uma estrutura da lógica de negócios que depende deles:
type Logger interface {
Log(context.Context, string)
}
type RequestDecorator func(*http.Request) *http.Request
type BusinessLogic struct {
RequestDecorator RequestDecorator
Logger Logger
Remote string
}
Em seguida, implementamos a lógica de negócios:
func (bl BusinessLogic) businessLogic(ctx context.Context, user string, data string) (string, error) {
bl.Logger.Log(ctx, "starting businessLogic for " + user + " with " + data)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, bl.Remote+"?query="+data, nil)
if err != nil {
bl.Logger.Log(ctx, "error building remote request:" + err)
return "", err
}
req = bl.RequestDecorator(req)
resp, err := http.DefaultClient.Do(req)
// processing continues
}
O GUID é passado para o criador de logs e o Decorator da request sem que a lógica de negócios saiba, separando os dados necessários da lógica do programa dos dados necessários para o gerenciamento do programa. O único lugar que está ciente da associação é o código main
que conecta nossas dependências:
bl := BusinessLogic {
RequestDecorator: tracker.Request,
Logger: tracker.Logger{},
Remote: "http://www.example.com/query",
}