Add Tracing to internal methods in Cloud Run - google-cloud-run

We would like to add tracing to methods used within services deployed on Cloud Run.
Tracing already provided Cloud Run requests:
Let's say we have the following gRPC method:
func (s *myServiceService) SyncTable(ctx context.Context, req *pb.SyncTableRequest) (*longrunning.Operation, error) {
//.... some stuff here...
// making a call to the internal method, which has a tracing span
err := dropRequestOnStorage(ctx, ...)
if err != nil {
return nil, err
}
return op, nil
}
Here is an example of an internal method to which we have added a Trace span and is called by the main gRPC method:
// dropRequestOnStorage loads the requests on the relevant bucket.
func dropRequestOnStorage(ctx context.Context, filename string, operationID string, req *pb.ExtractDataRequest) error {
// add tracing to this method.
ctx, span := otel.Tracer("").Start(ctx, "dropRequestOnStorage")
defer span.End()
// load json object to storage
reqByte, err := protojson.Marshal(req)
if err != nil {
fmt.Println(err)
}
wc := storageClient.Bucket("my-bucket-with-cool-stuff").Object(filename).NewWriter(ctx)
wc.ContentType = "application/json"
_, err = wc.Write(reqByte)
if err != nil {
fmt.Println(err)
}
wc.Close()
fmt.Println(filename)
return nil
}
Looking at Tracing for Google Cloud Run I see traces for the above method:
Despite passing the context from the main gRPC to the internal method, Tracing is not pulled through to the underlying internals. The traces generated by the internal methods does not 'receive' the main gRPC trace as a parent.
Is this because the default tracing provided by Cloud Run is done by the Cloud Run internals? And therefore not available to the context of the gRPC methods?

Tracing using gRPC Interceptors
The only way to get this to work was to add gRPC interceptors to create tracing spans for each gRPC method.
package main
import (
"context"
texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"google.golang.org/grpc"
"log"
"net"
"os"
)
func init() {
// Pre-declare err to avoid shadowing.
var err error
// initialising tracing exporter
//exporter, err := stdout.NewExporter(stdout.WithPrettyPrint())
exporter, err := texporter.NewExporter(texporter.WithProjectID("alis-de"))
if err != nil {
log.Fatalf("texporter.NewExporter: %v", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithSyncer(exporter),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
}
func main() {
log.Printf("starting server...")
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatalf("net.Listen: %v", err)
}
// Attaching grpc interceptors to automatically enable tracing at gRCP methods
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
)
pb.RegisterOperationsServer(grpcServer, &operationsService{})
if err = grpcServer.Serve(listener); err != nil {
log.Fatal(err)
}
}
Tracing now pulls through to the Console:
However, looking at the Traces, there are now (unfortunately??) two trace entries:
The default trace provided by Cloud Run (with no child traces)
The new trace generated by the gRPC interceptors (with child traces reflecting the internally called methods)

Related

Server in Docker container connection refused, should I add time.Sleep(100 * time.Millisecond) to my tests?

