Skip to content

Instantly share code, notes, and snippets.

@choonkeat
Last active September 16, 2017 06:22
Show Gist options
  • Save choonkeat/dbb040845214ad21eaabfc45590e84cb to your computer and use it in GitHub Desktop.
Save choonkeat/dbb040845214ad21eaabfc45590e84cb to your computer and use it in GitHub Desktop.
Golang package for logging in Google Cloud Function
package gcpsupervisor
import (
"log"
"net/http"
"os"
"google.golang.org/genproto/googleapis/api/monitoredres"
"cloud.google.com/go/logging"
"golang.org/x/net/context"
)
var client *logging.Client
var resource *monitoredres.MonitoredResource
func init() {
var err error
client, err = logging.NewClient(context.Background(), os.Getenv("GCLOUD_PROJECT"))
if err != nil {
log.Fatalln("missing GCLOUD_PROJECT env: " + err.Error())
}
resource = &monitoredres.MonitoredResource{
Labels: map[string]string{
"function_name": os.Getenv("FUNCTION_NAME"),
"project_id": os.Getenv("GCLOUD_PROJECT"), // GCP_PROJECT has same value ¯\_(ツ)_/¯
"region": "us-central1",
},
Type: "cloud_function",
}
}
// APILog write logs using the Logging API
func APILog(r *http.Request, severity logging.Severity, payload interface{}) {
logger := client.Logger(
"cloudfunctions.googleapis.com/cloud-functions",
logging.CommonResource(resource),
logging.CommonLabels(map[string]string{
"execution_id": r.Header.Get("Function-Execution-Id"),
}),
)
logger.Log(logging.Entry{
Severity: severity,
Payload: payload,
Trace: r.Header.Get("X-Cloud-Trace-Context"),
})
}
package gcpsupervisor
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"time"
"cloud.google.com/go/logging"
)
const maxPayloadLength = 5000
var supervisorURL = fmt.Sprintf("http://%s:%s%s",
os.Getenv("SUPERVISOR_HOSTNAME"),
os.Getenv("SUPERVISOR_INTERNAL_PORT"),
"/_ah/log",
)
type logBatch struct {
Entries []logEntry
}
type logEntry struct {
Severity string
Time string
ExecutionID string
TextPayload string `json:"TextPayload,omitempty"`
// JSONPayload interface{} `json:"JsonPayload,omitempty"` // doesn't work
}
// Log will send logs to SUPERVISOR
// if that fails, it will log the error via `cloud.google.com/go/logging` sdk
func Log(r *http.Request, payload interface{}) {
if details, err := logOrError(r, "INFO", payload); err != nil {
APILog(r, logging.Error, map[string]interface{}{
"err": err.Error(),
"details": details,
})
}
}
// Error will send logs to SUPERVISOR in `ERROR` severity
func Error(r *http.Request, payload interface{}) {
if details, err := logOrError(r, "ERROR", payload); err != nil {
APILog(r, logging.Error, map[string]interface{}{
"err": err.Error(),
"details": details,
})
}
}
func logOrError(r *http.Request, severity string, payload interface{}) (interface{}, error) {
// supervisor rejects time.RFC3339 and time.RFC3339Nano
entry := logEntry{
Severity: severity,
Time: time.Now().In(time.UTC).Format("2006-01-02T15:04:05") + ".000Z",
ExecutionID: r.Header.Get("Function-Execution-Id"),
}
switch t := payload.(type) {
case string:
sb := bytes.NewBufferString(t)
entry.TextPayload = string(sb.Next(maxPayloadLength))
default:
// how to transmit rich data? ¯\_(ツ)_/¯
sb := bytes.NewBufferString("")
if err := json.NewEncoder(sb).Encode(payload); err != nil {
return payload, err
}
entry.TextPayload = string(sb.Next(maxPayloadLength))
}
data, err := json.Marshal(logBatch{
Entries: []logEntry{entry},
})
if err != nil {
return entry, err
}
// for details, see https://gist.github.com/choonkeat/9476909becf29608844cba328cc5e3a3
client := http.DefaultClient
req, err := http.NewRequest("POST", supervisorURL, bytes.NewReader(data))
if err != nil {
return string(data), err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(data)))
req.Header.Set("User-Agent", "") // otherwise, supervisor server will reject
resp, err := client.Do(req)
if err != nil {
return fmt.Sprintf("POST %s %#v", supervisorURL, req.Header), err
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Sprintf("POST %s %#v", supervisorURL, req.Header), fmt.Errorf(resp.Status)
}
return io.Copy(ioutil.Discard, resp.Body)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment