diff --git a/commands/cleanup.go b/commands/cleanup.go index 4f3d3150..74472570 100644 --- a/commands/cleanup.go +++ b/commands/cleanup.go @@ -12,6 +12,7 @@ import ( "github.com/ms-henglu/armstrong/report" "github.com/ms-henglu/armstrong/tf" "github.com/ms-henglu/armstrong/types" + "github.com/ms-henglu/pal/trace" "github.com/sirupsen/logrus" ) @@ -83,7 +84,7 @@ func (c CleanupCommand) Execute() int { } passReport := tf.NewPassReportFromState(state) - idAddressMap := tf.NewIdAdressFromState(state) + idAddressMap := tf.NewIdAddressFromState(state) reportDir := fmt.Sprintf("armstrong_cleanup_reports_%s", time.Now().Format(time.Stamp)) reportDir = strings.ReplaceAll(reportDir, ":", "") @@ -98,48 +99,44 @@ func (c CleanupCommand) Execute() int { _ = terraform.Init() logrus.Infof("running terraform destroy...") destroyErr := terraform.Destroy() + + errorReport := types.ErrorReport{} if destroyErr != nil { logrus.Errorf("failed to destroy resources: %+v", destroyErr) - } else { - logrus.Infof("all resources are cleaned up") - storeCleanupReport(passReport, reportDir, allPassedReportFileName) - } - logs, err := report.ParseLogs(path.Join(wd, "log.txt")) - if err != nil { - logrus.Errorf("failed to parse log.txt: %+v", err) - } + logs, err := trace.RequestTracesFromFile(path.Join(wd, "log.txt")) + if err != nil { + logrus.Errorf("failed to parse log.txt: %+v", err) + } - errorReport := types.ErrorReport{} - if destroyErr != nil { - errorReport := tf.NewCleanupErrorReport(destroyErr, logs) + errorReport = tf.NewCleanupErrorReport(destroyErr, logs) for i := range errorReport.Errors { if address, ok := idAddressMap[errorReport.Errors[i].Id]; ok { errorReport.Errors[i].Label = address } } storeCleanupErrorReport(errorReport, reportDir) - } - resources := make([]types.Resource, 0) - if state, err := terraform.Show(); err == nil && state != nil && state.Values != nil && state.Values.RootModule != nil && state.Values.RootModule.Resources != nil { - for _, passRes := range passReport.Resources { - isDeleted := true - for _, res := range state.Values.RootModule.Resources { - if passRes.Address == res.Address { - isDeleted = false - break + resources := make([]types.Resource, 0) + if state, err := terraform.Show(); err == nil && state != nil && state.Values != nil && state.Values.RootModule != nil && state.Values.RootModule.Resources != nil { + for _, passRes := range passReport.Resources { + isDeleted := true + for _, res := range state.Values.RootModule.Resources { + if passRes.Address == res.Address { + isDeleted = false + break + } + } + if isDeleted { + resources = append(resources, passRes) } - } - if isDeleted { - resources = append(resources, passRes) } } - } - - if len(resources) > 0 { passReport.Resources = resources storeCleanupReport(passReport, reportDir, partialPassedReportFileName) + } else { + logrus.Infof("all resources are cleaned up") + storeCleanupReport(passReport, reportDir, allPassedReportFileName) } logrus.Infof("---------------- Summary ----------------") diff --git a/commands/test.go b/commands/test.go index 5ff6da84..fdb5cf28 100644 --- a/commands/test.go +++ b/commands/test.go @@ -4,7 +4,6 @@ import ( "flag" "fmt" "os" - "os/exec" "path" "path/filepath" "strings" @@ -15,6 +14,9 @@ import ( "github.com/ms-henglu/armstrong/tf" "github.com/ms-henglu/armstrong/types" "github.com/ms-henglu/armstrong/utils" + "github.com/ms-henglu/pal/formatter" + "github.com/ms-henglu/pal/trace" + paltypes "github.com/ms-henglu/pal/types" "github.com/sirupsen/logrus" ) @@ -132,7 +134,7 @@ func (c TestCommand) Execute() int { } logrus.Infof("parsing log.txt...") - logs, err := report.ParseLogs(path.Join(wd, "log.txt")) + logs, err := trace.RequestTracesFromFile(path.Join(wd, "log.txt")) if err != nil { logrus.Errorf("parsing log.txt: %+v", err) } @@ -185,12 +187,8 @@ func (c TestCommand) Execute() int { logrus.Errorf("error creating trace dir %s: %+v", traceDir, err) } } - cmd := exec.Command("pal", "-i", path.Join(wd, "log.txt"), "-m", "oav", "-o", traceDir) - err = cmd.Run() - if err != nil { - logrus.Errorf("error running pal: %+v", err) - } + storeOavTraffic(logs, traceDir) logrus.Infof("copying traces to report directory...") if err := utils.Copy(traceDir, path.Join(reportDir, "traces")); err != nil { logrus.Errorf("error copying traces: %+v", err) @@ -254,3 +252,22 @@ func storeDiffReport(diffReport types.DiffReport, reportDir string) { } } } + +func storeOavTraffic(traces []paltypes.RequestTrace, output string) { + format := formatter.OavTrafficFormatter{} + files, err := os.ReadDir(output) + if err != nil { + logrus.Warnf("failed to read trace output directory: %v", err) + } + index := len(files) + for _, t := range traces { + out := format.Format(t) + index = index + 1 + outputPath := path.Join(output, fmt.Sprintf("trace-%d.json", index)) + if err := os.WriteFile(outputPath, []byte(out), 0644); err != nil { + logrus.Warnf("failed to write file: %v", err) + } else { + logrus.Debugf("trace saved to %s", outputPath) + } + } +} diff --git a/go.mod b/go.mod index 6f83b442..ca4cd861 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/hashicorp/terraform-json v0.13.0 github.com/magodo/azure-rest-api-index v0.0.0-20230522080218-497fe558c02f github.com/mitchellh/cli v1.1.2 + github.com/ms-henglu/pal v0.4.0 github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 github.com/sirupsen/logrus v1.9.3 github.com/zclconf/go-cty v1.9.1 diff --git a/go.sum b/go.sum index 796cfb44..9c0787ee 100644 --- a/go.sum +++ b/go.sum @@ -522,6 +522,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/ms-henglu/pal v0.4.0 h1:6rzFslYszURQxH2sv0XTTtoLAr/kM2nGDR4iaN9T9k8= +github.com/ms-henglu/pal v0.4.0/go.mod h1:PunQwlMaYBFFPv1uhjqXCKm8YPDohtuTRWktvTrMPps= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= diff --git a/report/cleanup_error_report.go b/report/cleanup_error_report.go index 14d1f7e8..4d6fe8eb 100644 --- a/report/cleanup_error_report.go +++ b/report/cleanup_error_report.go @@ -5,12 +5,13 @@ import ( "strings" "github.com/ms-henglu/armstrong/types" + paltypes "github.com/ms-henglu/pal/types" ) //go:embed cleanup_error_report.md var cleanupErrorReportTemplate string -func CleanupErrorMarkdownReport(report types.Error, logs []types.RequestTrace) string { +func CleanupErrorMarkdownReport(report types.Error, logs []paltypes.RequestTrace) string { parts := strings.Split(report.Type, "@") resourceType := "" apiVersion := "" @@ -27,41 +28,20 @@ func CleanupErrorMarkdownReport(report types.Error, logs []types.RequestTrace) s return content } -func CleanupAllRequestTracesContent(id string, logs []types.RequestTrace) string { +func CleanupAllRequestTracesContent(id string, logs []paltypes.RequestTrace) string { content := "" - for i := len(logs) - 1; i >= 0; i-- { - if !strings.EqualFold(id, logs[i].ID) { - continue + index := len(logs) - 1 + for ; index >= 0; index-- { + if IsUrlMatchWithId(logs[index].Url, id) && logs[index].Method == "DELETE" { + content = RequestTraceToString(logs[index]) + break } - log := logs[i] - if log.HttpMethod == "GET" && strings.Contains(log.Content, "REQUEST/RESPONSE") { - st := strings.Index(log.Content, "GET https") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] - } - content = trimContent + "\n\n\n" + content - } else if log.HttpMethod == "DELETE" { - if strings.Contains(log.Content, "REQUEST/RESPONSE") { - st := strings.Index(log.Content, "RESPONSE Status") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] - } - content = trimContent + "\n\n\n" + content - } else if strings.Contains(log.Content, "OUTGOING REQUEST") { - st := strings.Index(log.Content, "DELETE https") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] - } - content = trimContent + "\n\n" + content - } + } + for ; index >= 0; index-- { + if IsUrlMatchWithId(logs[index].Url, id) && logs[index].Method == "GET" { + content = RequestTraceToString(logs[index]) + "\n\n\n" + content + break } } - return content } diff --git a/report/diff_report.go b/report/diff_report.go index 26d695c1..18f0d0a4 100644 --- a/report/diff_report.go +++ b/report/diff_report.go @@ -5,12 +5,13 @@ import ( "strings" "github.com/ms-henglu/armstrong/types" + paltypes "github.com/ms-henglu/pal/types" ) //go:embed diff_report.md var diffReportTemplate string -func DiffMarkdownReport(report types.Diff, logs []types.RequestTrace) string { +func DiffMarkdownReport(report types.Diff, logs []paltypes.RequestTrace) string { parts := strings.Split(report.Type, "@") resourceType := "" apiVersion := "" @@ -46,47 +47,20 @@ func DiffMarkdownReport(report types.Diff, logs []types.RequestTrace) string { return content } -func RequestTracesContent(id string, logs []types.RequestTrace) string { +func RequestTracesContent(id string, logs []paltypes.RequestTrace) string { content := "" index := len(logs) - 1 - if log, i := findLastLog(logs, id, "GET", "REQUEST/RESPONSE", index); i != -1 { - st := strings.Index(log.Content, "GET https") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] + for ; index >= 0; index-- { + if IsUrlMatchWithId(logs[index].Url, id) && logs[index].Method == "GET" { + content = RequestTraceToString(logs[index]) + break } - content = trimContent - index = i } - if log, i := findLastLog(logs, id, "PUT", "REQUEST/RESPONSE", index); i != -1 { - st := strings.Index(log.Content, "RESPONSE Status") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] + for ; index >= 0; index-- { + if IsUrlMatchWithId(logs[index].Url, id) && logs[index].Method == "PUT" { + content = RequestTraceToString(logs[index]) + "\n\n\n" + content + break } - content = trimContent + "\n\n\n" + content - index = i - } - if log, i := findLastLog(logs, id, "PUT", "OUTGOING REQUEST", index); i != -1 { - st := strings.Index(log.Content, "PUT https") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] - } - content = trimContent + "\n\n" + content } return content } - -func findLastLog(logs []types.RequestTrace, id string, method string, substr string, index int) (types.RequestTrace, int) { - for i := index; i >= 0; i-- { - log := logs[i] - if log.ID == id && log.HttpMethod == method && strings.Contains(log.Content, substr) { - return log, i - } - } - return types.RequestTrace{}, -1 -} diff --git a/report/error_report.go b/report/error_report.go index 45bec86d..69360050 100644 --- a/report/error_report.go +++ b/report/error_report.go @@ -2,6 +2,7 @@ package report import ( _ "embed" + paltypes "github.com/ms-henglu/pal/types" "strings" "github.com/ms-henglu/armstrong/types" @@ -10,7 +11,7 @@ import ( //go:embed error_report.md var errorReportTemplate string -func ErrorMarkdownReport(report types.Error, logs []types.RequestTrace) string { +func ErrorMarkdownReport(report types.Error, logs []paltypes.RequestTrace) string { parts := strings.Split(report.Type, "@") resourceType := "" apiVersion := "" @@ -27,39 +28,11 @@ func ErrorMarkdownReport(report types.Error, logs []types.RequestTrace) string { return content } -func AllRequestTracesContent(id string, logs []types.RequestTrace) string { +func AllRequestTracesContent(id string, logs []paltypes.RequestTrace) string { content := "" for i := len(logs) - 1; i >= 0; i-- { - if !strings.EqualFold(id, logs[i].ID) { - continue - } - log := logs[i] - if log.HttpMethod == "GET" && strings.Contains(log.Content, "REQUEST/RESPONSE") { - st := strings.Index(log.Content, "GET https") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] - } - content = trimContent + "\n\n\n" + content - } else if log.HttpMethod == "PUT" { - if strings.Contains(log.Content, "REQUEST/RESPONSE") { - st := strings.Index(log.Content, "RESPONSE Status") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] - } - content = trimContent + "\n\n\n" + content - } else if strings.Contains(log.Content, "OUTGOING REQUEST") { - st := strings.Index(log.Content, "PUT https") - ed := strings.Index(log.Content, ": timestamp=") - trimContent := log.Content - if st < ed { - trimContent = log.Content[st:ed] - } - content = trimContent + "\n\n" + content - } + if IsUrlMatchWithId(logs[i].Url, id) { + content += RequestTraceToString(logs[i]) + "\n\n\n" } } diff --git a/report/log.go b/report/log.go index 731f630d..7457ba67 100644 --- a/report/log.go +++ b/report/log.go @@ -1,54 +1,64 @@ package report import ( - "os" - "regexp" - "strconv" + "encoding/json" + "fmt" "strings" - "github.com/ms-henglu/armstrong/types" + paltypes "github.com/ms-henglu/pal/types" ) -func ParseLogs(filepath string) ([]types.RequestTrace, error) { - data, err := os.ReadFile(filepath) - if err != nil { - return nil, err +func IsUrlMatchWithId(url string, id string) bool { + return strings.HasPrefix(url, id+"?") +} + +func RequestTraceToString(r paltypes.RequestTrace) string { + return fmt.Sprintf(`%s %s +Status Code: %d +------------ Request ------------ +%s +------------ Response ------------ +%s + +`, r.Method, r.Url, r.StatusCode, HttpRequestToString(r.Request), HttpResponseToString(r.Response)) +} + +func HttpRequestToString(r *paltypes.HttpRequest) string { + if r == nil { + return "" + } + headers := "" + for k, v := range r.Headers { + headers += fmt.Sprintf("%s: %s\n", k, v) } - logs := make([]types.RequestTrace, 0) - logPrefixReg, _ := regexp.Compile(`^\d{4}-\d{2}-\d{2}`) - temp := "" - lines := strings.Split(string(data), "\n") - for _, line := range lines { - if logPrefixReg.MatchString(line) { - if (strings.Contains(temp, "OUTGOING REQUEST") || strings.Contains(temp, "REQUEST/RESPONSE")) && strings.Contains(temp, "management.azure.com") { - logs = append(logs, NewRequestTrace(temp)) - } - temp = "" + bodyContent := r.Body + var body interface{} + if err := json.Unmarshal([]byte(bodyContent), &body); err == nil { + if data, err := json.MarshalIndent(body, "", " "); err == nil { + bodyContent = string(data) } - temp += line + "\n" } - - return logs, nil + return fmt.Sprintf(`%s +--- +%s +`, headers, bodyContent) } -func NewRequestTrace(raw string) types.RequestTrace { - trace := types.RequestTrace{} - - methodReg, _ := regexp.Compile(`([A-Z]+)\shttps`) - if matches := methodReg.FindAllStringSubmatch(raw, -1); len(matches) > 0 && len(matches[0]) == 2 { - trace.HttpMethod = matches[0][1] +func HttpResponseToString(r *paltypes.HttpResponse) string { + if r == nil { + return "" } - - statusCodeReg, _ := regexp.Compile(`RESPONSE\sStatus:\s(\d+)`) - if matches := statusCodeReg.FindAllStringSubmatch(raw, -1); len(matches) > 0 && len(matches[0]) == 2 { - trace.StatusCode, _ = strconv.ParseInt(matches[0][1], 10, 32) + headers := "" + for k, v := range r.Headers { + headers += fmt.Sprintf("%s: %s\n", k, v) } - - idReg, _ := regexp.Compile(`management\.azure\.com(.+)\?api-version`) - if matches := idReg.FindAllStringSubmatch(raw, -1); len(matches) > 0 && len(matches[0]) == 2 { - trace.ID = matches[0][1] + bodyContent := r.Body + var body interface{} + if err := json.Unmarshal([]byte(bodyContent), &body); err == nil { + if data, err := json.MarshalIndent(body, "", " "); err == nil { + bodyContent = string(data) + } } - - trace.Content = raw - return trace + return fmt.Sprintf(`%s------ +%s`, headers, bodyContent) } diff --git a/tf/utils.go b/tf/utils.go index 7bc4dcff..7413ee16 100644 --- a/tf/utils.go +++ b/tf/utils.go @@ -10,6 +10,7 @@ import ( "github.com/ms-henglu/armstrong/coverage" "github.com/ms-henglu/armstrong/types" "github.com/ms-henglu/armstrong/utils" + paltypes "github.com/ms-henglu/pal/types" "github.com/sirupsen/logrus" ) @@ -54,7 +55,7 @@ func GetChanges(plan *tfjson.Plan) []Action { return actions } -func NewDiffReport(plan *tfjson.Plan, logs []types.RequestTrace) types.DiffReport { +func NewDiffReport(plan *tfjson.Plan, logs []paltypes.RequestTrace) types.DiffReport { out := types.DiffReport{ Diffs: make([]types.Diff, 0), Logs: logs, @@ -287,7 +288,7 @@ func expandIdentity(input []interface{}) map[string]interface{} { return config } -func NewErrorReport(applyErr error, logs []types.RequestTrace) types.ErrorReport { +func NewErrorReport(applyErr error, logs []paltypes.RequestTrace) types.ErrorReport { out := types.ErrorReport{ Errors: make([]types.Error, 0), Logs: logs, @@ -322,7 +323,7 @@ func NewErrorReport(applyErr error, logs []types.RequestTrace) types.ErrorReport return out } -func NewCleanupErrorReport(applyErr error, logs []types.RequestTrace) types.ErrorReport { +func NewCleanupErrorReport(applyErr error, logs []paltypes.RequestTrace) types.ErrorReport { out := types.ErrorReport{ Errors: make([]types.Error, 0), Logs: logs, @@ -350,7 +351,7 @@ func NewCleanupErrorReport(applyErr error, logs []types.RequestTrace) types.Erro return out } -func NewIdAdressFromState(state *tfjson.State) map[string]string { +func NewIdAddressFromState(state *tfjson.State) map[string]string { out := map[string]string{} if state == nil || state.Values == nil || state.Values.RootModule == nil || state.Values.RootModule.Resources == nil { logrus.Warnf("new id address mapping from state: state is nil") diff --git a/types/log.go b/types/log.go deleted file mode 100644 index 3e244680..00000000 --- a/types/log.go +++ /dev/null @@ -1,8 +0,0 @@ -package types - -type RequestTrace struct { - HttpMethod string - StatusCode int64 - ID string - Content string -} diff --git a/types/report.go b/types/report.go index 65e34a4d..f2adf06e 100644 --- a/types/report.go +++ b/types/report.go @@ -1,5 +1,7 @@ package types +import paltypes "github.com/ms-henglu/pal/types" + type PassReport struct { Resources []Resource } @@ -11,7 +13,7 @@ type Resource struct { type DiffReport struct { Diffs []Diff - Logs []RequestTrace + Logs []paltypes.RequestTrace } type Diff struct { @@ -28,7 +30,7 @@ type Change struct { type ErrorReport struct { Errors []Error - Logs []RequestTrace + Logs []paltypes.RequestTrace } type Error struct { diff --git a/vendor/github.com/ms-henglu/pal/formatter/base.go b/vendor/github.com/ms-henglu/pal/formatter/base.go new file mode 100644 index 00000000..5d822db8 --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/formatter/base.go @@ -0,0 +1,9 @@ +package formatter + +import ( + "github.com/ms-henglu/pal/types" +) + +type Formatter interface { + Format(r types.RequestTrace) string +} diff --git a/vendor/github.com/ms-henglu/pal/formatter/markdown.go b/vendor/github.com/ms-henglu/pal/formatter/markdown.go new file mode 100644 index 00000000..f1f85e08 --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/formatter/markdown.go @@ -0,0 +1,94 @@ +package formatter + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/ms-henglu/pal/types" + "github.com/ms-henglu/pal/utils" +) + +var _ Formatter = MarkdownFormatter{} + +type MarkdownFormatter struct { +} + +func (m MarkdownFormatter) Format(r types.RequestTrace) string { + content := markdownTemplate + content = strings.ReplaceAll(content, "{Time}", r.TimeStamp.Format("15:04:05")) + content = strings.ReplaceAll(content, "{Method}", r.Method) + content = strings.ReplaceAll(content, "{Host}", r.Host) + urlStr := r.Url + parsedUrl, err := url.Parse(r.Url) + if err == nil { + urlStr = parsedUrl.Path + if value := parsedUrl.Query()["api-version"]; len(value) > 0 { + urlStr += "?api-version=" + value[0] + } + } + content = strings.ReplaceAll(content, "{Url}", urlStr) + content = strings.ReplaceAll(content, "{StatusCode}", fmt.Sprintf("%d", r.StatusCode)) + content = strings.ReplaceAll(content, "{StatusMessage}", http.StatusText(r.StatusCode)) + content = strings.ReplaceAll(content, "{RequestHeaders}", m.formatHeaders(r.Request.Headers)) + content = strings.ReplaceAll(content, "{RequestBody}", utils.JsonPretty(r.Request.Body)) + if r.Response == nil { + content = strings.ReplaceAll(content, "{ResponseHeaders}", "") + content = strings.ReplaceAll(content, "{ResponseBody}", "") + return content + } + content = strings.ReplaceAll(content, "{ResponseHeaders}", m.formatHeaders(r.Response.Headers)) + content = strings.ReplaceAll(content, "{ResponseBody}", utils.JsonPretty(r.Response.Body)) + return content +} + +func (m MarkdownFormatter) formatHeaders(headers map[string]string) string { + var content string + for k, v := range headers { + content += fmt.Sprintf("| %s | %s |\n", k, v) + } + return content +} + +const markdownTemplate = ` +##### +
+ + {Time} {Method} {Host} {Url} {StatusCode} + +
+
+ Request + +| Header | Value | +| ----- | ----- | +{RequestHeaders} + +Request Body: +` + "```" + `json +{RequestBody} +` + "```" + ` + +
+
+ Response + + **Response Status: {StatusCode} {StatusMessage}** + +| Header | Value | +| ----- | ----- | +{ResponseHeaders} + +Response Body: +` + "```" + `json +{ResponseBody} +` + "```" + ` + +
+
+
+ +----- + +` diff --git a/vendor/github.com/ms-henglu/pal/formatter/oav_traffic.go b/vendor/github.com/ms-henglu/pal/formatter/oav_traffic.go new file mode 100644 index 00000000..73609a32 --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/formatter/oav_traffic.go @@ -0,0 +1,73 @@ +package formatter + +import ( + "encoding/json" + "fmt" + + "github.com/ms-henglu/pal/types" +) + +var _ Formatter = OavTrafficFormatter{} + +type OavTrafficFormatter struct { +} + +type OavTraffic struct { + LiveRequest LiveRequest `json:"liveRequest"` + LiveResponse LiveResponse `json:"liveResponse"` +} + +type LiveRequest struct { + Headers map[string]string `json:"headers"` + Method string `json:"method"` + Url string `json:"url"` + Body interface{} `json:"body"` +} + +type LiveResponse struct { + StatusCode string `json:"statusCode"` + Headers map[string]string `json:"headers"` + Body interface{} `json:"body"` +} + +func (o OavTrafficFormatter) Format(r types.RequestTrace) string { + var requestBody interface{} + requestHeaders := make(map[string]string) + if r.Request != nil { + err := json.Unmarshal([]byte(r.Request.Body), &requestBody) + if err != nil { + requestBody = nil + } + requestHeaders = r.Request.Headers + } + + var responseBody interface{} + responseHeaders := make(map[string]string) + if r.Response != nil { + err := json.Unmarshal([]byte(r.Response.Body), &responseBody) + if err != nil { + responseBody = nil + } + responseHeaders = r.Response.Headers + } + + out := OavTraffic{ + LiveRequest: LiveRequest{ + Headers: requestHeaders, + Method: r.Method, + Url: r.Url, + Body: requestBody, + }, + LiveResponse: LiveResponse{ + StatusCode: fmt.Sprintf("%d", r.StatusCode), + Headers: responseHeaders, + Body: responseBody, + }, + } + + content, err := json.MarshalIndent(out, "", " ") + if err != nil { + return "" + } + return string(content) +} diff --git a/vendor/github.com/ms-henglu/pal/provider/azapi.go b/vendor/github.com/ms-henglu/pal/provider/azapi.go new file mode 100644 index 00000000..c474b180 --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/provider/azapi.go @@ -0,0 +1,98 @@ +package provider + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/ms-henglu/pal/rawlog" + "github.com/ms-henglu/pal/types" +) + +var _ Provider = AzAPIProvider{} + +var r = regexp.MustCompile(`Live traffic: (.+): timestamp`) + +type AzAPIProvider struct { +} + +func (a AzAPIProvider) IsTrafficTrace(l rawlog.RawLog) bool { + return l.Level == "DEBUG" && strings.Contains(l.Message, "Live traffic:") +} + +func (a AzAPIProvider) ParseTraffic(l rawlog.RawLog) (*types.RequestTrace, error) { + matches := r.FindAllStringSubmatch(l.Message, -1) + if len(matches) == 0 || len(matches[0]) != 2 { + return nil, fmt.Errorf("failed to parse request trace, no matches found") + } + trafficJson := matches[0][1] + var liveTraffic traffic + err := json.Unmarshal([]byte(trafficJson), &liveTraffic) + if err != nil { + return nil, fmt.Errorf("failed to parse request trace, %v", err) + } + parsedUrl, err := url.Parse(liveTraffic.LiveRequest.Url) + if err != nil { + return nil, fmt.Errorf("failed to parse request trace, %v", err) + } + + if liveTraffic.LiveRequest.Headers == nil { + liveTraffic.LiveRequest.Headers = map[string]string{} + } + if liveTraffic.LiveResponse.Headers == nil { + liveTraffic.LiveResponse.Headers = map[string]string{} + } + + return &types.RequestTrace{ + TimeStamp: l.TimeStamp, + Method: liveTraffic.LiveRequest.Method, + Host: parsedUrl.Host, + Url: parsedUrl.Path + "?" + parsedUrl.RawQuery, + StatusCode: liveTraffic.LiveResponse.StatusCode, + Provider: "azapi", + Request: &types.HttpRequest{ + Headers: liveTraffic.LiveRequest.Headers, + Body: liveTraffic.LiveRequest.Body, + }, + Response: &types.HttpResponse{ + Headers: liveTraffic.LiveResponse.Headers, + Body: liveTraffic.LiveResponse.Body, + }, + }, nil +} + +func (a AzAPIProvider) IsRequestTrace(l rawlog.RawLog) bool { + return false +} + +func (a AzAPIProvider) IsResponseTrace(l rawlog.RawLog) bool { + return false +} + +func (a AzAPIProvider) ParseRequest(l rawlog.RawLog) (*types.RequestTrace, error) { + return nil, fmt.Errorf("not implemented") +} + +func (a AzAPIProvider) ParseResponse(l rawlog.RawLog) (*types.RequestTrace, error) { + return nil, fmt.Errorf("not implemented") +} + +type traffic struct { + LiveRequest liveRequest `json:"request"` + LiveResponse liveResponse `json:"response"` +} + +type liveRequest struct { + Headers map[string]string `json:"headers"` + Method string `json:"method"` + Url string `json:"url"` + Body string `json:"body"` +} + +type liveResponse struct { + StatusCode int `json:"statusCode"` + Headers map[string]string `json:"headers"` + Body string `json:"body"` +} diff --git a/vendor/github.com/ms-henglu/pal/provider/azuread.go b/vendor/github.com/ms-henglu/pal/provider/azuread.go new file mode 100644 index 00000000..7384ed90 --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/provider/azuread.go @@ -0,0 +1,121 @@ +package provider + +import ( + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/ms-henglu/pal/rawlog" + "github.com/ms-henglu/pal/types" + "github.com/ms-henglu/pal/utils" +) + +var _ Provider = AzureADProvider{} +var statusCodeRegex = regexp.MustCompile(`HTTP/\d.\d\s(\d{3})\s.+`) + +type AzureADProvider struct { +} + +func (a AzureADProvider) IsTrafficTrace(l rawlog.RawLog) bool { + return false +} + +func (a AzureADProvider) ParseTraffic(l rawlog.RawLog) (*types.RequestTrace, error) { + return nil, fmt.Errorf("not implemented") +} + +func (a AzureADProvider) IsRequestTrace(l rawlog.RawLog) bool { + return l.Level == "INFO" && strings.Contains(l.Message, "============================ Begin AzureAD Request") +} + +func (a AzureADProvider) IsResponseTrace(l rawlog.RawLog) bool { + return l.Level == "INFO" && strings.Contains(l.Message, "============================ Begin AzureAD Response") +} + +func (a AzureADProvider) ParseRequest(l rawlog.RawLog) (*types.RequestTrace, error) { + urlLine := "" + headers := make(map[string]string) + method := "" + url := "" + for _, line := range strings.Split(l.Message, "\n") { + switch { + case line == "" || strings.Contains(line, "======"): + continue + case strings.Contains(line, ": "): + key, value, err := utils.ParseHeader(line) + if err != nil { + return nil, err + } + headers[key] = value + default: + urlLine = line + if parts := strings.Split(urlLine, " "); len(parts) == 3 { + method = parts[0] + url = parts[1] + } + } + } + + return &types.RequestTrace{ + TimeStamp: l.TimeStamp, + Url: utils.NormalizeUrlPath(url), + Method: method, + Host: headers["Host"], + Provider: "azuread", + Request: &types.HttpRequest{ + Headers: headers, + }, + }, nil +} + +func (a AzureADProvider) ParseResponse(l rawlog.RawLog) (*types.RequestTrace, error) { + headers := make(map[string]string) + statusCode := 0 + method := "" + rawUrl := "" + host := "" + body := "" + for _, line := range strings.Split(l.Message, "\n") { + switch { + case line == "" || strings.Contains(line, "======"): + continue + case statusCodeRegex.FindAllStringSubmatch(line, -1) != nil: + matches := statusCodeRegex.FindAllStringSubmatch(line, -1) + if len(matches) > 0 && len(matches[0]) == 2 { + fmt.Sscanf(matches[0][1], "%d", &statusCode) + } + case utils.IsJson(line): + body = line + case strings.Contains(line, ": "): + key, value, err := utils.ParseHeader(line) + if err != nil { + return nil, err + } + headers[key] = value + default: + parts := strings.Split(line, " ") + if len(parts) == 2 { + method = parts[0] + parsedUrl, err := url.Parse(parts[1]) + if err == nil { + host = parsedUrl.Host + rawUrl = fmt.Sprintf("%s?%s", parsedUrl.Path, parsedUrl.RawQuery) + } + } + } + } + + return &types.RequestTrace{ + TimeStamp: l.TimeStamp, + Url: utils.NormalizeUrlPath(rawUrl), + Method: method, + Host: host, + StatusCode: statusCode, + Provider: "azuread", + Response: &types.HttpResponse{ + Headers: headers, + Body: body, + }, + }, nil +} diff --git a/vendor/github.com/ms-henglu/pal/provider/azurerm.go b/vendor/github.com/ms-henglu/pal/provider/azurerm.go new file mode 100644 index 00000000..4cb74979 --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/provider/azurerm.go @@ -0,0 +1,166 @@ +package provider + +import ( + "fmt" + "net/url" + "strings" + + "github.com/ms-henglu/pal/rawlog" + "github.com/ms-henglu/pal/types" + "github.com/ms-henglu/pal/utils" +) + +var _ Provider = AzureRMProvider{} + +type AzureRMProvider struct { +} + +func (a AzureRMProvider) IsTrafficTrace(l rawlog.RawLog) bool { + return false +} + +func (a AzureRMProvider) ParseTraffic(l rawlog.RawLog) (*types.RequestTrace, error) { + return nil, fmt.Errorf("not implemented") +} + +func (a AzureRMProvider) IsRequestTrace(l rawlog.RawLog) bool { + return l.Level == "DEBUG" && strings.Contains(l.Message, "AzureRM Request:") +} + +func (a AzureRMProvider) IsResponseTrace(l rawlog.RawLog) bool { + return l.Level == "DEBUG" && strings.Contains(l.Message, "AzureRM Response for") +} + +func (a AzureRMProvider) ParseRequest(l rawlog.RawLog) (*types.RequestTrace, error) { + urlPath := "" + method := "" + headers := make(map[string]string) + body := "" + + lines := strings.Split(l.Message, "\n") + i := 0 + foundBodySegment := false + for ; i < len(lines); i++ { + line := lines[i] + switch { + case strings.TrimSpace(line) == "": + foundBodySegment = true + case strings.Contains(line, ": "): + key, value, err := utils.ParseHeader(line) + if strings.HasPrefix(key, "provider.terraform-provider-azurerm") { + continue + } + if key == "AzureRM Request" { + continue + } + if err != nil { + return nil, err + } + headers[key] = value + default: + if parts := strings.Split(line, " "); len(parts) == 3 { + method = parts[0] + urlPath = parts[1] + } + } + if foundBodySegment { + break + } + } + + if i+1 < len(lines) { + line := strings.Join(lines[i+1:], "\n") + if strings.Contains(line, ": timestamp") { + index := strings.LastIndex(line, ": timestamp") + if utils.IsJson(line[0:index]) { + body = line[0:index] + } else { + lineTrimTimestamp := line[0:index] + key, value, err := utils.ParseHeader(lineTrimTimestamp) + if err == nil { + headers[key] = value + } + } + } else { + body = line + } + } + return &types.RequestTrace{ + TimeStamp: l.TimeStamp, + Url: utils.NormalizeUrlPath(urlPath), + Method: method, + Host: headers["Host"], + Provider: "azurerm", + Request: &types.HttpRequest{ + Headers: headers, + Body: body, + }, + }, nil +} + +func (a AzureRMProvider) ParseResponse(l rawlog.RawLog) (*types.RequestTrace, error) { + urlPath := "" + host := "" + method := "" // TODO: this is not available in the response + body := "" + headers := make(map[string]string) + statusCode := 0 + + lines := strings.Split(l.Message, "\n") + i := 0 + foundBodySegment := false + for ; i < len(lines); i++ { + line := lines[i] + switch { + case strings.TrimSpace(line) == "": + foundBodySegment = true + case strings.Contains(line, "AzureRM Response for "): + urlLine := line[strings.Index(line, "AzureRM Response for ")+len("AzureRM Response for "):] + urlLine = strings.Trim(urlLine, " \n\r:") + parsedUrl, err := url.Parse(urlLine) + if err != nil { + return nil, err + } + host = parsedUrl.Host + urlPath = fmt.Sprintf("%s?%s", parsedUrl.Path, parsedUrl.RawQuery) + case strings.Contains(line, ": "): + key, value, err := utils.ParseHeader(line) + if err != nil { + return nil, err + } + headers[key] = value + default: + if matches := statusCodeRegex.FindAllStringSubmatch(line, -1); len(matches) > 0 && len(matches[0]) == 2 { + fmt.Sscanf(matches[0][1], "%d", &statusCode) + } + } + if foundBodySegment { + break + } + } + + if i+1 < len(lines) { + line := strings.Join(lines[i+1:], "\n") + if strings.Contains(line, ": timestamp") { + index := strings.LastIndex(line, ": timestamp") + if utils.IsJson(line[0:index]) { + body = line[0:index] + } + } else { + body = line + } + } + + return &types.RequestTrace{ + TimeStamp: l.TimeStamp, + Url: utils.NormalizeUrlPath(urlPath), + Host: host, + Method: method, + StatusCode: statusCode, + Provider: "azurerm", + Response: &types.HttpResponse{ + Headers: headers, + Body: body, + }, + }, nil +} diff --git a/vendor/github.com/ms-henglu/pal/provider/base.go b/vendor/github.com/ms-henglu/pal/provider/base.go new file mode 100644 index 00000000..34e82457 --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/provider/base.go @@ -0,0 +1,15 @@ +package provider + +import ( + "github.com/ms-henglu/pal/rawlog" + "github.com/ms-henglu/pal/types" +) + +type Provider interface { + IsTrafficTrace(l rawlog.RawLog) bool + IsRequestTrace(l rawlog.RawLog) bool + IsResponseTrace(l rawlog.RawLog) bool + ParseTraffic(l rawlog.RawLog) (*types.RequestTrace, error) + ParseRequest(l rawlog.RawLog) (*types.RequestTrace, error) + ParseResponse(l rawlog.RawLog) (*types.RequestTrace, error) +} diff --git a/vendor/github.com/ms-henglu/pal/rawlog/raw_log.go b/vendor/github.com/ms-henglu/pal/rawlog/raw_log.go new file mode 100644 index 00000000..72beb99d --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/rawlog/raw_log.go @@ -0,0 +1,41 @@ +package rawlog + +import ( + "fmt" + "regexp" + "strings" + "time" +) + +type RawLog struct { + TimeStamp time.Time + Level string + Message string +} + +var regLayoutMap = map[*regexp.Regexp]string{ + regexp.MustCompile(`([\d+.:T\-]{28})\s\[([A-Z]+)]`): "2006-01-02T15:04:05.999-0700", + regexp.MustCompile(`([\d+.:T\- ]{19})\s\[([A-Z]+)]`): "2006-01-02 15:04:05", + regexp.MustCompile(`([\d+.:T/ ]{19})\s\[([A-Z]+)]`): "2006/01/02 15:04:05", +} + +func NewRawLog(message string) (*RawLog, error) { + for reg, layout := range regLayoutMap { + matches := reg.FindAllStringSubmatch(message, -1) + if len(matches) == 0 || len(matches[0]) != 3 { + continue + } + t, err := time.Parse(layout, matches[0][1]) + if err != nil { + continue + } + m := message[len(matches[0][0]):] + m = strings.Trim(m, " \n") + return &RawLog{ + TimeStamp: t, + Level: matches[0][2], + Message: m, + }, nil + } + return nil, fmt.Errorf("failed to parse log message: %s", message) +} diff --git a/vendor/github.com/ms-henglu/pal/trace/trace.go b/vendor/github.com/ms-henglu/pal/trace/trace.go new file mode 100644 index 00000000..d2cc339e --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/trace/trace.go @@ -0,0 +1,165 @@ +package trace + +import ( + "fmt" + "log" + "os" + "regexp" + "strconv" + + "github.com/ms-henglu/pal/provider" + "github.com/ms-henglu/pal/rawlog" + "github.com/ms-henglu/pal/types" + "github.com/ms-henglu/pal/utils" +) + +var providers = []provider.Provider{ + provider.AzureADProvider{}, + provider.AzureRMProvider{}, + provider.AzAPIProvider{}, +} + +var providerUrlRegex = regexp.MustCompile(`/subscriptions/[a-zA-Z\d\-]+/providers`) + +func RequestTracesFromFile(input string) ([]types.RequestTrace, error) { + data, err := os.ReadFile(input) + if err != nil { + return nil, fmt.Errorf("failed to read input file: %v", err) + } + logRegex := regexp.MustCompile(`([\d+.:T\-/ ]{19,28})\s\[([A-Z]+)]`) + lines := utils.SplitBefore(string(data), logRegex) + log.Printf("[INFO] total lines: %d", len(lines)) + + traces := make([]types.RequestTrace, 0) + for _, line := range lines { + l, err := rawlog.NewRawLog(line) + if err != nil { + log.Printf("[WARN] failed to parse log: %v", err) + } + if l == nil { + continue + } + t, err := NewRequestTrace(*l) + if err == nil { + traces = append(traces, *t) + } + } + requestCount, responseCount := 0, 0 + for _, t := range traces { + if t.Request != nil { + requestCount++ + } + if t.Response != nil { + responseCount++ + } + } + log.Printf("[INFO] total traces: %d", len(traces)) + log.Printf("[INFO] request count: %d", requestCount) + log.Printf("[INFO] response count: %d", responseCount) + + mergedTraces := make([]types.RequestTrace, 0) + for i := 0; i < len(traces); i++ { + // skip GET /subscriptions/******/providers + if traces[i].Method == "GET" && providerUrlRegex.MatchString(traces[i].Url) { + continue + } + + if traces[i].Request != nil && traces[i].Response != nil { + mergedTraces = append(mergedTraces, traces[i]) + continue + } + + if traces[i].Request != nil { + found := false + for j := i + 1; j < len(traces); j++ { + if traces[j].Response == nil || traces[i].Url != traces[j].Url || traces[i].Host != traces[j].Host { + continue + } + found = true + mergedTraces = append(mergedTraces, types.RequestTrace{ + TimeStamp: traces[i].TimeStamp, + Url: traces[i].Url, + Method: traces[i].Method, + Host: traces[i].Host, + StatusCode: traces[j].StatusCode, + Request: traces[i].Request, + Response: traces[j].Response, + }) + break + } + if !found { + log.Printf("[WARN] failed to find response for request: url %s, method %s", traces[i].Url, traces[i].Method) + mergedTraces = append(mergedTraces, traces[i]) + } + } + } + log.Printf("[INFO] merged traces: %d", len(mergedTraces)) + return mergedTraces, nil +} + +func VerifyRequestTrace(t types.RequestTrace) []string { + out := make([]string, 0) + if len(t.Url) == 0 { + out = append(out, "[ERROR] url is empty") + } + if len(t.Host) == 0 { + out = append(out, "[ERROR] host is empty") + } + if len(t.Method) == 0 { + out = append(out, "[ERROR] method is empty") + } + if t.StatusCode == 0 { + out = append(out, "[ERROR] status code is empty") + } + if t.TimeStamp.IsZero() { + out = append(out, "[ERROR] timestamp is empty") + } + switch { + case t.Request == nil: + out = append(out, "[ERROR] request is nil") + case t.Request.Headers == nil: + out = append(out, "[ERROR] request headers is nil") + default: + if contentLength, ok := t.Request.Headers["Content-Length"]; ok { + length, err := strconv.ParseInt(contentLength, 10, 64) + if err != nil { + out = append(out, fmt.Sprintf("[ERROR] failed to parse content length: %v", err)) + } + if length != 0 && len(t.Request.Body) == 0 { + out = append(out, "[ERROR] request body is empty") + } + } + } + switch { + case t.Response == nil: + out = append(out, "[ERROR] response is nil") + case t.Response.Headers == nil: + out = append(out, "[ERROR] response headers is nil") + default: + if contentLength, ok := t.Response.Headers["Content-Length"]; ok { + length, err := strconv.ParseInt(contentLength, 10, 64) + if err != nil { + out = append(out, fmt.Sprintf("[ERROR] failed to parse content length: %v", err)) + } + if length != 0 && len(t.Response.Body) == 0 { + out = append(out, "[ERROR] response body is empty") + } + } + } + return out +} + +func NewRequestTrace(l rawlog.RawLog) (*types.RequestTrace, error) { + for _, p := range providers { + if p.IsTrafficTrace(l) { + return p.ParseTraffic(l) + } + if p.IsRequestTrace(l) { + return p.ParseRequest(l) + } + if p.IsResponseTrace(l) { + return p.ParseResponse(l) + } + } + return nil, fmt.Errorf("TODO: implement other providers") +} diff --git a/vendor/github.com/ms-henglu/pal/types/types.go b/vendor/github.com/ms-henglu/pal/types/types.go new file mode 100644 index 00000000..f4e29b7b --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/types/types.go @@ -0,0 +1,24 @@ +package types + +import "time" + +type RequestTrace struct { + Url string + Method string + Host string + StatusCode int + Provider string + TimeStamp time.Time + Request *HttpRequest + Response *HttpResponse +} + +type HttpRequest struct { + Headers map[string]string + Body string +} + +type HttpResponse struct { + Headers map[string]string + Body string +} diff --git a/vendor/github.com/ms-henglu/pal/utils/utils.go b/vendor/github.com/ms-henglu/pal/utils/utils.go new file mode 100644 index 00000000..6fcc79fc --- /dev/null +++ b/vendor/github.com/ms-henglu/pal/utils/utils.go @@ -0,0 +1,69 @@ +package utils + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" +) + +func IsJson(input string) bool { + var out interface{} + err := json.Unmarshal([]byte(input), &out) + return err == nil +} + +func JsonPretty(input string) string { + var out interface{} + err := json.Unmarshal([]byte(input), &out) + if err != nil { + return input + } + b, err := json.MarshalIndent(out, "", " ") + if err != nil { + return input + } + return string(b) +} + +func SplitBefore(s string, re *regexp.Regexp) []string { + out := make([]string, 0) + is := re.FindAllStringIndex(s, -1) + if len(is) == 0 { + return append(out, s) + } + for i := 0; i < len(is)-1; i++ { + out = append(out, s[is[i][0]:is[i+1][0]]) + } + return append(out, s[is[len(is)-1][0]:]) +} + +func ParseHeader(input string) (string, string, error) { + deliminatorIndex := strings.Index(input, ":") + if deliminatorIndex == -1 { + return "", "", fmt.Errorf("failed to parse header, `:` is not found: %s", input) + } + key := strings.Trim(input[0:deliminatorIndex], " ") + value := input[deliminatorIndex+1:] + if index := strings.LastIndex(value, ": timestamp"); index != -1 { + value = value[0:index] + } + value = strings.Trim(value, " \n\r") + return key, value, nil +} + +func LineAt(input string, index int) string { + lines := strings.Split(input, "\n") + if len(lines) > index { + return lines[index] + } + return input +} + +func NormalizeUrlPath(input string) string { + if !strings.HasPrefix(input, "/") { + input = "/" + input + } + input = strings.ReplaceAll(input, "//", "/") + return input +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 07ea4be6..b4118cb2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -419,6 +419,14 @@ github.com/mitchellh/mapstructure # github.com/mitchellh/reflectwalk v1.0.2 ## explicit github.com/mitchellh/reflectwalk +# github.com/ms-henglu/pal v0.4.0 +## explicit; go 1.19 +github.com/ms-henglu/pal/formatter +github.com/ms-henglu/pal/provider +github.com/ms-henglu/pal/rawlog +github.com/ms-henglu/pal/trace +github.com/ms-henglu/pal/types +github.com/ms-henglu/pal/utils # github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 ## explicit; go 1.16 github.com/nsf/jsondiff