I am currently working on a server that is designed to be run in a Docker container.
Here is my setup method for my tests:
func TestMain(m *testing.M) {
schedulerName := "scheduler1"
IP, err := container.StartNewScheduler(schedulerName)
if err != nil {
log.Println("Could not create container.")
log.Fatal(err)
}
serverIP = IP
code := m.Run()
cleanupContainer(schedulerName)
os.Exit(code)
}
The line container.StartNewScheduler(schedulername) boots up a new docker container called "scheduler1" and tells it to run the server inside of it.
Next I run my tests with the container running in the background, right now I only have one test.
func TestNewScheduler(t *testing.T) {
testCodeInput := "THIS IS A TEST"
requestBody, err := json.Marshal(map[string]string{
"Code": fmt.Sprintf("print(\"%s\")", testCodeInput),
})
if err != nil {
t.Fatal(err)
}
url := fmt.Sprintf("http://%s:%d/execute/python", serverIP, 3000)
contentType := "application/json"
body := bytes.NewBuffer(requestBody)
response := post(url, contentType, body, t)
actual := parseOutput(response.Body, t)
response.Body.Close()
expected := fmt.Sprintf("{\"Stdout\":\"%s\\n\"}", testCodeInput)
if actual != expected {
t.Fatalf("Expected %s, but got %s", expected, actual)
}
}
The problem that I am running into is sometimes I get a connection refused and sometimes I don't.
server_container_test.go:51: Post http://172.20.0.2:3000/execute/python: dial tcp 172.20.0.2:3000: connect: connection refused
I noticed that whenever I try and debug the issue everything seems to work fine. My running theory is because when I step through my code the container has more time to start up and get the server running inside it.
In order to test my Hypothesis I added a second post call in my post method with a timer set before I call it.
func post(url string, contentType string, body io.Reader, t *testing.T) *http.Response {
t.Helper()
response, err := http.Post(url, contentType, body)
if err != nil {
//There is an error where the container takes a second to boot up and so
//the scheduler isn't running when the first request is sent, so we try
//one more time here and check again.
time.Sleep(100 * time.Millisecond) <--- Right here
response, err = http.Post(url, contentType, body)
if err != nil {
t.Fatal(err)
}
}
return response
}
Does anyone else have any guesses as to what could be causing me this issue?
If my hypothesis is correct is this the best way to fix this? Is it a bad idea to add a time.Sleep to your tests?
Thanks!
Ok so after some more thought I changed up my source code, please let me know if you think this is a good solution to my problem. I am still learning Go and HTTP servers so any input is appreciated.
Here is my fix/idea:
Previously once the container was created I just returned it's IP address and forgot about it.
Now I create a go routine that repeatedly tries to send a POST request to the server. If it doesn't fail then I send true through a channel and close the function.
IP := info.NetworkSettings.Networks[networkName].IPAddress
works := make(chan bool)
ctx, canelRoutine := context.WithCancel(context.Background())
defer canelRoutine()
go func(ctx context.Context) {
requestBody, _ := json.Marshal(map[string]string{
"Code": "print()",
})
select {
case <-ctx.Done():
return
default:
for {
_, err := http.Post(
fmt.Sprintf("http://%s:%d/execute/python", IP, 3000),
"application/json",
bytes.NewBuffer(requestBody),
)
if err == nil {
works <- true
return
}
}
}
}(ctx)
After sending the goroutine off I create a timer and and wait for either the timer to return or the goroutine.
timer := time.After(500 * time.Millisecond)
select {
case <-works:
return IP, nil
case <-timer:
return IP, &UnreachableContainerError{name: schedulerName}
}
The upside to this solution is I have now introduced an UnreachableContainerError which allows me to be more specific about my error messages and it can be checked on the receiving side. I also send the IP address back either way just in case the client needs it for some other reason.
Here is the full StartNewScheduler method in case you wanted to see it.
//StartNewScheduler starts a new scheduler with the given options.
//returns the IP address for the given scheduler.
func StartNewScheduler(schedulerName string) (string, error) {
///Defaults
dockerfile := "Dockerfile_standard"
networkName := "scheduler-cluster"
imageID := "lkelly93/scheduler_image:latest"
cli, err := client.NewEnvClient()
if err != nil {
return "", err
}
err = createDefaultImageIfNeeded(
cli,
imageID,
dockerfile)
if err != nil {
return "", err
}
err = createSchedulerClusterNetworkIfNeeded(cli, networkName)
if err != nil {
return "", err
}
ctx := context.Background()
resp, err := cli.ContainerCreate(
ctx,
&container.Config{Image: imageID},
&container.HostConfig{
NetworkMode: container.NetworkMode(networkName),
Privileged: true,
},
nil,
schedulerName,
)
if err != nil {
return "", err
}
err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
return "", err
}
//Get container IP
info, err := cli.ContainerInspect(ctx, resp.ID)
if err != nil {
return "", err
}
IP := info.NetworkSettings.Networks[networkName].IPAddress
works := make(chan bool)
ctx, canelRoutine := context.WithCancel(context.Background())
defer canelRoutine()
go func(ctx context.Context) {
requestBody, _ := json.Marshal(map[string]string{
"Code": "print()",
})
select {
case <-ctx.Done():
return
default:
for {
_, err := http.Post(
fmt.Sprintf("http://%s:%d/execute/python", IP, 3000),
"application/json",
bytes.NewBuffer(requestBody),
)
if err == nil {
works <- true
return
}
}
}
}(ctx)
timer := time.After(500 * time.Millisecond)
select {
case <-works:
return IP, nil
case <-timer:
return IP, &UnreachableContainerError{name: schedulerName}
}
}
You could run the test code inside of a container, started via docker compose with the option depends_on.

