Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture newlines in message #28

Merged
merged 6 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ jobs:
go-version: ${{ env.GOVERSION }}
- uses: golangci/golangci-lint-action@v3

test:
name: Test
runs-on: ubuntu-latest
needs:
- lint
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GOVERSION }}
- name: unit tests
run: make test

build:
name: Build
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
GOOS=linux

test:
GOOS=${GOOS} GOARCH=${GOARCH} go test ./...

build:
mkdir -p bin/extensions
GOOS=${GOOS} GOARCH=${GOARCH} go build -o bin/extensions/axiom-lambda-extension .
Expand Down
56 changes: 33 additions & 23 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ var (
axiomMetaInfo = map[string]string{}
)

var logLineRgx, _ = regexp.Compile(`^([0-9.:TZ-]{20,})\s+([0-9a-f-]{36})\s+(ERROR|INFO|WARN|DEBUG|TRACE)\s+(.*)`)
var logLineRgx, _ = regexp.Compile(`^([0-9.:TZ-]{20,})\s+([0-9a-f-]{36})\s+(ERROR|INFO|WARN|DEBUG|TRACE)\s+(?s:(.*))`)

func init() {
logger, _ = zap.NewProduction()
Expand Down Expand Up @@ -91,28 +91,7 @@ func httpHandler(ax *flusher.Axiom, runtimeDone chan struct{}) http.HandlerFunc
e["_time"], e["time"] = e["time"], nil

if e["type"] == "function" {
e["message"] = e["record"]
if recordStr, ok := e["record"].(string); ok && len(recordStr) > 0 {
recordStr = strings.Trim(recordStr, "\n")
// parse the record
// first check if the record is a json object, if not parse it as a text log line
if recordStr[0] == '{' && recordStr[len(recordStr)-1] == '}' {
var record map[string]any
err = json.Unmarshal([]byte(recordStr), &record)
if err != nil {
logger.Error("Error unmarshalling record:", zap.Error(err))
// do not return, we want to continue processing the event
} else {
e["record"] = record
}
} else {
matches := logLineRgx.FindStringSubmatch(recordStr)
if len(matches) == 5 {
e["record"] = map[string]any{"requestId": matches[2], "message": matches[4], "timestamp": matches[1], "level": e["level"]}
e["level"] = strings.ToLower(matches[3])
}
}
}
extractEventMessage(e)
}

// decide if the handler should notify the extension that the runtime is done
Expand All @@ -136,3 +115,34 @@ func httpHandler(ax *flusher.Axiom, runtimeDone chan struct{}) http.HandlerFunc
}
}
}

// extractEventMessage extracts the message from the record field and puts it in the message field
// it detects if the record is a json string or a text log line that confirms to AWS log line formatting.
func extractEventMessage(e map[string]any) {
e["message"] = e["record"]
if recordStr, ok := e["record"].(string); ok && len(recordStr) > 0 {
recordStr = strings.Trim(recordStr, "\n")
// parse the record
// first check if the record is a json object, if not parse it as a text log line
if recordStr[0] == '{' && recordStr[len(recordStr)-1] == '}' {
var record map[string]any
err := json.Unmarshal([]byte(recordStr), &record)
if err != nil {
logger.Error("Error unmarshalling record:", zap.Error(err))
// do not return, we want to continue processing the event
} else {
if level, ok := record["level"].(string); ok {
record["level"] = strings.ToLower(level)
}
e["level"] = record["level"]
e["record"] = record
}
} else {
matches := logLineRgx.FindStringSubmatch(recordStr)
if len(matches) == 5 {
e["level"] = strings.ToLower(matches[3])
e["record"] = map[string]any{"requestId": matches[2], "message": matches[4], "timestamp": matches[1], "level": e["level"]}
}
}
}
}
98 changes: 98 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package server

import (
"testing"
)

