Last active
September 16, 2017 06:22
-
-
Save choonkeat/dbb040845214ad21eaabfc45590e84cb to your computer and use it in GitHub Desktop.
Golang package for logging in Google Cloud Function
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"), | |
}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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