http.Post() from a gRPC server to an http server returns EOF error on a docker-compose setup

I have a gRPC server (server) written in Go that a Python gRPC client (client) talks to. The server occasionally sends http post requests to a Go based http server (sigsvc). All of these instances run as docker instances spun up through docker-compose sharing the same docker network.
This is the section of code on server that creates and sends the http request:
b := new(bytes.Buffer)
txbytes, err := json.Marshal(tx)
if err != nil {
log.WithError(err).Error("failed to marshal transaction")
return nil, err
}
b.Write(txbytes)
resp, err := http.Post(sigsvc.signerURL, "application/json; charset=utf-8", b)
if err != nil {
log.WithError(err).Errorf("error signing transaction with signer %s", sigsvc.signerURL)
return nil, err
}
defer resp.Body.Close()
var signedTx types.Transaction
err = json.NewDecoder(resp.Body).Decode(&signedTx)
if err != nil {
log.WithError(err).Error("couldn't decode signed transaction")
return nil, err
}
sigsvc.signerURL maps to something like http://signer:6666/sign which is the endpoint on the http signer service that handles the request.
signer refers to the service name listed on a docker-compose.yml specification.
This is how the handler looks like on sigsvc:
func (sv *SignerSv) handleSignTx() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Info("request received to sign transaction")
dump, err := httputil.DumpRequest(r, true)
if err != nil {
http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
}
log.Debugf("%q", dump)
if r.Body == nil {
log.Error("request body missing")
http.Error(w, "Please send a request body", 400)
return
}
log.Debugf("request body: %v", r.Body)
var tx types.Transaction
err = json.NewDecoder(r.Body).Decode(&tx)
if err != nil {
log.WithError(err).Error("failed to unmarshal transaction")
http.Error(w, err.Error(), 400)
return
}
log.WithFields(log.Fields{
"txhash": tx.Hash().Hex(),
"nonce": tx.Nonce(),
"to": tx.To().Hex(),
"data": tx.Data(),
"gasLimit": tx.Gas(),
"gasPrice": tx.GasPrice(),
"value": tx.Value(),
}).Debug("Decoded transaction from request body")
Both the request and request body are dumped successfully by the debug logs. However, apparently the line decoding the request body to the transaction type is never executed, since no error or decoded transaction logs are logged.
On server, I keep getting the following error:
error="Post http://signer:6666/sign: EOF"
This is how the request is logged on sigsvc:
msg="\"POST /sign HTTP/1.1\\r\\nHost: signer:6666\\r\\nConnection: close\\r\\nAccept-Encoding: gzip\\r\\nConnection: close\\r\\nContent-Length: 10708\\r\\nUser-Agent: Go-http-client/1.1\\r\\n\\r\\n{\\\"nonce\\\":\\\"0x0\\\",\\\"gasPrice\\\":\\\"0x2540be400\\\",\\\"gas\\\":\\\"0x15c285\\\",\\\"to\\\":null,\\\"value\\\":\\\"0x0\\\",\\\"input\\\":\\\"0x6080604055",\\\"v\\\":\\\"0x0\\\",\\\"r\\\":\\\"0x0\\\",\\\"s\\\":\\\"0x0\\\",\\\"hash\\\":\\\"0xab55920fb3d490fc55ccd76a29dfb380f4f8a9e5d0bda4155a3b114fca26da0a\\\"}\"
I have tried reproducing this error on similar but simplified docker setups, but I have failed at that.
I'm trying to understand the following:
If there is anything wrong with this code that is being exposed due
to a particular setup on docker?
Or, do I need to look at some docker setup specifics to debug the instances.
The problem was in the way the http handler code was logging the to field in this logrus call.
log.WithFields(log.Fields{
"txhash": tx.Hash().Hex(),
"nonce": tx.Nonce(),
"to": tx.To().Hex(),
"data": tx.Data(),
"gasLimit": tx.Gas(),
"gasPrice": tx.GasPrice(),
"value": tx.Value(),
}).Debug("Decoded transaction from request body")
Under specific circumstances, the tx.To() call returns nil, which implies calling tx.To().Hex() would lead to an error on account of trying to make a method call on a nil pointer. On the face of it, one would expect the log.WithFields() call to error out or panic, but instead the handler silently closes connection with the client side getting an EOF response.