func TestMessageExtraction(t *testing.T) {
testCases := []struct {
name string
input string
expected map[string]any
}{
{
name: "error messages on multiple lines",
input: "2024-01-16T08:53:51.919Z 4b995efa-75f8-4fdc-92af-0882c79f47a1 ERROR testing sending an error\nand this is a new line inside the error \n and a new line \n bye",
expected: map[string]any{
"level": "error",
"message": "SAME_AS_INPUT_NO_NEED_TO_DUPLICATE_INPUT_HERE",
"record": map[string]any{"requestId": "4b995efa-75f8-4fdc-92af-0882c79f47a1", "message": "testing sending an error\nand this is a new line inside the error \n and a new line \n bye", "timestamp": "2024-01-16T08:53:51.919Z", "level": "error"},
},
},
{
name: "info messages",
input: "2024-01-16T08:53:51.919Z 4b995efa-75f8-4fdc-92af-0882c79f47a2 INFO Hello, world!",
expected: map[string]any{
"level": "info",
"message": "SAME_AS_INPUT_NO_NEED_TO_DUPLICATE_INPUT_HERE",
"record": map[string]any{"requestId": "4b995efa-75f8-4fdc-92af-0882c79f47a2", "message": "Hello, world!", "timestamp": "2024-01-16T08:53:51.919Z", "level": "info"},
},
},
{
name: "warn messages",
input: "2024-01-16T08:53:51.919Z 4b995efa-75f8-4fdc-92af-0882c79f47a3 WARN head my warning",
expected: map[string]any{
"level": "warn",
"message": "SAME_AS_INPUT_NO_NEED_TO_DUPLICATE_INPUT_HERE",
"record": map[string]any{"requestId": "4b995efa-75f8-4fdc-92af-0882c79f47a3", "message": "head my warning", "timestamp": "2024-01-16T08:53:51.919Z", "level": "warn"},
},
},
{
name: "trace messages",
input: "2024-01-16T08:53:51.919Z 4b995efa-75f8-4fdc-92af-0882c79f47a4 TRACE this is a trace \n with information on a new line.",
expected: map[string]any{
"level": "trace",
"message": "SAME_AS_INPUT_NO_NEED_TO_DUPLICATE_INPUT_HERE",
"record": map[string]any{"requestId": "4b995efa-75f8-4fdc-92af-0882c79f47a4", "message": "this is a trace \n with information on a new line.", "timestamp": "2024-01-16T08:53:51.919Z", "level": "trace"},
},
},
{
name: "debug messages",
input: "2024-01-16T08:53:51.919Z 4b995efa-75f8-4fdc-92af-0882c79f47a5 DEBUG Debugging is fun!",
expected: map[string]any{
"level": "debug",
"message": "SAME_AS_INPUT_NO_NEED_TO_DUPLICATE_INPUT_HERE",
"record": map[string]any{"requestId": "4b995efa-75f8-4fdc-92af-0882c79f47a5", "message": "Debugging is fun!", "timestamp": "2024-01-16T08:53:51.919Z", "level": "debug"},
},
},
{
name: "testing json messages",
input: `{"timestamp":"2024-01-08T16:48:45.316Z","level":"INFO","requestId":"de126cf0-6124-426c-818a-174983fbfc4b","message":"foo != bar"}`,
expected: map[string]any{
"level": "info",
"message": "SAME_AS_INPUT_NO_NEED_TO_DUPLICATE_INPUT_HERE",
"record": map[string]any{"requestId": "de126cf0-6124-426c-818a-174983fbfc4b", "message": "foo != bar", "timestamp": "2024-01-08T16:48:45.316Z", "level": "info"},
},
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
e := make(map[string]any)
e["record"] = testCase.input
extractEventMessage(e)
if e["level"] != testCase.expected["level"] {
t.Errorf("Expected level to be %s, got %s", testCase.expected["level"], e["level"])
}
if e["message"] != testCase.input { // the message field should contain the original input
t.Errorf("Expected message to be %s, got %s", testCase.input, e["message"])
}

expectedRecord := testCase.expected["record"].(map[string]any)
outputRecord := e["record"].(map[string]any)

if outputRecord["timestamp"] != expectedRecord["timestamp"] {
t.Errorf("Expected timestamp to be %s, got %s", testCase.expected["timestamp"], e["timestamp"])
}
if outputRecord["level"] != expectedRecord["level"] {
t.Errorf("Expected record.level to be %s, got %s", expectedRecord["level"], outputRecord["level"])
}
if outputRecord["requestId"] != expectedRecord["requestId"] {
t.Errorf("Expected record.requestId to be %s, got %s", expectedRecord["requestId"], outputRecord["requestId"])
}
if outputRecord["message"] != expectedRecord["message"] {
t.Errorf("Expected record.message to be %s, got %s", expectedRecord["message"], outputRecord["message"])
}
})
}
}
2 changes: 1 addition & 1 deletion version/version.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package version

// manually set constant version
const version string = "v10"
const version string = "v11"

// Get returns the Go module version of the axiom-go module.
func Get() string {
Expand Down