How to get container ID by golang

I use golang to develop application . I want get container in application.I hava tired by shell.But I want to get container by go. thanks
You can use docker/client
https://godoc.org/github.com/docker/docker/client
Example code:
# listcontainers.go
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func main() {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
panic(err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
panic(err)
}
for _, container := range containers {
fmt.Printf("%s %s\n", container.ID[:10], container.Image)
}
}
Then execute it like this
DOCKER_API_VERSION=1.35 go run listcontainers.go
More about docker engine SDKs and API
https://docs.docker.com/develop/sdk/

How to wrap exec.Command inside an io.Writer

I'm trying to compress a JPEG image in go using mozjpeg. Since it doesn't have official go binding, I think I'll just invoke its CLI to do the compression.
I try to model the usage after compress/gzip:
c := jpeg.NewCompresser(destFile)
_, err := io.Copy(c, srcFile)
Now the question is, how do I wrap the CLI inside Compresser so it can support this usage?
I tried something like this:
type Compresser struct {
cmd exec.Command
}
func NewCompressor(w io.Writer) *Compresser {
cmd := exec.Command("jpegtran", "-copy", "none")
cmd.Stdout = w
c := &Compresser{cmd}
return c
}
func (c *Compresser) Write(p []byte) (n int, err error) {
if c.cmd.Process == nil {
err = c.cmd.Start()
if err != nil {
return
}
}
// How do I write p into c.cmd.Stdin?
}
But couldn't finish it.
Also, a second question is, when do I shut down the command? How to shut down the command?
You should take a look at the Cmd.StdinPipe. There is an example in the documentation, which suits your case:
package main
import (
"fmt"
"io"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("cat")
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
go func() {
defer stdin.Close()
io.WriteString(stdin, "values written to stdin are passed to cmd's standard input")
}()
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", out)
}
In this case, CombinedOutput() executes your command, and the execution is finished, when there are no more bytes to read from out.
As per Kiril's answer, use the cmd.StdInPipe to pass on the data you receive to Write.
However, in terms of closing, I'd be tempted to implement io.Closer. This would make *Compresser automatically implement the io.WriteCloser interface.
I would use Close() as the notification that there is no more data to be sent and that the command should be terminated. Any non-zero exit code returned from the command that indicates failure could be caught and returned as an error.
I would be wary of using CombinedOutput() inside Write() in case you have a slow input stream. The utility could finish processing the input stream and be waiting for more data. This would be incorrectly detected as command completion and would result in an invalid output.
Remember, the Write method can be called an indeterminate number of times during IO operations.

Golang - Docker API - parse result of ImagePull

I'm developing a Go script that uses the Docker API for the purposes of my project. After I login to my repository, I pull the Docker image I want, but the problem is that the ImagePull function returns an instance of io.ReadCloser, which I'm only able to pass to the system output via:
io.Copy(os.Stdout, pullResp)
It's cool that I can see the response, but I can't find a decent way to parse it and implement a logic depending on it, which will do some things if a new version of the image have been downloaded, and other things if the image was up to date.
I'll be glad if you share your experience, if you have ever faced this problem.
You can import github.com/docker/docker/pkg/jsonmessage and use both JSONMessage and JSONProgress to decode the stream but it's easier to call
DisplayJSONMessagesToStream: it both parses the stream and displays the messages as text. Here's how you can display the messages using stderr:
reader, err := cli.ImagePull(ctx, myImageRef, types.ImagePullOptions{})
if err != nil {
return err
}
defer reader.Close()
termFd, isTerm := term.GetFdInfo(os.Stderr)
jsonmessage.DisplayJSONMessagesStream(reader, os.Stderr, termFd, isTerm, nil)
The nice thing is that it adapts to the output: it updates the lines if this a TTY (the way docker pull does) but it doesn't if the output is redirected to a file.
#radoslav-stoyanov before use my example do
# docker rmi busybox
then run code
package main
import (
"encoding/json"
"fmt"
"github.com/docker/distribution/context"
docker "github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"io"
"strings"
)
func main() {
// DOCKER
cli, err := docker.NewClient("unix:///var/run/docker.sock", "v1.28", nil, map[string]string{"User-Agent": "engine-api-cli-1.0"})
if err != nil {
panic(err)
}
imageName := "busybox:latest"
events, err := cli.ImagePull(context.Background(), imageName, types.ImagePullOptions{})
if err != nil {
panic(err)
}
d := json.NewDecoder(events)
type Event struct {
Status string `json:"status"`
Error string `json:"error"`
Progress string `json:"progress"`
ProgressDetail struct {
Current int `json:"current"`
Total int `json:"total"`
} `json:"progressDetail"`
}
var event *Event
for {
if err := d.Decode(&event); err != nil {
if err == io.EOF {
break
}
panic(err)
}
fmt.Printf("EVENT: %+v\n", event)
}
// Latest event for new image
// EVENT: {Status:Status: Downloaded newer image for busybox:latest Error: Progress:[==================================================>] 699.2kB/699.2kB ProgressDetail:{Current:699243 Total:699243}}
// Latest event for up-to-date image
// EVENT: {Status:Status: Image is up to date for busybox:latest Error: Progress: ProgressDetail:{Current:0 Total:0}}
if event != nil {
if strings.Contains(event.Status, fmt.Sprintf("Downloaded newer image for %s", imageName)) {
// new
fmt.Println("new")
}
if strings.Contains(event.Status, fmt.Sprintf("Image is up to date for %s", imageName)) {
// up-to-date
fmt.Println("up-to-date")
}
}
}
You can see API formats to create your structures (like my Event) to read them here https://docs.docker.com/engine/api/v1.27/#operation/ImageCreate
I hope it helps you solve your problem, thanks.
I have used similar approach for my purpose (not a moby client). Typically idea is same for reading stream response. Give it a try and implement yours.
Reading stream response of any response type:
reader := bufio.NewReader(pullResp)
defer pullResp.Close() // pullResp is io.ReadCloser
var resp bytes.Buffer
for {
line, err := reader.ReadBytes('\n')
if err != nil {
// it could be EOF or read error
// handle it
break
}
resp.Write(line)
resp.WriteByte('\n')
}
// print it
fmt.Println(resp.String())
However your sample response in the comment seems valid JSON structure. The json.Decoder is best way to read JSON stream. This is just an idea-
type ImagePullResponse struct {
ID string `json"id"`
Status string `json:"status"`
ProgressDetail struct {
Current int64 `json:"current"`
Total int64 `json:"total"`
} `json:"progressDetail"`
Progress string `json:"progress"`
}
And do
d := json.NewDecoder(pullResp)
for {
var pullResult ImagePullResponse
if err := d.Decode(&pullResult); err != nil {
// handle the error
break
}
fmt.Println(pullResult)
}

Resources