From 9a33c6eaca0d314ac189a9dba3167bfc2a5ae145 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Sat, 8 Feb 2025 23:03:20 +0900 Subject: [PATCH 01/23] Adding document generator for tasks --- cmd/document-generator/main.go | 1 + docs/template/inspection-types.template.md | 0 pkg/document/generator/generator.go | 145 +++++++++++++++ pkg/document/generator/generator_test.go | 178 ++++++++++++++++++ pkg/document/splitter/splitter.go | 108 +++++++++++ pkg/document/splitter/spltter_test.go | 204 +++++++++++++++++++++ 6 files changed, 636 insertions(+) create mode 100644 cmd/document-generator/main.go create mode 100644 docs/template/inspection-types.template.md create mode 100644 pkg/document/generator/generator.go create mode 100644 pkg/document/generator/generator_test.go create mode 100644 pkg/document/splitter/splitter.go create mode 100644 pkg/document/splitter/spltter_test.go diff --git a/cmd/document-generator/main.go b/cmd/document-generator/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/cmd/document-generator/main.go @@ -0,0 +1 @@ +package main diff --git a/docs/template/inspection-types.template.md b/docs/template/inspection-types.template.md new file mode 100644 index 0000000..e69de29 diff --git a/pkg/document/generator/generator.go b/pkg/document/generator/generator.go new file mode 100644 index 0000000..acc77e2 --- /dev/null +++ b/pkg/document/generator/generator.go @@ -0,0 +1,145 @@ +package generator + +import ( + "bytes" + "errors" + "fmt" + "os" + "text/template" + + "github.com/GoogleCloudPlatform/khi/pkg/document/splitter" +) + +// DocumentGenerator generates a document from a template. +// If there is already text added other than the parts automatically generated by the template, the text added after the immediately preceding automatically generated part will be kept after the corresponding generated text section. +type DocumentGenerator struct { + template *template.Template +} + +func NewDocumentGeneratorFromTemplateFileGlob(templateFileGlob string) (*DocumentGenerator, error) { + template, err := template.ParseGlob(templateFileGlob) + if err != nil { + return nil, err + } + return &DocumentGenerator{ + template: template, + }, nil +} + +func newDocumentGeneratorFromStringTemplate(templateStr string) (*DocumentGenerator, error) { + return &DocumentGenerator{ + template: template.Must(template.New("").Parse(templateStr)), + }, nil +} + +// GenerateDocument creates or update document at the specified path with the specified template and parameter. +// When the ignoreNonMatchingGeneratedSection is false, this function returns error when it can't find the associated generated section preceding added text not to lose the edit. +func (g *DocumentGenerator) GenerateDocument(destination string, templateName string, parameters any, ignoreNonMatchingGeneratedSection bool) error { + exists, err := checkFileExists(destination) + if err != nil { + return err + } + currentDestinationContent := "" + if exists { + currentDestinationContent, err = readFromFile(destination) + if err != nil { + return err + } + } + + outputString, err := g.generateDocumentString(currentDestinationContent, templateName, parameters, ignoreNonMatchingGeneratedSection) + if err != nil { + return err + } + + return writeToFile(destination, outputString) +} + +func (g *DocumentGenerator) generateDocumentString(destinationString string, templateName string, parameters any, ignoreNonMatchingGeneratedSection bool) (string, error) { + outputBuffer := new(bytes.Buffer) + err := g.template.ExecuteTemplate(outputBuffer, templateName, parameters) + if err != nil { + return "", err + } + outputString := outputBuffer.String() + autoGeneratedSections, err := splitter.SplitToDocumentSections(outputString) + if err != nil { + return "", err + } + + prevGeneratedSections, err := splitter.SplitToDocumentSections(destinationString) + if err != nil { + return "", err + } + + outputString, err = concatAmendedContents(autoGeneratedSections, prevGeneratedSections, ignoreNonMatchingGeneratedSection) + if err != nil { + return "", err + } + return outputString, nil +} + +func concatAmendedContents(generated []*splitter.DocumentSection, prev []*splitter.DocumentSection, ignoreNonMatchingGeneratedSection bool) (string, error) { + var resultSections []*splitter.DocumentSection + prevToNextMap := make(map[string]*splitter.DocumentSection) + usedPrevIds := make(map[string]interface{}) + for index, section := range prev { + if section.Type == splitter.SectionTypeAmend { + if index == 0 { + resultSections = append(resultSections, section) + } else { + prevToNextMap[prev[index-1].Id] = section + usedPrevIds[prev[index-1].Id] = struct{}{} + } + } + } + + for _, section := range generated { + if section.Type != splitter.SectionTypeGenerated { + continue + } + resultSections = append(resultSections, section) + if next, ok := prevToNextMap[section.Id]; ok { + resultSections = append(resultSections, next) + delete(usedPrevIds, section.Id) + } + } + + if len(usedPrevIds) > 0 && !ignoreNonMatchingGeneratedSection { + var nonUsedIds []string + for k := range usedPrevIds { + nonUsedIds = append(nonUsedIds, k) + } + return "", fmt.Errorf("previous amended sections belongs to other generated sections is not used. Unused ids %v", nonUsedIds) + } + + result := "" + for _, section := range resultSections { + result += section.Body + "\n" + } + return result, nil +} + +func checkFileExists(path string) (bool, error) { + _, err := os.Stat(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return false, nil + } else { + return false, err + } + } + return true, nil +} + +func writeToFile(path string, content string) error { + return os.WriteFile(path, []byte(content), 0644) +} + +func readFromFile(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/pkg/document/generator/generator_test.go b/pkg/document/generator/generator_test.go new file mode 100644 index 0000000..d972b50 --- /dev/null +++ b/pkg/document/generator/generator_test.go @@ -0,0 +1,178 @@ +package generator + +import ( + "testing" + + "github.com/GoogleCloudPlatform/khi/pkg/document/splitter" + "github.com/google/go-cmp/cmp" +) + +func TestGenerateDocumentString(t *testing.T) { + testCases := []struct { + name string + destinationString string + templateString string + templateName string + ignoreNonMatchingSection bool + want string + wantErr error + }{ + { + name: "with empty destination string", + destinationString: ``, + templateString: `{{define "testTemplate"}} + +Generated content 1 + +{{end}}`, + templateName: "testTemplate", + ignoreNonMatchingSection: false, + want: ` +Generated content 1 + +`, + }, + { + name: "with a non-empty destination string", + destinationString: ` + +Generated content 1 + +This is additional string amended after content generation. +This is another line of amended line.`, + templateString: `{{define "testTemplate"}} + +Generated content 1 + + +Generated content 2 + +{{end}}`, + templateName: "testTemplate", + ignoreNonMatchingSection: false, + want: ` + +Generated content 1 + +This is additional string amended after content generation. +This is another line of amended line. + +Generated content 2 + +`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gen, err := newDocumentGeneratorFromStringTemplate(tc.templateString) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + actual, err := gen.generateDocumentString(tc.destinationString, tc.templateName, nil, tc.ignoreNonMatchingSection) + if tc.wantErr != nil { + if diff := cmp.Diff(tc.wantErr, err); diff != "" { + t.Errorf("Error mismatch (-want +got):\n%s", diff) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(tc.want, actual); diff != "" { + t.Errorf("generateDocumentString() mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestConcatAmendedContents(t *testing.T) { + testCases := []struct { + name string + generated []*splitter.DocumentSection + prev []*splitter.DocumentSection + ignoreNonMatchingGeneratedSection bool + wantResult string + wantErr string + }{ + { + name: "no amended sections", + generated: []*splitter.DocumentSection{ + {Id: "generated-1", Type: splitter.SectionTypeGenerated, Body: "Generated 1"}, + {Id: "generated-2", Type: splitter.SectionTypeGenerated, Body: "Generated 2"}, + }, + prev: []*splitter.DocumentSection{}, + ignoreNonMatchingGeneratedSection: false, + wantResult: "Generated 1\nGenerated 2\n", + }, + { + name: "single amended section at beginning", + generated: []*splitter.DocumentSection{ + {Id: "generated-1", Type: splitter.SectionTypeGenerated, Body: "Generated 1"}, + }, + prev: []*splitter.DocumentSection{ + {Id: "amended-1", Type: splitter.SectionTypeAmend, Body: "Amended 1"}, + }, + ignoreNonMatchingGeneratedSection: false, + wantResult: "Amended 1\nGenerated 1\n", + }, + { + name: "new generated section and a single amended section", + generated: []*splitter.DocumentSection{ + {Id: "generated-1", Type: splitter.SectionTypeGenerated, Body: "Generated 1"}, + {Id: "generated-2", Type: splitter.SectionTypeGenerated, Body: "Generated 2"}, + }, + prev: []*splitter.DocumentSection{ + {Id: "generated-1", Type: splitter.SectionTypeGenerated, Body: "Generated 1"}, + {Id: "amended-1", Type: splitter.SectionTypeAmend, Body: "Amended 1"}, + }, + ignoreNonMatchingGeneratedSection: false, + wantResult: "Generated 1\nAmended 1\nGenerated 2\n", + }, + { + name: "multiple amended sections", + generated: []*splitter.DocumentSection{ + {Id: "generated-1", Body: "Generated 1"}, + {Id: "generated-2", Body: "Generated 2"}, + }, + prev: []*splitter.DocumentSection{ + {Id: "amended-1", Type: splitter.SectionTypeAmend, Body: "Amended 1"}, + {Id: "generated-1", Body: "Generated 1"}, + {Id: "amended-2", Type: splitter.SectionTypeAmend, Body: "Amended 2"}, + {Id: "generated-2", Body: "Generated 2"}, + {Id: "amended-3", Type: splitter.SectionTypeAmend, Body: "Amended 3"}, + }, + ignoreNonMatchingGeneratedSection: false, + wantResult: "Amended 1\nGenerated 1\nAmended 2\nGenerated 2\nAmended 3\n", + }, + { + name: "no generated sections", + generated: []*splitter.DocumentSection{}, + prev: []*splitter.DocumentSection{ + {Id: "amended-1", Type: splitter.SectionTypeAmend, Body: "Amended 1"}, + {Id: "generated-1", Body: "Generated 1"}, + {Id: "amended-2", Type: splitter.SectionTypeAmend, Body: "Amended 2"}, + }, + ignoreNonMatchingGeneratedSection: false, + wantErr: "previous amended sections belongs to other generated sections is not used. Unused ids [generated-1]", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := concatAmendedContents(tc.generated, tc.prev, tc.ignoreNonMatchingGeneratedSection) + if tc.wantErr != "" { + if diff := cmp.Diff(tc.wantErr, err.Error()); diff != "" { + t.Errorf("Error message mismatch (-want +got):\n%s", diff) + } + + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantResult, actual); diff != "" { + t.Errorf("concatAmendedContents() mismatch (-want +got):\n%s", diff) + } + } + }) + } +} diff --git a/pkg/document/splitter/splitter.go b/pkg/document/splitter/splitter.go new file mode 100644 index 0000000..3e701df --- /dev/null +++ b/pkg/document/splitter/splitter.go @@ -0,0 +1,108 @@ +package splitter + +import ( + "crypto/sha256" + "fmt" + "strings" +) + +// Generated section comments are expected to use a line. Document contents must not be on the same line. +const beginGeneratedSectionPrefix = "" + +type SectionType int + +const ( + // SectionTypeGenerated is the section type indicating the section is generated automatically. + SectionTypeGenerated = 0 + // SectionTypeAmend is the section type indicating the section is added after the generation. + SectionTypeAmend = 1 +) + +// DocumentSection represents a section inside the document. +// The splitter read given text and split them in multiple DocumentSection. +type DocumentSection struct { + Type SectionType + Id string + Body string +} + +// SplitToDocumentSections splits text to array of DocumentSection +func SplitToDocumentSections(text string) ([]*DocumentSection, error) { + lines := strings.Split(text, "\n") + var sections []*DocumentSection + var currentSection *DocumentSection + + for lineIndex, line := range lines { + lineWithoutSpace := strings.TrimSpace(line) + if strings.HasPrefix(lineWithoutSpace, beginGeneratedSectionPrefix) && strings.HasSuffix(lineWithoutSpace, generatedSectionSuffix) { + if currentSection != nil { + if currentSection.Type == SectionTypeGenerated { + return nil, fmt.Errorf("invalid begin of section. section began twice. line %d", lineIndex+1) + } + sections = append(sections, currentSection) + } + id := readIdFromGeneratedSectionComment(lineWithoutSpace) + currentSection = &DocumentSection{ + Type: SectionTypeGenerated, + Id: id, + Body: line, + } + continue + } + if strings.HasPrefix(lineWithoutSpace, endGeneratedSectionPrefix) && strings.HasSuffix(lineWithoutSpace, generatedSectionSuffix) { + id := readIdFromGeneratedSectionComment(lineWithoutSpace) + if currentSection == nil { + return nil, fmt.Errorf("invalid end of section. section id %s ended but not began. line %d", id, lineIndex+1) + } + if currentSection.Id != id { + return nil, fmt.Errorf("invalid end of section. section id %s ended but the id is not matching with the previous section id %s. line %d", id, currentSection.Id, lineIndex+1) + } + currentSection.Body += "\n" + line + sections = append(sections, currentSection) + currentSection = nil + continue + } + + if currentSection == nil { + currentSection = &DocumentSection{ + Type: SectionTypeAmend, + Id: "", + Body: line, + } + continue + } + + currentSection.Body += "\n" + line + } + + if currentSection != nil { + if currentSection.Type == SectionTypeGenerated { + return nil, fmt.Errorf("invalid end of section. section id %s began but not ended", currentSection.Id) + } + if currentSection.Body != "" { + sections = append(sections, currentSection) + } + } + + for _, section := range sections { + // generate section id for ammended section. This uses hash just because I don't want to use random string to improve testability. + if section.Id == "" { + section.Id = getHashFromText(section.Body) + } + } + return sections, nil +} + +// readIdFromGeneratedSectionComment extract the id part of the comment of generated section. +// Example: input: returns "my-id" +func readIdFromGeneratedSectionComment(line string) string { + return strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(line), beginGeneratedSectionPrefix), endGeneratedSectionPrefix), generatedSectionSuffix)) +} + +func getHashFromText(text string) string { + h := sha256.New() + h.Write([]byte(text)) + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/pkg/document/splitter/spltter_test.go b/pkg/document/splitter/spltter_test.go new file mode 100644 index 0000000..04a2c61 --- /dev/null +++ b/pkg/document/splitter/spltter_test.go @@ -0,0 +1,204 @@ +package splitter + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" +) + +func TestSplitToDocumentSections(t *testing.T) { + testCases := []struct { + name string + input string + expected []*DocumentSection + expectedErrMsg string + }{ + { + name: "single generated section", + input: ` +Generated content 1 + +`, + expected: []*DocumentSection{ + { + Type: SectionTypeGenerated, + Id: "generated-id-1", + Body: "\nGenerated content 1\n", + }, + }, + }, + { + name: "multiple generated sections", + input: ` +Generated content 1 + + + +Generated content 2 + +`, + expected: []*DocumentSection{ + { + Type: SectionTypeGenerated, + Id: "generated-id-1", + Body: "\nGenerated content 1\n", + }, + { + Type: SectionTypeAmend, + Id: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", // Hash of amend content + Body: "", + }, + { + Type: SectionTypeGenerated, + Id: "generated-id-2", + Body: "\nGenerated content 2\n", + }, + }, + }, + { + name: "generated section with amend", + input: ` +Generated content 1 + + +Amend content 1 + + +Generated content 2 + + +Amend content 2 +`, + expected: []*DocumentSection{ + { + Type: SectionTypeGenerated, + Id: "generated-id-1", + Body: "\nGenerated content 1\n", + }, + { + Type: SectionTypeAmend, + Id: "8425062b6f9c5ce9895ebb6fcd8d3c58c68887c14c8628a33e8f604dac84e919", // Hash of amend content + Body: "\nAmend content 1\n", + }, + { + Type: SectionTypeGenerated, + Id: "generated-id-2", + Body: "\nGenerated content 2\n", + }, + { + Type: SectionTypeAmend, + Id: "76b08fe06ecd34111bc58e645360d32593cdfdb797d70f84e7ed0c3cf3103374", // Hash of amend content + Body: "\nAmend content 2\n", + }, + }, + }, + { + name: "mismatched end tag", + input: ` +Generated content 1 + +`, + expectedErrMsg: "invalid end of section. section id generated-id-2 ended but the id is not matching with the previous section id generated-id-1. line 3", + }, + { + name: "end tag without begin tag", + input: ` +`, + expectedErrMsg: "invalid end of section. section id generated-id-1 ended but not began. line 1", + }, + { + name: "empty input", + input: ``, + expected: nil, + }, + { + name: "only amend", + input: ` +Amend content +`, + expected: []*DocumentSection{ + { + Type: SectionTypeAmend, + Id: "1ff547697cd3b7542f7bb024812b201fc77e31bd605f95f247f41b585286c464", + Body: "\nAmend content\n", + }, + }, + }, + // Test case for begin tag without matching end tag -- generated section at the end + { + name: "begin tag without end tag", + input: ` +Some amend content + + +Generated content at end +`, + expectedErrMsg: "invalid end of section. section id generated-id-1 began but not ended", + }, + { + name: "begin tag appears twice", + input: ` +Generated content 1 + +Generated content 2 + +`, + expectedErrMsg: "invalid begin of section. section began twice. line 3", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := SplitToDocumentSections(tc.input) + + if tc.expectedErrMsg != "" { + if diff := cmp.Diff(tc.expectedErrMsg, err.Error()); diff != "" { + t.Errorf("Error message mismatch (-want +got):\n%s", diff) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if diff := cmp.Diff(tc.expected, actual); diff != "" { + t.Errorf("Sections do not match (-want +got):\n%s", diff) + } + } + + }) + + } +} + +func TestReadIdFromGeneratedSectionComment(t *testing.T) { + testCases := []struct { + input string + expected string + }{ + { + input: "", + expected: "my-id", + }, + { + input: "", + expected: "my-id", + }, + { + input: "", + expected: "my-id", + }, + { + input: " +# Inspection types + +Inspection type is ... + + + + +## [Google Kubernetes Engine](#gcp-gke) + +### Supported features + + + +* Kubernetes Audit Log(v2) + + +* Kubernetes Event Logs + + +* Kubernetes Node Logs + + +* Kubernetes container logs + + +* GKE Audit logs + + +* Compute API Logs + + +* GCE Network Logs + + +* Autoscaler Logs + + +* Kubernetes Control plane component logs + + +* Node serial port logs + + +## [Cloud Composer](#gcp-composer) + +### Supported features + + + +* Kubernetes Audit Log(v2) + + +* Kubernetes Event Logs + + +* Kubernetes Node Logs + + +* Kubernetes container logs + + +* GKE Audit logs + + +* Compute API Logs + + +* GCE Network Logs + + +* Autoscaler Logs + + +* Kubernetes Control plane component logs + + +* Node serial port logs + + +* (Alpha) Composer / Airflow Scheduler + + +* (Alpha) Cloud Composer / Airflow Worker + + +* (Alpha) Composer / Airflow DagProcessorManager + + +## [GKE on AWS(Anthos on AWS)](#gcp-gke-on-aws) + +### Supported features + + + +* Kubernetes Audit Log(v2) + + +* Kubernetes Event Logs + + +* Kubernetes Node Logs + + +* Kubernetes container logs + + +* MultiCloud API logs + + +* Kubernetes Control plane component logs + + +## [GKE on Azure(Anthos on Azure)](#gcp-gke-on-azure) + +### Supported features + + + +* Kubernetes Audit Log(v2) + + +* Kubernetes Event Logs + + +* Kubernetes Node Logs + + +* Kubernetes container logs + + +* MultiCloud API logs + + +* Kubernetes Control plane component logs + + +## [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](#gcp-gdcv-for-baremetal) + +### Supported features + + + +* Kubernetes Audit Log(v2) + + +* Kubernetes Event Logs + + +* Kubernetes Node Logs + + +* Kubernetes container logs + + +* OnPrem API logs + + +* Kubernetes Control plane component logs + + +## [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](#gcp-gdcv-for-vmware) + +### Supported features + + + +* Kubernetes Audit Log(v2) + + +* Kubernetes Event Logs + + +* Kubernetes Node Logs + + +* Kubernetes container logs + + +* OnPrem API logs + + +* Kubernetes Control plane component logs + diff --git a/docs/en/log-types.md b/docs/en/log-types.md new file mode 100644 index 0000000..46d504f --- /dev/null +++ b/docs/en/log-types.md @@ -0,0 +1,38 @@ +# Log types + + +## [![#3fb549](https://placehold.co/15x15/3fb549/3fb549.png) k8s_event](#LogTypeEvent) + + +## [![#000000](https://placehold.co/15x15/000000/000000.png) k8s_audit](#LogTypeAudit) + + +## [![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png) k8s_container](#LogTypeContainer) + + +## [![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png) k8s_node](#LogTypeNode) + + +## [![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png) gke_audit](#LogTypeGkeAudit) + + +## [![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png) compute_api](#LogTypeComputeApi) + + +## [![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png) multicloud_api](#LogTypeMulticloudAPI) + + +## [![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png) onprem_api](#LogTypeOnPremAPI) + + +## [![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png) network_api](#LogTypeNetworkAPI) + + +## [![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png) autoscaler](#LogTypeAutoscaler) + + +## [![#88AA55](https://placehold.co/15x15/88AA55/88AA55.png) composer_environment](#LogTypeComposerEnvironment) + + +## [![#FF3333](https://placehold.co/15x15/FF3333/FF3333.png) control_plane_component](#LogTypeControlPlaneComponent) + diff --git a/docs/template/inspection-types.template.md b/docs/template/inspection-types.template.md index e69de29..ba224f3 100644 --- a/docs/template/inspection-types.template.md +++ b/docs/template/inspection-types.template.md @@ -0,0 +1,22 @@ +{{define "inspection-type-template"}} + +# Inspection types + +Inspection type is ... + + +{{range $index,$type := .InspectionTypes }} + +## [{{$type.Name}}](#{{$type.ID}}) + +### Supported features + + + +{{range $feature := $type.SupportedFeatures}} + +* {{$feature.Name}} + +{{end}} +{{end}} +{{end}} \ No newline at end of file diff --git a/docs/template/log-types.template.md b/docs/template/log-types.template.md new file mode 100644 index 0000000..88e3d20 --- /dev/null +++ b/docs/template/log-types.template.md @@ -0,0 +1,7 @@ +{{define "log-types-template"}} +{{range $index,$type := .LogTypes }} + +## [![#{{$type.ColorCode}}](https://placehold.co/15x15/{{$type.ColorCode}}/{{$type.ColorCode}}.png) {{$type.Name}}](#{{$type.ID}}) + +{{end}} +{{end}} \ No newline at end of file diff --git a/pkg/document/model/inspection-type.go b/pkg/document/model/inspection-type.go new file mode 100644 index 0000000..e016618 --- /dev/null +++ b/pkg/document/model/inspection-type.go @@ -0,0 +1,51 @@ +package model + +import ( + "github.com/GoogleCloudPlatform/khi/pkg/inspection" + inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" + "github.com/GoogleCloudPlatform/khi/pkg/inspection/taskfilter" +) + +// InspectionTypeDocumentModel is a document model type for generating docs/en/inspection-type.md +type InspectionTypeDocumentModel struct { + InspectionTypes []InspectionTypeDocumentElement +} + +type InspectionTypeDocumentElement struct { + ID string + Name string + + SupportedFeatures []InspectionTypeDocumentElementFeature +} + +type InspectionTypeDocumentElementFeature struct { + ID string + Name string + Description string +} + +// GetInspectionTypeDocumentModel returns the document model from task server. +func GetInspectionTypeDocumentModel(taskServer *inspection.InspectionTaskServer) InspectionTypeDocumentModel { + result := InspectionTypeDocumentModel{} + inspectionTypes := taskServer.GetAllInspectionTypes() + for _, inspectionType := range inspectionTypes { + tasks := taskServer.RootTaskSet. + FilteredSubset(inspection_task.LabelKeyInspectionTypes, taskfilter.ContainsElement(inspectionType.Id), true). + FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false). + GetAll() + features := []InspectionTypeDocumentElementFeature{} + for _, task := range tasks { + features = append(features, InspectionTypeDocumentElementFeature{ + ID: task.ID().String(), + Name: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), + Description: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), + }) + } + result.InspectionTypes = append(result.InspectionTypes, InspectionTypeDocumentElement{ + ID: inspectionType.Id, + Name: inspectionType.Name, + SupportedFeatures: features, + }) + } + return result +} diff --git a/pkg/document/model/log-type.go b/pkg/document/model/log-type.go new file mode 100644 index 0000000..b9b494f --- /dev/null +++ b/pkg/document/model/log-type.go @@ -0,0 +1,32 @@ +package model + +import ( + "strings" + + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" +) + +type LogTypeDocumentModel struct { + LogTypes []LogTypeDocumentElement +} + +type LogTypeDocumentElement struct { + ID string + Name string + ColorCode string +} + +func GetLogTypeDocumentModel() LogTypeDocumentModel { + logTypes := []LogTypeDocumentElement{} + for i := 1; i < int(enum.MaxLogTypeEnumNumber); i++ { + logType := enum.LogTypes[enum.LogType(i)] + logTypes = append(logTypes, LogTypeDocumentElement{ + ID: logType.EnumKeyName, + Name: logType.Label, + ColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), + }) + } + return LogTypeDocumentModel{ + LogTypes: logTypes, + } +} diff --git a/pkg/inspection/runner.go b/pkg/inspection/runner.go index 45f4f2e..da9ec0f 100644 --- a/pkg/inspection/runner.go +++ b/pkg/inspection/runner.go @@ -33,6 +33,7 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata/progress" inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" "github.com/GoogleCloudPlatform/khi/pkg/inspection/task/serializer" + "github.com/GoogleCloudPlatform/khi/pkg/inspection/taskfilter" "github.com/GoogleCloudPlatform/khi/pkg/lifecycle" "github.com/GoogleCloudPlatform/khi/pkg/parameters" "github.com/GoogleCloudPlatform/khi/pkg/task" @@ -84,13 +85,9 @@ func (i *InspectionRunner) SetInspectionType(inspectionType string) error { if !typeFound { return fmt.Errorf("inspection type %s was not found", inspectionType) } - i.availableDefinitions = i.inspectionServer.rootTaskSet.FilteredSubset(inspection_task.LabelKeyInspectionTypes, generateFilterByIncluded(inspectionType), true) - defaultFeatures := i.availableDefinitions.FilteredSubset(inspection_task.LabelKeyInspectionDefaultFeatureFlag, func(v any) bool { - return v.(bool) - }, false) - i.requiredDefinitions = i.availableDefinitions.FilteredSubset(inspection_task.LabelKeyInspectionRequiredFlag, func(v any) bool { - return v.(bool) - }, false) + i.availableDefinitions = i.inspectionServer.RootTaskSet.FilteredSubset(inspection_task.LabelKeyInspectionTypes, taskfilter.ContainsElement(inspectionType), true) + defaultFeatures := i.availableDefinitions.FilteredSubset(inspection_task.LabelKeyInspectionDefaultFeatureFlag, taskfilter.HasTrue, false) + i.requiredDefinitions = i.availableDefinitions.FilteredSubset(inspection_task.LabelKeyInspectionRequiredFlag, taskfilter.HasTrue, false) defaultFeatureIds := []string{} for _, featureTask := range defaultFeatures.GetAll() { defaultFeatureIds = append(defaultFeatureIds, featureTask.ID().String()) @@ -103,9 +100,7 @@ func (i *InspectionRunner) FeatureList() ([]FeatureListItem, error) { if i.availableDefinitions == nil { return nil, fmt.Errorf("inspection type is not yet initialized") } - featureSet := i.availableDefinitions.FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, func(v any) bool { - return v.(bool) - }, false) + featureSet := i.availableDefinitions.FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false) features := []FeatureListItem{} for _, definition := range featureSet.GetAll() { label := definition.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, fmt.Sprintf("No label Set!(%s)", definition.ID())) @@ -403,18 +398,6 @@ func generateInspectionId() string { return string(randomid) } -func generateFilterByIncluded(value string) func(v any) bool { - return func(v any) bool { - strArr := v.([]string) - for _, element := range strArr { - if element == value { - return true - } - } - return false - } -} - func getLogLevel() slog.Level { if parameters.Debug.Verbose != nil && *parameters.Debug.Verbose { return slog.LevelDebug diff --git a/pkg/inspection/server.go b/pkg/inspection/server.go index decd934..d80f689 100644 --- a/pkg/inspection/server.go +++ b/pkg/inspection/server.go @@ -32,6 +32,9 @@ type InspectionType struct { Description string `json:"description"` Icon string `json:"icon"` Priority int `json:"-"` + + // Document properties + DocumentDescription string `json:"-"` } type FeatureListItem struct { @@ -52,8 +55,8 @@ type InspectionRunResult struct { // InspectionTaskServer manages tasks and provides apis to get task related information in JSON convertible type. type InspectionTaskServer struct { - // rootTaskSet is the set of the all definitions in KHI. - rootTaskSet *task.DefinitionSet + // RootTaskSet is the set of the all definitions in KHI. + RootTaskSet *task.DefinitionSet // inspectionTypes are kinds of tasks. Users will select this at first to filter togglable feature tasks. inspectionTypes []*InspectionType // tasks are generated tasks @@ -66,7 +69,7 @@ func NewServer() (*InspectionTaskServer, error) { return nil, err } return &InspectionTaskServer{ - rootTaskSet: ns, + RootTaskSet: ns, inspectionTypes: make([]*InspectionType, 0), tasks: map[string]*InspectionRunner{}, }, nil @@ -94,7 +97,7 @@ func (s *InspectionTaskServer) AddInspectionType(newInspectionType InspectionTyp // AddTaskDefinition register a task definition usable for the inspection tasks func (s *InspectionTaskServer) AddTaskDefinition(taskDefinition task.Definition) error { - return s.rootTaskSet.Add(taskDefinition) + return s.RootTaskSet.Add(taskDefinition) } // CreateInspection generates an inspection and returns inspection ID @@ -136,5 +139,5 @@ func (s *InspectionTaskServer) GetAllRunners() []*InspectionRunner { // GetAllRegisteredTasks returns a cloned list of all definitions registered in this server. func (s *InspectionTaskServer) GetAllRegisteredTasks() []task.Definition { - return s.rootTaskSet.GetAll() + return s.RootTaskSet.GetAll() } diff --git a/pkg/inspection/taskfilter/filter.go b/pkg/inspection/taskfilter/filter.go new file mode 100644 index 0000000..4481f35 --- /dev/null +++ b/pkg/inspection/taskfilter/filter.go @@ -0,0 +1,19 @@ +package taskfilter + +// ContainsElement returns a function that represents a condition to filter only tasks that have the specified element in the specified label value. +func ContainsElement[T comparable](comparedWith T) func(taskLabelValueAny any) bool { + return func(v any) bool { + taskLabelValue := v.([]T) + for _, element := range taskLabelValue { + if element == comparedWith { + return true + } + } + return false + } +} + +// HasTrue is a function that represents a condition to filter only tasks with true value. +func HasTrue(taskLabelValueAny any) bool { + return taskLabelValueAny.(bool) +} diff --git a/pkg/model/enum/log_type.go b/pkg/model/enum/log_type.go index ff8ccaa..7f46e77 100644 --- a/pkg/model/enum/log_type.go +++ b/pkg/model/enum/log_type.go @@ -35,6 +35,8 @@ const ( logTypeUnusedEnd ) +const MaxLogTypeEnumNumber = logTypeUnusedEnd + type LogTypeFrontendMetadata struct { // EnumKeyName is the name of this enum value. Must match with the enum key. EnumKeyName string From e54fa5a54fd090f320ec803837d4a71b820f6fcb Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 00:06:23 +0900 Subject: [PATCH 03/23] Add generator for timeline reference --- cmd/document-generator/main.go | 6 +- docs/en/relationships.md | 190 +++++++++++++++++ docs/template/log-types.template.md | 2 +- docs/template/relationship.template.md | 47 +++++ pkg/document/model/feature.go | 1 + ...{inspection-type.go => inspection_type.go} | 0 .../model/{log-type.go => log_type.go} | 2 +- pkg/document/model/relationship.go | 122 +++++++++++ pkg/model/enum/log_type.go | 2 +- pkg/model/enum/parent_relationship.go | 194 +++++++++++++++++- pkg/model/enum/parent_relationship_test.go | 12 ++ 11 files changed, 569 insertions(+), 9 deletions(-) create mode 100644 docs/en/relationships.md create mode 100644 docs/template/relationship.template.md create mode 100644 pkg/document/model/feature.go rename pkg/document/model/{inspection-type.go => inspection_type.go} (100%) rename pkg/document/model/{log-type.go => log_type.go} (92%) create mode 100644 pkg/document/model/relationship.go diff --git a/cmd/document-generator/main.go b/cmd/document-generator/main.go index 39b4b5c..c68c6b2 100644 --- a/cmd/document-generator/main.go +++ b/cmd/document-generator/main.go @@ -48,6 +48,10 @@ func main() { fatal(err, "failed to generate inspection type document") logTypeDocumentModel := model.GetLogTypeDocumentModel() - err = generator.GenerateDocument("./docs/en/log-types.md", "log-types-template", logTypeDocumentModel, false) + err = generator.GenerateDocument("./docs/en/log-types.md", "log-type-template", logTypeDocumentModel, false) fatal(err, "failed to generate log type document") + + relationshipDocumentModel := model.GetRelationshipDocumentModel() + err = generator.GenerateDocument("./docs/en/relationships.md", "relationship-template", relationshipDocumentModel, false) + fatal(err, "failed to generate relationship document") } diff --git a/docs/en/relationships.md b/docs/en/relationships.md new file mode 100644 index 0000000..62bfccb --- /dev/null +++ b/docs/en/relationships.md @@ -0,0 +1,190 @@ + +## [The default resource timeline](#RelationshipChild) + + +### Revisions + +This timeline can have the following revisions. + + +|State|Source log|Description| +|---|---|---| +|![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png)Resource is existing|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource exits at the time| +|![#CC0000](https://placehold.co/15x15/CC0000/CC0000.png)Resource is deleted|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource is deleted at the time.| +|![#CC5500](https://placehold.co/15x15/CC5500/CC5500.png)Resource is under deleting with graceful period|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource is being deleted with grace period at the time.| + + + +### Events + +This timeline can have the following events. + + +|Source log|Description| +|---|---| +|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|An event that related to a resource but not changing the resource. This is often an error log for an operation to the resource.| +|![#3fb549](https://placehold.co/15x15/3fb549/3fb549.png)k8s_event|An event that related to a resource| + + + +## [![#4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png) condition - Status condition field timeline](#RelationshipResourceCondition) + + +### Revisions + +This timeline can have the following revisions. + + +|State|Source log|Description| +|---|---|---| +|![#004400](https://placehold.co/15x15/004400/004400.png)State is 'True'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)State is 'False'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| +|![#663366](https://placehold.co/15x15/663366/663366.png)State is 'Unknown'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| + + + +## [![#000000](https://placehold.co/15x15/000000/000000.png) operation - Operation timeline](#RelationshipOperation) + + +### Revisions + +This timeline can have the following revisions. + + +|State|Source log|Description| +|---|---|---| +|![#004400](https://placehold.co/15x15/004400/004400.png)Processing operation|![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api|| +|![#333333](https://placehold.co/15x15/333333/333333.png)Operation is finished|![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api|| + + + +## [![#008000](https://placehold.co/15x15/008000/008000.png) endpointslice - Endpoint serving state timeline](#RelationshipEndpointSlice) + + +### Revisions + +This timeline can have the following revisions. + + +|State|Source log|Description| +|---|---|---| +|![#004400](https://placehold.co/15x15/004400/004400.png)Endpoint is ready|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)Endpoint is not ready|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| +|![#fed700](https://placehold.co/15x15/fed700/fed700.png)Endpoint is being terminated|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| + + + +## [![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png) container - Container timeline](#RelationshipContainer) + + +### Revisions + +This timeline can have the following revisions. + + +|State|Source log|Description| +|---|---|---| +|![#997700](https://placehold.co/15x15/997700/997700.png)Waiting for starting container|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)Container is not ready|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| +|![#007700](https://placehold.co/15x15/007700/007700.png)Container is ready|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| +|![#113333](https://placehold.co/15x15/113333/113333.png)Container exited with healthy exit code|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| +|![#331111](https://placehold.co/15x15/331111/331111.png)Container exited with errornous exit code|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| + + + +### Events + +This timeline can have the following events. + + +|Source log|Description| +|---|---| +|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| +|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| + + + +## [![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png) node-component - Node component timeline](#RelationshipNodeComponent) + + +### Events + +This timeline can have the following events. + + +|Source log|Description| +|---|---| +|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| + + + +## [![#33DD88](https://placehold.co/15x15/33DD88/33DD88.png) owns - Owning children timeline](#RelationshipOwnerReference) + + +### Aliases + +This timeline can have the following aliases. + + +|Aliased timeline|Source log|Description| +|---|---|---| +|![#CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)resource|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This timeline shows the events and revisions of the owning resources.| + + + +## [![#FF8855](https://placehold.co/15x15/FF8855/FF8855.png) binds - Pod binding timeline](#RelationshipPodBinding) + + +### Aliases + +This timeline can have the following aliases. + + +|Aliased timeline|Source log|Description| +|---|---|---| +|![#CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)resource|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This timeline shows the binding subresources associated on a node| + + + +## [![#A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png) neg - NEG timeline](#RelationshipNetworkEndpointGroup) + + +### Revisions + +This timeline can have the following revisions. + + +|State|Source log|Description| +|---|---|---| +|![#004400](https://placehold.co/15x15/004400/004400.png)State is 'True'|![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api|| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)State is 'False'|![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api|| + + + +## [![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png) mig - Managed instance group timeline](#RelationshipManagedInstanceGroup) + + +### Events + +This timeline can have the following events. + + +|Source log|Description| +|---|---| +|![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler|| + + + +## [![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png) controlplane - Control plane component timeline](#RelationshipControlPlaneComponent) + + +### Events + +This timeline can have the following events. + + +|Source log|Description| +|---|---| +|![#FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component|| + + diff --git a/docs/template/log-types.template.md b/docs/template/log-types.template.md index 88e3d20..2720837 100644 --- a/docs/template/log-types.template.md +++ b/docs/template/log-types.template.md @@ -1,4 +1,4 @@ -{{define "log-types-template"}} +{{define "log-type-template"}} {{range $index,$type := .LogTypes }} ## [![#{{$type.ColorCode}}](https://placehold.co/15x15/{{$type.ColorCode}}/{{$type.ColorCode}}.png) {{$type.Name}}](#{{$type.ID}}) diff --git a/docs/template/relationship.template.md b/docs/template/relationship.template.md new file mode 100644 index 0000000..934c792 --- /dev/null +++ b/docs/template/relationship.template.md @@ -0,0 +1,47 @@ +{{define "relationship-template"}} +{{range $index,$relationship := .Relationships }} + +## [{{with $relationship.HasVisibleChip}}![#{{$relationship.ColorCode}}](https://placehold.co/15x15/{{$relationship.ColorCode}}/{{$relationship.ColorCode}}.png) {{$relationship.Label}} - {{end}}{{$relationship.LongName}}](#{{$relationship.ID}}) + +{{with $relationship.GeneratableRevisions}} + +### Revisions + +This timeline can have the following revisions. + + +|State|Source log|Description| +|---|---|---| +{{range $index,$revision := $relationship.GeneratableRevisions}}|![#{{$revision.RevisionStateColorCode}}](https://placehold.co/15x15/{{$revision.RevisionStateColorCode}}/{{$revision.RevisionStateColorCode}}.png){{$revision.RevisionStateLabel}}|![#{{$revision.SourceLogTypeColorCode}}](https://placehold.co/15x15/{{$revision.SourceLogTypeColorCode}}/{{$revision.SourceLogTypeColorCode}}.png){{$revision.SourceLogTypeLabel}}|{{$revision.Description}}| +{{end}} + +{{end}} +{{with $relationship.GeneratableEvents}} + +### Events + +This timeline can have the following events. + + +|Source log|Description| +|---|---| +{{range $index,$event := $relationship.GeneratableEvents}}|![#{{$event.ColorCode}}](https://placehold.co/15x15/{{$event.ColorCode}}/{{$event.ColorCode}}.png){{$event.SourceLogTypeLabel}}|{{$event.Description}}| +{{end}} + +{{end}} +{{with $relationship.GeneratableAliases}} + +### Aliases + +This timeline can have the following aliases. + + +|Aliased timeline|Source log|Description| +|---|---|---| +{{range $index,$alias := $relationship.GeneratableAliases}}|![#{{$alias.AliasedTimelineRelationshipColorCode}}](https://placehold.co/15x15/{{$alias.AliasedTimelineRelationshipColorCode}}/{{$alias.AliasedTimelineRelationshipColorCode}}.png){{$alias.AliasedTimelineRelationshipLabel}}|![#{{$alias.SourceLogTypeColorCode}}](https://placehold.co/15x15/{{$alias.SourceLogTypeColorCode}}/{{$alias.SourceLogTypeColorCode}}.png){{$alias.SourceLogTypeLabel}}|{{$alias.Description}}| +{{end}} + +{{end}} + +{{end}} +{{end}} \ No newline at end of file diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go new file mode 100644 index 0000000..8b53790 --- /dev/null +++ b/pkg/document/model/feature.go @@ -0,0 +1 @@ +package model diff --git a/pkg/document/model/inspection-type.go b/pkg/document/model/inspection_type.go similarity index 100% rename from pkg/document/model/inspection-type.go rename to pkg/document/model/inspection_type.go diff --git a/pkg/document/model/log-type.go b/pkg/document/model/log_type.go similarity index 92% rename from pkg/document/model/log-type.go rename to pkg/document/model/log_type.go index b9b494f..aa72c36 100644 --- a/pkg/document/model/log-type.go +++ b/pkg/document/model/log_type.go @@ -18,7 +18,7 @@ type LogTypeDocumentElement struct { func GetLogTypeDocumentModel() LogTypeDocumentModel { logTypes := []LogTypeDocumentElement{} - for i := 1; i < int(enum.MaxLogTypeEnumNumber); i++ { + for i := 1; i < enum.EnumLogTypeCount; i++ { logType := enum.LogTypes[enum.LogType(i)] logTypes = append(logTypes, LogTypeDocumentElement{ ID: logType.EnumKeyName, diff --git a/pkg/document/model/relationship.go b/pkg/document/model/relationship.go new file mode 100644 index 0000000..0820a40 --- /dev/null +++ b/pkg/document/model/relationship.go @@ -0,0 +1,122 @@ +package model + +import ( + "strings" + + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" +) + +type RelationshipDocumentModel struct { + Relationships []RelationshipDocumentElement +} + +type RelationshipDocumentElement struct { + ID string + HasVisibleChip bool + Label string + LongName string + ColorCode string + + GeneratableEvents []RelationshipGeneratableEvent + GeneratableRevisions []RelationshipGeneratableRevisions + GeneratableAliases []RelationshipGeneratableAliases +} + +type RelationshipGeneratableEvent struct { + ID string + SourceLogTypeLabel string + ColorCode string + Description string +} + +type RelationshipGeneratableRevisions struct { + ID string + SourceLogTypeLabel string + SourceLogTypeColorCode string + RevisionStateColorCode string + RevisionStateLabel string + Description string +} + +type RelationshipGeneratableAliases struct { + ID string + AliasedTimelineRelationshipLabel string + AliasedTimelineRelationshipColorCode string + SourceLogTypeLabel string + SourceLogTypeColorCode string + Description string +} + +func GetRelationshipDocumentModel() RelationshipDocumentModel { + relationships := []RelationshipDocumentElement{} + for i := 0; i < int(enum.EnumParentRelationshipLength); i++ { + relationshipKey := enum.ParentRelationship(i) + relationship := enum.ParentRelationships[relationshipKey] + relationships = append(relationships, RelationshipDocumentElement{ + ID: relationship.EnumKeyName, + HasVisibleChip: relationship.Visible, + Label: relationship.Label, + LongName: relationship.LongName, + ColorCode: strings.TrimLeft(relationship.LabelBackgroundColor, "#"), + + GeneratableEvents: getRelationshipGeneratableEvents(relationshipKey), + GeneratableRevisions: getRelationshipGeneratableRevisions(relationshipKey), + GeneratableAliases: getRelationshipGeneratableAliases(relationshipKey), + }) + } + + return RelationshipDocumentModel{ + Relationships: relationships, + } +} + +func getRelationshipGeneratableEvents(reltionship enum.ParentRelationship) []RelationshipGeneratableEvent { + result := []RelationshipGeneratableEvent{} + relationship := enum.ParentRelationships[reltionship] + for _, event := range relationship.GeneratableEvents { + logType := enum.LogTypes[event.SourceLogType] + result = append(result, RelationshipGeneratableEvent{ + ID: logType.EnumKeyName, + SourceLogTypeLabel: logType.Label, + ColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), + Description: event.Description, + }) + } + return result +} + +func getRelationshipGeneratableRevisions(reltionship enum.ParentRelationship) []RelationshipGeneratableRevisions { + result := []RelationshipGeneratableRevisions{} + relationship := enum.ParentRelationships[reltionship] + for _, revision := range relationship.GeneratableRevisions { + logType := enum.LogTypes[revision.SourceLogType] + revisionState := enum.RevisionStates[revision.State] + result = append(result, RelationshipGeneratableRevisions{ + ID: logType.EnumKeyName, + SourceLogTypeLabel: logType.Label, + SourceLogTypeColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), + RevisionStateColorCode: strings.TrimLeft(revisionState.BackgroundColor, "#"), + RevisionStateLabel: revisionState.Label, + Description: revision.Description, + }) + } + return result +} + +func getRelationshipGeneratableAliases(reltionship enum.ParentRelationship) []RelationshipGeneratableAliases { + result := []RelationshipGeneratableAliases{} + relationship := enum.ParentRelationships[reltionship] + for _, alias := range relationship.GeneratableAliasTimelineInfo { + aliasedRelationship := enum.ParentRelationships[alias.AliasedTimelineRelationship] + logType := enum.LogTypes[alias.SourceLogType] + result = append(result, RelationshipGeneratableAliases{ + ID: logType.EnumKeyName, + AliasedTimelineRelationshipLabel: aliasedRelationship.Label, + AliasedTimelineRelationshipColorCode: strings.TrimLeft(aliasedRelationship.LabelBackgroundColor, "#"), + SourceLogTypeLabel: logType.Label, + SourceLogTypeColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), + Description: alias.Description, + }) + } + return result +} diff --git a/pkg/model/enum/log_type.go b/pkg/model/enum/log_type.go index 7f46e77..96e2481 100644 --- a/pkg/model/enum/log_type.go +++ b/pkg/model/enum/log_type.go @@ -35,7 +35,7 @@ const ( logTypeUnusedEnd ) -const MaxLogTypeEnumNumber = logTypeUnusedEnd +const EnumLogTypeCount = int(logTypeUnusedEnd) type LogTypeFrontendMetadata struct { // EnumKeyName is the name of this enum value. Must match with the enum key. diff --git a/pkg/model/enum/parent_relationship.go b/pkg/model/enum/parent_relationship.go index ef8f00d..1952065 100644 --- a/pkg/model/enum/parent_relationship.go +++ b/pkg/model/enum/parent_relationship.go @@ -32,123 +32,307 @@ const ( relationshipUnusedEnd // Add items above. This field is used for counting items in this enum to test. ) +// EnumParentRelationshipLength is the count of ParentRelationship enum elements. +const EnumParentRelationshipLength = int(relationshipUnusedEnd) + // parentRelationshipFrontendMetadata is a type defined for each parent relationship types. type ParentRelationshipFrontendMetadata struct { - Visible bool - EnumKeyName string - Label string + // Visible is a flag if this relationship is visible as a chip left of timeline name. + Visible bool + // EnumKeyName is the name of enum exactly matching with the constant variable defined in this file. + EnumKeyName string + // Label is a short name shown on frontend as the chip on the left of timeline name. + Label string + // LongName is a descriptive name of the ralationship. This value is used in the document. + LongName string + // Hint explains the meaning of this timeline. This is shown as the tooltip on front end. Hint string LabelColor string LabelBackgroundColor string SortPriority int + + // GeneratableEvents contains the list of possible event types put on a timeline with the relationship type. This field is used for document generation. + GeneratableEvents []GeneratableEventInfo + // GeneratableRevisions contains the list of possible revision types put on a timeline with the relationship type. This field is used for document generation. + GeneratableRevisions []GeneratableRevisionInfo + // GeneratableAliasTimelineInfo contains the list of possible target timelines aliased from the timeline of this relationship. This field is used for document generation. + GeneratableAliasTimelineInfo []GeneratableAliasTimelineInfo +} + +type GeneratableEventInfo struct { + SourceLogType LogType + Description string +} + +type GeneratableRevisionInfo struct { + State RevisionState + SourceLogType LogType + Description string +} + +type GeneratableAliasTimelineInfo struct { + AliasedTimelineRelationship ParentRelationship + SourceLogType LogType + Description string } var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetadata{ RelationshipChild: { Visible: false, EnumKeyName: "RelationshipChild", - Label: "subresource", + Label: "resource", + LongName: "The default resource timeline", LabelColor: "#000000", LabelBackgroundColor: "#CCCCCC", SortPriority: 1000, + GeneratableRevisions: []GeneratableRevisionInfo{ + { + State: RevisionStateExisting, + SourceLogType: LogTypeAudit, + Description: "This state indicates the resource exits at the time", + }, + { + State: RevisionStateDeleted, + SourceLogType: LogTypeAudit, + Description: "This state indicates the resource is deleted at the time.", + }, + { + State: RevisionStateDeleting, + SourceLogType: LogTypeAudit, + Description: "This state indicates the resource is being deleted with grace period at the time.", + }, + }, + GeneratableEvents: []GeneratableEventInfo{ + { + SourceLogType: LogTypeAudit, + Description: "An event that related to a resource but not changing the resource. This is often an error log for an operation to the resource.", + }, + { + SourceLogType: LogTypeEvent, + Description: "An event that related to a resource", + }, + }, }, RelationshipResourceCondition: { Visible: true, EnumKeyName: "RelationshipResourceCondition", Label: "condition", + LongName: "Status condition field timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#4c29e8", Hint: "Resource condition written on .status.conditions", SortPriority: 2000, + GeneratableRevisions: []GeneratableRevisionInfo{ + { + State: RevisionStateConditionTrue, + SourceLogType: LogTypeAudit, + }, + { + State: RevisionStateConditionFalse, + SourceLogType: LogTypeAudit, + }, + { + State: RevisionStateConditionUnknown, + SourceLogType: LogTypeAudit, + }, + }, }, RelationshipOperation: { Visible: true, EnumKeyName: "RelationshipOperation", Label: "operation", + LongName: "Operation timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#000000", Hint: "GCP operations associated with this resource", SortPriority: 3000, + GeneratableRevisions: []GeneratableRevisionInfo{ + { + State: RevisionStateOperationStarted, + SourceLogType: LogTypeComputeApi, + }, + { + State: RevisionStateOperationFinished, + SourceLogType: LogTypeComputeApi, + }, + }, }, RelationshipEndpointSlice: { Visible: true, EnumKeyName: "RelationshipEndpointSlice", Label: "endpointslice", + LongName: "Endpoint serving state timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#008000", Hint: "Pod serving status obtained from endpoint slice", SortPriority: 20000, // later than container + GeneratableRevisions: []GeneratableRevisionInfo{ + { + State: RevisionStateEndpointReady, + SourceLogType: LogTypeAudit, + }, + { + State: RevisionStateEndpointUnready, + SourceLogType: LogTypeAudit, + }, + { + State: RevisionStateEndpointTerminating, + SourceLogType: LogTypeAudit, + }, + }, }, RelationshipContainer: { Visible: true, EnumKeyName: "RelationshipContainer", Label: "container", + LongName: "Container timeline", LabelColor: "#000000", LabelBackgroundColor: "#fe9bab", - Hint: "Containers statuses/logs in Pods", + Hint: "Statuses/logs of a container", SortPriority: 5000, + GeneratableRevisions: []GeneratableRevisionInfo{ + { + State: RevisionStateContainerWaiting, + SourceLogType: LogTypeContainer, + }, + { + State: RevisionStateContainerRunningNonReady, + SourceLogType: LogTypeContainer, + }, + { + State: RevisionStateContainerRunningReady, + SourceLogType: LogTypeContainer, + }, + { + State: RevisionStateContainerTerminatedWithSuccess, + SourceLogType: LogTypeContainer, + }, + { + State: RevisionStateContainerTerminatedWithError, + SourceLogType: LogTypeContainer, + }, + }, + GeneratableEvents: []GeneratableEventInfo{ + { + SourceLogType: LogTypeContainer, + }, + { + SourceLogType: LogTypeNode, + }, + }, }, RelationshipNodeComponent: { Visible: true, EnumKeyName: "RelationshipNodeComponent", Label: "node-component", + LongName: "Node component timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#0077CC", Hint: "Non container resource running on a node", SortPriority: 6000, + GeneratableEvents: []GeneratableEventInfo{ + { + SourceLogType: LogTypeNode, + }, + }, }, RelationshipOwnerReference: { Visible: true, EnumKeyName: "RelationshipOwnerReference", Label: "owns", + LongName: "Owning children timeline", LabelColor: "#000000", LabelBackgroundColor: "#33DD88", Hint: "A k8s resource related to this resource from .metadata.ownerReference field", SortPriority: 7000, + GeneratableAliasTimelineInfo: []GeneratableAliasTimelineInfo{ + { + AliasedTimelineRelationship: RelationshipChild, + SourceLogType: LogTypeAudit, + Description: "This timeline shows the events and revisions of the owning resources.", + }, + }, }, RelationshipPodBinding: { Visible: true, EnumKeyName: "RelationshipPodBinding", Label: "binds", + LongName: "Pod binding timeline", LabelColor: "#000000", LabelBackgroundColor: "#FF8855", Hint: "Pod binding subresource associated with this node", SortPriority: 8000, + GeneratableAliasTimelineInfo: []GeneratableAliasTimelineInfo{ + { + AliasedTimelineRelationship: RelationshipChild, + SourceLogType: LogTypeAudit, + Description: "This timeline shows the binding subresources associated on a node", + }, + }, }, RelationshipNetworkEndpointGroup: { Visible: true, EnumKeyName: "RelationshipNetworkEndpointGroup", Label: "neg", + LongName: "NEG timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#A52A2A", Hint: "Pod serving status obtained from the associated NEG status", SortPriority: 20500, // later than endpoint slice + GeneratableRevisions: []GeneratableRevisionInfo{ + { + State: RevisionStateConditionTrue, + SourceLogType: LogTypeNetworkAPI, + }, + { + State: RevisionStateConditionFalse, + SourceLogType: LogTypeNetworkAPI, + }, + }, }, RelationshipManagedInstanceGroup: { Visible: true, EnumKeyName: "RelationshipManagedInstanceGroup", Label: "mig", + LongName: "Managed instance group timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#FF5555", Hint: "MIG logs associated to the parent node pool", SortPriority: 10000, + GeneratableEvents: []GeneratableEventInfo{ + { + SourceLogType: LogTypeAutoscaler, + }, + }, }, RelationshipControlPlaneComponent: { Visible: true, EnumKeyName: "RelationshipControlPlaneComponent", Label: "controlplane", + LongName: "Control plane component timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#FF5555", Hint: "control plane component of the cluster", SortPriority: 11000, + GeneratableEvents: []GeneratableEventInfo{ + { + SourceLogType: LogTypeControlPlaneComponent, + }, + }, }, RelationshipSerialPort: { Visible: true, EnumKeyName: "RelationshipSerialPort", Label: "serialport", + LongName: "Serialport log timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#333333", Hint: "Serial port logs of the node", SortPriority: 1500, // in the middle of direct children and status. + GeneratableEvents: []GeneratableEventInfo{ + { + SourceLogType: LogTypeSerialPort, + }, + }, }, } diff --git a/pkg/model/enum/parent_relationship_test.go b/pkg/model/enum/parent_relationship_test.go index 773f795..f103c61 100644 --- a/pkg/model/enum/parent_relationship_test.go +++ b/pkg/model/enum/parent_relationship_test.go @@ -42,6 +42,9 @@ func TestParentRelationshipMetadataIsValid(t *testing.T) { if relationship.LabelColor == "" { t.Errorf("LabelColor in `%s(%d)` is empty", relationship.Label, i) } + if relationship.LongName == "" { + t.Errorf("LongName in `%s(%d)` is empty", relationship.Label, i) + } if relationship.LabelBackgroundColor == "" { t.Errorf("LabelBackgroundColor in `%s(%d)` is empty", relationship.Label, i) } @@ -57,6 +60,15 @@ func TestParentRelationshipMetadataIsValid(t *testing.T) { } } +func TestParentRelationshipMustHaveGeneratableEventsOrRevisions(t *testing.T) { + for i := 0; i <= int(relationshipUnusedEnd); i++ { + relationship := ParentRelationships[ParentRelationship(i)] + if len(relationship.GeneratableEvents) == 0 && len(relationship.GeneratableRevisions) == 0 && len(relationship.GeneratableAliasTimelineInfo) == 0 { + t.Errorf("GeneratableEvents and GeneratableRevisions in `%s(%d)` are both empty", relationship.Label, i) + } + } +} + func TestParentRelationshipOrderIsValid(t *testing.T) { testCase := []struct { name string From e5e728f1b7ce759e9207dd01ce26cebc4d8c0276 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 11:26:15 +0900 Subject: [PATCH 04/23] Added query info on feature document --- cmd/document-generator/main.go | 5 + docs/en/features.md | 394 ++++++++++++++++++ docs/en/inspection-type.md | 94 ++--- docs/template/feature.template.md | 28 ++ docs/template/inspection-types.template.md | 2 +- pkg/document/model/feature.go | 70 ++++ pkg/inspection/task/label.go | 2 + pkg/inspection/task/label/query.go | 36 ++ pkg/inspection/task/label/query_test.go | 37 ++ pkg/source/gcp/query/query.go | 5 +- pkg/source/gcp/task/cloud-composer/query.go | 4 + pkg/source/gcp/task/gke/autoscaler/query.go | 2 +- pkg/source/gcp/task/gke/compute_api/query.go | 5 +- pkg/source/gcp/task/gke/gke_audit/query.go | 2 +- .../gcp/task/gke/k8s_audit/query/query.go | 6 +- .../gcp/task/gke/k8s_container/query.go | 2 +- .../gke/k8s_control_plane_component/query.go | 2 +- pkg/source/gcp/task/gke/k8s_event/query.go | 6 +- pkg/source/gcp/task/gke/k8s_node/query.go | 2 +- pkg/source/gcp/task/gke/network_api/query.go | 6 +- pkg/source/gcp/task/gke/serialport/query.go | 5 +- pkg/source/gcp/task/multicloud_api/query.go | 2 +- pkg/source/gcp/task/onprem_api/query.go | 2 +- 23 files changed, 655 insertions(+), 64 deletions(-) create mode 100644 docs/en/features.md create mode 100644 docs/template/feature.template.md create mode 100644 pkg/inspection/task/label/query.go create mode 100644 pkg/inspection/task/label/query_test.go diff --git a/cmd/document-generator/main.go b/cmd/document-generator/main.go index c68c6b2..e21bde6 100644 --- a/cmd/document-generator/main.go +++ b/cmd/document-generator/main.go @@ -47,6 +47,11 @@ func main() { err = generator.GenerateDocument("./docs/en/inspection-type.md", "inspection-type-template", inspectionTypeDocumentModel, false) fatal(err, "failed to generate inspection type document") + featureDocumentModel, err := model.GetFeatureDocumentModel(inspectionServer) + fatal(err, "failed to generate feature document model") + err = generator.GenerateDocument("./docs/en/features.md", "feature-template", featureDocumentModel, false) + fatal(err, "failed to generate feature document") + logTypeDocumentModel := model.GetLogTypeDocumentModel() err = generator.GenerateDocument("./docs/en/log-types.md", "log-type-template", logTypeDocumentModel, false) fatal(err, "failed to generate log type document") diff --git a/docs/en/features.md b/docs/en/features.md new file mode 100644 index 0000000..2e418e2 --- /dev/null +++ b/docs/en/features.md @@ -0,0 +1,394 @@ + +## [Kubernetes Audit Log(v2)](#cloud.google.com//feature/audit-parser-v2) + +Visualize Kubernetes audit logs in GKE. +This parser reveals how these resources are created,updated or deleted. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + +**Sample used query** + +``` +resource.type="k8s_cluster" +resource.labels.cluster_name="gcp-cluster-name" +protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") +-- Invalid: none of the resources will be selected. Ignoreing kind filter. +-- Invalid: none of the resources will be selected. Ignoreing namespace filter. + +``` + + +## [Kubernetes Event Logs](#cloud.google.com/feature/event-parser) + +Visualize Kubernetes event logs on GKE. +This parser shows events associated to K8s resources + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![3fb549](https://placehold.co/15x15/3fb549/3fb549.png)k8s_event + +**Sample used query** + +``` +logName="projects/gcp-project-id/logs/events" +resource.labels.cluster_name="gcp-cluster-name" +-- Invalid: none of the resources will be selected. Ignoreing namespace filter. +``` + + +## [Kubernetes Node Logs](#cloud.google.com/feature/nodelog-parser) + +GKE worker node components logs mainly from kubelet,containerd and dockerd. + +(WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node + +**Sample used query** + +``` +resource.type="k8s_node" +-logName="projects/gcp-project-id/logs/events" +resource.labels.cluster_name="gcp-cluster-name" +resource.labels.node_name:("gke-test-cluster-node-1" OR "gke-test-cluster-node-2") + +``` + + +## [Kubernetes container logs](#cloud.google.com/feature/container-parser) + +Container logs ingested from stdout/stderr of workload Pods. + +(WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter or target namespace fewer. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container + +**Sample used query** + +``` +resource.type="k8s_container" +resource.labels.cluster_name="gcp-cluster-name" +-- Invalid: none of the resources will be selected. Ignoreing kind filter. +-- Invalid: none of the resources will be selected. Ignoreing kind filter. +``` + + +## [GKE Audit logs](#cloud.google.com/feature/gke-audit-parser) + +GKE audit log including cluster creation,deletion and upgrades. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit + +**Sample used query** + +``` +resource.type=("gke_cluster" OR "gke_nodepool") +logName="projects/gcp-project-id/logs/cloudaudit.googleapis.com%2Factivity" +resource.labels.cluster_name="gcp-cluster-name" +``` + + +## [Compute API Logs](#cloud.google.com/feature/compute-api-parser) + +Compute API audit logs used for cluster related logs. This also visualize operations happened during the query time. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + +**Sample used query** + +``` +resource.type="k8s_cluster" +resource.labels.cluster_name="gcp-cluster-name" +protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") +-- Invalid: none of the resources will be selected. Ignoreing kind filter. +-- Invalid: none of the resources will be selected. Ignoreing namespace filter. + +``` + + +#### ![FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api + +**Sample used query** + +``` +resource.type="gce_instance" + -protoPayload.methodName:("list" OR "get" OR "watch") + protoPayload.resourceName:(instances/gke-test-cluster-node-1 OR instances/gke-test-cluster-node-2) + +``` + + +## [GCE Network Logs](#cloud.google.com/feature/network-api-parser) + +GCE network API audit log including NEG related audit logs to identify when the associated NEG was attached/detached. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + +**Sample used query** + +``` +resource.type="k8s_cluster" +resource.labels.cluster_name="gcp-cluster-name" +protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") +-- Invalid: none of the resources will be selected. Ignoreing kind filter. +-- Invalid: none of the resources will be selected. Ignoreing namespace filter. + +``` + + +#### ![33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api + +**Sample used query** + +``` +resource.type="gce_network" +-protoPayload.methodName:("list" OR "get" OR "watch") +protoPayload.resourceName:(networkEndpointGroups/neg-id-1 OR networkEndpointGroups/neg-id-2) + +``` + + +## [MultiCloud API logs](#cloud.google.com/feature/multicloud-audit-parser) + +Anthos Multicloud audit log including cluster creation,deletion and upgrades. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)multicloud_api + +**Sample used query** + +``` +resource.type="audited_resource" +resource.labels.service="gkemulticloud.googleapis.com" +resource.labels.method:("Update" OR "Create" OR "Delete") +protoPayload.resourceName:"awsClusters/cluster-foo" + +``` + + +## [Autoscaler Logs](#cloud.google.com/feature/autoscaler-parser) + +Autoscaler logs including decision reasons why they scale up/down or why they didn't. +This log type also includes Node Auto Provisioner logs. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler + +**Sample used query** + +``` +resource.type="k8s_cluster" +resource.labels.project_id="gcp-project-id" +resource.labels.cluster_name="gcp-cluster-name" +-jsonPayload.status: "" +logName="projects/gcp-project-id/logs/container.googleapis.com%2Fcluster-autoscaler-visibility" +``` + + +## [OnPrem API logs](#cloud.google.com/feature/onprem-audit-parser) + +Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)onprem_api + +**Sample used query** + +``` +resource.type="audited_resource" +resource.labels.service="gkeonprem.googleapis.com" +resource.labels.method:("Update" OR "Create" OR "Delete" OR "Enroll" OR "Unenroll") +protoPayload.resourceName:"baremetalClusters/my-cluster" + +``` + + +## [Kubernetes Control plane component logs](#cloud.google.com/feature/controlplane-component-parser) + +Visualize Kubernetes control plane component logs on a cluster + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component + +**Sample used query** + +``` +resource.type="k8s_control_plane_component" +resource.labels.cluster_name="gcp-cluster-name" +resource.labels.project_id="gcp-project-id" +-sourceLocation.file="httplog.go" +-- Invalid: none of the controlplane component will be selected. Ignoreing component name filter. +``` + + +## [Node serial port logs](#cloud.google.com/feature/serialport) + +Serial port logs of worker nodes. Serial port logging feature must be enabled on instances to query logs correctly. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + +**Sample used query** + +``` +resource.type="k8s_cluster" +resource.labels.cluster_name="gcp-cluster-name" +protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") +-- Invalid: none of the resources will be selected. Ignoreing kind filter. +-- Invalid: none of the resources will be selected. Ignoreing namespace filter. + +``` + + +#### ![333333](https://placehold.co/15x15/333333/333333.png)serial_port + +**Sample used query** + +``` +LOG_ID("serialconsole.googleapis.com%2Fserial_port_1_output") OR +LOG_ID("serialconsole.googleapis.com%2Fserial_port_2_output") OR +LOG_ID("serialconsole.googleapis.com%2Fserial_port_3_output") OR +LOG_ID("serialconsole.googleapis.com%2Fserial_port_debug_output") + +labels."compute.googleapis.com/resource_name"=("gke-test-cluster-node-1" OR "gke-test-cluster-node-2") + +-- No node name substring filters are specified. +``` + + +## [(Alpha) Composer / Airflow Scheduler](#cloud.google.com/composer/scheduler) + +Airflow Scheduler logs contain information related to the scheduling of TaskInstances, making it an ideal source for understanding the lifecycle of TaskInstances. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment + +**Sample used query** + +``` +TODO: add sample query +``` + + +## [(Alpha) Cloud Composer / Airflow Worker](#cloud.google.com/composer/worker) + +Airflow Worker logs contain information related to the execution of TaskInstances. By including these logs, you can gain insights into where and how each TaskInstance was executed. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment + +**Sample used query** + +``` +TODO: add sample query +``` + + +## [(Alpha) Composer / Airflow DagProcessorManager](#cloud.google.com/composer/dagprocessor) + +The DagProcessorManager logs contain information for investigating the number of DAGs included in each Python file and the time it took to parse them. You can get information about missing DAGs and load. + + + +### Depending Queries + +Following log queries are used with this feature. + + +#### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment + +**Sample used query** + +``` +TODO: add sample query +``` + diff --git a/docs/en/inspection-type.md b/docs/en/inspection-type.md index 3d3896c..ec0575f 100644 --- a/docs/en/inspection-type.md +++ b/docs/en/inspection-type.md @@ -12,34 +12,34 @@ Inspection type is ... -* Kubernetes Audit Log(v2) +* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) -* Kubernetes Event Logs +* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) -* Kubernetes Node Logs +* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) -* Kubernetes container logs +* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) -* GKE Audit logs +* [GKE Audit logs](./features.md#cloud.google.com/feature/gke-audit-parser) -* Compute API Logs +* [Compute API Logs](./features.md#cloud.google.com/feature/compute-api-parser) -* GCE Network Logs +* [GCE Network Logs](./features.md#cloud.google.com/feature/network-api-parser) -* Autoscaler Logs +* [Autoscaler Logs](./features.md#cloud.google.com/feature/autoscaler-parser) -* Kubernetes Control plane component logs +* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) -* Node serial port logs +* [Node serial port logs](./features.md#cloud.google.com/feature/serialport) ## [Cloud Composer](#gcp-composer) @@ -48,43 +48,43 @@ Inspection type is ... -* Kubernetes Audit Log(v2) +* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) -* Kubernetes Event Logs +* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) -* Kubernetes Node Logs +* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) -* Kubernetes container logs +* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) -* GKE Audit logs +* [GKE Audit logs](./features.md#cloud.google.com/feature/gke-audit-parser) -* Compute API Logs +* [Compute API Logs](./features.md#cloud.google.com/feature/compute-api-parser) -* GCE Network Logs +* [GCE Network Logs](./features.md#cloud.google.com/feature/network-api-parser) -* Autoscaler Logs +* [Autoscaler Logs](./features.md#cloud.google.com/feature/autoscaler-parser) -* Kubernetes Control plane component logs +* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) -* Node serial port logs +* [Node serial port logs](./features.md#cloud.google.com/feature/serialport) -* (Alpha) Composer / Airflow Scheduler +* [(Alpha) Composer / Airflow Scheduler](./features.md#cloud.google.com/composer/scheduler) -* (Alpha) Cloud Composer / Airflow Worker +* [(Alpha) Cloud Composer / Airflow Worker](./features.md#cloud.google.com/composer/worker) -* (Alpha) Composer / Airflow DagProcessorManager +* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#cloud.google.com/composer/dagprocessor) ## [GKE on AWS(Anthos on AWS)](#gcp-gke-on-aws) @@ -93,22 +93,22 @@ Inspection type is ... -* Kubernetes Audit Log(v2) +* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) -* Kubernetes Event Logs +* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) -* Kubernetes Node Logs +* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) -* Kubernetes container logs +* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) -* MultiCloud API logs +* [MultiCloud API logs](./features.md#cloud.google.com/feature/multicloud-audit-parser) -* Kubernetes Control plane component logs +* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) ## [GKE on Azure(Anthos on Azure)](#gcp-gke-on-azure) @@ -117,22 +117,22 @@ Inspection type is ... -* Kubernetes Audit Log(v2) +* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) -* Kubernetes Event Logs +* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) -* Kubernetes Node Logs +* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) -* Kubernetes container logs +* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) -* MultiCloud API logs +* [MultiCloud API logs](./features.md#cloud.google.com/feature/multicloud-audit-parser) -* Kubernetes Control plane component logs +* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) ## [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](#gcp-gdcv-for-baremetal) @@ -141,22 +141,22 @@ Inspection type is ... -* Kubernetes Audit Log(v2) +* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) -* Kubernetes Event Logs +* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) -* Kubernetes Node Logs +* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) -* Kubernetes container logs +* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) -* OnPrem API logs +* [OnPrem API logs](./features.md#cloud.google.com/feature/onprem-audit-parser) -* Kubernetes Control plane component logs +* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) ## [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](#gcp-gdcv-for-vmware) @@ -165,20 +165,20 @@ Inspection type is ... -* Kubernetes Audit Log(v2) +* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) -* Kubernetes Event Logs +* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) -* Kubernetes Node Logs +* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) -* Kubernetes container logs +* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) -* OnPrem API logs +* [OnPrem API logs](./features.md#cloud.google.com/feature/onprem-audit-parser) -* Kubernetes Control plane component logs +* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) diff --git a/docs/template/feature.template.md b/docs/template/feature.template.md new file mode 100644 index 0000000..855bcf4 --- /dev/null +++ b/docs/template/feature.template.md @@ -0,0 +1,28 @@ +{{define "feature-template"}} +{{range $index,$feature := .Features }} + +## [{{$feature.Name}}](#{{$feature.ID}}) + +{{$feature.Description}} + + +{{with $feature.Queries}} + +### Depending Queries + +Following log queries are used with this feature. + +{{range $index,$query := $feature.Queries}} + +#### ![{{$query.LogTypeColorCode}}](https://placehold.co/15x15/{{$query.LogTypeColorCode}}/{{$query.LogTypeColorCode}}.png){{$query.LogTypeLabel}} + +**Sample used query** + +``` +{{$query.SampleQuery}} +``` + +{{end}} +{{end}} +{{end}} +{{end}} \ No newline at end of file diff --git a/docs/template/inspection-types.template.md b/docs/template/inspection-types.template.md index ba224f3..0eb22db 100644 --- a/docs/template/inspection-types.template.md +++ b/docs/template/inspection-types.template.md @@ -15,7 +15,7 @@ Inspection type is ... {{range $feature := $type.SupportedFeatures}} -* {{$feature.Name}} +* [{{$feature.Name}}](./features.md#{{$feature.ID}}) {{end}} {{end}} diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index 8b53790..19b299a 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -1 +1,71 @@ package model + +import ( + "strings" + + "github.com/GoogleCloudPlatform/khi/pkg/inspection" + inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" + "github.com/GoogleCloudPlatform/khi/pkg/inspection/task/label" + "github.com/GoogleCloudPlatform/khi/pkg/inspection/taskfilter" + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" + "github.com/GoogleCloudPlatform/khi/pkg/task" +) + +type FeatureDocumentModel struct { + Features []FeatureDocumentElement +} + +type FeatureDocumentElement struct { + ID string + Name string + Description string + + Queries []FeatureDependentQueryElement +} + +type FeatureDependentQueryElement struct { + ID string + LogType enum.LogType + LogTypeLabel string + LogTypeColorCode string + SampleQuery string +} + +func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*FeatureDocumentModel, error) { + result := FeatureDocumentModel{} + features := taskServer.RootTaskSet.FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false) + for _, feature := range features.GetAll() { + queryElements := []FeatureDependentQueryElement{} + + // Get query related tasks required by this feature. + resolveSource, err := task.NewSet([]task.Definition{feature}) + if err != nil { + return nil, err + } + resolved, err := resolveSource.ResolveTask(taskServer.RootTaskSet) + if err != nil { + return nil, err + } + queryTasks := resolved.FilteredSubset(label.TaskLabelKeyIsQueryTask, taskfilter.HasTrue, false).GetAll() + for _, queryTask := range queryTasks { + logTypeKey := enum.LogType(queryTask.Labels().GetOrDefault(label.TaskLabelKeyQueryTaskTargetLogType, enum.LogTypeUnknown).(enum.LogType)) + logType := enum.LogTypes[logTypeKey] + queryElements = append(queryElements, FeatureDependentQueryElement{ + ID: queryTask.ID().String(), + LogType: logTypeKey, + LogTypeLabel: logType.Label, + LogTypeColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), + SampleQuery: queryTask.Labels().GetOrDefault(label.TaskLabelKeyQueryTaskSampleQuery, "").(string), + }) + } + + result.Features = append(result.Features, FeatureDocumentElement{ + ID: feature.ID().String(), + Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), + Description: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), + Queries: queryElements, + }) + + } + return &result, nil +} diff --git a/pkg/inspection/task/label.go b/pkg/inspection/task/label.go index 9969dc3..3f3a7c6 100644 --- a/pkg/inspection/task/label.go +++ b/pkg/inspection/task/label.go @@ -16,6 +16,8 @@ package task import common_task "github.com/GoogleCloudPlatform/khi/pkg/task" +//TODO: move task label related constants to ./label + const ( InspectionTaskPrefix = common_task.KHISystemPrefix + "inspection/" LabelKeyInspectionFeatureFlag = InspectionTaskPrefix + "feature" diff --git a/pkg/inspection/task/label/query.go b/pkg/inspection/task/label/query.go new file mode 100644 index 0000000..46405d8 --- /dev/null +++ b/pkg/inspection/task/label/query.go @@ -0,0 +1,36 @@ +package label + +import ( + inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" + "github.com/GoogleCloudPlatform/khi/pkg/task" +) + +const ( + TaskLabelKeyIsQueryTask = inspection_task.InspectionTaskPrefix + "is-query-task" + TaskLabelKeyQueryTaskTargetLogType = inspection_task.InspectionTaskPrefix + "query-task-target-log-type" + TaskLabelKeyQueryTaskSampleQuery = inspection_task.InspectionTaskPrefix + "query-task-sample-query" +) + +type QueryTaskLabelOpt struct { + TargetLogType enum.LogType + SampleQuery string +} + +// Write implements task.LabelOpt. +func (q *QueryTaskLabelOpt) Write(label *task.LabelSet) { + label.Set(TaskLabelKeyIsQueryTask, true) + label.Set(TaskLabelKeyQueryTaskTargetLogType, q.TargetLogType) + label.Set(TaskLabelKeyQueryTaskSampleQuery, q.SampleQuery) + +} + +var _ (task.LabelOpt) = (*QueryTaskLabelOpt)(nil) + +// NewQueryTaskLabelOpt constucts a new instance of task.LabelOpt for query related tasks. +func NewQueryTaskLabelOpt(targetLogType enum.LogType, sampleQuery string) *QueryTaskLabelOpt { + return &QueryTaskLabelOpt{ + TargetLogType: targetLogType, + SampleQuery: sampleQuery, + } +} diff --git a/pkg/inspection/task/label/query_test.go b/pkg/inspection/task/label/query_test.go new file mode 100644 index 0000000..ad9d9bb --- /dev/null +++ b/pkg/inspection/task/label/query_test.go @@ -0,0 +1,37 @@ +package label + +import ( + "testing" + + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" + "github.com/GoogleCloudPlatform/khi/pkg/task" +) + +func TestQueryTaskLabelOpt(t *testing.T) { + labelOpt := NewQueryTaskLabelOpt(enum.LogTypeComputeApi, "sample query") + label := task.NewLabelSet(labelOpt) + + anyQueryTask, exists := label.Get(TaskLabelKeyIsQueryTask) + if !exists { + t.Errorf("TaskLabel %s is expected to be set, but it is not", TaskLabelKeyIsQueryTask) + } + if anyQueryTask.(bool) != true { + t.Errorf("TaskLabel %s is expected to be true, but it is %v", TaskLabelKeyIsQueryTask, anyQueryTask) + } + + targetLogType, exists := label.Get(TaskLabelKeyQueryTaskTargetLogType) + if !exists { + t.Errorf("TaskLabel %s is expected to be set, but it is not", TaskLabelKeyQueryTaskTargetLogType) + } + if targetLogType.(enum.LogType) != enum.LogTypeComputeApi { + t.Errorf("TaskLabel %s is expected to be %v, but it is %v", TaskLabelKeyQueryTaskTargetLogType, enum.LogTypeComputeApi, targetLogType) + } + + sampleQuery, exists := label.Get(TaskLabelKeyQueryTaskSampleQuery) + if !exists { + t.Errorf("TaskLabel %s is expected to be set, but it is not", TaskLabelKeyQueryTaskSampleQuery) + } + if sampleQuery.(string) != "sample query" { + t.Errorf("TaskLabel %s is expected to be sample query, but it is %v", TaskLabelKeyQueryTaskSampleQuery, sampleQuery) + } +} diff --git a/pkg/source/gcp/query/query.go b/pkg/source/gcp/query/query.go index ef2bdc3..6d51226 100644 --- a/pkg/source/gcp/query/query.go +++ b/pkg/source/gcp/query/query.go @@ -26,6 +26,7 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata/progress" "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata/query" inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" + "github.com/GoogleCloudPlatform/khi/pkg/inspection/task/label" "github.com/GoogleCloudPlatform/khi/pkg/log" "github.com/GoogleCloudPlatform/khi/pkg/model/enum" "github.com/GoogleCloudPlatform/khi/pkg/source/gcp/api" @@ -43,7 +44,7 @@ type QueryGeneratorFunc = func(context.Context, int, *task.VariableSet) ([]strin var queryThreadPool = worker.NewPool(16) -func NewQueryGeneratorTask(taskId string, readableQueryName string, logType enum.LogType, dependencies []string, generator QueryGeneratorFunc) task.Definition { +func NewQueryGeneratorTask(taskId string, readableQueryName string, logType enum.LogType, dependencies []string, generator QueryGeneratorFunc, sampleQuery string) task.Definition { return inspection_task.NewInspectionProcessor(taskId, append(dependencies, gcp_task.InputProjectIdTaskID, gcp_task.InputStartTimeTaskID, gcp_task.InputEndTimeTaskID, inspection_task.ReaderFactoryGeneratorTaskID), func(ctx context.Context, taskMode int, v *task.VariableSet, progress *progress.TaskProgress) (any, error) { client, err := api.DefaultGCPClientFactory.NewClient() if err != nil { @@ -124,5 +125,5 @@ func NewQueryGeneratorTask(taskId string, readableQueryName string, logType enum } return []*log.LogEntity{}, err - }) + }, label.NewQueryTaskLabelOpt(logType, sampleQuery)) } diff --git a/pkg/source/gcp/task/cloud-composer/query.go b/pkg/source/gcp/task/cloud-composer/query.go index c7d1d25..e38c2d8 100644 --- a/pkg/source/gcp/task/cloud-composer/query.go +++ b/pkg/source/gcp/task/cloud-composer/query.go @@ -40,6 +40,7 @@ var ComposerSchedulerLogQueryTask = query.NewQueryGeneratorTask( InputComposerEnvironmentTaskID, }, createGenerator("airflow-scheduler"), + "TODO: add sample query", ) var ComposerDagProcessorManagerLogQueryTask = query.NewQueryGeneratorTask( @@ -51,6 +52,7 @@ var ComposerDagProcessorManagerLogQueryTask = query.NewQueryGeneratorTask( InputComposerEnvironmentTaskID, }, createGenerator("dag-processor-manager"), + "TODO: add sample query", ) var ComposerMonitoringLogQueryTask = query.NewQueryGeneratorTask( @@ -62,6 +64,7 @@ var ComposerMonitoringLogQueryTask = query.NewQueryGeneratorTask( InputComposerEnvironmentTaskID, }, createGenerator("airflow-monitoring"), + "TODO: add sample query", ) var ComposerWorkerLogQueryTask = query.NewQueryGeneratorTask( @@ -73,6 +76,7 @@ var ComposerWorkerLogQueryTask = query.NewQueryGeneratorTask( InputComposerEnvironmentTaskID, }, createGenerator("airflow-worker"), + "TODO: add sample query", ) func createGenerator(componentName string) func(ctx context.Context, i int, vs *task.VariableSet) ([]string, error) { diff --git a/pkg/source/gcp/task/gke/autoscaler/query.go b/pkg/source/gcp/task/gke/autoscaler/query.go index cdf96f1..ff9195b 100644 --- a/pkg/source/gcp/task/gke/autoscaler/query.go +++ b/pkg/source/gcp/task/gke/autoscaler/query.go @@ -51,4 +51,4 @@ var AutoscalerQueryTask = query.NewQueryGeneratorTask(AutoscalerQueryTaskID, "Au return []string{}, err } return []string{GenerateAutoscalerQuery(projectId, clusterName, true)}, nil -}) +}, GenerateAutoscalerQuery("gcp-project-id", "gcp-cluster-name", true)) diff --git a/pkg/source/gcp/task/gke/compute_api/query.go b/pkg/source/gcp/task/gke/compute_api/query.go index 88d2b8b..38c598f 100644 --- a/pkg/source/gcp/task/gke/compute_api/query.go +++ b/pkg/source/gcp/task/gke/compute_api/query.go @@ -64,4 +64,7 @@ var ComputeAPIQueryTask = query.NewQueryGeneratorTask(ComputeAPIQueryTaskID, "Co return []string{}, err } return GenerateComputeAPIQuery(i, builder.ClusterResource.GetNodes()), nil -}) +}, GenerateComputeAPIQuery(inspection_task.TaskModeRun, []string{ + "gke-test-cluster-node-1", + "gke-test-cluster-node-2", +})[0]) diff --git a/pkg/source/gcp/task/gke/gke_audit/query.go b/pkg/source/gcp/task/gke/gke_audit/query.go index f4b0a99..a73faf6 100644 --- a/pkg/source/gcp/task/gke/gke_audit/query.go +++ b/pkg/source/gcp/task/gke/gke_audit/query.go @@ -45,4 +45,4 @@ var GKEAuditQueryTask = query.NewQueryGeneratorTask(GKEAuditLogQueryTaskID, "GKE } return []string{GenerateGKEAuditQuery(projectId, clusterName)}, nil -}) +}, GenerateGKEAuditQuery("gcp-project-id", "gcp-cluster-name")) diff --git a/pkg/source/gcp/task/gke/k8s_audit/query/query.go b/pkg/source/gcp/task/gke/k8s_audit/query/query.go index 570f74f..9b58435 100644 --- a/pkg/source/gcp/task/gke/k8s_audit/query/query.go +++ b/pkg/source/gcp/task/gke/k8s_audit/query/query.go @@ -46,7 +46,11 @@ var Task = query.NewQueryGeneratorTask(k8saudittask.K8sAuditQueryTaskID, "K8s au return []string{}, err } return []string{GenerateK8sAuditQuery(clusterName, kindFilter, namespaceFilter)}, nil -}) +}, GenerateK8sAuditQuery( + "gcp-cluster-name", + &queryutil.SetFilterParseResult{}, + &queryutil.SetFilterParseResult{}, +)) func GenerateK8sAuditQuery(clusterName string, auditKindFilter *queryutil.SetFilterParseResult, namespaceFilter *queryutil.SetFilterParseResult) string { return fmt.Sprintf(`resource.type="k8s_cluster" diff --git a/pkg/source/gcp/task/gke/k8s_container/query.go b/pkg/source/gcp/task/gke/k8s_container/query.go index 6b2709d..96ad00e 100644 --- a/pkg/source/gcp/task/gke/k8s_container/query.go +++ b/pkg/source/gcp/task/gke/k8s_container/query.go @@ -103,4 +103,4 @@ var GKEContainerQueryTask = query.NewQueryGeneratorTask(GKEContainerLogQueryTask return []string{}, err } return []string{GenerateK8sContainerQuery(clusterName, namespacesFilter, podNamesFilter)}, nil -}) +}, GenerateK8sContainerQuery("gcp-cluster-name", &queryutil.SetFilterParseResult{}, &queryutil.SetFilterParseResult{})) diff --git a/pkg/source/gcp/task/gke/k8s_control_plane_component/query.go b/pkg/source/gcp/task/gke/k8s_control_plane_component/query.go index 28389f3..3a610a2 100644 --- a/pkg/source/gcp/task/gke/k8s_control_plane_component/query.go +++ b/pkg/source/gcp/task/gke/k8s_control_plane_component/query.go @@ -54,7 +54,7 @@ var GKEK8sControlPlaneLogQueryTask = query.NewQueryGeneratorTask(GKEK8sControlPl return []string{}, err } return []string{GenerateK8sControlPlaneQuery(clusterName, projectId, controlPlaneComponentNameFilter)}, nil -}) +}, GenerateK8sControlPlaneQuery("gcp-cluster-name", "gcp-project-id", &queryutil.SetFilterParseResult{})) func generateK8sControlPlaneComponentFilter(filter *queryutil.SetFilterParseResult) string { if filter.ValidationError != "" { diff --git a/pkg/source/gcp/task/gke/k8s_event/query.go b/pkg/source/gcp/task/gke/k8s_event/query.go index ffb9230..3572fdd 100644 --- a/pkg/source/gcp/task/gke/k8s_event/query.go +++ b/pkg/source/gcp/task/gke/k8s_event/query.go @@ -88,4 +88,8 @@ var GKEK8sEventLogQueryTask = query.NewQueryGeneratorTask(GKEK8sEventLogQueryTas return []string{}, err } return []string{GenerateK8sEventQuery(clusterName, projectId, namespaceFilter)}, nil -}) +}, GenerateK8sEventQuery( + "gcp-cluster-name", + "gcp-project-id", + &queryutil.SetFilterParseResult{}, +)) diff --git a/pkg/source/gcp/task/gke/k8s_node/query.go b/pkg/source/gcp/task/gke/k8s_node/query.go index 47d1d91..9d6777e 100644 --- a/pkg/source/gcp/task/gke/k8s_node/query.go +++ b/pkg/source/gcp/task/gke/k8s_node/query.go @@ -62,4 +62,4 @@ var GKENodeQueryTask = query.NewQueryGeneratorTask(GKENodeLogQueryTaskID, "Kuber return nil, err } return []string{GenerateK8sNodeLogQuery(projectId, clusterName, nodeNameSubstrings)}, nil -}) +}, GenerateK8sNodeLogQuery("gcp-project-id", "gcp-cluster-name", []string{"gke-test-cluster-node-1", "gke-test-cluster-node-2"})) diff --git a/pkg/source/gcp/task/gke/network_api/query.go b/pkg/source/gcp/task/gke/network_api/query.go index f17fce3..6da6b5f 100644 --- a/pkg/source/gcp/task/gke/network_api/query.go +++ b/pkg/source/gcp/task/gke/network_api/query.go @@ -32,8 +32,8 @@ const GCPNetworkLogQueryTaskID = query.GKEQueryPrefix + "network-api" func GenerateGCPNetworkAPIQuery(taskMode int, negNames []string) []string { nodeNamesWithNetworkEndpointGroups := []string{} - for _, nodeName := range negNames { - nodeNamesWithNetworkEndpointGroups = append(nodeNamesWithNetworkEndpointGroups, fmt.Sprintf("networkEndpointGroups/%s", nodeName)) + for _, negName := range negNames { + nodeNamesWithNetworkEndpointGroups = append(nodeNamesWithNetworkEndpointGroups, fmt.Sprintf("networkEndpointGroups/%s", negName)) } if taskMode == inspection_task.TaskModeDryRun { return []string{queryFromNegNameFilter("-- neg name filters to be determined after audit log query")} @@ -63,4 +63,4 @@ var GCPNetworkLogQueryTask = query.NewQueryGeneratorTask(GCPNetworkLogQueryTaskI return []string{}, err } return GenerateGCPNetworkAPIQuery(i, builder.ClusterResource.NEGs.GetAllIdentifiers()), nil -}) +}, GenerateGCPNetworkAPIQuery(inspection_task.TaskModeRun, []string{"neg-id-1", "neg-id-2"})[0]) diff --git a/pkg/source/gcp/task/gke/serialport/query.go b/pkg/source/gcp/task/gke/serialport/query.go index ef01925..982df4f 100644 --- a/pkg/source/gcp/task/gke/serialport/query.go +++ b/pkg/source/gcp/task/gke/serialport/query.go @@ -81,4 +81,7 @@ var GKESerialPortLogQueryTask = query.NewQueryGeneratorTask(SerialPortLogQueryTa return nil, err } return GenerateSerialPortQuery(taskMode, builder.ClusterResource.GetNodes(), nodeNameSubstrings), nil -}) +}, GenerateSerialPortQuery(inspection_task.TaskModeRun, []string{ + "gke-test-cluster-node-1", + "gke-test-cluster-node-2", +}, []string{})[0]) diff --git a/pkg/source/gcp/task/multicloud_api/query.go b/pkg/source/gcp/task/multicloud_api/query.go index bb69985..b86aacf 100644 --- a/pkg/source/gcp/task/multicloud_api/query.go +++ b/pkg/source/gcp/task/multicloud_api/query.go @@ -43,4 +43,4 @@ var MultiCloudAPIQueryTask = query.NewQueryGeneratorTask(MultiCloudAPIQueryTaskI return []string{}, err } return []string{GenerateMultiCloudAPIQuery(clusterName)}, nil -}) +}, GenerateMultiCloudAPIQuery("awsClusters/cluster-foo")) diff --git a/pkg/source/gcp/task/onprem_api/query.go b/pkg/source/gcp/task/onprem_api/query.go index 0226afc..d22da29 100644 --- a/pkg/source/gcp/task/onprem_api/query.go +++ b/pkg/source/gcp/task/onprem_api/query.go @@ -43,4 +43,4 @@ var OnPremAPIQueryTask = query.NewQueryGeneratorTask(OnPremCloudAPIQueryTaskID, return []string{}, err } return []string{GenerateOnPremAPIQuery(clusterName)}, nil -}) +}, GenerateOnPremAPIQuery("baremetalClusters/my-cluster")) From 9bca5a8661b965bbad667b57abb73311d21b6ae0 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 11:57:03 +0900 Subject: [PATCH 05/23] Use shorter ID for anchoring headers for feature tasks --- docs/en/features.md | 222 +++++++------- docs/en/inspection-type.md | 282 +++++++++--------- pkg/document/model/feature.go | 2 +- pkg/document/model/inspection_type.go | 2 +- pkg/inspection/task/label.go | 7 +- pkg/parser/parser.go | 6 +- pkg/server/server_test.go | 8 +- pkg/source/gcp/task/cloud-composer/parser.go | 15 + pkg/source/gcp/task/gke/autoscaler/parser.go | 5 + pkg/source/gcp/task/gke/compute_api/parser.go | 5 + pkg/source/gcp/task/gke/gke_audit/parser.go | 5 + .../gcp/task/gke/k8s_audit/recorder/task.go | 2 +- .../gcp/task/gke/k8s_container/parser.go | 5 + .../gke/k8s_control_plane_component/parser.go | 5 + pkg/source/gcp/task/gke/k8s_event/parser.go | 5 + pkg/source/gcp/task/gke/k8s_node/parser.go | 5 + pkg/source/gcp/task/gke/network_api/parser.go | 5 + pkg/source/gcp/task/gke/serialport/parser.go | 5 + pkg/source/gcp/task/multicloud_api/parser.go | 5 + pkg/source/gcp/task/onprem_api/parser.go | 5 + 20 files changed, 340 insertions(+), 261 deletions(-) diff --git a/docs/en/features.md b/docs/en/features.md index 2e418e2..8d123e8 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -1,16 +1,16 @@ - -## [Kubernetes Audit Log(v2)](#cloud.google.com//feature/audit-parser-v2) + +## [Kubernetes Audit Log(v2)](#k8s_audit) Visualize Kubernetes audit logs in GKE. This parser reveals how these resources are created,updated or deleted. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit **Sample used query** @@ -23,20 +23,20 @@ protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing namespace filter. ``` - - -## [Kubernetes Event Logs](#cloud.google.com/feature/event-parser) + + +## [Kubernetes Event Logs](#k8s_event) Visualize Kubernetes event logs on GKE. This parser shows events associated to K8s resources - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![3fb549](https://placehold.co/15x15/3fb549/3fb549.png)k8s_event **Sample used query** @@ -46,21 +46,21 @@ logName="projects/gcp-project-id/logs/events" resource.labels.cluster_name="gcp-cluster-name" -- Invalid: none of the resources will be selected. Ignoreing namespace filter. ``` - - -## [Kubernetes Node Logs](#cloud.google.com/feature/nodelog-parser) + + +## [Kubernetes Node Logs](#k8s_node) GKE worker node components logs mainly from kubelet,containerd and dockerd. (WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node **Sample used query** @@ -72,21 +72,21 @@ resource.labels.cluster_name="gcp-cluster-name" resource.labels.node_name:("gke-test-cluster-node-1" OR "gke-test-cluster-node-2") ``` - - -## [Kubernetes container logs](#cloud.google.com/feature/container-parser) + + +## [Kubernetes container logs](#k8s_container) Container logs ingested from stdout/stderr of workload Pods. (WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter or target namespace fewer. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container **Sample used query** @@ -97,19 +97,19 @@ resource.labels.cluster_name="gcp-cluster-name" -- Invalid: none of the resources will be selected. Ignoreing kind filter. -- Invalid: none of the resources will be selected. Ignoreing kind filter. ``` - - -## [GKE Audit logs](#cloud.google.com/feature/gke-audit-parser) + + +## [GKE Audit logs](#gke_audit) GKE audit log including cluster creation,deletion and upgrades. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit **Sample used query** @@ -119,19 +119,19 @@ resource.type=("gke_cluster" OR "gke_nodepool") logName="projects/gcp-project-id/logs/cloudaudit.googleapis.com%2Factivity" resource.labels.cluster_name="gcp-cluster-name" ``` - - -## [Compute API Logs](#cloud.google.com/feature/compute-api-parser) + + +## [Compute API Logs](#compute_api) Compute API audit logs used for cluster related logs. This also visualize operations happened during the query time. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit **Sample used query** @@ -144,8 +144,8 @@ protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing namespace filter. ``` - - + + #### ![FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api **Sample used query** @@ -156,19 +156,19 @@ resource.type="gce_instance" protoPayload.resourceName:(instances/gke-test-cluster-node-1 OR instances/gke-test-cluster-node-2) ``` - - -## [GCE Network Logs](#cloud.google.com/feature/network-api-parser) + + +## [GCE Network Logs](#gce_network) GCE network API audit log including NEG related audit logs to identify when the associated NEG was attached/detached. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit **Sample used query** @@ -181,8 +181,8 @@ protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing namespace filter. ``` - - + + #### ![33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api **Sample used query** @@ -193,19 +193,19 @@ resource.type="gce_network" protoPayload.resourceName:(networkEndpointGroups/neg-id-1 OR networkEndpointGroups/neg-id-2) ``` - - -## [MultiCloud API logs](#cloud.google.com/feature/multicloud-audit-parser) + + +## [MultiCloud API logs](#multicloud_api) Anthos Multicloud audit log including cluster creation,deletion and upgrades. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)multicloud_api **Sample used query** @@ -217,20 +217,20 @@ resource.labels.method:("Update" OR "Create" OR "Delete") protoPayload.resourceName:"awsClusters/cluster-foo" ``` - - -## [Autoscaler Logs](#cloud.google.com/feature/autoscaler-parser) + + +## [Autoscaler Logs](#autoscaler) Autoscaler logs including decision reasons why they scale up/down or why they didn't. This log type also includes Node Auto Provisioner logs. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler **Sample used query** @@ -242,19 +242,19 @@ resource.labels.cluster_name="gcp-cluster-name" -jsonPayload.status: "" logName="projects/gcp-project-id/logs/container.googleapis.com%2Fcluster-autoscaler-visibility" ``` - - -## [OnPrem API logs](#cloud.google.com/feature/onprem-audit-parser) + + +## [OnPrem API logs](#onprem_api) Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)onprem_api **Sample used query** @@ -266,19 +266,19 @@ resource.labels.method:("Update" OR "Create" OR "Delete" OR "Enroll" OR "Unenrol protoPayload.resourceName:"baremetalClusters/my-cluster" ``` - - -## [Kubernetes Control plane component logs](#cloud.google.com/feature/controlplane-component-parser) + + +## [Kubernetes Control plane component logs](#k8s_control_plane_component) Visualize Kubernetes control plane component logs on a cluster - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component **Sample used query** @@ -290,19 +290,19 @@ resource.labels.project_id="gcp-project-id" -sourceLocation.file="httplog.go" -- Invalid: none of the controlplane component will be selected. Ignoreing component name filter. ``` - - -## [Node serial port logs](#cloud.google.com/feature/serialport) + + +## [Node serial port logs](#serialport) Serial port logs of worker nodes. Serial port logging feature must be enabled on instances to query logs correctly. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit **Sample used query** @@ -315,8 +315,8 @@ protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing namespace filter. ``` - - + + #### ![333333](https://placehold.co/15x15/333333/333333.png)serial_port **Sample used query** @@ -331,19 +331,19 @@ labels."compute.googleapis.com/resource_name"=("gke-test-cluster-node-1" OR "gke -- No node name substring filters are specified. ``` - - -## [(Alpha) Composer / Airflow Scheduler](#cloud.google.com/composer/scheduler) + + +## [(Alpha) Composer / Airflow Scheduler](#airflow_schedule) Airflow Scheduler logs contain information related to the scheduling of TaskInstances, making it an ideal source for understanding the lifecycle of TaskInstances. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment **Sample used query** @@ -351,19 +351,19 @@ Following log queries are used with this feature. ``` TODO: add sample query ``` - - -## [(Alpha) Cloud Composer / Airflow Worker](#cloud.google.com/composer/worker) + + +## [(Alpha) Cloud Composer / Airflow Worker](#airflow_worker) Airflow Worker logs contain information related to the execution of TaskInstances. By including these logs, you can gain insights into where and how each TaskInstance was executed. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment **Sample used query** @@ -371,19 +371,19 @@ Following log queries are used with this feature. ``` TODO: add sample query ``` - - -## [(Alpha) Composer / Airflow DagProcessorManager](#cloud.google.com/composer/dagprocessor) + + +## [(Alpha) Composer / Airflow DagProcessorManager](#airflow_dag_processor) The DagProcessorManager logs contain information for investigating the number of DAGs included in each Python file and the time it took to parse them. You can get information about missing DAGs and load. - - + + ### Depending Queries Following log queries are used with this feature. - - + + #### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment **Sample used query** @@ -391,4 +391,4 @@ Following log queries are used with this feature. ``` TODO: add sample query ``` - + diff --git a/docs/en/inspection-type.md b/docs/en/inspection-type.md index ec0575f..851fd58 100644 --- a/docs/en/inspection-type.md +++ b/docs/en/inspection-type.md @@ -11,174 +11,174 @@ Inspection type is ... ### Supported features - -* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) - - -* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) - - -* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) - - -* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) - - -* [GKE Audit logs](./features.md#cloud.google.com/feature/gke-audit-parser) - - -* [Compute API Logs](./features.md#cloud.google.com/feature/compute-api-parser) - - -* [GCE Network Logs](./features.md#cloud.google.com/feature/network-api-parser) - - -* [Autoscaler Logs](./features.md#cloud.google.com/feature/autoscaler-parser) - - -* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) - - -* [Node serial port logs](./features.md#cloud.google.com/feature/serialport) - + +* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) + + +* [Kubernetes Event Logs](./features.md#k8s_event) + + +* [Kubernetes Node Logs](./features.md#k8s_node) + + +* [Kubernetes container logs](./features.md#k8s_container) + + +* [GKE Audit logs](./features.md#gke_audit) + + +* [Compute API Logs](./features.md#compute_api) + + +* [GCE Network Logs](./features.md#gce_network) + + +* [Autoscaler Logs](./features.md#autoscaler) + + +* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) + + +* [Node serial port logs](./features.md#serialport) + ## [Cloud Composer](#gcp-composer) ### Supported features - -* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) - - -* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) - - -* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) - - -* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) - - -* [GKE Audit logs](./features.md#cloud.google.com/feature/gke-audit-parser) - - -* [Compute API Logs](./features.md#cloud.google.com/feature/compute-api-parser) - - -* [GCE Network Logs](./features.md#cloud.google.com/feature/network-api-parser) - - -* [Autoscaler Logs](./features.md#cloud.google.com/feature/autoscaler-parser) - - -* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) - - -* [Node serial port logs](./features.md#cloud.google.com/feature/serialport) - - -* [(Alpha) Composer / Airflow Scheduler](./features.md#cloud.google.com/composer/scheduler) - - -* [(Alpha) Cloud Composer / Airflow Worker](./features.md#cloud.google.com/composer/worker) - - -* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#cloud.google.com/composer/dagprocessor) - + +* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) + + +* [Kubernetes Event Logs](./features.md#k8s_event) + + +* [Kubernetes Node Logs](./features.md#k8s_node) + + +* [Kubernetes container logs](./features.md#k8s_container) + + +* [GKE Audit logs](./features.md#gke_audit) + + +* [Compute API Logs](./features.md#compute_api) + + +* [GCE Network Logs](./features.md#gce_network) + + +* [Autoscaler Logs](./features.md#autoscaler) + + +* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) + + +* [Node serial port logs](./features.md#serialport) + + +* [(Alpha) Composer / Airflow Scheduler](./features.md#airflow_schedule) + + +* [(Alpha) Cloud Composer / Airflow Worker](./features.md#airflow_worker) + + +* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#airflow_dag_processor) + ## [GKE on AWS(Anthos on AWS)](#gcp-gke-on-aws) ### Supported features - -* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) - - -* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) - - -* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) - - -* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) - - -* [MultiCloud API logs](./features.md#cloud.google.com/feature/multicloud-audit-parser) - - -* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) - + +* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) + + +* [Kubernetes Event Logs](./features.md#k8s_event) + + +* [Kubernetes Node Logs](./features.md#k8s_node) + + +* [Kubernetes container logs](./features.md#k8s_container) + + +* [MultiCloud API logs](./features.md#multicloud_api) + + +* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) + ## [GKE on Azure(Anthos on Azure)](#gcp-gke-on-azure) ### Supported features - -* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) - - -* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) - - -* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) - - -* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) - - -* [MultiCloud API logs](./features.md#cloud.google.com/feature/multicloud-audit-parser) - - -* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) - + +* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) + + +* [Kubernetes Event Logs](./features.md#k8s_event) + + +* [Kubernetes Node Logs](./features.md#k8s_node) + + +* [Kubernetes container logs](./features.md#k8s_container) + + +* [MultiCloud API logs](./features.md#multicloud_api) + + +* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) + ## [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](#gcp-gdcv-for-baremetal) ### Supported features - -* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) - - -* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) - - -* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) - - -* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) - - -* [OnPrem API logs](./features.md#cloud.google.com/feature/onprem-audit-parser) - - -* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) - + +* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) + + +* [Kubernetes Event Logs](./features.md#k8s_event) + + +* [Kubernetes Node Logs](./features.md#k8s_node) + + +* [Kubernetes container logs](./features.md#k8s_container) + + +* [OnPrem API logs](./features.md#onprem_api) + + +* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) + ## [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](#gcp-gdcv-for-vmware) ### Supported features - -* [Kubernetes Audit Log(v2)](./features.md#cloud.google.com//feature/audit-parser-v2) - - -* [Kubernetes Event Logs](./features.md#cloud.google.com/feature/event-parser) - - -* [Kubernetes Node Logs](./features.md#cloud.google.com/feature/nodelog-parser) - - -* [Kubernetes container logs](./features.md#cloud.google.com/feature/container-parser) - - -* [OnPrem API logs](./features.md#cloud.google.com/feature/onprem-audit-parser) - - -* [Kubernetes Control plane component logs](./features.md#cloud.google.com/feature/controlplane-component-parser) - + +* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) + + +* [Kubernetes Event Logs](./features.md#k8s_event) + + +* [Kubernetes Node Logs](./features.md#k8s_node) + + +* [Kubernetes container logs](./features.md#k8s_container) + + +* [OnPrem API logs](./features.md#onprem_api) + + +* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) + diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index 19b299a..136691b 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -60,7 +60,7 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat } result.Features = append(result.Features, FeatureDocumentElement{ - ID: feature.ID().String(), + ID: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string), Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), Description: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), Queries: queryElements, diff --git a/pkg/document/model/inspection_type.go b/pkg/document/model/inspection_type.go index e016618..b268f1f 100644 --- a/pkg/document/model/inspection_type.go +++ b/pkg/document/model/inspection_type.go @@ -36,7 +36,7 @@ func GetInspectionTypeDocumentModel(taskServer *inspection.InspectionTaskServer) features := []InspectionTypeDocumentElementFeature{} for _, task := range tasks { features = append(features, InspectionTypeDocumentElementFeature{ - ID: task.ID().String(), + ID: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string), Name: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), Description: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), }) diff --git a/pkg/inspection/task/label.go b/pkg/inspection/task/label.go index 3f3a7c6..ccf9a2f 100644 --- a/pkg/inspection/task/label.go +++ b/pkg/inspection/task/label.go @@ -27,6 +27,8 @@ const ( // A []string typed label of Definition. Task registry will filter task units by given inspection type at first. LabelKeyInspectionTypes = InspectionTaskPrefix + "inspection-type" LabelKeyFeatureTaskTitle = InspectionTaskPrefix + "feature/title" + // LabelKeyFeatureDocumentAnchorID is a key of label for a short length task ID assigned to feature tasks. This is used in link anchors in documents. + LabelKeyFeatureDocumentAnchorID = InspectionTaskPrefix + "feature/short-title" LabelKeyFeatureTaskDescription = InspectionTaskPrefix + "feature/description" @@ -48,6 +50,7 @@ var _ common_task.LabelOpt = (*ProgressReportableTaskLabelOptImpl)(nil) // FeatureTaskLabelImpl is an implementation of task.LabelOpt. // This annotate a task definition to be a feature in inspection. type FeatureTaskLabelImpl struct { + documentAnchorID string title string description string isDefaultFeature bool @@ -55,6 +58,7 @@ type FeatureTaskLabelImpl struct { func (ftl *FeatureTaskLabelImpl) Write(label *common_task.LabelSet) { label.Set(LabelKeyInspectionFeatureFlag, true) + label.Set(LabelKeyFeatureDocumentAnchorID, ftl.documentAnchorID) label.Set(LabelKeyFeatureTaskTitle, ftl.title) label.Set(LabelKeyFeatureTaskDescription, ftl.description) label.Set(LabelKeyInspectionDefaultFeatureFlag, ftl.isDefaultFeature) @@ -67,9 +71,10 @@ func (ftl *FeatureTaskLabelImpl) WithDescription(description string) *FeatureTas var _ common_task.LabelOpt = (*FeatureTaskLabelImpl)(nil) -func FeatureTaskLabel(title string, description string, isDefaultFeature bool) *FeatureTaskLabelImpl { +func FeatureTaskLabel(documentAnchorID string, title string, description string, isDefaultFeature bool) *FeatureTaskLabelImpl { return &FeatureTaskLabelImpl{ title: title, + documentAnchorID: documentAnchorID, description: description, isDefaultFeature: isDefaultFeature, } diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 9c0a645..6c1b988 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -37,6 +37,10 @@ var PARSER_MAX_THREADS = 16 type Parser interface { // GetParserName Returns it's own parser name. It must be unique by each instances. GetParserName() string + + // GetDocumentAnchorID returns a unique ID within feature tasks. This is used as the link anchor ID in document. + GetDocumentAnchorID() string + // Parse a log. Return an error to decide skip to parse the log and delegate later parsers. Parse(ctx context.Context, l *log.LogEntity, cs *history.ChangeSet, builder *history.Builder, variables *task.VariableSet) error @@ -170,6 +174,6 @@ func NewParserTaskFromParser(taskId string, parser Parser, isDefaultFeature bool return struct{}{}, nil }, append([]task.LabelOpt{ - inspection_task.FeatureTaskLabel(parser.GetParserName(), parser.Description(), isDefaultFeature), + inspection_task.FeatureTaskLabel(parser.GetDocumentAnchorID(), parser.GetParserName(), parser.Description(), isDefaultFeature), }, labelOpts...)...) } diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index c75701f..245cb4d 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -111,16 +111,16 @@ func createTestInspectionServer() (*inspection.InspectionTaskServer, error) { form.NewInputFormDefinitionBuilder("bar-input", 1, "A input field for bar").Build(inspection_task.InspectionTypeLabel("bar")), inspection_task.NewInspectionProcessor("feature-foo1", []string{"foo-input"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-foo1-value", nil - }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo feature1", "test-feature", false)), + }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo1", "foo feature1", "test-feature", false)), inspection_task.NewInspectionProcessor("feature-foo2", []string{"foo-input"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-foo2-value", nil - }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo feature2", "test-feature", false)), + }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo2", "foo feature2", "test-feature", false)), inspection_task.NewInspectionProcessor("feature-bar", []string{"bar-input", "neverend"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-bar1-value", nil - }, inspection_task.InspectionTypeLabel("bar"), inspection_task.FeatureTaskLabel("bar feature1", "test-feature", false)), + }, inspection_task.InspectionTypeLabel("bar"), inspection_task.FeatureTaskLabel("bar", "bar feature1", "test-feature", false)), inspection_task.NewInspectionProcessor("feature-qux", []string{"errorend"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-bar1-value", nil - }, inspection_task.InspectionTypeLabel("qux"), inspection_task.FeatureTaskLabel("qux feature1", "test-feature", false)), + }, inspection_task.InspectionTypeLabel("qux"), inspection_task.FeatureTaskLabel("qux", "qux feature1", "test-feature", false)), ioconfig.TestIOConfig, } diff --git a/pkg/source/gcp/task/cloud-composer/parser.go b/pkg/source/gcp/task/cloud-composer/parser.go index bf2d252..226fe98 100644 --- a/pkg/source/gcp/task/cloud-composer/parser.go +++ b/pkg/source/gcp/task/cloud-composer/parser.go @@ -91,6 +91,11 @@ var AirflowSchedulerLogParseJob = parser.NewParserTaskFromParser(gcp_task.GCPPre type AirflowSchedulerParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (t *AirflowSchedulerParser) GetDocumentAnchorID() string { + return "airflow_schedule" +} + var _ parser.Parser = &AirflowSchedulerParser{} func (*AirflowSchedulerParser) Dependencies() []string { @@ -216,6 +221,11 @@ var _ parser.Parser = &AirflowWorkerParser{} type AirflowWorkerParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (a *AirflowWorkerParser) GetDocumentAnchorID() string { + return "airflow_worker" +} + // Dependencies implements parser.Parser. func (*AirflowWorkerParser) Dependencies() []string { return []string{ComposerWorkerLogQueryTaskName} @@ -362,6 +372,11 @@ type AirflowDagProcessorParser struct { dagFilePath string } +// GetDocumentAnchorID implements parser.Parser. +func (a *AirflowDagProcessorParser) GetDocumentAnchorID() string { + return "airflow_dag_processor" +} + var _ parser.Parser = (*AirflowDagProcessorParser)(nil) func (*AirflowDagProcessorParser) Dependencies() []string { diff --git a/pkg/source/gcp/task/gke/autoscaler/parser.go b/pkg/source/gcp/task/gke/autoscaler/parser.go index 7b9833e..8393ad6 100644 --- a/pkg/source/gcp/task/gke/autoscaler/parser.go +++ b/pkg/source/gcp/task/gke/autoscaler/parser.go @@ -36,6 +36,11 @@ import ( type autoscalerLogParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (p *autoscalerLogParser) GetDocumentAnchorID() string { + return "autoscaler" +} + // Dependencies implements parser.Parser. func (*autoscalerLogParser) Dependencies() []string { return []string{ diff --git a/pkg/source/gcp/task/gke/compute_api/parser.go b/pkg/source/gcp/task/gke/compute_api/parser.go index 241c579..97ecb01 100644 --- a/pkg/source/gcp/task/gke/compute_api/parser.go +++ b/pkg/source/gcp/task/gke/compute_api/parser.go @@ -35,6 +35,11 @@ import ( type computeAPIParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (c *computeAPIParser) GetDocumentAnchorID() string { + return "compute_api" +} + // Dependencies implements parser.Parser. func (*computeAPIParser) Dependencies() []string { return []string{} diff --git a/pkg/source/gcp/task/gke/gke_audit/parser.go b/pkg/source/gcp/task/gke/gke_audit/parser.go index ab10a63..81b35f9 100644 --- a/pkg/source/gcp/task/gke/gke_audit/parser.go +++ b/pkg/source/gcp/task/gke/gke_audit/parser.go @@ -35,6 +35,11 @@ import ( type gkeAuditLogParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (p *gkeAuditLogParser) GetDocumentAnchorID() string { + return "gke_audit" +} + // Dependencies implements parser.Parser. func (*gkeAuditLogParser) Dependencies() []string { return []string{} diff --git a/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go b/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go index 0754ba6..cd5f65a 100644 --- a/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go +++ b/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go @@ -127,7 +127,7 @@ func (r *RecorderTaskManager) Register(server *inspection.InspectionTaskServer) } waiterTask := inspection_task.NewInspectionProcessor(fmt.Sprintf("%s/feature/audit-parser-v2", gcp_task.GCPPrefix), recorderTaskIds, func(ctx context.Context, taskMode int, v *task.VariableSet, progress *progress.TaskProgress) (any, error) { return struct{}{}, nil - }, inspection_task.FeatureTaskLabel("Kubernetes Audit Log(v2)", `Visualize Kubernetes audit logs in GKE. + }, inspection_task.FeatureTaskLabel("k8s_audit", "Kubernetes Audit Log(v2)", `Visualize Kubernetes audit logs in GKE. This parser reveals how these resources are created,updated or deleted. `, true)) err := server.AddTaskDefinition(waiterTask) return err diff --git a/pkg/source/gcp/task/gke/k8s_container/parser.go b/pkg/source/gcp/task/gke/k8s_container/parser.go index dce0e7a..de82fee 100644 --- a/pkg/source/gcp/task/gke/k8s_container/parser.go +++ b/pkg/source/gcp/task/gke/k8s_container/parser.go @@ -32,6 +32,11 @@ import ( type k8sContainerParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (k *k8sContainerParser) GetDocumentAnchorID() string { + return "k8s_container" +} + // Description implements parser.Parser. func (*k8sContainerParser) Description() string { return `Container logs ingested from stdout/stderr of workload Pods. diff --git a/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go b/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go index cb2b0ab..c24b59a 100644 --- a/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go +++ b/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go @@ -29,6 +29,11 @@ import ( type k8sControlPlaneComponentParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (k *k8sControlPlaneComponentParser) GetDocumentAnchorID() string { + return "k8s_control_plane_component" +} + // Dependencies implements parser.Parser. func (k *k8sControlPlaneComponentParser) Dependencies() []string { return []string{} diff --git a/pkg/source/gcp/task/gke/k8s_event/parser.go b/pkg/source/gcp/task/gke/k8s_event/parser.go index e027a2c..9864506 100644 --- a/pkg/source/gcp/task/gke/k8s_event/parser.go +++ b/pkg/source/gcp/task/gke/k8s_event/parser.go @@ -33,6 +33,11 @@ var GKEK8sEventLogParseJob = parser.NewParserTaskFromParser(gcp_task.GCPPrefix+" type k8sEventParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (k *k8sEventParser) GetDocumentAnchorID() string { + return "k8s_event" +} + // Description implements parser.Parser. func (*k8sEventParser) Description() string { return `Visualize Kubernetes event logs on GKE. diff --git a/pkg/source/gcp/task/gke/k8s_node/parser.go b/pkg/source/gcp/task/gke/k8s_node/parser.go index 9da9b5c..5ba858f 100644 --- a/pkg/source/gcp/task/gke/k8s_node/parser.go +++ b/pkg/source/gcp/task/gke/k8s_node/parser.go @@ -47,6 +47,11 @@ const ConfigureHelperShTerminatingMsg = "Done for the configuration for kubernet type k8sNodeParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (p *k8sNodeParser) GetDocumentAnchorID() string { + return "k8s_node" +} + // Description implements parser.Parser. func (*k8sNodeParser) Description() string { return `GKE worker node components logs mainly from kubelet,containerd and dockerd. diff --git a/pkg/source/gcp/task/gke/network_api/parser.go b/pkg/source/gcp/task/gke/network_api/parser.go index c431b53..65ccbd6 100644 --- a/pkg/source/gcp/task/gke/network_api/parser.go +++ b/pkg/source/gcp/task/gke/network_api/parser.go @@ -36,6 +36,11 @@ import ( type gceNetworkParser struct{} +// GetDocumentAnchorID implements parser.Parser. +func (g *gceNetworkParser) GetDocumentAnchorID() string { + return "gce_network" +} + // Dependencies implements parser.Parser. func (*gceNetworkParser) Dependencies() []string { return []string{} diff --git a/pkg/source/gcp/task/gke/serialport/parser.go b/pkg/source/gcp/task/gke/serialport/parser.go index 4f79164..3fadc5e 100644 --- a/pkg/source/gcp/task/gke/serialport/parser.go +++ b/pkg/source/gcp/task/gke/serialport/parser.go @@ -44,6 +44,11 @@ var serialportSequenceConverters = []parserutil.SpecialSequenceConverter{ type SerialPortLogParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (s *SerialPortLogParser) GetDocumentAnchorID() string { + return "serialport" +} + // Description implements parser.Parser. func (*SerialPortLogParser) Description() string { return `Serial port logs of worker nodes. Serial port logging feature must be enabled on instances to query logs correctly.` diff --git a/pkg/source/gcp/task/multicloud_api/parser.go b/pkg/source/gcp/task/multicloud_api/parser.go index e4cb6a7..e55f9c1 100644 --- a/pkg/source/gcp/task/multicloud_api/parser.go +++ b/pkg/source/gcp/task/multicloud_api/parser.go @@ -36,6 +36,11 @@ import ( type multiCloudAuditLogParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (m *multiCloudAuditLogParser) GetDocumentAnchorID() string { + return "multicloud_api" +} + // Dependencies implements parser.Parser. func (*multiCloudAuditLogParser) Dependencies() []string { return []string{} diff --git a/pkg/source/gcp/task/onprem_api/parser.go b/pkg/source/gcp/task/onprem_api/parser.go index 2d156ce..2c1ec52 100644 --- a/pkg/source/gcp/task/onprem_api/parser.go +++ b/pkg/source/gcp/task/onprem_api/parser.go @@ -36,6 +36,11 @@ import ( type onpremCloudAuditLogParser struct { } +// GetDocumentAnchorID implements parser.Parser. +func (o *onpremCloudAuditLogParser) GetDocumentAnchorID() string { + return "onprem_api" +} + // Dependencies implements parser.Parser. func (*onpremCloudAuditLogParser) Dependencies() []string { return []string{} From 65868c9c59c324e5e242f669bac0f950e1f73696 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 13:22:33 +0900 Subject: [PATCH 06/23] Add dependent form parameters in feature document --- docs/en/features.md | 285 +++++++++++++++++++++++++++--- docs/template/feature.template.md | 11 +- pkg/document/model/feature.go | 54 +++++- pkg/inspection/form/textform.go | 6 +- pkg/inspection/task/label/form.go | 35 ++++ 5 files changed, 359 insertions(+), 32 deletions(-) create mode 100644 pkg/inspection/task/label/form.go diff --git a/docs/en/features.md b/docs/en/features.md index 8d123e8..bfa28a0 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -5,8 +5,26 @@ Visualize Kubernetes audit logs in GKE. This parser reveals how these resources are created,updated or deleted. + +### Parameters + + +* **Kind** : + +* **Namespaces** : + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -21,7 +39,6 @@ resource.labels.cluster_name="gcp-cluster-name" protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing kind filter. -- Invalid: none of the resources will be selected. Ignoreing namespace filter. - ``` @@ -31,8 +48,24 @@ Visualize Kubernetes event logs on GKE. This parser shows events associated to K8s resources + +### Parameters + + +* **Namespaces** : + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -55,8 +88,24 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. (WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter. + +### Parameters + + +* **Node names** : A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster. + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -70,7 +119,6 @@ resource.type="k8s_node" -logName="projects/gcp-project-id/logs/events" resource.labels.cluster_name="gcp-cluster-name" resource.labels.node_name:("gke-test-cluster-node-1" OR "gke-test-cluster-node-2") - ``` @@ -81,8 +129,29 @@ Container logs ingested from stdout/stderr of workload Pods. (WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter or target namespace fewer. + +### Parameters + + +* **Namespaces(Container logs)** : Container logs tend to be a lot and take very long time to query. +Specify the space splitted namespace lists to query container logs only in the specific namespaces. + +* **Pod names(Container logs)** : Container logs tend to be a lot and take very long time to query. + Specify the space splitted pod names lists to query container logs only in the specific pods. + This parameter is evaluated as the partial match not the perfect match. You can use the prefix of the pod names. + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -104,8 +173,22 @@ resource.labels.cluster_name="gcp-cluster-name" GKE audit log including cluster creation,deletion and upgrades. + +### Parameters + + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -126,8 +209,26 @@ resource.labels.cluster_name="gcp-cluster-name" Compute API audit logs used for cluster related logs. This also visualize operations happened during the query time. + +### Parameters + + +* **Kind** : + +* **Namespaces** : + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -142,7 +243,6 @@ resource.labels.cluster_name="gcp-cluster-name" protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing kind filter. -- Invalid: none of the resources will be selected. Ignoreing namespace filter. - ``` @@ -163,8 +263,26 @@ resource.type="gce_instance" GCE network API audit log including NEG related audit logs to identify when the associated NEG was attached/detached. + +### Parameters + + +* **Kind** : + +* **Namespaces** : + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -179,7 +297,6 @@ resource.labels.cluster_name="gcp-cluster-name" protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing kind filter. -- Invalid: none of the resources will be selected. Ignoreing namespace filter. - ``` @@ -191,7 +308,6 @@ protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") resource.type="gce_network" -protoPayload.methodName:("list" OR "get" OR "watch") protoPayload.resourceName:(networkEndpointGroups/neg-id-1 OR networkEndpointGroups/neg-id-2) - ``` @@ -200,8 +316,22 @@ protoPayload.resourceName:(networkEndpointGroups/neg-id-1 OR networkEndpointGrou Anthos Multicloud audit log including cluster creation,deletion and upgrades. + +### Parameters + + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -215,7 +345,6 @@ resource.type="audited_resource" resource.labels.service="gkemulticloud.googleapis.com" resource.labels.method:("Update" OR "Create" OR "Delete") protoPayload.resourceName:"awsClusters/cluster-foo" - ``` @@ -225,8 +354,22 @@ Autoscaler logs including decision reasons why they scale up/down or why they di This log type also includes Node Auto Provisioner logs. + +### Parameters + + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -249,8 +392,22 @@ logName="projects/gcp-project-id/logs/container.googleapis.com%2Fcluster-autosca Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades. + +### Parameters + + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -264,7 +421,6 @@ resource.type="audited_resource" resource.labels.service="gkeonprem.googleapis.com" resource.labels.method:("Update" OR "Create" OR "Delete" OR "Enroll" OR "Unenroll") protoPayload.resourceName:"baremetalClusters/my-cluster" - ``` @@ -273,8 +429,24 @@ protoPayload.resourceName:"baremetalClusters/my-cluster" Visualize Kubernetes control plane component logs on a cluster + +### Parameters + + +* **Control plane component names** : Control plane component names to query(e.g. apiserver, controller-manager...etc) + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -297,8 +469,28 @@ resource.labels.project_id="gcp-project-id" Serial port logs of worker nodes. Serial port logging feature must be enabled on instances to query logs correctly. + +### Parameters + + +* **Kind** : + +* **Namespaces** : + +* **Node names** : A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster. + +* **Project ID** : A project ID containing the cluster to inspect + +* **Cluster name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -313,7 +505,6 @@ resource.labels.cluster_name="gcp-cluster-name" protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing kind filter. -- Invalid: none of the resources will be selected. Ignoreing namespace filter. - ``` @@ -338,8 +529,24 @@ labels."compute.googleapis.com/resource_name"=("gke-test-cluster-node-1" OR "gke Airflow Scheduler logs contain information related to the scheduling of TaskInstances, making it an ideal source for understanding the lifecycle of TaskInstances. + +### Parameters + + +* **Location** : A location(regions) containing the environments to inspect + +* **Project ID** : A project ID containing the cluster to inspect + +* **Composer Environment Name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -358,8 +565,24 @@ TODO: add sample query Airflow Worker logs contain information related to the execution of TaskInstances. By including these logs, you can gain insights into where and how each TaskInstance was executed. + +### Parameters + + +* **Location** : A location(regions) containing the environments to inspect + +* **Project ID** : A project ID containing the cluster to inspect + +* **Composer Environment Name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. @@ -378,8 +601,24 @@ TODO: add sample query The DagProcessorManager logs contain information for investigating the number of DAGs included in each Python file and the time it took to parse them. You can get information about missing DAGs and load. + +### Parameters + + +* **Location** : A location(regions) containing the environments to inspect + +* **Project ID** : A project ID containing the cluster to inspect + +* **Composer Environment Name** : + +* **End time** : The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00) + +* **Duration** : + + -### Depending Queries +### Target logs Following log queries are used with this feature. diff --git a/docs/template/feature.template.md b/docs/template/feature.template.md index 855bcf4..adfe5ea 100644 --- a/docs/template/feature.template.md +++ b/docs/template/feature.template.md @@ -6,9 +6,18 @@ {{$feature.Description}} +{{with $feature.Forms}} + +### Parameters + +{{range $index,$form := $feature.Forms}} +* **{{$form.Label}}** : {{$form.Description}} +{{end}} + +{{end}} {{with $feature.Queries}} -### Depending Queries +### Target logs Following log queries are used with this feature. diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index 136691b..f0afd5f 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -21,6 +21,7 @@ type FeatureDocumentElement struct { Description string Queries []FeatureDependentQueryElement + Forms []FeatureDependentFormElement } type FeatureDependentQueryElement struct { @@ -31,6 +32,12 @@ type FeatureDependentQueryElement struct { SampleQuery string } +type FeatureDependentFormElement struct { + ID string + Label string + Description string +} + func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*FeatureDocumentModel, error) { result := FeatureDocumentModel{} features := taskServer.RootTaskSet.FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false) @@ -38,15 +45,10 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat queryElements := []FeatureDependentQueryElement{} // Get query related tasks required by this feature. - resolveSource, err := task.NewSet([]task.Definition{feature}) + queryTasks, err := getDependentQueryTasks(taskServer, feature) if err != nil { return nil, err } - resolved, err := resolveSource.ResolveTask(taskServer.RootTaskSet) - if err != nil { - return nil, err - } - queryTasks := resolved.FilteredSubset(label.TaskLabelKeyIsQueryTask, taskfilter.HasTrue, false).GetAll() for _, queryTask := range queryTasks { logTypeKey := enum.LogType(queryTask.Labels().GetOrDefault(label.TaskLabelKeyQueryTaskTargetLogType, enum.LogTypeUnknown).(enum.LogType)) logType := enum.LogTypes[logTypeKey] @@ -55,7 +57,20 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat LogType: logTypeKey, LogTypeLabel: logType.Label, LogTypeColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), - SampleQuery: queryTask.Labels().GetOrDefault(label.TaskLabelKeyQueryTaskSampleQuery, "").(string), + SampleQuery: strings.TrimRight(queryTask.Labels().GetOrDefault(label.TaskLabelKeyQueryTaskSampleQuery, "").(string), "\n"), + }) + } + + formElements := []FeatureDependentFormElement{} + formTasks, err := getDependentFormTasks(taskServer, feature) + if err != nil { + return nil, err + } + for _, formTask := range formTasks { + formElements = append(formElements, FeatureDependentFormElement{ + ID: formTask.ID().String(), + Label: formTask.Labels().GetOrDefault(label.TaskLabelKeyFormFieldLabel, "").(string), + Description: formTask.Labels().GetOrDefault(label.TaskLabelKeyFormFieldDescription, "").(string), }) } @@ -64,8 +79,33 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), Description: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), Queries: queryElements, + Forms: formElements, }) } return &result, nil } + +func getDependentQueryTasks(taskServer *inspection.InspectionTaskServer, featureTask task.Definition) ([]task.Definition, error) { + resolveSource, err := task.NewSet([]task.Definition{featureTask}) + if err != nil { + return nil, err + } + resolved, err := resolveSource.ResolveTask(taskServer.RootTaskSet) + if err != nil { + return nil, err + } + return resolved.FilteredSubset(label.TaskLabelKeyIsQueryTask, taskfilter.HasTrue, false).GetAll(), nil +} + +func getDependentFormTasks(taskServer *inspection.InspectionTaskServer, featureTask task.Definition) ([]task.Definition, error) { + resolveSource, err := task.NewSet([]task.Definition{featureTask}) + if err != nil { + return nil, err + } + resolved, err := resolveSource.ResolveTask(taskServer.RootTaskSet) + if err != nil { + return nil, err + } + return resolved.FilteredSubset(label.TaskLabelKeyIsFormTask, taskfilter.HasTrue, false).GetAll(), nil +} diff --git a/pkg/inspection/form/textform.go b/pkg/inspection/form/textform.go index 126c7d9..6a94128 100644 --- a/pkg/inspection/form/textform.go +++ b/pkg/inspection/form/textform.go @@ -20,6 +20,7 @@ import ( form_metadata "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata/form" "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" + "github.com/GoogleCloudPlatform/khi/pkg/inspection/task/label" common_task "github.com/GoogleCloudPlatform/khi/pkg/task" ) @@ -245,5 +246,8 @@ func (b *TextFormDefinitionBuilder) Build(labelOpts ...common_task.LabelOpt) com } } return convertedValue, nil - }, labelOpts...) + }, append(labelOpts, label.NewFormTaskLabelOpt( + b.label, + b.description, + ))...) } diff --git a/pkg/inspection/task/label/form.go b/pkg/inspection/task/label/form.go new file mode 100644 index 0000000..6dcb40d --- /dev/null +++ b/pkg/inspection/task/label/form.go @@ -0,0 +1,35 @@ +package label + +import ( + inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" + "github.com/GoogleCloudPlatform/khi/pkg/task" +) + +const ( + TaskLabelKeyIsFormTask = inspection_task.InspectionTaskPrefix + "is-form-task" + TaskLabelKeyFormFieldLabel = inspection_task.InspectionTaskPrefix + "form-field-label" + TaskLabelKeyFormFieldDescription = inspection_task.InspectionTaskPrefix + "form-field-description" +) + +type FormTaskLabelOpt struct { + description string + label string +} + +// Write implements task.LabelOpt. +func (f *FormTaskLabelOpt) Write(label *task.LabelSet) { + label.Set(TaskLabelKeyIsFormTask, true) + label.Set(TaskLabelKeyFormFieldLabel, f.label) + label.Set(TaskLabelKeyFormFieldDescription, f.description) + +} + +// NewFormTaskLabelOpt constucts a new instance of task.LabelOpt for form related tasks. +func NewFormTaskLabelOpt(label, description string) *FormTaskLabelOpt { + return &FormTaskLabelOpt{ + label: label, + description: description, + } +} + +var _ (task.LabelOpt) = (*FormTaskLabelOpt)(nil) From 9249d77a311148b09abe4eb13d0dd2155dedf90f Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 13:44:55 +0900 Subject: [PATCH 07/23] Added TargetLogType() on Parser interface --- docs/en/features.md | 310 +++++++----------- docs/template/feature.template.md | 8 +- pkg/inspection/task/label.go | 15 +- pkg/parser/parser.go | 6 +- pkg/server/server_test.go | 9 +- pkg/source/gcp/task/cloud-composer/parser.go | 15 + pkg/source/gcp/task/gke/autoscaler/parser.go | 5 + pkg/source/gcp/task/gke/compute_api/parser.go | 5 + pkg/source/gcp/task/gke/gke_audit/parser.go | 5 + .../gcp/task/gke/k8s_audit/recorder/task.go | 5 +- .../gcp/task/gke/k8s_container/parser.go | 5 + .../gke/k8s_control_plane_component/parser.go | 6 + pkg/source/gcp/task/gke/k8s_event/parser.go | 6 + pkg/source/gcp/task/gke/k8s_node/parser.go | 5 + pkg/source/gcp/task/gke/network_api/parser.go | 5 + pkg/source/gcp/task/gke/serialport/parser.go | 6 + pkg/source/gcp/task/multicloud_api/parser.go | 5 + pkg/source/gcp/task/onprem_api/parser.go | 5 + 18 files changed, 226 insertions(+), 200 deletions(-) diff --git a/docs/en/features.md b/docs/en/features.md index bfa28a0..faee08b 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -8,20 +8,15 @@ This parser reveals how these resources are created,updated or deleted. ### Parameters - -* **Kind** : - -* **Namespaces** : - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Kind|| +|Namespaces|| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -51,18 +46,14 @@ This parser shows events associated to K8s resources ### Parameters - -* **Namespaces** : - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Namespaces|| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -91,18 +82,14 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. ### Parameters - -* **Node names** : A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster. - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Node names|A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster.| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -132,23 +119,18 @@ Container logs ingested from stdout/stderr of workload Pods. ### Parameters - -* **Namespaces(Container logs)** : Container logs tend to be a lot and take very long time to query. -Specify the space splitted namespace lists to query container logs only in the specific namespaces. - -* **Pod names(Container logs)** : Container logs tend to be a lot and take very long time to query. +|Parameter name|Description| +|:-:|---| +|Namespaces(Container logs)|Container logs tend to be a lot and take very long time to query. +Specify the space splitted namespace lists to query container logs only in the specific namespaces.| +|Pod names(Container logs)|Container logs tend to be a lot and take very long time to query. Specify the space splitted pod names lists to query container logs only in the specific pods. - This parameter is evaluated as the partial match not the perfect match. You can use the prefix of the pod names. - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - + This parameter is evaluated as the partial match not the perfect match. You can use the prefix of the pod names.| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -176,16 +158,13 @@ GKE audit log including cluster creation,deletion and upgrades. ### Parameters - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -212,20 +191,15 @@ Compute API audit logs used for cluster related logs. This also visualize operat ### Parameters - -* **Kind** : - -* **Namespaces** : - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Kind|| +|Namespaces|| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -266,20 +240,15 @@ GCE network API audit log including NEG related audit logs to identify when the ### Parameters - -* **Kind** : - -* **Namespaces** : - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Kind|| +|Namespaces|| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -319,16 +288,13 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. ### Parameters - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -357,16 +323,13 @@ This log type also includes Node Auto Provisioner logs. ### Parameters - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -395,16 +358,13 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and ### Parameters - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -432,18 +392,14 @@ Visualize Kubernetes control plane component logs on a cluster ### Parameters - -* **Control plane component names** : Control plane component names to query(e.g. apiserver, controller-manager...etc) - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Control plane component names|Control plane component names to query(e.g. apiserver, controller-manager...etc)| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -472,22 +428,16 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on ### Parameters - -* **Kind** : - -* **Namespaces** : - -* **Node names** : A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster. - -* **Project ID** : A project ID containing the cluster to inspect - -* **Cluster name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Kind|| +|Namespaces|| +|Node names|A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster.| +|Project ID|A project ID containing the cluster to inspect| +|Cluster name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -532,18 +482,14 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst ### Parameters - -* **Location** : A location(regions) containing the environments to inspect - -* **Project ID** : A project ID containing the cluster to inspect - -* **Composer Environment Name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Location|A location(regions) containing the environments to inspect| +|Project ID|A project ID containing the cluster to inspect| +|Composer Environment Name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -568,18 +514,14 @@ Airflow Worker logs contain information related to the execution of TaskInstance ### Parameters - -* **Location** : A location(regions) containing the environments to inspect - -* **Project ID** : A project ID containing the cluster to inspect - -* **Composer Environment Name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Location|A location(regions) containing the environments to inspect| +|Project ID|A project ID containing the cluster to inspect| +|Composer Environment Name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs @@ -604,18 +546,14 @@ The DagProcessorManager logs contain information for investigating the number of ### Parameters - -* **Location** : A location(regions) containing the environments to inspect - -* **Project ID** : A project ID containing the cluster to inspect - -* **Composer Environment Name** : - -* **End time** : The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00) - -* **Duration** : - +|Parameter name|Description| +|:-:|---| +|Location|A location(regions) containing the environments to inspect| +|Project ID|A project ID containing the cluster to inspect| +|Composer Environment Name|| +|End time|The endtime of query. Please input it in the format of RFC3339 +(example: 2006-01-02T15:04:05-07:00)| +|Duration|| ### Target logs diff --git a/docs/template/feature.template.md b/docs/template/feature.template.md index adfe5ea..7c83ec9 100644 --- a/docs/template/feature.template.md +++ b/docs/template/feature.template.md @@ -10,9 +10,11 @@ ### Parameters -{{range $index,$form := $feature.Forms}} -* **{{$form.Label}}** : {{$form.Description}} -{{end}} +|Parameter name|Description| +|:-:|---| +{{- range $index,$form := $feature.Forms}} +|{{$form.Label}}|{{$form.Description}}| +{{- end}} {{end}} {{with $feature.Queries}} diff --git a/pkg/inspection/task/label.go b/pkg/inspection/task/label.go index ccf9a2f..0d6b031 100644 --- a/pkg/inspection/task/label.go +++ b/pkg/inspection/task/label.go @@ -14,7 +14,10 @@ package task -import common_task "github.com/GoogleCloudPlatform/khi/pkg/task" +import ( + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" + common_task "github.com/GoogleCloudPlatform/khi/pkg/task" +) //TODO: move task label related constants to ./label @@ -25,8 +28,9 @@ const ( LabelKeyInspectionRequiredFlag = InspectionTaskPrefix + "required" LabelKeyProgressReportable = InspectionTaskPrefix + "progress-reportable" // A []string typed label of Definition. Task registry will filter task units by given inspection type at first. - LabelKeyInspectionTypes = InspectionTaskPrefix + "inspection-type" - LabelKeyFeatureTaskTitle = InspectionTaskPrefix + "feature/title" + LabelKeyInspectionTypes = InspectionTaskPrefix + "inspection-type" + LabelKeyFeatureTaskTitle = InspectionTaskPrefix + "feature/title" + LabelKeyFeatureTaskTargetLogType = InspectionTaskPrefix + "feature/log-type" // LabelKeyFeatureDocumentAnchorID is a key of label for a short length task ID assigned to feature tasks. This is used in link anchors in documents. LabelKeyFeatureDocumentAnchorID = InspectionTaskPrefix + "feature/short-title" @@ -53,12 +57,14 @@ type FeatureTaskLabelImpl struct { documentAnchorID string title string description string + logType enum.LogType isDefaultFeature bool } func (ftl *FeatureTaskLabelImpl) Write(label *common_task.LabelSet) { label.Set(LabelKeyInspectionFeatureFlag, true) label.Set(LabelKeyFeatureDocumentAnchorID, ftl.documentAnchorID) + label.Set(LabelKeyFeatureTaskTargetLogType, ftl.logType) label.Set(LabelKeyFeatureTaskTitle, ftl.title) label.Set(LabelKeyFeatureTaskDescription, ftl.description) label.Set(LabelKeyInspectionDefaultFeatureFlag, ftl.isDefaultFeature) @@ -71,11 +77,12 @@ func (ftl *FeatureTaskLabelImpl) WithDescription(description string) *FeatureTas var _ common_task.LabelOpt = (*FeatureTaskLabelImpl)(nil) -func FeatureTaskLabel(documentAnchorID string, title string, description string, isDefaultFeature bool) *FeatureTaskLabelImpl { +func FeatureTaskLabel(documentAnchorID string, title string, description string, logType enum.LogType, isDefaultFeature bool) *FeatureTaskLabelImpl { return &FeatureTaskLabelImpl{ title: title, documentAnchorID: documentAnchorID, description: description, + logType: logType, isDefaultFeature: isDefaultFeature, } } diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 6c1b988..ecfc156 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata/progress" inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" "github.com/GoogleCloudPlatform/khi/pkg/log" + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" "github.com/GoogleCloudPlatform/khi/pkg/model/history" "github.com/GoogleCloudPlatform/khi/pkg/model/history/grouper" "github.com/GoogleCloudPlatform/khi/pkg/task" @@ -41,6 +42,9 @@ type Parser interface { // GetDocumentAnchorID returns a unique ID within feature tasks. This is used as the link anchor ID in document. GetDocumentAnchorID() string + // TargetLogType returns the log type which this parser should mainly parse and generate revisions or events for. + TargetLogType() enum.LogType + // Parse a log. Return an error to decide skip to parse the log and delegate later parsers. Parse(ctx context.Context, l *log.LogEntity, cs *history.ChangeSet, builder *history.Builder, variables *task.VariableSet) error @@ -174,6 +178,6 @@ func NewParserTaskFromParser(taskId string, parser Parser, isDefaultFeature bool return struct{}{}, nil }, append([]task.LabelOpt{ - inspection_task.FeatureTaskLabel(parser.GetDocumentAnchorID(), parser.GetParserName(), parser.Description(), isDefaultFeature), + inspection_task.FeatureTaskLabel(parser.GetDocumentAnchorID(), parser.GetParserName(), parser.Description(), parser.TargetLogType(), isDefaultFeature), }, labelOpts...)...) } diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 245cb4d..46f062d 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -32,6 +32,7 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/inspection/logger" "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata/progress" inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" "github.com/GoogleCloudPlatform/khi/pkg/model/history" "github.com/GoogleCloudPlatform/khi/pkg/parameters" "github.com/GoogleCloudPlatform/khi/pkg/popup" @@ -111,16 +112,16 @@ func createTestInspectionServer() (*inspection.InspectionTaskServer, error) { form.NewInputFormDefinitionBuilder("bar-input", 1, "A input field for bar").Build(inspection_task.InspectionTypeLabel("bar")), inspection_task.NewInspectionProcessor("feature-foo1", []string{"foo-input"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-foo1-value", nil - }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo1", "foo feature1", "test-feature", false)), + }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo1", "foo feature1", "test-feature", enum.LogTypeAudit, false)), inspection_task.NewInspectionProcessor("feature-foo2", []string{"foo-input"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-foo2-value", nil - }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo2", "foo feature2", "test-feature", false)), + }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo2", "foo feature2", "test-feature", enum.LogTypeAudit, false)), inspection_task.NewInspectionProcessor("feature-bar", []string{"bar-input", "neverend"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-bar1-value", nil - }, inspection_task.InspectionTypeLabel("bar"), inspection_task.FeatureTaskLabel("bar", "bar feature1", "test-feature", false)), + }, inspection_task.InspectionTypeLabel("bar"), inspection_task.FeatureTaskLabel("bar", "bar feature1", "test-feature", enum.LogTypeAudit, false)), inspection_task.NewInspectionProcessor("feature-qux", []string{"errorend"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-bar1-value", nil - }, inspection_task.InspectionTypeLabel("qux"), inspection_task.FeatureTaskLabel("qux", "qux feature1", "test-feature", false)), + }, inspection_task.InspectionTypeLabel("qux"), inspection_task.FeatureTaskLabel("qux", "qux feature1", "test-feature", enum.LogTypeAudit, false)), ioconfig.TestIOConfig, } diff --git a/pkg/source/gcp/task/cloud-composer/parser.go b/pkg/source/gcp/task/cloud-composer/parser.go index 226fe98..d1c7dec 100644 --- a/pkg/source/gcp/task/cloud-composer/parser.go +++ b/pkg/source/gcp/task/cloud-composer/parser.go @@ -91,6 +91,11 @@ var AirflowSchedulerLogParseJob = parser.NewParserTaskFromParser(gcp_task.GCPPre type AirflowSchedulerParser struct { } +// TargetLogType implements parser.Parser. +func (t *AirflowSchedulerParser) TargetLogType() enum.LogType { + return enum.LogTypeComposerEnvironment +} + // GetDocumentAnchorID implements parser.Parser. func (t *AirflowSchedulerParser) GetDocumentAnchorID() string { return "airflow_schedule" @@ -221,6 +226,11 @@ var _ parser.Parser = &AirflowWorkerParser{} type AirflowWorkerParser struct { } +// TargetLogType implements parser.Parser. +func (a *AirflowWorkerParser) TargetLogType() enum.LogType { + return enum.LogTypeComposerEnvironment +} + // GetDocumentAnchorID implements parser.Parser. func (a *AirflowWorkerParser) GetDocumentAnchorID() string { return "airflow_worker" @@ -372,6 +382,11 @@ type AirflowDagProcessorParser struct { dagFilePath string } +// TargetLogType implements parser.Parser. +func (a *AirflowDagProcessorParser) TargetLogType() enum.LogType { + return enum.LogTypeComposerEnvironment +} + // GetDocumentAnchorID implements parser.Parser. func (a *AirflowDagProcessorParser) GetDocumentAnchorID() string { return "airflow_dag_processor" diff --git a/pkg/source/gcp/task/gke/autoscaler/parser.go b/pkg/source/gcp/task/gke/autoscaler/parser.go index 8393ad6..4d9e22f 100644 --- a/pkg/source/gcp/task/gke/autoscaler/parser.go +++ b/pkg/source/gcp/task/gke/autoscaler/parser.go @@ -36,6 +36,11 @@ import ( type autoscalerLogParser struct { } +// TargetLogType implements parser.Parser. +func (p *autoscalerLogParser) TargetLogType() enum.LogType { + return enum.LogTypeAutoscaler +} + // GetDocumentAnchorID implements parser.Parser. func (p *autoscalerLogParser) GetDocumentAnchorID() string { return "autoscaler" diff --git a/pkg/source/gcp/task/gke/compute_api/parser.go b/pkg/source/gcp/task/gke/compute_api/parser.go index 97ecb01..7b5edd2 100644 --- a/pkg/source/gcp/task/gke/compute_api/parser.go +++ b/pkg/source/gcp/task/gke/compute_api/parser.go @@ -35,6 +35,11 @@ import ( type computeAPIParser struct { } +// TargetLogType implements parser.Parser. +func (c *computeAPIParser) TargetLogType() enum.LogType { + return enum.LogTypeComputeApi +} + // GetDocumentAnchorID implements parser.Parser. func (c *computeAPIParser) GetDocumentAnchorID() string { return "compute_api" diff --git a/pkg/source/gcp/task/gke/gke_audit/parser.go b/pkg/source/gcp/task/gke/gke_audit/parser.go index 81b35f9..5bb0930 100644 --- a/pkg/source/gcp/task/gke/gke_audit/parser.go +++ b/pkg/source/gcp/task/gke/gke_audit/parser.go @@ -35,6 +35,11 @@ import ( type gkeAuditLogParser struct { } +// TargetLogType implements parser.Parser. +func (p *gkeAuditLogParser) TargetLogType() enum.LogType { + return enum.LogTypeGkeAudit +} + // GetDocumentAnchorID implements parser.Parser. func (p *gkeAuditLogParser) GetDocumentAnchorID() string { return "gke_audit" diff --git a/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go b/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go index cd5f65a..d87c32a 100644 --- a/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go +++ b/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go @@ -24,6 +24,7 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/inspection" "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata/progress" inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" "github.com/GoogleCloudPlatform/khi/pkg/model/history" gcp_task "github.com/GoogleCloudPlatform/khi/pkg/source/gcp/task" @@ -127,8 +128,8 @@ func (r *RecorderTaskManager) Register(server *inspection.InspectionTaskServer) } waiterTask := inspection_task.NewInspectionProcessor(fmt.Sprintf("%s/feature/audit-parser-v2", gcp_task.GCPPrefix), recorderTaskIds, func(ctx context.Context, taskMode int, v *task.VariableSet, progress *progress.TaskProgress) (any, error) { return struct{}{}, nil - }, inspection_task.FeatureTaskLabel("k8s_audit", "Kubernetes Audit Log(v2)", `Visualize Kubernetes audit logs in GKE. -This parser reveals how these resources are created,updated or deleted. `, true)) + }, inspection_task.FeatureTaskLabel("k8s_audit", "Kubernetes Audit Log", `Visualize Kubernetes audit logs in GKE. +This parser reveals how these resources are created,updated or deleted. `, enum.LogTypeAudit, true)) err := server.AddTaskDefinition(waiterTask) return err } diff --git a/pkg/source/gcp/task/gke/k8s_container/parser.go b/pkg/source/gcp/task/gke/k8s_container/parser.go index de82fee..55f2295 100644 --- a/pkg/source/gcp/task/gke/k8s_container/parser.go +++ b/pkg/source/gcp/task/gke/k8s_container/parser.go @@ -32,6 +32,11 @@ import ( type k8sContainerParser struct { } +// TargetLogType implements parser.Parser. +func (k *k8sContainerParser) TargetLogType() enum.LogType { + return enum.LogTypeContainer +} + // GetDocumentAnchorID implements parser.Parser. func (k *k8sContainerParser) GetDocumentAnchorID() string { return "k8s_container" diff --git a/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go b/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go index c24b59a..26ea83a 100644 --- a/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go +++ b/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go @@ -18,6 +18,7 @@ import ( "context" "github.com/GoogleCloudPlatform/khi/pkg/log" + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" "github.com/GoogleCloudPlatform/khi/pkg/model/history" "github.com/GoogleCloudPlatform/khi/pkg/model/history/grouper" "github.com/GoogleCloudPlatform/khi/pkg/parser" @@ -29,6 +30,11 @@ import ( type k8sControlPlaneComponentParser struct { } +// TargetLogType implements parser.Parser. +func (k *k8sControlPlaneComponentParser) TargetLogType() enum.LogType { + return enum.LogTypeControlPlaneComponent +} + // GetDocumentAnchorID implements parser.Parser. func (k *k8sControlPlaneComponentParser) GetDocumentAnchorID() string { return "k8s_control_plane_component" diff --git a/pkg/source/gcp/task/gke/k8s_event/parser.go b/pkg/source/gcp/task/gke/k8s_event/parser.go index 9864506..75ed3ba 100644 --- a/pkg/source/gcp/task/gke/k8s_event/parser.go +++ b/pkg/source/gcp/task/gke/k8s_event/parser.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/GoogleCloudPlatform/khi/pkg/log" + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" "github.com/GoogleCloudPlatform/khi/pkg/model/history" "github.com/GoogleCloudPlatform/khi/pkg/model/history/grouper" "github.com/GoogleCloudPlatform/khi/pkg/model/history/resourcepath" @@ -33,6 +34,11 @@ var GKEK8sEventLogParseJob = parser.NewParserTaskFromParser(gcp_task.GCPPrefix+" type k8sEventParser struct { } +// TargetLogType implements parser.Parser. +func (k *k8sEventParser) TargetLogType() enum.LogType { + return enum.LogTypeEvent +} + // GetDocumentAnchorID implements parser.Parser. func (k *k8sEventParser) GetDocumentAnchorID() string { return "k8s_event" diff --git a/pkg/source/gcp/task/gke/k8s_node/parser.go b/pkg/source/gcp/task/gke/k8s_node/parser.go index 5ba858f..99b5278 100644 --- a/pkg/source/gcp/task/gke/k8s_node/parser.go +++ b/pkg/source/gcp/task/gke/k8s_node/parser.go @@ -47,6 +47,11 @@ const ConfigureHelperShTerminatingMsg = "Done for the configuration for kubernet type k8sNodeParser struct { } +// TargetLogType implements parser.Parser. +func (p *k8sNodeParser) TargetLogType() enum.LogType { + return enum.LogTypeNode +} + // GetDocumentAnchorID implements parser.Parser. func (p *k8sNodeParser) GetDocumentAnchorID() string { return "k8s_node" diff --git a/pkg/source/gcp/task/gke/network_api/parser.go b/pkg/source/gcp/task/gke/network_api/parser.go index 65ccbd6..66e1c5d 100644 --- a/pkg/source/gcp/task/gke/network_api/parser.go +++ b/pkg/source/gcp/task/gke/network_api/parser.go @@ -36,6 +36,11 @@ import ( type gceNetworkParser struct{} +// TargetLogType implements parser.Parser. +func (g *gceNetworkParser) TargetLogType() enum.LogType { + return enum.LogTypeNetworkAPI +} + // GetDocumentAnchorID implements parser.Parser. func (g *gceNetworkParser) GetDocumentAnchorID() string { return "gce_network" diff --git a/pkg/source/gcp/task/gke/serialport/parser.go b/pkg/source/gcp/task/gke/serialport/parser.go index 3fadc5e..c645c44 100644 --- a/pkg/source/gcp/task/gke/serialport/parser.go +++ b/pkg/source/gcp/task/gke/serialport/parser.go @@ -21,6 +21,7 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/common/parserutil" "github.com/GoogleCloudPlatform/khi/pkg/log" + "github.com/GoogleCloudPlatform/khi/pkg/model/enum" "github.com/GoogleCloudPlatform/khi/pkg/model/history" "github.com/GoogleCloudPlatform/khi/pkg/model/history/grouper" "github.com/GoogleCloudPlatform/khi/pkg/model/history/resourcepath" @@ -44,6 +45,11 @@ var serialportSequenceConverters = []parserutil.SpecialSequenceConverter{ type SerialPortLogParser struct { } +// TargetLogType implements parser.Parser. +func (s *SerialPortLogParser) TargetLogType() enum.LogType { + return enum.LogTypeSerialPort +} + // GetDocumentAnchorID implements parser.Parser. func (s *SerialPortLogParser) GetDocumentAnchorID() string { return "serialport" diff --git a/pkg/source/gcp/task/multicloud_api/parser.go b/pkg/source/gcp/task/multicloud_api/parser.go index e55f9c1..f4f114d 100644 --- a/pkg/source/gcp/task/multicloud_api/parser.go +++ b/pkg/source/gcp/task/multicloud_api/parser.go @@ -36,6 +36,11 @@ import ( type multiCloudAuditLogParser struct { } +// TargetLogType implements parser.Parser. +func (m *multiCloudAuditLogParser) TargetLogType() enum.LogType { + return enum.LogTypeMulticloudAPI +} + // GetDocumentAnchorID implements parser.Parser. func (m *multiCloudAuditLogParser) GetDocumentAnchorID() string { return "multicloud_api" diff --git a/pkg/source/gcp/task/onprem_api/parser.go b/pkg/source/gcp/task/onprem_api/parser.go index 2c1ec52..69014ea 100644 --- a/pkg/source/gcp/task/onprem_api/parser.go +++ b/pkg/source/gcp/task/onprem_api/parser.go @@ -36,6 +36,11 @@ import ( type onpremCloudAuditLogParser struct { } +// TargetLogType implements parser.Parser. +func (o *onpremCloudAuditLogParser) TargetLogType() enum.LogType { + return enum.LogTypeOnPremAPI +} + // GetDocumentAnchorID implements parser.Parser. func (o *onpremCloudAuditLogParser) GetDocumentAnchorID() string { return "onprem_api" From 12fa71e70e329ec0b0262a517aace0143b1a752a Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 14:20:41 +0900 Subject: [PATCH 08/23] Add link to timeline output from feature --- docs/en/features.md | 389 +++++++++++-------- docs/en/inspection-type.md | 12 +- docs/template/feature.template.md | 39 +- pkg/document/model/feature.go | 99 ++++- pkg/document/model/log_type.go | 2 +- pkg/model/enum/log_type.go | 2 +- pkg/source/gcp/task/gke/compute_api/query.go | 6 +- 7 files changed, 348 insertions(+), 201 deletions(-) diff --git a/docs/en/features.md b/docs/en/features.md index faee08b..81ae11d 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -1,5 +1,5 @@ -## [Kubernetes Audit Log(v2)](#k8s_audit) +## [Kubernetes Audit Log](#k8s_audit) Visualize Kubernetes audit logs in GKE. This parser reveals how these resources are created,updated or deleted. @@ -18,15 +18,24 @@ This parser reveals how these resources are created,updated or deleted. (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit +|Timeline type|Short name on chip| +|:-:|:-:| +|[The default resource timeline](./relationships.md#RelationshipChild)|resource| +|[Status condition field timeline](./relationships.md#RelationshipResourceCondition)|condition| +|[Endpoint serving state timeline](./relationships.md#RelationshipEndpointSlice)|endpointslice| +|[Owning children timeline](./relationships.md#RelationshipOwnerReference)|owns| +|[Pod binding timeline](./relationships.md#RelationshipPodBinding)|binds| + + + +### Target log type + +**![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit** -**Sample used query** +Sample query: ``` resource.type="k8s_cluster" @@ -34,8 +43,10 @@ resource.labels.cluster_name="gcp-cluster-name" protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") -- Invalid: none of the resources will be selected. Ignoreing kind filter. -- Invalid: none of the resources will be selected. Ignoreing namespace filter. + ``` - + + ## [Kubernetes Event Logs](#k8s_event) @@ -55,22 +66,28 @@ This parser shows events associated to K8s resources (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![3fb549](https://placehold.co/15x15/3fb549/3fb549.png)k8s_event +|Timeline type|Short name on chip| +|:-:|:-:| +|[The default resource timeline](./relationships.md#RelationshipChild)|resource| -**Sample used query** + + +### Target log type + +**![3fb549](https://placehold.co/15x15/3fb549/3fb549.png)k8s_event** + +Sample query: ``` logName="projects/gcp-project-id/logs/events" resource.labels.cluster_name="gcp-cluster-name" -- Invalid: none of the resources will be selected. Ignoreing namespace filter. ``` - + + ## [Kubernetes Node Logs](#k8s_node) @@ -91,23 +108,31 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node +|Timeline type|Short name on chip| +|:-:|:-:| +|[Container timeline](./relationships.md#RelationshipContainer)|container| +|[Node component timeline](./relationships.md#RelationshipNodeComponent)|node-component| + + + +### Target log type + +**![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node** -**Sample used query** +Sample query: ``` resource.type="k8s_node" -logName="projects/gcp-project-id/logs/events" resource.labels.cluster_name="gcp-cluster-name" resource.labels.node_name:("gke-test-cluster-node-1" OR "gke-test-cluster-node-2") + ``` - + + ## [Kubernetes container logs](#k8s_container) @@ -132,15 +157,20 @@ Specify the space splitted namespace lists to query container logs only in the s (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container +|Timeline type|Short name on chip| +|:-:|:-:| +|[Container timeline](./relationships.md#RelationshipContainer)|container| -**Sample used query** + + +### Target log type + +**![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container** + +Sample query: ``` resource.type="k8s_container" @@ -148,7 +178,8 @@ resource.labels.cluster_name="gcp-cluster-name" -- Invalid: none of the resources will be selected. Ignoreing kind filter. -- Invalid: none of the resources will be selected. Ignoreing kind filter. ``` - + + ## [GKE Audit logs](#gke_audit) @@ -166,22 +197,27 @@ GKE audit log including cluster creation,deletion and upgrades. (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit +|Timeline type|Short name on chip| +|:-:|:-:| + + + +### Target log type + +**![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit** -**Sample used query** +Sample query: ``` resource.type=("gke_cluster" OR "gke_nodepool") logName="projects/gcp-project-id/logs/cloudaudit.googleapis.com%2Factivity" resource.labels.cluster_name="gcp-cluster-name" ``` - + + ## [Compute API Logs](#compute_api) @@ -201,36 +237,36 @@ Compute API audit logs used for cluster related logs. This also visualize operat (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit +|Timeline type|Short name on chip| +|:-:|:-:| +|[Operation timeline](./relationships.md#RelationshipOperation)|operation| -**Sample used query** + + +### Target log type -``` -resource.type="k8s_cluster" -resource.labels.cluster_name="gcp-cluster-name" -protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") --- Invalid: none of the resources will be selected. Ignoreing kind filter. --- Invalid: none of the resources will be selected. Ignoreing namespace filter. -``` - - -#### ![FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api +**![FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api** -**Sample used query** +Sample query: ``` resource.type="gce_instance" - -protoPayload.methodName:("list" OR "get" OR "watch") - protoPayload.resourceName:(instances/gke-test-cluster-node-1 OR instances/gke-test-cluster-node-2) - +-protoPayload.methodName:("list" OR "get" OR "watch") +protoPayload.resourceName:(instances/gke-test-cluster-node-1 OR instances/gke-test-cluster-node-2) + ``` - + + + +### Dependent queries + +Following log queries are used with this feature. + +* ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + ## [GCE Network Logs](#gce_network) @@ -250,35 +286,36 @@ GCE network API audit log including NEG related audit logs to identify when the (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit +|Timeline type|Short name on chip| +|:-:|:-:| +|[NEG timeline](./relationships.md#RelationshipNetworkEndpointGroup)|neg| -**Sample used query** + + +### Target log type -``` -resource.type="k8s_cluster" -resource.labels.cluster_name="gcp-cluster-name" -protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") --- Invalid: none of the resources will be selected. Ignoreing kind filter. --- Invalid: none of the resources will be selected. Ignoreing namespace filter. -``` - - -#### ![33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api +**![33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api** -**Sample used query** +Sample query: ``` resource.type="gce_network" -protoPayload.methodName:("list" OR "get" OR "watch") protoPayload.resourceName:(networkEndpointGroups/neg-id-1 OR networkEndpointGroups/neg-id-2) + ``` - + + + +### Dependent queries + +Following log queries are used with this feature. + +* ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + ## [MultiCloud API logs](#multicloud_api) @@ -296,23 +333,29 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)multicloud_api +|Timeline type|Short name on chip| +|:-:|:-:| + + + +### Target log type + +**![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)multicloud_api** -**Sample used query** +Sample query: ``` resource.type="audited_resource" resource.labels.service="gkemulticloud.googleapis.com" resource.labels.method:("Update" OR "Create" OR "Delete") protoPayload.resourceName:"awsClusters/cluster-foo" + ``` - + + ## [Autoscaler Logs](#autoscaler) @@ -331,15 +374,20 @@ This log type also includes Node Auto Provisioner logs. (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler +|Timeline type|Short name on chip| +|:-:|:-:| +|[Managed instance group timeline](./relationships.md#RelationshipManagedInstanceGroup)|mig| -**Sample used query** + + +### Target log type + +**![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler** + +Sample query: ``` resource.type="k8s_cluster" @@ -348,7 +396,8 @@ resource.labels.cluster_name="gcp-cluster-name" -jsonPayload.status: "" logName="projects/gcp-project-id/logs/container.googleapis.com%2Fcluster-autoscaler-visibility" ``` - + + ## [OnPrem API logs](#onprem_api) @@ -366,23 +415,29 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)onprem_api +|Timeline type|Short name on chip| +|:-:|:-:| + + + +### Target log type + +**![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)onprem_api** -**Sample used query** +Sample query: ``` resource.type="audited_resource" resource.labels.service="gkeonprem.googleapis.com" resource.labels.method:("Update" OR "Create" OR "Delete" OR "Enroll" OR "Unenroll") protoPayload.resourceName:"baremetalClusters/my-cluster" + ``` - + + ## [Kubernetes Control plane component logs](#k8s_control_plane_component) @@ -401,15 +456,20 @@ Visualize Kubernetes control plane component logs on a cluster (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component +|Timeline type|Short name on chip| +|:-:|:-:| +|[Control plane component timeline](./relationships.md#RelationshipControlPlaneComponent)|controlplane| -**Sample used query** + + +### Target log type + +**![FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component** + +Sample query: ``` resource.type="k8s_control_plane_component" @@ -418,7 +478,8 @@ resource.labels.project_id="gcp-project-id" -sourceLocation.file="httplog.go" -- Invalid: none of the controlplane component will be selected. Ignoreing component name filter. ``` - + + ## [Node serial port logs](#serialport) @@ -439,28 +500,19 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit +|Timeline type|Short name on chip| +|:-:|:-:| -**Sample used query** + + +### Target log type -``` -resource.type="k8s_cluster" -resource.labels.cluster_name="gcp-cluster-name" -protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") --- Invalid: none of the resources will be selected. Ignoreing kind filter. --- Invalid: none of the resources will be selected. Ignoreing namespace filter. -``` - - -#### ![333333](https://placehold.co/15x15/333333/333333.png)serial_port +**![333333](https://placehold.co/15x15/333333/333333.png)serial_port** -**Sample used query** +Sample query: ``` LOG_ID("serialconsole.googleapis.com%2Fserial_port_1_output") OR @@ -472,7 +524,15 @@ labels."compute.googleapis.com/resource_name"=("gke-test-cluster-node-1" OR "gke -- No node name substring filters are specified. ``` - + + + +### Dependent queries + +Following log queries are used with this feature. + +* ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + ## [(Alpha) Composer / Airflow Scheduler](#airflow_schedule) @@ -491,20 +551,25 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment +|Timeline type|Short name on chip| +|:-:|:-:| -**Sample used query** + + +### Target log type + +**![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment** + +Sample query: ``` TODO: add sample query ``` - + + ## [(Alpha) Cloud Composer / Airflow Worker](#airflow_worker) @@ -523,20 +588,25 @@ Airflow Worker logs contain information related to the execution of TaskInstance (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment +|Timeline type|Short name on chip| +|:-:|:-:| + + + +### Target log type + +**![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment** -**Sample used query** +Sample query: ``` TODO: add sample query ``` - + + ## [(Alpha) Composer / Airflow DagProcessorManager](#airflow_dag_processor) @@ -555,17 +625,22 @@ The DagProcessorManager logs contain information for investigating the number of (example: 2006-01-02T15:04:05-07:00)| |Duration|| - -### Target logs + +### Output timelines -Following log queries are used with this feature. - - -#### ![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment +|Timeline type|Short name on chip| +|:-:|:-:| + + + +### Target log type -**Sample used query** +**![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment** + +Sample query: ``` TODO: add sample query ``` - + + diff --git a/docs/en/inspection-type.md b/docs/en/inspection-type.md index 851fd58..4def083 100644 --- a/docs/en/inspection-type.md +++ b/docs/en/inspection-type.md @@ -12,7 +12,7 @@ Inspection type is ... -* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) +* [Kubernetes Audit Log](./features.md#k8s_audit) * [Kubernetes Event Logs](./features.md#k8s_event) @@ -48,7 +48,7 @@ Inspection type is ... -* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) +* [Kubernetes Audit Log](./features.md#k8s_audit) * [Kubernetes Event Logs](./features.md#k8s_event) @@ -93,7 +93,7 @@ Inspection type is ... -* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) +* [Kubernetes Audit Log](./features.md#k8s_audit) * [Kubernetes Event Logs](./features.md#k8s_event) @@ -117,7 +117,7 @@ Inspection type is ... -* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) +* [Kubernetes Audit Log](./features.md#k8s_audit) * [Kubernetes Event Logs](./features.md#k8s_event) @@ -141,7 +141,7 @@ Inspection type is ... -* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) +* [Kubernetes Audit Log](./features.md#k8s_audit) * [Kubernetes Event Logs](./features.md#k8s_event) @@ -165,7 +165,7 @@ Inspection type is ... -* [Kubernetes Audit Log(v2)](./features.md#k8s_audit) +* [Kubernetes Audit Log](./features.md#k8s_audit) * [Kubernetes Event Logs](./features.md#k8s_event) diff --git a/docs/template/feature.template.md b/docs/template/feature.template.md index 7c83ec9..c2501df 100644 --- a/docs/template/feature.template.md +++ b/docs/template/feature.template.md @@ -17,23 +17,38 @@ {{- end}} {{end}} -{{with $feature.Queries}} - -### Target logs -Following log queries are used with this feature. - -{{range $index,$query := $feature.Queries}} - -#### ![{{$query.LogTypeColorCode}}](https://placehold.co/15x15/{{$query.LogTypeColorCode}}/{{$query.LogTypeColorCode}}.png){{$query.LogTypeLabel}} + +### Output timelines + +|Timeline type|Short name on chip| +|:-:|:-:| +{{- range $index,$timeline := $feature.OutputTimelines}} +|[{{$timeline.LongName}}](./relationships.md#{{$timeline.RelationshipID}})|{{$timeline.Name}}| +{{- end}} + + + +### Target log type + +**![{{$feature.TargetQueryDependency.LogTypeColorCode}}](https://placehold.co/15x15/{{$feature.TargetQueryDependency.LogTypeColorCode}}/{{$feature.TargetQueryDependency.LogTypeColorCode}}.png){{$feature.TargetQueryDependency.LogTypeLabel}}** -**Sample used query** +Sample query: ``` -{{$query.SampleQuery}} +{{$feature.TargetQueryDependency.SampleQuery}} ``` - -{{end}} + + +{{with $feature.IndirectQueryDependency}} + +### Dependent queries + +Following log queries are used with this feature. +{{range $index,$query := $feature.IndirectQueryDependency}} +* ![{{$query.LogTypeColorCode}}](https://placehold.co/15x15/{{$query.LogTypeColorCode}}/{{$query.LogTypeColorCode}}.png){{$query.LogTypeLabel}} +{{- end}} + {{end}} {{end}} {{end}} \ No newline at end of file diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index f0afd5f..921f467 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -20,13 +20,20 @@ type FeatureDocumentElement struct { Name string Description string - Queries []FeatureDependentQueryElement - Forms []FeatureDependentFormElement + IndirectQueryDependency []FeatureIndirectDependentQueryElement + TargetQueryDependency FeatureDependentTargetQueryElement + Forms []FeatureDependentFormElement + OutputTimelines []FeatureOutputTimelineElement } -type FeatureDependentQueryElement struct { +type FeatureIndirectDependentQueryElement struct { + ID string + LogTypeLabel string + LogTypeColorCode string +} + +type FeatureDependentTargetQueryElement struct { ID string - LogType enum.LogType LogTypeLabel string LogTypeColorCode string SampleQuery string @@ -38,27 +45,42 @@ type FeatureDependentFormElement struct { Description string } +type FeatureOutputTimelineElement struct { + RelationshipID string + LongName string + Name string +} + func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*FeatureDocumentModel, error) { result := FeatureDocumentModel{} features := taskServer.RootTaskSet.FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false) for _, feature := range features.GetAll() { - queryElements := []FeatureDependentQueryElement{} + indirectQueryDependencyElement := []FeatureIndirectDependentQueryElement{} + targetQueryDependencyElement := FeatureDependentTargetQueryElement{} + targetLogTypeKey := feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTargetLogType, enum.LogTypeUnknown).(enum.LogType) - // Get query related tasks required by this feature. - queryTasks, err := getDependentQueryTasks(taskServer, feature) + // Get query related tasks in the dependency of this feature. + queryTasksInDependency, err := getDependentQueryTasks(taskServer, feature) if err != nil { return nil, err } - for _, queryTask := range queryTasks { + for _, queryTask := range queryTasksInDependency { logTypeKey := enum.LogType(queryTask.Labels().GetOrDefault(label.TaskLabelKeyQueryTaskTargetLogType, enum.LogTypeUnknown).(enum.LogType)) - logType := enum.LogTypes[logTypeKey] - queryElements = append(queryElements, FeatureDependentQueryElement{ - ID: queryTask.ID().String(), - LogType: logTypeKey, - LogTypeLabel: logType.Label, - LogTypeColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), - SampleQuery: strings.TrimRight(queryTask.Labels().GetOrDefault(label.TaskLabelKeyQueryTaskSampleQuery, "").(string), "\n"), - }) + if targetLogTypeKey != logTypeKey { + logType := enum.LogTypes[logTypeKey] + indirectQueryDependencyElement = append(indirectQueryDependencyElement, FeatureIndirectDependentQueryElement{ + ID: queryTask.ID().String(), + LogTypeLabel: logType.Label, + LogTypeColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), + }) + } else { + targetQueryDependencyElement = FeatureDependentTargetQueryElement{ + ID: queryTask.ID().String(), + LogTypeLabel: enum.LogTypes[targetLogTypeKey].Label, + LogTypeColorCode: strings.TrimLeft(enum.LogTypes[targetLogTypeKey].LabelBackgroundColor, "#"), + SampleQuery: queryTask.Labels().GetOrDefault(label.TaskLabelKeyQueryTaskSampleQuery, "").(string), + } + } } formElements := []FeatureDependentFormElement{} @@ -74,12 +96,47 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat }) } + outputTimelines := []FeatureOutputTimelineElement{} + for i := 0; i < enum.EnumParentRelationshipLength; i++ { + relationshipKey := enum.ParentRelationship(i) + relationship := enum.ParentRelationships[relationshipKey] + + isRelated := false + for _, event := range relationship.GeneratableEvents { + if event.SourceLogType == targetLogTypeKey { + isRelated = true + break + } + } + for _, revision := range relationship.GeneratableRevisions { + if revision.SourceLogType == targetLogTypeKey { + isRelated = true + break + } + } + for _, alias := range relationship.GeneratableAliasTimelineInfo { + if alias.SourceLogType == targetLogTypeKey { + isRelated = true + break + } + } + if isRelated { + outputTimelines = append(outputTimelines, FeatureOutputTimelineElement{ + RelationshipID: relationship.EnumKeyName, + LongName: relationship.LongName, + Name: relationship.Label, + }) + } + } + result.Features = append(result.Features, FeatureDocumentElement{ - ID: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string), - Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), - Description: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), - Queries: queryElements, - Forms: formElements, + ID: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string), + Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), + Description: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), + IndirectQueryDependency: indirectQueryDependencyElement, + TargetQueryDependency: targetQueryDependencyElement, + Forms: formElements, + OutputTimelines: outputTimelines, }) } diff --git a/pkg/document/model/log_type.go b/pkg/document/model/log_type.go index aa72c36..9b95dd8 100644 --- a/pkg/document/model/log_type.go +++ b/pkg/document/model/log_type.go @@ -18,7 +18,7 @@ type LogTypeDocumentElement struct { func GetLogTypeDocumentModel() LogTypeDocumentModel { logTypes := []LogTypeDocumentElement{} - for i := 1; i < enum.EnumLogTypeCount; i++ { + for i := 1; i < enum.EnumLogTypeLength; i++ { logType := enum.LogTypes[enum.LogType(i)] logTypes = append(logTypes, LogTypeDocumentElement{ ID: logType.EnumKeyName, diff --git a/pkg/model/enum/log_type.go b/pkg/model/enum/log_type.go index 96e2481..36163d3 100644 --- a/pkg/model/enum/log_type.go +++ b/pkg/model/enum/log_type.go @@ -35,7 +35,7 @@ const ( logTypeUnusedEnd ) -const EnumLogTypeCount = int(logTypeUnusedEnd) +const EnumLogTypeLength = int(logTypeUnusedEnd) type LogTypeFrontendMetadata struct { // EnumKeyName is the name of this enum value. Must match with the enum key. diff --git a/pkg/source/gcp/task/gke/compute_api/query.go b/pkg/source/gcp/task/gke/compute_api/query.go index 38c598f..a763f79 100644 --- a/pkg/source/gcp/task/gke/compute_api/query.go +++ b/pkg/source/gcp/task/gke/compute_api/query.go @@ -51,9 +51,9 @@ func GenerateComputeAPIQuery(taskMode int, nodeNames []string) []string { func generateComputeAPIQueryWithInstanceNameFilter(instanceNameFilter string) string { return fmt.Sprintf(`resource.type="gce_instance" - -protoPayload.methodName:("list" OR "get" OR "watch") - %s - `, instanceNameFilter) +-protoPayload.methodName:("list" OR "get" OR "watch") +%s +`, instanceNameFilter) } var ComputeAPIQueryTask = query.NewQueryGeneratorTask(ComputeAPIQueryTaskID, "Compute API Logs", enum.LogTypeComputeApi, []string{ From bcb7cba1b86bc6ce1a3c654607f6c389c74e9265 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 14:24:54 +0900 Subject: [PATCH 09/23] Removed log type related document - **fix: Make node log parser remap severity correctly** --- cmd/document-generator/main.go | 4 ---- docs/en/log-types.md | 38 ---------------------------------- pkg/document/model/log_type.go | 32 ---------------------------- 3 files changed, 74 deletions(-) delete mode 100644 docs/en/log-types.md delete mode 100644 pkg/document/model/log_type.go diff --git a/cmd/document-generator/main.go b/cmd/document-generator/main.go index e21bde6..c51774f 100644 --- a/cmd/document-generator/main.go +++ b/cmd/document-generator/main.go @@ -52,10 +52,6 @@ func main() { err = generator.GenerateDocument("./docs/en/features.md", "feature-template", featureDocumentModel, false) fatal(err, "failed to generate feature document") - logTypeDocumentModel := model.GetLogTypeDocumentModel() - err = generator.GenerateDocument("./docs/en/log-types.md", "log-type-template", logTypeDocumentModel, false) - fatal(err, "failed to generate log type document") - relationshipDocumentModel := model.GetRelationshipDocumentModel() err = generator.GenerateDocument("./docs/en/relationships.md", "relationship-template", relationshipDocumentModel, false) fatal(err, "failed to generate relationship document") diff --git a/docs/en/log-types.md b/docs/en/log-types.md deleted file mode 100644 index 46d504f..0000000 --- a/docs/en/log-types.md +++ /dev/null @@ -1,38 +0,0 @@ -# Log types - - -## [![#3fb549](https://placehold.co/15x15/3fb549/3fb549.png) k8s_event](#LogTypeEvent) - - -## [![#000000](https://placehold.co/15x15/000000/000000.png) k8s_audit](#LogTypeAudit) - - -## [![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png) k8s_container](#LogTypeContainer) - - -## [![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png) k8s_node](#LogTypeNode) - - -## [![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png) gke_audit](#LogTypeGkeAudit) - - -## [![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png) compute_api](#LogTypeComputeApi) - - -## [![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png) multicloud_api](#LogTypeMulticloudAPI) - - -## [![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png) onprem_api](#LogTypeOnPremAPI) - - -## [![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png) network_api](#LogTypeNetworkAPI) - - -## [![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png) autoscaler](#LogTypeAutoscaler) - - -## [![#88AA55](https://placehold.co/15x15/88AA55/88AA55.png) composer_environment](#LogTypeComposerEnvironment) - - -## [![#FF3333](https://placehold.co/15x15/FF3333/FF3333.png) control_plane_component](#LogTypeControlPlaneComponent) - diff --git a/pkg/document/model/log_type.go b/pkg/document/model/log_type.go deleted file mode 100644 index 9b95dd8..0000000 --- a/pkg/document/model/log_type.go +++ /dev/null @@ -1,32 +0,0 @@ -package model - -import ( - "strings" - - "github.com/GoogleCloudPlatform/khi/pkg/model/enum" -) - -type LogTypeDocumentModel struct { - LogTypes []LogTypeDocumentElement -} - -type LogTypeDocumentElement struct { - ID string - Name string - ColorCode string -} - -func GetLogTypeDocumentModel() LogTypeDocumentModel { - logTypes := []LogTypeDocumentElement{} - for i := 1; i < enum.EnumLogTypeLength; i++ { - logType := enum.LogTypes[enum.LogType(i)] - logTypes = append(logTypes, LogTypeDocumentElement{ - ID: logType.EnumKeyName, - Name: logType.Label, - ColorCode: strings.TrimLeft(logType.LabelBackgroundColor, "#"), - }) - } - return LogTypeDocumentModel{ - LogTypes: logTypes, - } -} From dded09e29529dc100ef79ba9ef8d46c31be762be Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 14:44:27 +0900 Subject: [PATCH 10/23] Add timeline references --- docs/en/features.md | 192 ++++++++---------- docs/template/feature.template.md | 2 +- pkg/document/model/feature.go | 14 +- pkg/inspection/form/textform.go | 16 +- pkg/source/gcp/task/form.go | 14 +- pkg/source/gcp/task/gke/k8s_container/form.go | 4 +- .../gke/k8s_control_plane_component/form.go | 2 +- 7 files changed, 120 insertions(+), 124 deletions(-) diff --git a/docs/en/features.md b/docs/en/features.md index 81ae11d..9ac7be2 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -10,24 +10,23 @@ This parser reveals how these resources are created,updated or deleted. |Parameter name|Description| |:-:|---| -|Kind|| -|Namespaces|| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Kind|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|[The default resource timeline](./relationships.md#RelationshipChild)|resource| -|[Status condition field timeline](./relationships.md#RelationshipResourceCondition)|condition| -|[Endpoint serving state timeline](./relationships.md#RelationshipEndpointSlice)|endpointslice| -|[Owning children timeline](./relationships.md#RelationshipOwnerReference)|owns| -|[Pod binding timeline](./relationships.md#RelationshipPodBinding)|binds| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| +|![4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png)[Status condition field timeline](./relationships.md#RelationshipResourceCondition)|condition| +|![008000](https://placehold.co/15x15/008000/008000.png)[Endpoint serving state timeline](./relationships.md#RelationshipEndpointSlice)|endpointslice| +|![33DD88](https://placehold.co/15x15/33DD88/33DD88.png)[Owning children timeline](./relationships.md#RelationshipOwnerReference)|owns| +|![FF8855](https://placehold.co/15x15/FF8855/FF8855.png)[Pod binding timeline](./relationships.md#RelationshipPodBinding)|binds| @@ -59,19 +58,18 @@ This parser shows events associated to K8s resources |Parameter name|Description| |:-:|---| -|Namespaces|| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|[The default resource timeline](./relationships.md#RelationshipChild)|resource| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| @@ -101,20 +99,19 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. |Parameter name|Description| |:-:|---| -|Node names|A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster.| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Node names|| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|[Container timeline](./relationships.md#RelationshipContainer)|container| -|[Node component timeline](./relationships.md#RelationshipNodeComponent)|node-component| +|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#RelationshipContainer)|container| +|![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)[Node component timeline](./relationships.md#RelationshipNodeComponent)|node-component| @@ -146,23 +143,19 @@ Container logs ingested from stdout/stderr of workload Pods. |Parameter name|Description| |:-:|---| -|Namespaces(Container logs)|Container logs tend to be a lot and take very long time to query. -Specify the space splitted namespace lists to query container logs only in the specific namespaces.| -|Pod names(Container logs)|Container logs tend to be a lot and take very long time to query. - Specify the space splitted pod names lists to query container logs only in the specific pods. - This parameter is evaluated as the partial match not the perfect match. You can use the prefix of the pod names.| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Namespaces(Container logs)|| +|Pod names(Container logs)|| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|[Container timeline](./relationships.md#RelationshipContainer)|container| +|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#RelationshipContainer)|container| @@ -191,11 +184,10 @@ GKE audit log including cluster creation,deletion and upgrades. |Parameter name|Description| |:-:|---| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -229,20 +221,19 @@ Compute API audit logs used for cluster related logs. This also visualize operat |Parameter name|Description| |:-:|---| -|Kind|| -|Namespaces|| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Kind|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|[Operation timeline](./relationships.md#RelationshipOperation)|operation| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| @@ -278,20 +269,19 @@ GCE network API audit log including NEG related audit logs to identify when the |Parameter name|Description| |:-:|---| -|Kind|| -|Namespaces|| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Kind|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|[NEG timeline](./relationships.md#RelationshipNetworkEndpointGroup)|neg| +|![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[NEG timeline](./relationships.md#RelationshipNetworkEndpointGroup)|neg| @@ -327,11 +317,10 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. |Parameter name|Description| |:-:|---| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -368,18 +357,17 @@ This log type also includes Node Auto Provisioner logs. |Parameter name|Description| |:-:|---| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|[Managed instance group timeline](./relationships.md#RelationshipManagedInstanceGroup)|mig| +|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Managed instance group timeline](./relationships.md#RelationshipManagedInstanceGroup)|mig| @@ -409,11 +397,10 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and |Parameter name|Description| |:-:|---| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -449,19 +436,18 @@ Visualize Kubernetes control plane component logs on a cluster |Parameter name|Description| |:-:|---| -|Control plane component names|Control plane component names to query(e.g. apiserver, controller-manager...etc)| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Control plane component names|| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|[Control plane component timeline](./relationships.md#RelationshipControlPlaneComponent)|controlplane| +|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Control plane component timeline](./relationships.md#RelationshipControlPlaneComponent)|controlplane| @@ -491,14 +477,13 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on |Parameter name|Description| |:-:|---| -|Kind|| -|Namespaces|| -|Node names|A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster.| -|Project ID|A project ID containing the cluster to inspect| -|Cluster name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|Kind|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|Node names|| +|Project ID|The project ID containing the logs of cluster to query| +|Cluster name|The cluster name to gather logs.| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -544,12 +529,11 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst |Parameter name|Description| |:-:|---| -|Location|A location(regions) containing the environments to inspect| -|Project ID|A project ID containing the cluster to inspect| +|Location|| +|Project ID|The project ID containing the logs of cluster to query| |Composer Environment Name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -581,12 +565,11 @@ Airflow Worker logs contain information related to the execution of TaskInstance |Parameter name|Description| |:-:|---| -|Location|A location(regions) containing the environments to inspect| -|Project ID|A project ID containing the cluster to inspect| +|Location|| +|Project ID|The project ID containing the logs of cluster to query| |Composer Environment Name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -618,12 +601,11 @@ The DagProcessorManager logs contain information for investigating the number of |Parameter name|Description| |:-:|---| -|Location|A location(regions) containing the environments to inspect| -|Project ID|A project ID containing the cluster to inspect| +|Location|| +|Project ID|The project ID containing the logs of cluster to query| |Composer Environment Name|| -|End time|The endtime of query. Please input it in the format of RFC3339 -(example: 2006-01-02T15:04:05-07:00)| -|Duration|| +|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines diff --git a/docs/template/feature.template.md b/docs/template/feature.template.md index c2501df..810fd91 100644 --- a/docs/template/feature.template.md +++ b/docs/template/feature.template.md @@ -24,7 +24,7 @@ |Timeline type|Short name on chip| |:-:|:-:| {{- range $index,$timeline := $feature.OutputTimelines}} -|[{{$timeline.LongName}}](./relationships.md#{{$timeline.RelationshipID}})|{{$timeline.Name}}| +|![{{$timeline.RelationshipColorCode}}](https://placehold.co/15x15/{{$timeline.RelationshipColorCode}}/{{$timeline.RelationshipColorCode}}.png)[{{$timeline.LongName}}](./relationships.md#{{$timeline.RelationshipID}})|{{$timeline.Name}}| {{- end}} diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index 921f467..8840141 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -46,9 +46,10 @@ type FeatureDependentFormElement struct { } type FeatureOutputTimelineElement struct { - RelationshipID string - LongName string - Name string + RelationshipID string + RelationshipColorCode string + LongName string + Name string } func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*FeatureDocumentModel, error) { @@ -122,9 +123,10 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat } if isRelated { outputTimelines = append(outputTimelines, FeatureOutputTimelineElement{ - RelationshipID: relationship.EnumKeyName, - LongName: relationship.LongName, - Name: relationship.Label, + RelationshipID: relationship.EnumKeyName, + RelationshipColorCode: strings.TrimLeft(relationship.LabelBackgroundColor, "#"), + LongName: relationship.LongName, + Name: relationship.Label, }) } } diff --git a/pkg/inspection/form/textform.go b/pkg/inspection/form/textform.go index 6a94128..4695653 100644 --- a/pkg/inspection/form/textform.go +++ b/pkg/inspection/form/textform.go @@ -52,7 +52,8 @@ type TextFormDefinitionBuilder struct { label string priority int dependencies []string - description string + uiDescription string + documentDescription string defaultValue TextFormDefaultValueGenerator validator TextFormValidator allowEditProvider TextFormAllowEditProvider @@ -102,8 +103,13 @@ func (b *TextFormDefinitionBuilder) WithDependencies(dependencies []string) *Tex return b } -func (b *TextFormDefinitionBuilder) WithDescription(description string) *TextFormDefinitionBuilder { - b.description = description +func (b *TextFormDefinitionBuilder) WithUIDescription(uiDescription string) *TextFormDefinitionBuilder { + b.uiDescription = uiDescription + return b +} + +func (b *TextFormDefinitionBuilder) WithDocumentDescription(documentDescription string) *TextFormDefinitionBuilder { + b.documentDescription = documentDescription return b } @@ -198,7 +204,7 @@ func (b *TextFormDefinitionBuilder) Build(labelOpts ...common_task.LabelOpt) com field.Type = "Text" field.Priority = b.priority field.Label = b.label - field.Description = b.description + field.Description = b.uiDescription field.HintType = form_metadata.HintTypeInfo suggestions, err := b.suggestionsProvider(ctx, currentValue, v, prevValue) @@ -248,6 +254,6 @@ func (b *TextFormDefinitionBuilder) Build(labelOpts ...common_task.LabelOpt) com return convertedValue, nil }, append(labelOpts, label.NewFormTaskLabelOpt( b.label, - b.description, + b.documentDescription, ))...) } diff --git a/pkg/source/gcp/task/form.go b/pkg/source/gcp/task/form.go index 0bdb0e7..9f46a08 100644 --- a/pkg/source/gcp/task/form.go +++ b/pkg/source/gcp/task/form.go @@ -43,7 +43,8 @@ const InputProjectIdTaskID = GCPPrefix + "input/project-id" var projectIdValidator = regexp.MustCompile(`^\s*[0-9a-z\.:\-]+\s*$`) var InputProjectIdTask = form.NewInputFormDefinitionBuilder(InputProjectIdTaskID, PriorityForResourceIdentifierGroup+5000, "Project ID"). - WithDescription("A project ID containing the cluster to inspect"). + WithUIDescription("The project ID containing the logs of cluster to query"). + WithDocumentDescription("The project ID containing the logs of cluster to query"). WithDependencies([]string{}). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { if !projectIdValidator.Match([]byte(value)) { @@ -81,6 +82,7 @@ var clusterNameValidator = regexp.MustCompile(`^\s*[0-9a-z\-]+\s*$`) var InputClusterNameTask = form.NewInputFormDefinitionBuilder(InputClusterNameTaskID, PriorityForResourceIdentifierGroup+4000, "Cluster name"). WithDependencies([]string{AutocompleteClusterNamesTaskID, ClusterNamePrefixTaskID}). + WithDocumentDescription("The cluster name to gather logs."). WithDefaultValueFunc(func(ctx context.Context, variables *task.VariableSet, previousValues []string) (string, error) { clusters, err := GetAutocompleteClusterNamesFromTaskVariable(variables) if err != nil { @@ -151,6 +153,7 @@ var InputDurationTask = form.NewInputFormDefinitionBuilder(InputDurationTaskID, InputEndTimeTaskID, TimeZoneShiftInputTaskID, }). + WithDocumentDescription("The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)"). WithDefaultValueFunc(func(ctx context.Context, variables *task.VariableSet, previousValues []string) (string, error) { if len(previousValues) > 0 { return previousValues[0], nil @@ -217,8 +220,9 @@ var InputEndTimeTask = form.NewInputFormDefinitionBuilder(InputEndTimeTaskID, Pr common_task.InspectionTimeTaskID, TimeZoneShiftInputTaskID, }). - WithDescription(`The endtime of query. Please input it in the format of RFC3339 + WithUIDescription(`The endtime of query. Please input it in the format of RFC3339 (example: 2006-01-02T15:04:05-07:00)`). + WithDocumentDescription("The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter."). WithSuggestionsFunc(func(ctx context.Context, value string, variables *task.VariableSet, previousValues []string) ([]string, error) { return previousValues, nil }). @@ -300,6 +304,7 @@ var inputKindNameAliasMap queryutil.SetFilterAliasToItemsMap = map[string][]stri } var InputKindFilterTask = form.NewInputFormDefinitionBuilder(InputKindFilterTaskID, PriorityForK8sResourceFilterGroup+5000, "Kind"). WithDefaultValueConstant("@default", true). + WithDocumentDescription("The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources"). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { if value == "" { return "kind filter can't be empty", nil @@ -331,6 +336,7 @@ var inputNamespacesAliasMap queryutil.SetFilterAliasToItemsMap = map[string][]st } var InputNamespaceFilterTask = form.NewInputFormDefinitionBuilder(InputNamespaceFilterTaskID, PriorityForK8sResourceFilterGroup+4000, "Namespaces"). WithDefaultValueConstant("@all_cluster_scoped @all_namespaced", true). + WithDocumentDescription("The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources."). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { if value == "" { return "namespace filter can't be empty", nil @@ -375,7 +381,7 @@ func getNodeNameSubstringsFromRawInput(value string) []string { // InputNodeNameFilterTask is a task to collect list of substrings of node names. This input value is used in querying k8s_node or serialport logs. var InputNodeNameFilterTask = form.NewInputFormDefinitionBuilder(InputNodeNameFilterTaskID, PriorityForK8sResourceFilterGroup+3000, "Node names"). WithDefaultValueConstant("", true). - WithDescription("A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster."). + WithUIDescription("A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster."). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { nodeNameSubstrings := getNodeNameSubstringsFromRawInput(value) for _, name := range nodeNameSubstrings { @@ -394,7 +400,7 @@ func GetNodeNameFilterFromTaskVaraible(tv *task.VariableSet) ([]string, error) { const InputLocationsTaskID = GCPPrefix + "input/location" -var InputLocationsTask = form.NewInputFormDefinitionBuilder(InputLocationsTaskID, PriorityForResourceIdentifierGroup+4500, "Location").WithDescription( +var InputLocationsTask = form.NewInputFormDefinitionBuilder(InputLocationsTaskID, PriorityForResourceIdentifierGroup+4500, "Location").WithUIDescription( "A location(regions) containing the environments to inspect", ).Build() diff --git a/pkg/source/gcp/task/gke/k8s_container/form.go b/pkg/source/gcp/task/gke/k8s_container/form.go index e7b18d5..56fed5f 100644 --- a/pkg/source/gcp/task/gke/k8s_container/form.go +++ b/pkg/source/gcp/task/gke/k8s_container/form.go @@ -32,7 +32,7 @@ var inputNamespacesAliasMap queryutil.SetFilterAliasToItemsMap = map[string][]st } var InputContainerQueryNamespaceFilterTask = form.NewInputFormDefinitionBuilder(InputContainerQueryNamespacesTaskID, priorityForContainerGroup+1000, "Namespaces(Container logs)"). WithDefaultValueConstant("@managed", true). - WithDescription(`Container logs tend to be a lot and take very long time to query. + WithUIDescription(`Container logs tend to be a lot and take very long time to query. Specify the space splitted namespace lists to query container logs only in the specific namespaces.`). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { result, err := queryutil.ParseSetFilter(value, inputNamespacesAliasMap, true, true, true) @@ -59,7 +59,7 @@ const InputContainerQueryPodNamesTaskID = gcp_task.GCPPrefix + "input/container- var inputPodNamesAliasMap queryutil.SetFilterAliasToItemsMap = map[string][]string{} var InputContainerQueryPodNamesFilterMask = form.NewInputFormDefinitionBuilder(InputContainerQueryPodNamesTaskID, priorityForContainerGroup+2000, "Pod names(Container logs)"). WithDefaultValueConstant("@any", true). - WithDescription(`Container logs tend to be a lot and take very long time to query. + WithUIDescription(`Container logs tend to be a lot and take very long time to query. Specify the space splitted pod names lists to query container logs only in the specific pods. This parameter is evaluated as the partial match not the perfect match. You can use the prefix of the pod names.`). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { diff --git a/pkg/source/gcp/task/gke/k8s_control_plane_component/form.go b/pkg/source/gcp/task/gke/k8s_control_plane_component/form.go index bb94908..3e69bf9 100644 --- a/pkg/source/gcp/task/gke/k8s_control_plane_component/form.go +++ b/pkg/source/gcp/task/gke/k8s_control_plane_component/form.go @@ -40,7 +40,7 @@ var InputControlPlaneComponentNameFilterTask = form.NewInputFormDefinitionBuilde "controller-manager", "scheduler", }). - WithDescription("Control plane component names to query(e.g. apiserver, controller-manager...etc)"). + WithUIDescription("Control plane component names to query(e.g. apiserver, controller-manager...etc)"). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { result, err := queryutil.ParseSetFilter(value, inputControlPlaneComponentNameAliasMap, true, true, true) if err != nil { From 66317b95180acc5779fdde2d1b1b08b5a756ac9c Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 15:18:16 +0900 Subject: [PATCH 11/23] Added headers of references --- docs/en/features.md | 5 +++++ docs/en/inspection-type.md | 19 ++++++++++--------- docs/template/inspection-types.template.md | 8 +------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/en/features.md b/docs/en/features.md index 9ac7be2..0042408 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -1,3 +1,8 @@ +# Features + +The output timelnes of KHI is formed in the `feature tasks`. A feature may depends on parameters, other log query. +User will select features on the 2nd menu of the dialog after clicking `New inspection` button. + ## [Kubernetes Audit Log](#k8s_audit) diff --git a/docs/en/inspection-type.md b/docs/en/inspection-type.md index 4def083..d081081 100644 --- a/docs/en/inspection-type.md +++ b/docs/en/inspection-type.md @@ -1,14 +1,15 @@ - # Inspection types -Inspection type is ... +Log querying and parsing procedures in KHI is done on a DAG based task execution system. +Each tasks can have dependency and KHI automatically resolves them and run them parallelly as much as possible. - +Inspection type is the first menu item users will select on the `New inspection` menu. Inspection type is usually a cluster type. +KHI filters out unsupported parser for the selected inspection type at first. ## [Google Kubernetes Engine](#gcp-gke) -### Supported features +### Features @@ -44,7 +45,7 @@ Inspection type is ... ## [Cloud Composer](#gcp-composer) -### Supported features +### Features @@ -89,7 +90,7 @@ Inspection type is ... ## [GKE on AWS(Anthos on AWS)](#gcp-gke-on-aws) -### Supported features +### Features @@ -113,7 +114,7 @@ Inspection type is ... ## [GKE on Azure(Anthos on Azure)](#gcp-gke-on-azure) -### Supported features +### Features @@ -137,7 +138,7 @@ Inspection type is ... ## [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](#gcp-gdcv-for-baremetal) -### Supported features +### Features @@ -161,7 +162,7 @@ Inspection type is ... ## [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](#gcp-gdcv-for-vmware) -### Supported features +### Features diff --git a/docs/template/inspection-types.template.md b/docs/template/inspection-types.template.md index 0eb22db..3399113 100644 --- a/docs/template/inspection-types.template.md +++ b/docs/template/inspection-types.template.md @@ -1,15 +1,9 @@ {{define "inspection-type-template"}} - -# Inspection types - -Inspection type is ... - - {{range $index,$type := .InspectionTypes }} ## [{{$type.Name}}](#{{$type.ID}}) -### Supported features +### Features From a0144a0d0d6cfb0eddeda4da1f85e8bce4cf1c74 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 15:27:18 +0900 Subject: [PATCH 12/23] Fix sample queries --- docs/en/features.md | 12 ++++++------ pkg/source/gcp/task/gke/k8s_audit/query/query.go | 8 ++++++-- pkg/source/gcp/task/gke/k8s_container/query.go | 7 ++++++- .../task/gke/k8s_control_plane_component/query.go | 4 +++- pkg/source/gcp/task/gke/k8s_event/query.go | 4 +++- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/en/features.md b/docs/en/features.md index 0042408..e906158 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -45,8 +45,8 @@ Sample query: resource.type="k8s_cluster" resource.labels.cluster_name="gcp-cluster-name" protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") --- Invalid: none of the resources will be selected. Ignoreing kind filter. --- Invalid: none of the resources will be selected. Ignoreing namespace filter. +protoPayload.methodName=~"\.(deployments|replicasets|pods|nodes)\." +-- No namespace filter ``` @@ -87,7 +87,7 @@ Sample query: ``` logName="projects/gcp-project-id/logs/events" resource.labels.cluster_name="gcp-cluster-name" --- Invalid: none of the resources will be selected. Ignoreing namespace filter. +-- No namespace filter ``` @@ -173,8 +173,8 @@ Sample query: ``` resource.type="k8s_container" resource.labels.cluster_name="gcp-cluster-name" --- Invalid: none of the resources will be selected. Ignoreing kind filter. --- Invalid: none of the resources will be selected. Ignoreing kind filter. +resource.labels.namespace_name=("default") +-resource.labels.pod_name:("nginx-" OR "redis") ``` @@ -467,7 +467,7 @@ resource.type="k8s_control_plane_component" resource.labels.cluster_name="gcp-cluster-name" resource.labels.project_id="gcp-project-id" -sourceLocation.file="httplog.go" --- Invalid: none of the controlplane component will be selected. Ignoreing component name filter. +-- No component name filter ``` diff --git a/pkg/source/gcp/task/gke/k8s_audit/query/query.go b/pkg/source/gcp/task/gke/k8s_audit/query/query.go index 9b58435..613b737 100644 --- a/pkg/source/gcp/task/gke/k8s_audit/query/query.go +++ b/pkg/source/gcp/task/gke/k8s_audit/query/query.go @@ -48,8 +48,12 @@ var Task = query.NewQueryGeneratorTask(k8saudittask.K8sAuditQueryTaskID, "K8s au return []string{GenerateK8sAuditQuery(clusterName, kindFilter, namespaceFilter)}, nil }, GenerateK8sAuditQuery( "gcp-cluster-name", - &queryutil.SetFilterParseResult{}, - &queryutil.SetFilterParseResult{}, + &queryutil.SetFilterParseResult{ + Additives: []string{"deployments", "replicasets", "pods", "nodes"}, + }, + &queryutil.SetFilterParseResult{ + Additives: []string{"#cluster-scoped", "#namespaced"}, + }, )) func GenerateK8sAuditQuery(clusterName string, auditKindFilter *queryutil.SetFilterParseResult, namespaceFilter *queryutil.SetFilterParseResult) string { diff --git a/pkg/source/gcp/task/gke/k8s_container/query.go b/pkg/source/gcp/task/gke/k8s_container/query.go index 96ad00e..4bdc06e 100644 --- a/pkg/source/gcp/task/gke/k8s_container/query.go +++ b/pkg/source/gcp/task/gke/k8s_container/query.go @@ -103,4 +103,9 @@ var GKEContainerQueryTask = query.NewQueryGeneratorTask(GKEContainerLogQueryTask return []string{}, err } return []string{GenerateK8sContainerQuery(clusterName, namespacesFilter, podNamesFilter)}, nil -}, GenerateK8sContainerQuery("gcp-cluster-name", &queryutil.SetFilterParseResult{}, &queryutil.SetFilterParseResult{})) +}, GenerateK8sContainerQuery("gcp-cluster-name", &queryutil.SetFilterParseResult{ + Additives: []string{"default"}, +}, &queryutil.SetFilterParseResult{ + Subtractives: []string{"nginx-", "redis"}, + SubtractMode: true, +})) diff --git a/pkg/source/gcp/task/gke/k8s_control_plane_component/query.go b/pkg/source/gcp/task/gke/k8s_control_plane_component/query.go index 3a610a2..1c00b97 100644 --- a/pkg/source/gcp/task/gke/k8s_control_plane_component/query.go +++ b/pkg/source/gcp/task/gke/k8s_control_plane_component/query.go @@ -54,7 +54,9 @@ var GKEK8sControlPlaneLogQueryTask = query.NewQueryGeneratorTask(GKEK8sControlPl return []string{}, err } return []string{GenerateK8sControlPlaneQuery(clusterName, projectId, controlPlaneComponentNameFilter)}, nil -}, GenerateK8sControlPlaneQuery("gcp-cluster-name", "gcp-project-id", &queryutil.SetFilterParseResult{})) +}, GenerateK8sControlPlaneQuery("gcp-cluster-name", "gcp-project-id", &queryutil.SetFilterParseResult{ + SubtractMode: true, +})) func generateK8sControlPlaneComponentFilter(filter *queryutil.SetFilterParseResult) string { if filter.ValidationError != "" { diff --git a/pkg/source/gcp/task/gke/k8s_event/query.go b/pkg/source/gcp/task/gke/k8s_event/query.go index 3572fdd..4091ec7 100644 --- a/pkg/source/gcp/task/gke/k8s_event/query.go +++ b/pkg/source/gcp/task/gke/k8s_event/query.go @@ -91,5 +91,7 @@ var GKEK8sEventLogQueryTask = query.NewQueryGeneratorTask(GKEK8sEventLogQueryTas }, GenerateK8sEventQuery( "gcp-cluster-name", "gcp-project-id", - &queryutil.SetFilterParseResult{}, + &queryutil.SetFilterParseResult{ + Additives: []string{"#cluster-scoped", "#namespaced"}, + }, )) From add1225cc1aa4c05b8a1640526de9fef0bd2cfab Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 16:27:23 +0900 Subject: [PATCH 13/23] Added missing elements in parent relationships --- docs/en/features.md | 9 +++ docs/en/relationships.md | 42 +++++++++++-- pkg/model/enum/parent_relationship.go | 85 +++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/docs/en/features.md b/docs/en/features.md index e906158..5ac41e5 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -115,6 +115,7 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. |Timeline type|Short name on chip| |:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| |![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#RelationshipContainer)|container| |![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)[Node component timeline](./relationships.md#RelationshipNodeComponent)|node-component| @@ -199,6 +200,8 @@ GKE audit log including cluster creation,deletion and upgrades. |Timeline type|Short name on chip| |:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| @@ -238,6 +241,7 @@ Compute API audit logs used for cluster related logs. This also visualize operat |Timeline type|Short name on chip| |:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| |![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| @@ -286,6 +290,7 @@ GCE network API audit log including NEG related audit logs to identify when the |Timeline type|Short name on chip| |:-:|:-:| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| |![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[NEG timeline](./relationships.md#RelationshipNetworkEndpointGroup)|neg| @@ -332,6 +337,7 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. |Timeline type|Short name on chip| |:-:|:-:| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| @@ -372,6 +378,7 @@ This log type also includes Node Auto Provisioner logs. |Timeline type|Short name on chip| |:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| |![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Managed instance group timeline](./relationships.md#RelationshipManagedInstanceGroup)|mig| @@ -412,6 +419,7 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and |Timeline type|Short name on chip| |:-:|:-:| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| @@ -452,6 +460,7 @@ Visualize Kubernetes control plane component logs on a cluster |Timeline type|Short name on chip| |:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| |![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Control plane component timeline](./relationships.md#RelationshipControlPlaneComponent)|controlplane| diff --git a/docs/en/relationships.md b/docs/en/relationships.md index 62bfccb..cff4cda 100644 --- a/docs/en/relationships.md +++ b/docs/en/relationships.md @@ -1,3 +1,8 @@ +# Relationships + +KHI timelines are basically placed in the order of `Kind` -> `Namespace` -> `Resource name` -> `Subresource name`. +The relationship between its parent and children is usually interpretted as the order of its hierarchy, but some subresources are not actual kubernetes resources and it's associated with the parent timeline for convenience. Each timeline color meanings and type of logs put on them are different by this relationship. + ## [The default resource timeline](#RelationshipChild) @@ -9,9 +14,11 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| +|![#997700](https://placehold.co/15x15/997700/997700.png)Resource may be existing|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource exits at the time, but this existence is inferred from the other logs later. The detailed resource information is not available.| |![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png)Resource is existing|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource exits at the time| |![#CC0000](https://placehold.co/15x15/CC0000/CC0000.png)Resource is deleted|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource is deleted at the time.| |![#CC5500](https://placehold.co/15x15/CC5500/CC5500.png)Resource is under deleting with graceful period|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource is being deleted with grace period at the time.| +|![#4444ff](https://placehold.co/15x15/4444ff/4444ff.png)Resource is being provisioned|![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit|This state indicates the resource is being provisioned. Currently this state is only used for cluster/nodepool status only.| @@ -24,6 +31,10 @@ This timeline can have the following events. |---|---| |![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|An event that related to a resource but not changing the resource. This is often an error log for an operation to the resource.| |![#3fb549](https://placehold.co/15x15/3fb549/3fb549.png)k8s_event|An event that related to a resource| +|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|An event that related to a node resource| +|![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api|An event that related to a compute resource| +|![#FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component|A log related to the timeline resource related to control plane component| +|![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler|A log related to the Pod which triggered or prevented autoscaler| @@ -37,9 +48,9 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| -|![#004400](https://placehold.co/15x15/004400/004400.png)State is 'True'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| -|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)State is 'False'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| -|![#663366](https://placehold.co/15x15/663366/663366.png)State is 'Unknown'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| +|![#004400](https://placehold.co/15x15/004400/004400.png)State is 'True'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|The condition state is `True`. **This doesn't always mean a good status** (For example, `NetworkUnreachabel` condition on a Node means a bad condition when it is `True`)| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)State is 'False'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|The condition state is `False`. **This doesn't always mean a bad status** (For example, `NetworkUnreachabel` condition on a Node means a good condition when it is `False`)| +|![#663366](https://placehold.co/15x15/663366/663366.png)State is 'Unknown'|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|The condition state is `Unknown`| @@ -53,8 +64,16 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| -|![#004400](https://placehold.co/15x15/004400/004400.png)Processing operation|![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api|| -|![#333333](https://placehold.co/15x15/333333/333333.png)Operation is finished|![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api|| +|![#004400](https://placehold.co/15x15/004400/004400.png)Processing operation|![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api|A long running operation is running| +|![#333333](https://placehold.co/15x15/333333/333333.png)Operation is finished|![#FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api|An operation is finished at the time of left edge of this operation.| +|![#004400](https://placehold.co/15x15/004400/004400.png)Processing operation|![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit|A long running operation is running| +|![#333333](https://placehold.co/15x15/333333/333333.png)Operation is finished|![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit|An operation is finished at the time of left edge of this operation.| +|![#004400](https://placehold.co/15x15/004400/004400.png)Processing operation|![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api|A long running operation is running| +|![#333333](https://placehold.co/15x15/333333/333333.png)Operation is finished|![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api|An operation is finished at the time of left edge of this operation.| +|![#004400](https://placehold.co/15x15/004400/004400.png)Processing operation|![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)multicloud_api|A long running operation is running| +|![#333333](https://placehold.co/15x15/333333/333333.png)Operation is finished|![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)multicloud_api|An operation is finished at the time of left edge of this operation.| +|![#004400](https://placehold.co/15x15/004400/004400.png)Processing operation|![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)onprem_api|A long running operation is running| +|![#333333](https://placehold.co/15x15/333333/333333.png)Operation is finished|![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)onprem_api|An operation is finished at the time of left edge of this operation.| @@ -106,6 +125,19 @@ This timeline can have the following events. ## [![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png) node-component - Node component timeline](#RelationshipNodeComponent) + +### Revisions + +This timeline can have the following revisions. + + +|State|Source log|Description| +|---|---|---| +|![#997700](https://placehold.co/15x15/997700/997700.png)Resource may be existing|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| +|![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png)Resource is existing|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| +|![#CC0000](https://placehold.co/15x15/CC0000/CC0000.png)Resource is deleted|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| + + ### Events diff --git a/pkg/model/enum/parent_relationship.go b/pkg/model/enum/parent_relationship.go index 1952065..c3a4581 100644 --- a/pkg/model/enum/parent_relationship.go +++ b/pkg/model/enum/parent_relationship.go @@ -86,6 +86,11 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad LabelBackgroundColor: "#CCCCCC", SortPriority: 1000, GeneratableRevisions: []GeneratableRevisionInfo{ + { + State: RevisionStateInferred, + SourceLogType: LogTypeAudit, + Description: "This state indicates the resource exits at the time, but this existence is inferred from the other logs later. The detailed resource information is not available.", + }, { State: RevisionStateExisting, SourceLogType: LogTypeAudit, @@ -101,6 +106,11 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad SourceLogType: LogTypeAudit, Description: "This state indicates the resource is being deleted with grace period at the time.", }, + { + State: RevisionStateProvisioning, + SourceLogType: LogTypeGkeAudit, + Description: "This state indicates the resource is being provisioned. Currently this state is only used for cluster/nodepool status only.", + }, }, GeneratableEvents: []GeneratableEventInfo{ { @@ -111,6 +121,22 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad SourceLogType: LogTypeEvent, Description: "An event that related to a resource", }, + { + SourceLogType: LogTypeNode, + Description: "An event that related to a node resource", + }, + { + SourceLogType: LogTypeComputeApi, + Description: "An event that related to a compute resource", + }, + { + SourceLogType: LogTypeControlPlaneComponent, + Description: "A log related to the timeline resource related to control plane component", + }, + { + SourceLogType: LogTypeAutoscaler, + Description: "A log related to the Pod which triggered or prevented autoscaler", + }, }, }, RelationshipResourceCondition: { @@ -126,14 +152,17 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad { State: RevisionStateConditionTrue, SourceLogType: LogTypeAudit, + Description: "The condition state is `True`. **This doesn't always mean a good status** (For example, `NetworkUnreachabel` condition on a Node means a bad condition when it is `True`)", }, { State: RevisionStateConditionFalse, SourceLogType: LogTypeAudit, + Description: "The condition state is `False`. **This doesn't always mean a bad status** (For example, `NetworkUnreachabel` condition on a Node means a good condition when it is `False`)", }, { State: RevisionStateConditionUnknown, SourceLogType: LogTypeAudit, + Description: "The condition state is `Unknown`", }, }, }, @@ -150,10 +179,52 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad { State: RevisionStateOperationStarted, SourceLogType: LogTypeComputeApi, + Description: "A long running operation is running", }, { State: RevisionStateOperationFinished, SourceLogType: LogTypeComputeApi, + Description: "An operation is finished at the time of left edge of this operation.", + }, + { + State: RevisionStateOperationStarted, + SourceLogType: LogTypeGkeAudit, + Description: "A long running operation is running", + }, + { + State: RevisionStateOperationFinished, + SourceLogType: LogTypeGkeAudit, + Description: "An operation is finished at the time of left edge of this operation.", + }, + { + State: RevisionStateOperationStarted, + SourceLogType: LogTypeNetworkAPI, + Description: "A long running operation is running", + }, + { + State: RevisionStateOperationFinished, + SourceLogType: LogTypeNetworkAPI, + Description: "An operation is finished at the time of left edge of this operation.", + }, + { + State: RevisionStateOperationStarted, + SourceLogType: LogTypeMulticloudAPI, + Description: "A long running operation is running", + }, + { + State: RevisionStateOperationFinished, + SourceLogType: LogTypeMulticloudAPI, + Description: "An operation is finished at the time of left edge of this operation.", + }, + { + State: RevisionStateOperationStarted, + SourceLogType: LogTypeOnPremAPI, + Description: "A long running operation is running", + }, + { + State: RevisionStateOperationFinished, + SourceLogType: LogTypeOnPremAPI, + Description: "An operation is finished at the time of left edge of this operation.", }, }, }, @@ -230,6 +301,20 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad LabelBackgroundColor: "#0077CC", Hint: "Non container resource running on a node", SortPriority: 6000, + GeneratableRevisions: []GeneratableRevisionInfo{ + { + State: RevisionStateInferred, + SourceLogType: LogTypeNode, + }, + { + State: RevisionStateExisting, + SourceLogType: LogTypeNode, + }, + { + State: RevisionStateDeleted, + SourceLogType: LogTypeNode, + }, + }, GeneratableEvents: []GeneratableEventInfo{ { SourceLogType: LogTypeNode, From 7e2d12e951add2de6ec0fe18f1d4562f9f48e429 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 17:23:38 +0900 Subject: [PATCH 14/23] Added form template --- cmd/document-generator/main.go | 5 + docs/en/features.md | 157 +++++++++--------- docs/en/forms.md | 60 +++++++ docs/en/images/reference/default-timeline.png | Bin 0 -> 384809 bytes docs/en/inspection-type.md | 112 ++----------- docs/en/relationships.md | 60 ++++--- docs/template/feature.template.md | 2 +- docs/template/form.template.md | 9 + ...emplate.md => inspection-type.template.md} | 6 +- docs/template/log-types.template.md | 7 - pkg/document/model/form.go | 30 ++++ pkg/document/model/inspection_type.go | 4 +- pkg/model/enum/log_type.go | 2 +- pkg/model/enum/parent_relationship.go | 23 ++- 14 files changed, 271 insertions(+), 206 deletions(-) create mode 100644 docs/en/forms.md create mode 100644 docs/en/images/reference/default-timeline.png create mode 100644 docs/template/form.template.md rename docs/template/{inspection-types.template.md => inspection-type.template.md} (67%) delete mode 100644 docs/template/log-types.template.md create mode 100644 pkg/document/model/form.go diff --git a/cmd/document-generator/main.go b/cmd/document-generator/main.go index c51774f..f422253 100644 --- a/cmd/document-generator/main.go +++ b/cmd/document-generator/main.go @@ -52,6 +52,11 @@ func main() { err = generator.GenerateDocument("./docs/en/features.md", "feature-template", featureDocumentModel, false) fatal(err, "failed to generate feature document") + formDocumentModel, err := model.GetFormDocumentModel(inspectionServer) + fatal(err, "failed to generate form document model") + err = generator.GenerateDocument("./docs/en/forms.md", "form-template", formDocumentModel, false) + fatal(err, "failed to generate form document") + relationshipDocumentModel := model.GetRelationshipDocumentModel() err = generator.GenerateDocument("./docs/en/relationships.md", "relationship-template", relationshipDocumentModel, false) fatal(err, "failed to generate relationship document") diff --git a/docs/en/features.md b/docs/en/features.md index 5ac41e5..29d56bc 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -15,12 +15,12 @@ This parser reveals how these resources are created,updated or deleted. |Parameter name|Description| |:-:|---| -|Kind|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| -|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Kind](./forms.md#cloud.google.com/input/kinds)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -63,11 +63,11 @@ This parser shows events associated to K8s resources |Parameter name|Description| |:-:|---| -|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -104,11 +104,11 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. |Parameter name|Description| |:-:|---| -|Node names|| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Node names](./forms.md#cloud.google.com/input/node-name-filter)|| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -149,12 +149,12 @@ Container logs ingested from stdout/stderr of workload Pods. |Parameter name|Description| |:-:|---| -|Namespaces(Container logs)|| -|Pod names(Container logs)|| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Namespaces(Container logs)](./forms.md#cloud.google.com/input/container-query-namespaces)|| +|[Pod names(Container logs)](./forms.md#cloud.google.com/input/container-query-podnames)|| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -190,10 +190,10 @@ GKE audit log including cluster creation,deletion and upgrades. |Parameter name|Description| |:-:|---| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -229,12 +229,12 @@ Compute API audit logs used for cluster related logs. This also visualize operat |Parameter name|Description| |:-:|---| -|Kind|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| -|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Kind](./forms.md#cloud.google.com/input/kinds)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -278,12 +278,12 @@ GCE network API audit log including NEG related audit logs to identify when the |Parameter name|Description| |:-:|---| -|Kind|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| -|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Kind](./forms.md#cloud.google.com/input/kinds)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -291,7 +291,7 @@ GCE network API audit log including NEG related audit logs to identify when the |Timeline type|Short name on chip| |:-:|:-:| |![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| -|![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[NEG timeline](./relationships.md#RelationshipNetworkEndpointGroup)|neg| +|![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[Network Endpoint Group timeline](./relationships.md#RelationshipNetworkEndpointGroup)|neg| @@ -327,10 +327,10 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. |Parameter name|Description| |:-:|---| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -368,10 +368,10 @@ This log type also includes Node Auto Provisioner logs. |Parameter name|Description| |:-:|---| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -409,10 +409,10 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and |Parameter name|Description| |:-:|---| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -449,11 +449,11 @@ Visualize Kubernetes control plane component logs on a cluster |Parameter name|Description| |:-:|---| -|Control plane component names|| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Control plane component names](./forms.md#cloud.google.com/input/component-names)|| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -491,19 +491,20 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on |Parameter name|Description| |:-:|---| -|Kind|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| -|Namespaces|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|Node names|| -|Project ID|The project ID containing the logs of cluster to query| -|Cluster name|The cluster name to gather logs.| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Kind](./forms.md#cloud.google.com/input/kinds)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Node names](./forms.md#cloud.google.com/input/node-name-filter)|| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| +|![333333](https://placehold.co/15x15/333333/333333.png)[Serialport log timeline](./relationships.md#RelationshipSerialPort)|serialport| @@ -543,11 +544,11 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst |Parameter name|Description| |:-:|---| -|Location|| -|Project ID|The project ID containing the logs of cluster to query| -|Composer Environment Name|| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Location](./forms.md#cloud.google.com/input/location)|| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Composer Environment Name](./forms.md#cloud.google.com/input/composer/environment_name)|| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -579,11 +580,11 @@ Airflow Worker logs contain information related to the execution of TaskInstance |Parameter name|Description| |:-:|---| -|Location|| -|Project ID|The project ID containing the logs of cluster to query| -|Composer Environment Name|| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Location](./forms.md#cloud.google.com/input/location)|| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Composer Environment Name](./forms.md#cloud.google.com/input/composer/environment_name)|| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -615,11 +616,11 @@ The DagProcessorManager logs contain information for investigating the number of |Parameter name|Description| |:-:|---| -|Location|| -|Project ID|The project ID containing the logs of cluster to query| -|Composer Environment Name|| -|End time|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|Duration|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Location](./forms.md#cloud.google.com/input/location)|| +|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| +|[Composer Environment Name](./forms.md#cloud.google.com/input/composer/environment_name)|| +|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines diff --git a/docs/en/forms.md b/docs/en/forms.md new file mode 100644 index 0000000..c40238b --- /dev/null +++ b/docs/en/forms.md @@ -0,0 +1,60 @@ + +## Project ID + +The project ID containing the logs of cluster to query + + +## Cluster name + +The cluster name to gather logs. + + +## Duration + +The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`) + + +## End time + +The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter. + + +## Kind + +The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources + + +## Location + + + + +## Namespaces + +The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources. + + +## Node names + + + + +## Namespaces(Container logs) + + + + +## Pod names(Container logs) + + + + +## Control plane component names + + + + +## Composer Environment Name + + + diff --git a/docs/en/images/reference/default-timeline.png b/docs/en/images/reference/default-timeline.png new file mode 100644 index 0000000000000000000000000000000000000000..51cd335b1af0bcf3c8b881bedfb363289645e3f0 GIT binary patch literal 384809 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�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

i%*3~w!=d2omh1GR6 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 literal 0 HcmV?d00001 diff --git a/docs/en/inspection-type.md b/docs/en/inspection-type.md index d081081..91cd1c8 100644 --- a/docs/en/inspection-type.md +++ b/docs/en/inspection-type.md @@ -12,174 +12,98 @@ KHI filters out unsupported parser for the selected inspection type at first. ### Features - + + * [Kubernetes Audit Log](./features.md#k8s_audit) - - * [Kubernetes Event Logs](./features.md#k8s_event) - - * [Kubernetes Node Logs](./features.md#k8s_node) - - * [Kubernetes container logs](./features.md#k8s_container) - - * [GKE Audit logs](./features.md#gke_audit) - - * [Compute API Logs](./features.md#compute_api) - - * [GCE Network Logs](./features.md#gce_network) - - * [Autoscaler Logs](./features.md#autoscaler) - - * [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) - - * [Node serial port logs](./features.md#serialport) - + ## [Cloud Composer](#gcp-composer) ### Features - + + * [Kubernetes Audit Log](./features.md#k8s_audit) - - * [Kubernetes Event Logs](./features.md#k8s_event) - - * [Kubernetes Node Logs](./features.md#k8s_node) - - * [Kubernetes container logs](./features.md#k8s_container) - - * [GKE Audit logs](./features.md#gke_audit) - - * [Compute API Logs](./features.md#compute_api) - - * [GCE Network Logs](./features.md#gce_network) - - * [Autoscaler Logs](./features.md#autoscaler) - - * [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) - - * [Node serial port logs](./features.md#serialport) - - * [(Alpha) Composer / Airflow Scheduler](./features.md#airflow_schedule) - - * [(Alpha) Cloud Composer / Airflow Worker](./features.md#airflow_worker) - - * [(Alpha) Composer / Airflow DagProcessorManager](./features.md#airflow_dag_processor) - + ## [GKE on AWS(Anthos on AWS)](#gcp-gke-on-aws) ### Features - + + * [Kubernetes Audit Log](./features.md#k8s_audit) - - * [Kubernetes Event Logs](./features.md#k8s_event) - - * [Kubernetes Node Logs](./features.md#k8s_node) - - * [Kubernetes container logs](./features.md#k8s_container) - - * [MultiCloud API logs](./features.md#multicloud_api) - - * [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) - + ## [GKE on Azure(Anthos on Azure)](#gcp-gke-on-azure) ### Features - + + * [Kubernetes Audit Log](./features.md#k8s_audit) - - * [Kubernetes Event Logs](./features.md#k8s_event) - - * [Kubernetes Node Logs](./features.md#k8s_node) - - * [Kubernetes container logs](./features.md#k8s_container) - - * [MultiCloud API logs](./features.md#multicloud_api) - - * [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) - + ## [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](#gcp-gdcv-for-baremetal) ### Features - + + * [Kubernetes Audit Log](./features.md#k8s_audit) - - * [Kubernetes Event Logs](./features.md#k8s_event) - - * [Kubernetes Node Logs](./features.md#k8s_node) - - * [Kubernetes container logs](./features.md#k8s_container) - - * [OnPrem API logs](./features.md#onprem_api) - - * [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) - + ## [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](#gcp-gdcv-for-vmware) ### Features - + + * [Kubernetes Audit Log](./features.md#k8s_audit) - - * [Kubernetes Event Logs](./features.md#k8s_event) - - * [Kubernetes Node Logs](./features.md#k8s_node) - - * [Kubernetes container logs](./features.md#k8s_container) - - * [OnPrem API logs](./features.md#onprem_api) - - * [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) - + diff --git a/docs/en/relationships.md b/docs/en/relationships.md index cff4cda..272e558 100644 --- a/docs/en/relationships.md +++ b/docs/en/relationships.md @@ -6,6 +6,9 @@ The relationship between its parent and children is usually interpretted as the ## [The default resource timeline](#RelationshipChild) + +![](./images/reference/default-timeline.png) + ### Revisions @@ -87,9 +90,9 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| -|![#004400](https://placehold.co/15x15/004400/004400.png)Endpoint is ready|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| -|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)Endpoint is not ready|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| -|![#fed700](https://placehold.co/15x15/fed700/fed700.png)Endpoint is being terminated|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|| +|![#004400](https://placehold.co/15x15/004400/004400.png)Endpoint is ready|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|An endpoint associated with the parent resource is ready| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)Endpoint is not ready|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|An endpoint associated with the parent resource is not ready. Traffic shouldn't be routed during this time.| +|![#fed700](https://placehold.co/15x15/fed700/fed700.png)Endpoint is being terminated|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|An endpoint associated with the parent resource is being terminated. New traffic shouldn't be routed to this endpoint during this time, but the endpoint can still have pending requests.| @@ -103,13 +106,18 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| -|![#997700](https://placehold.co/15x15/997700/997700.png)Waiting for starting container|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| -|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)Container is not ready|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| -|![#007700](https://placehold.co/15x15/007700/007700.png)Container is ready|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| -|![#113333](https://placehold.co/15x15/113333/113333.png)Container exited with healthy exit code|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| -|![#331111](https://placehold.co/15x15/331111/331111.png)Container exited with errornous exit code|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| +|![#997700](https://placehold.co/15x15/997700/997700.png)Waiting for starting container|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is not started yet and waiting for something.(Example: Pulling images, mounting volumes ...etc)| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)Container is not ready|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is started but the readiness is not ready.| +|![#007700](https://placehold.co/15x15/007700/007700.png)Container is ready|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is started and the readiness is ready| +|![#113333](https://placehold.co/15x15/113333/113333.png)Container exited with healthy exit code|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is already terminated with successful exit code = 0| +|![#331111](https://placehold.co/15x15/331111/331111.png)Container exited with errornous exit code|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is already terminated with errornous exit code != 0| + +> [!TIP] +> Detailed container statuses are only available when your project enabled `DATA_WRITE` audit log for Kubernetes Engine API. +> Check [README](../../README.md) more details to configure `DATA_WRITE` audit log. + ### Events @@ -118,8 +126,8 @@ This timeline can have the following events. |Source log|Description| |---|---| -|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|| -|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| +|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|A container log on stdout/etderr| +|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|kubelet/containerd logs associated with the container| @@ -133,9 +141,9 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| -|![#997700](https://placehold.co/15x15/997700/997700.png)Resource may be existing|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| -|![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png)Resource is existing|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| -|![#CC0000](https://placehold.co/15x15/CC0000/CC0000.png)Resource is deleted|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| +|![#997700](https://placehold.co/15x15/997700/997700.png)Resource may be existing|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|The component is infrred to be running because of the logs from it| +|![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png)Resource is existing|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|The component is running running. (Few node components supports this state because the parser knows logs on startup for specific components)| +|![#CC0000](https://placehold.co/15x15/CC0000/CC0000.png)Resource is deleted|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|The component is no longer running. (Few node components supports this state because the parser knows logs on termination for specific components)| @@ -146,7 +154,7 @@ This timeline can have the following events. |Source log|Description| |---|---| -|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|| +|![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node|A log from the component on the log| @@ -178,7 +186,7 @@ This timeline can have the following aliases. -## [![#A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png) neg - NEG timeline](#RelationshipNetworkEndpointGroup) +## [![#A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png) neg - Network Endpoint Group timeline](#RelationshipNetworkEndpointGroup) ### Revisions @@ -188,8 +196,8 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| -|![#004400](https://placehold.co/15x15/004400/004400.png)State is 'True'|![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api|| -|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)State is 'False'|![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api|| +|![#004400](https://placehold.co/15x15/004400/004400.png)State is 'True'|![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api|indicates the NEG is already attached to the Pod.| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)State is 'False'|![#33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api|indicates the NEG is detached from the Pod| @@ -203,7 +211,7 @@ This timeline can have the following events. |Source log|Description| |---|---| -|![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler|| +|![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler|Autoscaler logs associated to a MIG(e.g The mig was scaled up by the austoscaler)| @@ -217,6 +225,20 @@ This timeline can have the following events. |Source log|Description| |---|---| -|![#FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component|| +|![#FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component|A log from the control plane component| + +## [![#333333](https://placehold.co/15x15/333333/333333.png) serialport - Serialport log timeline](#RelationshipSerialPort) + + +### Events + +This timeline can have the following events. + + +|Source log|Description| +|---|---| +|![#333333](https://placehold.co/15x15/333333/333333.png)serial_port|A serialport log from the node| + + diff --git a/docs/template/feature.template.md b/docs/template/feature.template.md index 810fd91..af1c470 100644 --- a/docs/template/feature.template.md +++ b/docs/template/feature.template.md @@ -13,7 +13,7 @@ |Parameter name|Description| |:-:|---| {{- range $index,$form := $feature.Forms}} -|{{$form.Label}}|{{$form.Description}}| +|[{{$form.Label}}](./forms.md#{{$form.ID}})|{{$form.Description}}| {{- end}} {{end}} diff --git a/docs/template/form.template.md b/docs/template/form.template.md new file mode 100644 index 0000000..639b5b4 --- /dev/null +++ b/docs/template/form.template.md @@ -0,0 +1,9 @@ +{{define "form-template"}} +{{range $index,$form := .Forms }} + +## {{$form.Label}} + +{{$form.Description}} + +{{end}} +{{end}} \ No newline at end of file diff --git a/docs/template/inspection-types.template.md b/docs/template/inspection-type.template.md similarity index 67% rename from docs/template/inspection-types.template.md rename to docs/template/inspection-type.template.md index 3399113..faebc26 100644 --- a/docs/template/inspection-types.template.md +++ b/docs/template/inspection-type.template.md @@ -7,10 +7,10 @@ + {{range $feature := $type.SupportedFeatures}} - * [{{$feature.Name}}](./features.md#{{$feature.ID}}) - -{{end}} +{{- end}} + {{end}} {{end}} \ No newline at end of file diff --git a/docs/template/log-types.template.md b/docs/template/log-types.template.md deleted file mode 100644 index 2720837..0000000 --- a/docs/template/log-types.template.md +++ /dev/null @@ -1,7 +0,0 @@ -{{define "log-type-template"}} -{{range $index,$type := .LogTypes }} - -## [![#{{$type.ColorCode}}](https://placehold.co/15x15/{{$type.ColorCode}}/{{$type.ColorCode}}.png) {{$type.Name}}](#{{$type.ID}}) - -{{end}} -{{end}} \ No newline at end of file diff --git a/pkg/document/model/form.go b/pkg/document/model/form.go new file mode 100644 index 0000000..3acb430 --- /dev/null +++ b/pkg/document/model/form.go @@ -0,0 +1,30 @@ +package model + +import ( + "github.com/GoogleCloudPlatform/khi/pkg/inspection" + "github.com/GoogleCloudPlatform/khi/pkg/inspection/task/label" + "github.com/GoogleCloudPlatform/khi/pkg/inspection/taskfilter" +) + +type FormDocumentModel struct { + Forms []FormDocumentElement +} + +type FormDocumentElement struct { + ID string + Label string + Description string +} + +func GetFormDocumentModel(taskServer *inspection.InspectionTaskServer) (*FormDocumentModel, error) { + result := FormDocumentModel{} + forms := taskServer.RootTaskSet.FilteredSubset(label.TaskLabelKeyIsFormTask, taskfilter.HasTrue, false) + for _, form := range forms.GetAll() { + result.Forms = append(result.Forms, FormDocumentElement{ + ID: form.ID().String(), + Label: form.Labels().GetOrDefault(label.TaskLabelKeyFormFieldLabel, "").(string), + Description: form.Labels().GetOrDefault(label.TaskLabelKeyFormFieldDescription, "").(string), + }) + } + return &result, nil +} diff --git a/pkg/document/model/inspection_type.go b/pkg/document/model/inspection_type.go index b268f1f..0d19d88 100644 --- a/pkg/document/model/inspection_type.go +++ b/pkg/document/model/inspection_type.go @@ -1,6 +1,8 @@ package model import ( + "strings" + "github.com/GoogleCloudPlatform/khi/pkg/inspection" inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" "github.com/GoogleCloudPlatform/khi/pkg/inspection/taskfilter" @@ -36,7 +38,7 @@ func GetInspectionTypeDocumentModel(taskServer *inspection.InspectionTaskServer) features := []InspectionTypeDocumentElementFeature{} for _, task := range tasks { features = append(features, InspectionTypeDocumentElementFeature{ - ID: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string), + ID: strings.ToLower(task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string)), Name: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), Description: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), }) diff --git a/pkg/model/enum/log_type.go b/pkg/model/enum/log_type.go index 36163d3..f756a97 100644 --- a/pkg/model/enum/log_type.go +++ b/pkg/model/enum/log_type.go @@ -35,7 +35,7 @@ const ( logTypeUnusedEnd ) -const EnumLogTypeLength = int(logTypeUnusedEnd) +const EnumLogTypeLength = int(logTypeUnusedEnd) + 1 type LogTypeFrontendMetadata struct { // EnumKeyName is the name of this enum value. Must match with the enum key. diff --git a/pkg/model/enum/parent_relationship.go b/pkg/model/enum/parent_relationship.go index c3a4581..53151e9 100644 --- a/pkg/model/enum/parent_relationship.go +++ b/pkg/model/enum/parent_relationship.go @@ -33,7 +33,7 @@ const ( ) // EnumParentRelationshipLength is the count of ParentRelationship enum elements. -const EnumParentRelationshipLength = int(relationshipUnusedEnd) +const EnumParentRelationshipLength = int(relationshipUnusedEnd) + 1 // parentRelationshipFrontendMetadata is a type defined for each parent relationship types. type ParentRelationshipFrontendMetadata struct { @@ -241,14 +241,17 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad { State: RevisionStateEndpointReady, SourceLogType: LogTypeAudit, + Description: "An endpoint associated with the parent resource is ready", }, { State: RevisionStateEndpointUnready, SourceLogType: LogTypeAudit, + Description: "An endpoint associated with the parent resource is not ready. Traffic shouldn't be routed during this time.", }, { State: RevisionStateEndpointTerminating, SourceLogType: LogTypeAudit, + Description: "An endpoint associated with the parent resource is being terminated. New traffic shouldn't be routed to this endpoint during this time, but the endpoint can still have pending requests.", }, }, }, @@ -265,30 +268,37 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad { State: RevisionStateContainerWaiting, SourceLogType: LogTypeContainer, + Description: "The container is not started yet and waiting for something.(Example: Pulling images, mounting volumes ...etc)", }, { State: RevisionStateContainerRunningNonReady, SourceLogType: LogTypeContainer, + Description: "The container is started but the readiness is not ready.", }, { State: RevisionStateContainerRunningReady, SourceLogType: LogTypeContainer, + Description: "The container is started and the readiness is ready", }, { State: RevisionStateContainerTerminatedWithSuccess, SourceLogType: LogTypeContainer, + Description: "The container is already terminated with successful exit code = 0", }, { State: RevisionStateContainerTerminatedWithError, SourceLogType: LogTypeContainer, + Description: "The container is already terminated with errornous exit code != 0", }, }, GeneratableEvents: []GeneratableEventInfo{ { SourceLogType: LogTypeContainer, + Description: "A container log on stdout/etderr", }, { SourceLogType: LogTypeNode, + Description: "kubelet/containerd logs associated with the container", }, }, }, @@ -305,19 +315,23 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad { State: RevisionStateInferred, SourceLogType: LogTypeNode, + Description: "The component is infrred to be running because of the logs from it", }, { State: RevisionStateExisting, SourceLogType: LogTypeNode, + Description: "The component is running running. (Few node components supports this state because the parser knows logs on startup for specific components)", }, { State: RevisionStateDeleted, SourceLogType: LogTypeNode, + Description: "The component is no longer running. (Few node components supports this state because the parser knows logs on termination for specific components)", }, }, GeneratableEvents: []GeneratableEventInfo{ { SourceLogType: LogTypeNode, + Description: "A log from the component on the log", }, }, }, @@ -359,7 +373,7 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad Visible: true, EnumKeyName: "RelationshipNetworkEndpointGroup", Label: "neg", - LongName: "NEG timeline", + LongName: "Network Endpoint Group timeline", LabelColor: "#FFFFFF", LabelBackgroundColor: "#A52A2A", Hint: "Pod serving status obtained from the associated NEG status", @@ -368,10 +382,12 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad { State: RevisionStateConditionTrue, SourceLogType: LogTypeNetworkAPI, + Description: "indicates the NEG is already attached to the Pod.", }, { State: RevisionStateConditionFalse, SourceLogType: LogTypeNetworkAPI, + Description: "indicates the NEG is detached from the Pod", }, }, }, @@ -387,6 +403,7 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad GeneratableEvents: []GeneratableEventInfo{ { SourceLogType: LogTypeAutoscaler, + Description: "Autoscaler logs associated to a MIG(e.g The mig was scaled up by the austoscaler)", }, }, }, @@ -402,6 +419,7 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad GeneratableEvents: []GeneratableEventInfo{ { SourceLogType: LogTypeControlPlaneComponent, + Description: "A log from the control plane component", }, }, }, @@ -417,6 +435,7 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad GeneratableEvents: []GeneratableEventInfo{ { SourceLogType: LogTypeSerialPort, + Description: "A serialport log from the node", }, }, }, From 120d35dae591257e477f3a7401b2d9d293bba0c5 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Mon, 10 Feb 2025 21:27:07 +0900 Subject: [PATCH 15/23] Fix anchor links --- docs/en/features.md | 230 +++++++++++----------- docs/en/inspection-type.md | 94 ++++----- docs/en/relationships.md | 24 +-- docs/template/feature.template.md | 6 +- docs/template/inspection-type.template.md | 2 +- docs/template/relationship.template.md | 2 +- pkg/document/generator/generator.go | 8 +- pkg/document/generator/util.go | 14 ++ pkg/document/generator/util_test.go | 38 ++++ 9 files changed, 237 insertions(+), 181 deletions(-) create mode 100644 pkg/document/generator/util.go create mode 100644 pkg/document/generator/util_test.go diff --git a/docs/en/features.md b/docs/en/features.md index 29d56bc..1fa6d9c 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -4,7 +4,7 @@ The output timelnes of KHI is formed in the `feature tasks`. A feature may depen User will select features on the 2nd menu of the dialog after clicking `New inspection` button. -## [Kubernetes Audit Log](#k8s_audit) +## Kubernetes Audit Log Visualize Kubernetes audit logs in GKE. This parser reveals how these resources are created,updated or deleted. @@ -15,23 +15,23 @@ This parser reveals how these resources are created,updated or deleted. |Parameter name|Description| |:-:|---| -|[Kind](./forms.md#cloud.google.com/input/kinds)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| -|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| -|![4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png)[Status condition field timeline](./relationships.md#RelationshipResourceCondition)|condition| -|![008000](https://placehold.co/15x15/008000/008000.png)[Endpoint serving state timeline](./relationships.md#RelationshipEndpointSlice)|endpointslice| -|![33DD88](https://placehold.co/15x15/33DD88/33DD88.png)[Owning children timeline](./relationships.md#RelationshipOwnerReference)|owns| -|![FF8855](https://placehold.co/15x15/FF8855/FF8855.png)[Pod binding timeline](./relationships.md#RelationshipPodBinding)|binds| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| +|![4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png)[Status condition field timeline](./relationships.md#status-condition-field-timeline)|condition| +|![008000](https://placehold.co/15x15/008000/008000.png)[Endpoint serving state timeline](./relationships.md#endpoint-serving-state-timeline)|endpointslice| +|![33DD88](https://placehold.co/15x15/33DD88/33DD88.png)[Owning children timeline](./relationships.md#owning-children-timeline)|owns| +|![FF8855](https://placehold.co/15x15/FF8855/FF8855.png)[Pod binding timeline](./relationships.md#pod-binding-timeline)|binds| @@ -52,7 +52,7 @@ protoPayload.methodName=~"\.(deployments|replicasets|pods|nodes)\." -## [Kubernetes Event Logs](#k8s_event) +## Kubernetes Event Logs Visualize Kubernetes event logs on GKE. This parser shows events associated to K8s resources @@ -63,18 +63,18 @@ This parser shows events associated to K8s resources |Parameter name|Description| |:-:|---| -|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| @@ -92,7 +92,7 @@ resource.labels.cluster_name="gcp-cluster-name" -## [Kubernetes Node Logs](#k8s_node) +## Kubernetes Node Logs GKE worker node components logs mainly from kubelet,containerd and dockerd. @@ -104,20 +104,20 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. |Parameter name|Description| |:-:|---| -|[Node names](./forms.md#cloud.google.com/input/node-name-filter)|| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Node names](./forms.md#node-names)|| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| -|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#RelationshipContainer)|container| -|![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)[Node component timeline](./relationships.md#RelationshipNodeComponent)|node-component| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| +|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container| +|![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)[Node component timeline](./relationships.md#node-component-timeline)|node-component| @@ -137,7 +137,7 @@ resource.labels.node_name:("gke-test-cluster-node-1" OR "gke-test-cluster-node-2 -## [Kubernetes container logs](#k8s_container) +## Kubernetes container logs Container logs ingested from stdout/stderr of workload Pods. @@ -149,19 +149,19 @@ Container logs ingested from stdout/stderr of workload Pods. |Parameter name|Description| |:-:|---| -|[Namespaces(Container logs)](./forms.md#cloud.google.com/input/container-query-namespaces)|| -|[Pod names(Container logs)](./forms.md#cloud.google.com/input/container-query-podnames)|| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Namespaces(Container logs)](./forms.md#namespacescontainer-logs)|| +|[Pod names(Container logs)](./forms.md#pod-namescontainer-logs)|| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#RelationshipContainer)|container| +|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container| @@ -180,7 +180,7 @@ resource.labels.namespace_name=("default") -## [GKE Audit logs](#gke_audit) +## GKE Audit logs GKE audit log including cluster creation,deletion and upgrades. @@ -190,18 +190,18 @@ GKE audit log including cluster creation,deletion and upgrades. |Parameter name|Description| |:-:|---| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| @@ -219,7 +219,7 @@ resource.labels.cluster_name="gcp-cluster-name" -## [Compute API Logs](#compute_api) +## Compute API Logs Compute API audit logs used for cluster related logs. This also visualize operations happened during the query time. @@ -229,20 +229,20 @@ Compute API audit logs used for cluster related logs. This also visualize operat |Parameter name|Description| |:-:|---| -|[Kind](./forms.md#cloud.google.com/input/kinds)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| -|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| @@ -268,7 +268,7 @@ Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit -## [GCE Network Logs](#gce_network) +## GCE Network Logs GCE network API audit log including NEG related audit logs to identify when the associated NEG was attached/detached. @@ -278,20 +278,20 @@ GCE network API audit log including NEG related audit logs to identify when the |Parameter name|Description| |:-:|---| -|[Kind](./forms.md#cloud.google.com/input/kinds)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| -|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| -|![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[Network Endpoint Group timeline](./relationships.md#RelationshipNetworkEndpointGroup)|neg| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| +|![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[Network Endpoint Group timeline](./relationships.md#network-endpoint-group-timeline)|neg| @@ -317,7 +317,7 @@ Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit -## [MultiCloud API logs](#multicloud_api) +## MultiCloud API logs Anthos Multicloud audit log including cluster creation,deletion and upgrades. @@ -327,17 +327,17 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. |Parameter name|Description| |:-:|---| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| @@ -357,7 +357,7 @@ protoPayload.resourceName:"awsClusters/cluster-foo" -## [Autoscaler Logs](#autoscaler) +## Autoscaler Logs Autoscaler logs including decision reasons why they scale up/down or why they didn't. This log type also includes Node Auto Provisioner logs. @@ -368,18 +368,18 @@ This log type also includes Node Auto Provisioner logs. |Parameter name|Description| |:-:|---| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| -|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Managed instance group timeline](./relationships.md#RelationshipManagedInstanceGroup)|mig| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| +|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Managed instance group timeline](./relationships.md#managed-instance-group-timeline)|mig| @@ -399,7 +399,7 @@ logName="projects/gcp-project-id/logs/container.googleapis.com%2Fcluster-autosca -## [OnPrem API logs](#onprem_api) +## OnPrem API logs Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades. @@ -409,17 +409,17 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and |Parameter name|Description| |:-:|---| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#RelationshipOperation)|operation| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| @@ -439,7 +439,7 @@ protoPayload.resourceName:"baremetalClusters/my-cluster" -## [Kubernetes Control plane component logs](#k8s_control_plane_component) +## Kubernetes Control plane component logs Visualize Kubernetes control plane component logs on a cluster @@ -449,19 +449,19 @@ Visualize Kubernetes control plane component logs on a cluster |Parameter name|Description| |:-:|---| -|[Control plane component names](./forms.md#cloud.google.com/input/component-names)|| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Control plane component names](./forms.md#control-plane-component-names)|| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#RelationshipChild)|resource| -|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Control plane component timeline](./relationships.md#RelationshipControlPlaneComponent)|controlplane| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| +|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Control plane component timeline](./relationships.md#control-plane-component-timeline)|controlplane| @@ -481,7 +481,7 @@ resource.labels.project_id="gcp-project-id" -## [Node serial port logs](#serialport) +## Node serial port logs Serial port logs of worker nodes. Serial port logging feature must be enabled on instances to query logs correctly. @@ -491,20 +491,20 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on |Parameter name|Description| |:-:|---| -|[Kind](./forms.md#cloud.google.com/input/kinds)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| -|[Namespaces](./forms.md#cloud.google.com/input/namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Node names](./forms.md#cloud.google.com/input/node-name-filter)|| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Cluster name](./forms.md#cloud.google.com/input/cluster-name)|The cluster name to gather logs.| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| +|[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| +|[Node names](./forms.md#node-names)|| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines |Timeline type|Short name on chip| |:-:|:-:| -|![333333](https://placehold.co/15x15/333333/333333.png)[Serialport log timeline](./relationships.md#RelationshipSerialPort)|serialport| +|![333333](https://placehold.co/15x15/333333/333333.png)[Serialport log timeline](./relationships.md#serialport-log-timeline)|serialport| @@ -534,7 +534,7 @@ Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit -## [(Alpha) Composer / Airflow Scheduler](#airflow_schedule) +## (Alpha) Composer / Airflow Scheduler Airflow Scheduler logs contain information related to the scheduling of TaskInstances, making it an ideal source for understanding the lifecycle of TaskInstances. @@ -544,11 +544,11 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst |Parameter name|Description| |:-:|---| -|[Location](./forms.md#cloud.google.com/input/location)|| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Composer Environment Name](./forms.md#cloud.google.com/input/composer/environment_name)|| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Location](./forms.md#location)|| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Composer Environment Name](./forms.md#composer-environment-name)|| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -570,7 +570,7 @@ TODO: add sample query -## [(Alpha) Cloud Composer / Airflow Worker](#airflow_worker) +## (Alpha) Cloud Composer / Airflow Worker Airflow Worker logs contain information related to the execution of TaskInstances. By including these logs, you can gain insights into where and how each TaskInstance was executed. @@ -580,11 +580,11 @@ Airflow Worker logs contain information related to the execution of TaskInstance |Parameter name|Description| |:-:|---| -|[Location](./forms.md#cloud.google.com/input/location)|| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Composer Environment Name](./forms.md#cloud.google.com/input/composer/environment_name)|| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Location](./forms.md#location)|| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Composer Environment Name](./forms.md#composer-environment-name)|| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines @@ -606,7 +606,7 @@ TODO: add sample query -## [(Alpha) Composer / Airflow DagProcessorManager](#airflow_dag_processor) +## (Alpha) Composer / Airflow DagProcessorManager The DagProcessorManager logs contain information for investigating the number of DAGs included in each Python file and the time it took to parse them. You can get information about missing DAGs and load. @@ -616,11 +616,11 @@ The DagProcessorManager logs contain information for investigating the number of |Parameter name|Description| |:-:|---| -|[Location](./forms.md#cloud.google.com/input/location)|| -|[Project ID](./forms.md#cloud.google.com/input/project-id)|The project ID containing the logs of cluster to query| -|[Composer Environment Name](./forms.md#cloud.google.com/input/composer/environment_name)|| -|[End time](./forms.md#cloud.google.com/input/end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| -|[Duration](./forms.md#cloud.google.com/input/duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| +|[Location](./forms.md#location)|| +|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Composer Environment Name](./forms.md#composer-environment-name)|| +|[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| +|[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| ### Output timelines diff --git a/docs/en/inspection-type.md b/docs/en/inspection-type.md index 91cd1c8..ab7e786 100644 --- a/docs/en/inspection-type.md +++ b/docs/en/inspection-type.md @@ -14,16 +14,16 @@ KHI filters out unsupported parser for the selected inspection type at first. -* [Kubernetes Audit Log](./features.md#k8s_audit) -* [Kubernetes Event Logs](./features.md#k8s_event) -* [Kubernetes Node Logs](./features.md#k8s_node) -* [Kubernetes container logs](./features.md#k8s_container) -* [GKE Audit logs](./features.md#gke_audit) -* [Compute API Logs](./features.md#compute_api) -* [GCE Network Logs](./features.md#gce_network) -* [Autoscaler Logs](./features.md#autoscaler) -* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) -* [Node serial port logs](./features.md#serialport) +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [GKE Audit logs](./features.md#gke-audit-logs) +* [Compute API Logs](./features.md#compute-api-logs) +* [GCE Network Logs](./features.md#gce-network-logs) +* [Autoscaler Logs](./features.md#autoscaler-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) ## [Cloud Composer](#gcp-composer) @@ -33,19 +33,19 @@ KHI filters out unsupported parser for the selected inspection type at first. -* [Kubernetes Audit Log](./features.md#k8s_audit) -* [Kubernetes Event Logs](./features.md#k8s_event) -* [Kubernetes Node Logs](./features.md#k8s_node) -* [Kubernetes container logs](./features.md#k8s_container) -* [GKE Audit logs](./features.md#gke_audit) -* [Compute API Logs](./features.md#compute_api) -* [GCE Network Logs](./features.md#gce_network) -* [Autoscaler Logs](./features.md#autoscaler) -* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) -* [Node serial port logs](./features.md#serialport) -* [(Alpha) Composer / Airflow Scheduler](./features.md#airflow_schedule) -* [(Alpha) Cloud Composer / Airflow Worker](./features.md#airflow_worker) -* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#airflow_dag_processor) +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [GKE Audit logs](./features.md#gke-audit-logs) +* [Compute API Logs](./features.md#compute-api-logs) +* [GCE Network Logs](./features.md#gce-network-logs) +* [Autoscaler Logs](./features.md#autoscaler-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) +* [(Alpha) Composer / Airflow Scheduler](./features.md#alpha-composer--airflow-scheduler) +* [(Alpha) Cloud Composer / Airflow Worker](./features.md#alpha-cloud-composer--airflow-worker) +* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#alpha-composer--airflow-dagprocessormanager) ## [GKE on AWS(Anthos on AWS)](#gcp-gke-on-aws) @@ -55,12 +55,12 @@ KHI filters out unsupported parser for the selected inspection type at first. -* [Kubernetes Audit Log](./features.md#k8s_audit) -* [Kubernetes Event Logs](./features.md#k8s_event) -* [Kubernetes Node Logs](./features.md#k8s_node) -* [Kubernetes container logs](./features.md#k8s_container) -* [MultiCloud API logs](./features.md#multicloud_api) -* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [MultiCloud API logs](./features.md#multicloud-api-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) ## [GKE on Azure(Anthos on Azure)](#gcp-gke-on-azure) @@ -70,12 +70,12 @@ KHI filters out unsupported parser for the selected inspection type at first. -* [Kubernetes Audit Log](./features.md#k8s_audit) -* [Kubernetes Event Logs](./features.md#k8s_event) -* [Kubernetes Node Logs](./features.md#k8s_node) -* [Kubernetes container logs](./features.md#k8s_container) -* [MultiCloud API logs](./features.md#multicloud_api) -* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [MultiCloud API logs](./features.md#multicloud-api-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) ## [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](#gcp-gdcv-for-baremetal) @@ -85,12 +85,12 @@ KHI filters out unsupported parser for the selected inspection type at first. -* [Kubernetes Audit Log](./features.md#k8s_audit) -* [Kubernetes Event Logs](./features.md#k8s_event) -* [Kubernetes Node Logs](./features.md#k8s_node) -* [Kubernetes container logs](./features.md#k8s_container) -* [OnPrem API logs](./features.md#onprem_api) -* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [OnPrem API logs](./features.md#onprem-api-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) ## [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](#gcp-gdcv-for-vmware) @@ -100,10 +100,10 @@ KHI filters out unsupported parser for the selected inspection type at first. -* [Kubernetes Audit Log](./features.md#k8s_audit) -* [Kubernetes Event Logs](./features.md#k8s_event) -* [Kubernetes Node Logs](./features.md#k8s_node) -* [Kubernetes container logs](./features.md#k8s_container) -* [OnPrem API logs](./features.md#onprem_api) -* [Kubernetes Control plane component logs](./features.md#k8s_control_plane_component) +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [OnPrem API logs](./features.md#onprem-api-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) diff --git a/docs/en/relationships.md b/docs/en/relationships.md index 272e558..5704439 100644 --- a/docs/en/relationships.md +++ b/docs/en/relationships.md @@ -4,7 +4,7 @@ KHI timelines are basically placed in the order of `Kind` -> `Namespace` -> `Res The relationship between its parent and children is usually interpretted as the order of its hierarchy, but some subresources are not actual kubernetes resources and it's associated with the parent timeline for convenience. Each timeline color meanings and type of logs put on them are different by this relationship. -## [The default resource timeline](#RelationshipChild) +## ![#CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)The default resource timeline ![](./images/reference/default-timeline.png) @@ -41,7 +41,7 @@ This timeline can have the following events. -## [![#4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png) condition - Status condition field timeline](#RelationshipResourceCondition) +## ![#4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png)Status condition field timeline ### Revisions @@ -57,7 +57,7 @@ This timeline can have the following revisions. -## [![#000000](https://placehold.co/15x15/000000/000000.png) operation - Operation timeline](#RelationshipOperation) +## ![#000000](https://placehold.co/15x15/000000/000000.png)Operation timeline ### Revisions @@ -80,7 +80,7 @@ This timeline can have the following revisions. -## [![#008000](https://placehold.co/15x15/008000/008000.png) endpointslice - Endpoint serving state timeline](#RelationshipEndpointSlice) +## ![#008000](https://placehold.co/15x15/008000/008000.png)Endpoint serving state timeline ### Revisions @@ -96,7 +96,7 @@ This timeline can have the following revisions. -## [![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png) container - Container timeline](#RelationshipContainer) +## ![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)Container timeline ### Revisions @@ -131,7 +131,7 @@ This timeline can have the following events. -## [![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png) node-component - Node component timeline](#RelationshipNodeComponent) +## ![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)Node component timeline ### Revisions @@ -158,7 +158,7 @@ This timeline can have the following events. -## [![#33DD88](https://placehold.co/15x15/33DD88/33DD88.png) owns - Owning children timeline](#RelationshipOwnerReference) +## ![#33DD88](https://placehold.co/15x15/33DD88/33DD88.png)Owning children timeline ### Aliases @@ -172,7 +172,7 @@ This timeline can have the following aliases. -## [![#FF8855](https://placehold.co/15x15/FF8855/FF8855.png) binds - Pod binding timeline](#RelationshipPodBinding) +## ![#FF8855](https://placehold.co/15x15/FF8855/FF8855.png)Pod binding timeline ### Aliases @@ -186,7 +186,7 @@ This timeline can have the following aliases. -## [![#A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png) neg - Network Endpoint Group timeline](#RelationshipNetworkEndpointGroup) +## ![#A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)Network Endpoint Group timeline ### Revisions @@ -201,7 +201,7 @@ This timeline can have the following revisions. -## [![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png) mig - Managed instance group timeline](#RelationshipManagedInstanceGroup) +## ![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)Managed instance group timeline ### Events @@ -215,7 +215,7 @@ This timeline can have the following events. -## [![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png) controlplane - Control plane component timeline](#RelationshipControlPlaneComponent) +## ![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)Control plane component timeline ### Events @@ -229,7 +229,7 @@ This timeline can have the following events. -## [![#333333](https://placehold.co/15x15/333333/333333.png) serialport - Serialport log timeline](#RelationshipSerialPort) +## ![#333333](https://placehold.co/15x15/333333/333333.png)Serialport log timeline ### Events diff --git a/docs/template/feature.template.md b/docs/template/feature.template.md index af1c470..c695be0 100644 --- a/docs/template/feature.template.md +++ b/docs/template/feature.template.md @@ -1,7 +1,7 @@ {{define "feature-template"}} {{range $index,$feature := .Features }} -## [{{$feature.Name}}](#{{$feature.ID}}) +## {{$feature.Name}} {{$feature.Description}} @@ -13,7 +13,7 @@ |Parameter name|Description| |:-:|---| {{- range $index,$form := $feature.Forms}} -|[{{$form.Label}}](./forms.md#{{$form.ID}})|{{$form.Description}}| +|[{{$form.Label}}](./forms.md#{{$form.Label | anchor}})|{{$form.Description}}| {{- end}} {{end}} @@ -24,7 +24,7 @@ |Timeline type|Short name on chip| |:-:|:-:| {{- range $index,$timeline := $feature.OutputTimelines}} -|![{{$timeline.RelationshipColorCode}}](https://placehold.co/15x15/{{$timeline.RelationshipColorCode}}/{{$timeline.RelationshipColorCode}}.png)[{{$timeline.LongName}}](./relationships.md#{{$timeline.RelationshipID}})|{{$timeline.Name}}| +|![{{$timeline.RelationshipColorCode}}](https://placehold.co/15x15/{{$timeline.RelationshipColorCode}}/{{$timeline.RelationshipColorCode}}.png)[{{$timeline.LongName}}](./relationships.md#{{$timeline.LongName | anchor}})|{{$timeline.Name}}| {{- end}} diff --git a/docs/template/inspection-type.template.md b/docs/template/inspection-type.template.md index faebc26..fef3445 100644 --- a/docs/template/inspection-type.template.md +++ b/docs/template/inspection-type.template.md @@ -9,7 +9,7 @@ {{range $feature := $type.SupportedFeatures}} -* [{{$feature.Name}}](./features.md#{{$feature.ID}}) +* [{{$feature.Name}}](./features.md#{{$feature.Name | anchor }}) {{- end}} {{end}} diff --git a/docs/template/relationship.template.md b/docs/template/relationship.template.md index 934c792..1c5557f 100644 --- a/docs/template/relationship.template.md +++ b/docs/template/relationship.template.md @@ -1,7 +1,7 @@ {{define "relationship-template"}} {{range $index,$relationship := .Relationships }} -## [{{with $relationship.HasVisibleChip}}![#{{$relationship.ColorCode}}](https://placehold.co/15x15/{{$relationship.ColorCode}}/{{$relationship.ColorCode}}.png) {{$relationship.Label}} - {{end}}{{$relationship.LongName}}](#{{$relationship.ID}}) +## ![#{{$relationship.ColorCode}}](https://placehold.co/15x15/{{$relationship.ColorCode}}/{{$relationship.ColorCode}}.png){{$relationship.LongName}} {{with $relationship.GeneratableRevisions}} diff --git a/pkg/document/generator/generator.go b/pkg/document/generator/generator.go index acc77e2..1f9a241 100644 --- a/pkg/document/generator/generator.go +++ b/pkg/document/generator/generator.go @@ -10,6 +10,10 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/document/splitter" ) +var documentFuncs = map[string]any{ + "anchor": ToGithubAnchorHash, +} + // DocumentGenerator generates a document from a template. // If there is already text added other than the parts automatically generated by the template, the text added after the immediately preceding automatically generated part will be kept after the corresponding generated text section. type DocumentGenerator struct { @@ -17,7 +21,7 @@ type DocumentGenerator struct { } func NewDocumentGeneratorFromTemplateFileGlob(templateFileGlob string) (*DocumentGenerator, error) { - template, err := template.ParseGlob(templateFileGlob) + template, err := template.New("").Funcs(documentFuncs).ParseGlob(templateFileGlob) if err != nil { return nil, err } @@ -28,7 +32,7 @@ func NewDocumentGeneratorFromTemplateFileGlob(templateFileGlob string) (*Documen func newDocumentGeneratorFromStringTemplate(templateStr string) (*DocumentGenerator, error) { return &DocumentGenerator{ - template: template.Must(template.New("").Parse(templateStr)), + template: template.Must(template.New("").Funcs(documentFuncs).Parse(templateStr)), }, nil } diff --git a/pkg/document/generator/util.go b/pkg/document/generator/util.go new file mode 100644 index 0000000..38adb77 --- /dev/null +++ b/pkg/document/generator/util.go @@ -0,0 +1,14 @@ +package generator + +import ( + "regexp" + "strings" +) + +var regexToRemoveInGithubAnchorHash = regexp.MustCompile(`[^a-z0-9\s\-]+`) +var regexToHyphenInGithubAnchorHash = regexp.MustCompile(`\s+`) + +// ToGithubAnchorHash convert the given text of header to the hash of anchor link. +func ToGithubAnchorHash(text string) string { + return regexToRemoveInGithubAnchorHash.ReplaceAllString(regexToHyphenInGithubAnchorHash.ReplaceAllString(strings.ToLower(strings.TrimSpace(text)), "-"), "") +} diff --git a/pkg/document/generator/util_test.go b/pkg/document/generator/util_test.go new file mode 100644 index 0000000..90941e9 --- /dev/null +++ b/pkg/document/generator/util_test.go @@ -0,0 +1,38 @@ +package generator + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToGithubAnchorHash(t *testing.T) { + testCases := []struct { + input string + want string + }{ + { + input: "Simple Text", + want: "simple-text", + }, + { + input: "Simple_Text", + want: "simpletext", + }, + { + input: " simple text ", + want: "simple-text", + }, + { + input: "Simple(Text)", + want: "simpletext", + }, + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + actual := ToGithubAnchorHash(tc.input) + assert.Equal(t, tc.want, actual) + }) + } +} From 87b467b8d123163fdd6086e24d8badb9e10ba39e Mon Sep 17 00:00:00 2001 From: kyasbal Date: Tue, 11 Feb 2025 10:25:50 +0900 Subject: [PATCH 16/23] Added reverse link to inspection type from features --- docs/en/features.md | 312 ++++++++++++++---- docs/en/forms.md | 167 +++++++++- docs/en/relationships.md | 10 +- docs/template/feature.template.md | 23 +- docs/template/form.template.md | 13 + pkg/document/model/feature.go | 56 +++- pkg/document/model/form.go | 44 ++- pkg/model/enum/parent_relationship.go | 28 +- pkg/source/gcp/task/form.go | 1 + pkg/source/gcp/task/gke/k8s_container/form.go | 2 + pkg/task/testutil.go | 19 ++ 11 files changed, 564 insertions(+), 111 deletions(-) diff --git a/docs/en/features.md b/docs/en/features.md index 1fa6d9c..606fe7c 100644 --- a/docs/en/features.md +++ b/docs/en/features.md @@ -25,13 +25,16 @@ This parser reveals how these resources are created,updated or deleted. ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| -|![4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png)[Status condition field timeline](./relationships.md#status-condition-field-timeline)|condition| -|![008000](https://placehold.co/15x15/008000/008000.png)[Endpoint serving state timeline](./relationships.md#endpoint-serving-state-timeline)|endpointslice| -|![33DD88](https://placehold.co/15x15/33DD88/33DD88.png)[Owning children timeline](./relationships.md#owning-children-timeline)|owns| -|![FF8855](https://placehold.co/15x15/FF8855/FF8855.png)[Pod binding timeline](./relationships.md#pod-binding-timeline)|binds| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| +|![4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png)[Status condition field timeline](./relationships.md#status-condition-field-timeline)|condition|A timeline showing the state changes on `.status.conditions` of the parent resource| +|![008000](https://placehold.co/15x15/008000/008000.png)[Endpoint serving state timeline](./relationships.md#endpoint-serving-state-timeline)|endpointslice|A timeline indicates the status of endpoint related to the parent resource(Pod or Service)| +|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container|A timline of a container included in the parent timeline of a Pod| +|![33DD88](https://placehold.co/15x15/33DD88/33DD88.png)[Owning children timeline](./relationships.md#owning-children-timeline)|owns|| +|![FF8855](https://placehold.co/15x15/FF8855/FF8855.png)[Pod binding timeline](./relationships.md#pod-binding-timeline)|binds|| @@ -41,7 +44,7 @@ This parser reveals how these resources are created,updated or deleted. Sample query: -``` +```ada resource.type="k8s_cluster" resource.labels.cluster_name="gcp-cluster-name" protoPayload.methodName: ("create" OR "update" OR "patch" OR "delete") @@ -51,6 +54,18 @@ protoPayload.methodName=~"\.(deployments|replicasets|pods|nodes)\." ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) +* [GKE on AWS(Anthos on AWS)](./inspection-type.md#gke-on-awsanthos-on-aws) +* [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) +* [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) +* [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) + ## Kubernetes Event Logs @@ -72,9 +87,11 @@ This parser shows events associated to K8s resources ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| @@ -84,13 +101,25 @@ This parser shows events associated to K8s resources Sample query: -``` +```ada logName="projects/gcp-project-id/logs/events" resource.labels.cluster_name="gcp-cluster-name" -- No namespace filter ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) +* [GKE on AWS(Anthos on AWS)](./inspection-type.md#gke-on-awsanthos-on-aws) +* [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) +* [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) +* [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) + ## Kubernetes Node Logs @@ -104,7 +133,7 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. |Parameter name|Description| |:-:|---| -|[Node names](./forms.md#node-names)|| +|[Node names](./forms.md#node-names)|A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster.| |[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| @@ -113,11 +142,13 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| -|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container| -|![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)[Node component timeline](./relationships.md#node-component-timeline)|node-component| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| +|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container|A timline of a container included in the parent timeline of a Pod| +|![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)[Node component timeline](./relationships.md#node-component-timeline)|node-component|A component running inside of the parent timeline of a Node| @@ -127,7 +158,7 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. Sample query: -``` +```ada resource.type="k8s_node" -logName="projects/gcp-project-id/logs/events" resource.labels.cluster_name="gcp-cluster-name" @@ -136,6 +167,18 @@ resource.labels.node_name:("gke-test-cluster-node-1" OR "gke-test-cluster-node-2 ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) +* [GKE on AWS(Anthos on AWS)](./inspection-type.md#gke-on-awsanthos-on-aws) +* [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) +* [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) +* [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) + ## Kubernetes container logs @@ -149,8 +192,8 @@ Container logs ingested from stdout/stderr of workload Pods. |Parameter name|Description| |:-:|---| -|[Namespaces(Container logs)](./forms.md#namespacescontainer-logs)|| -|[Pod names(Container logs)](./forms.md#pod-namescontainer-logs)|| +|[Namespaces(Container logs)](./forms.md#namespacescontainer-logs)|The namespace of Pods to gather container logs. Specify `@managed` to gather logs of system components.| +|[Pod names(Container logs)](./forms.md#pod-namescontainer-logs)|The substring of Pod name to gather container logs. Specify `@any` to gather logs of all pods.| |[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| @@ -159,9 +202,11 @@ Container logs ingested from stdout/stderr of workload Pods. ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container|A timline of a container included in the parent timeline of a Pod| @@ -171,7 +216,7 @@ Container logs ingested from stdout/stderr of workload Pods. Sample query: -``` +```ada resource.type="k8s_container" resource.labels.cluster_name="gcp-cluster-name" resource.labels.namespace_name=("default") @@ -179,6 +224,18 @@ resource.labels.namespace_name=("default") ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) +* [GKE on AWS(Anthos on AWS)](./inspection-type.md#gke-on-awsanthos-on-aws) +* [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) +* [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) +* [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) + ## GKE Audit logs @@ -198,10 +255,12 @@ GKE audit log including cluster creation,deletion and upgrades. ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| @@ -211,13 +270,21 @@ GKE audit log including cluster creation,deletion and upgrades. Sample query: -``` +```ada resource.type=("gke_cluster" OR "gke_nodepool") logName="projects/gcp-project-id/logs/cloudaudit.googleapis.com%2Factivity" resource.labels.cluster_name="gcp-cluster-name" ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) + ## Compute API Logs @@ -239,10 +306,12 @@ Compute API audit logs used for cluster related logs. This also visualize operat ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| @@ -252,7 +321,7 @@ Compute API audit logs used for cluster related logs. This also visualize operat Sample query: -``` +```ada resource.type="gce_instance" -protoPayload.methodName:("list" OR "get" OR "watch") protoPayload.resourceName:(instances/gke-test-cluster-node-1 OR instances/gke-test-cluster-node-2) @@ -267,6 +336,14 @@ Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) + ## GCE Network Logs @@ -288,10 +365,12 @@ GCE network API audit log including NEG related audit logs to identify when the ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| -|![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[Network Endpoint Group timeline](./relationships.md#network-endpoint-group-timeline)|neg| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| +|![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[Network Endpoint Group timeline](./relationships.md#network-endpoint-group-timeline)|neg|| @@ -301,7 +380,7 @@ GCE network API audit log including NEG related audit logs to identify when the Sample query: -``` +```ada resource.type="gce_network" -protoPayload.methodName:("list" OR "get" OR "watch") protoPayload.resourceName:(networkEndpointGroups/neg-id-1 OR networkEndpointGroups/neg-id-2) @@ -316,6 +395,14 @@ Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) + ## MultiCloud API logs @@ -335,9 +422,11 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| @@ -347,7 +436,7 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. Sample query: -``` +```ada resource.type="audited_resource" resource.labels.service="gkemulticloud.googleapis.com" resource.labels.method:("Update" OR "Create" OR "Delete") @@ -356,6 +445,14 @@ protoPayload.resourceName:"awsClusters/cluster-foo" ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [GKE on AWS(Anthos on AWS)](./inspection-type.md#gke-on-awsanthos-on-aws) +* [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) + ## Autoscaler Logs @@ -376,10 +473,12 @@ This log type also includes Node Auto Provisioner logs. ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| -|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Managed instance group timeline](./relationships.md#managed-instance-group-timeline)|mig| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| +|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Managed instance group timeline](./relationships.md#managed-instance-group-timeline)|mig|| @@ -389,7 +488,7 @@ This log type also includes Node Auto Provisioner logs. Sample query: -``` +```ada resource.type="k8s_cluster" resource.labels.project_id="gcp-project-id" resource.labels.cluster_name="gcp-cluster-name" @@ -398,6 +497,14 @@ logName="projects/gcp-project-id/logs/container.googleapis.com%2Fcluster-autosca ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) + ## OnPrem API logs @@ -417,9 +524,11 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| @@ -429,7 +538,7 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and Sample query: -``` +```ada resource.type="audited_resource" resource.labels.service="gkeonprem.googleapis.com" resource.labels.method:("Update" OR "Create" OR "Delete" OR "Enroll" OR "Unenroll") @@ -438,6 +547,14 @@ protoPayload.resourceName:"baremetalClusters/my-cluster" ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) +* [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) + ## Kubernetes Control plane component logs @@ -458,10 +575,12 @@ Visualize Kubernetes control plane component logs on a cluster ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource| -|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Control plane component timeline](./relationships.md#control-plane-component-timeline)|controlplane| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| +|![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Control plane component timeline](./relationships.md#control-plane-component-timeline)|controlplane|| @@ -471,7 +590,7 @@ Visualize Kubernetes control plane component logs on a cluster Sample query: -``` +```ada resource.type="k8s_control_plane_component" resource.labels.cluster_name="gcp-cluster-name" resource.labels.project_id="gcp-project-id" @@ -480,6 +599,18 @@ resource.labels.project_id="gcp-project-id" ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) +* [GKE on AWS(Anthos on AWS)](./inspection-type.md#gke-on-awsanthos-on-aws) +* [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) +* [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) +* [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) + ## Node serial port logs @@ -493,7 +624,7 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on |:-:|---| |[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| |[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Node names](./forms.md#node-names)|| +|[Node names](./forms.md#node-names)|A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster.| |[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| @@ -502,9 +633,11 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| -|![333333](https://placehold.co/15x15/333333/333333.png)[Serialport log timeline](./relationships.md#serialport-log-timeline)|serialport| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| +|![333333](https://placehold.co/15x15/333333/333333.png)[Serialport log timeline](./relationships.md#serialport-log-timeline)|serialport|| @@ -514,7 +647,7 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on Sample query: -``` +```ada LOG_ID("serialconsole.googleapis.com%2Fserial_port_1_output") OR LOG_ID("serialconsole.googleapis.com%2Fserial_port_2_output") OR LOG_ID("serialconsole.googleapis.com%2Fserial_port_3_output") OR @@ -533,6 +666,14 @@ Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit + +### Inspection types + +This feature is supported in the following inspection types. + +* [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) +* [Cloud Composer](./inspection-type.md#cloud-composer) + ## (Alpha) Composer / Airflow Scheduler @@ -553,8 +694,10 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| @@ -564,11 +707,18 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst Sample query: -``` +```ada TODO: add sample query ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Cloud Composer](./inspection-type.md#cloud-composer) + ## (Alpha) Cloud Composer / Airflow Worker @@ -589,8 +739,10 @@ Airflow Worker logs contain information related to the execution of TaskInstance ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| @@ -600,11 +752,18 @@ Airflow Worker logs contain information related to the execution of TaskInstance Sample query: -``` +```ada TODO: add sample query ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Cloud Composer](./inspection-type.md#cloud-composer) + ## (Alpha) Composer / Airflow DagProcessorManager @@ -625,8 +784,10 @@ The DagProcessorManager logs contain information for investigating the number of ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| @@ -636,8 +797,15 @@ The DagProcessorManager logs contain information for investigating the number of Sample query: -``` +```ada TODO: add sample query ``` + +### Inspection types + +This feature is supported in the following inspection types. + +* [Cloud Composer](./inspection-type.md#cloud-composer) + diff --git a/docs/en/forms.md b/docs/en/forms.md index c40238b..7d7b46b 100644 --- a/docs/en/forms.md +++ b/docs/en/forms.md @@ -3,58 +3,219 @@ The project ID containing the logs of cluster to query + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [GKE Audit logs](./features.md#gke-audit-logs) +* [Compute API Logs](./features.md#compute-api-logs) +* [GCE Network Logs](./features.md#gce-network-logs) +* [MultiCloud API logs](./features.md#multicloud-api-logs) +* [Autoscaler Logs](./features.md#autoscaler-logs) +* [OnPrem API logs](./features.md#onprem-api-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) +* [(Alpha) Composer / Airflow Scheduler](./features.md#alpha-composer--airflow-scheduler) +* [(Alpha) Cloud Composer / Airflow Worker](./features.md#alpha-cloud-composer--airflow-worker) +* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#alpha-composer--airflow-dagprocessormanager) + ## Cluster name The cluster name to gather logs. + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [GKE Audit logs](./features.md#gke-audit-logs) +* [Compute API Logs](./features.md#compute-api-logs) +* [GCE Network Logs](./features.md#gce-network-logs) +* [MultiCloud API logs](./features.md#multicloud-api-logs) +* [Autoscaler Logs](./features.md#autoscaler-logs) +* [OnPrem API logs](./features.md#onprem-api-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) + ## Duration The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`) + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [GKE Audit logs](./features.md#gke-audit-logs) +* [Compute API Logs](./features.md#compute-api-logs) +* [GCE Network Logs](./features.md#gce-network-logs) +* [MultiCloud API logs](./features.md#multicloud-api-logs) +* [Autoscaler Logs](./features.md#autoscaler-logs) +* [OnPrem API logs](./features.md#onprem-api-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) +* [(Alpha) Composer / Airflow Scheduler](./features.md#alpha-composer--airflow-scheduler) +* [(Alpha) Cloud Composer / Airflow Worker](./features.md#alpha-cloud-composer--airflow-worker) +* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#alpha-composer--airflow-dagprocessormanager) + ## End time The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter. + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Kubernetes container logs](./features.md#kubernetes-container-logs) +* [GKE Audit logs](./features.md#gke-audit-logs) +* [Compute API Logs](./features.md#compute-api-logs) +* [GCE Network Logs](./features.md#gce-network-logs) +* [MultiCloud API logs](./features.md#multicloud-api-logs) +* [Autoscaler Logs](./features.md#autoscaler-logs) +* [OnPrem API logs](./features.md#onprem-api-logs) +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) +* [(Alpha) Composer / Airflow Scheduler](./features.md#alpha-composer--airflow-scheduler) +* [(Alpha) Cloud Composer / Airflow Worker](./features.md#alpha-cloud-composer--airflow-worker) +* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#alpha-composer--airflow-dagprocessormanager) + ## Kind The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Compute API Logs](./features.md#compute-api-logs) +* [GCE Network Logs](./features.md#gce-network-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) + ## Location + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [(Alpha) Composer / Airflow Scheduler](./features.md#alpha-composer--airflow-scheduler) +* [(Alpha) Cloud Composer / Airflow Worker](./features.md#alpha-cloud-composer--airflow-worker) +* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#alpha-composer--airflow-dagprocessormanager) + ## Namespaces The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources. + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) +* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) +* [Compute API Logs](./features.md#compute-api-logs) +* [GCE Network Logs](./features.md#gce-network-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) + ## Node names - +A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster. + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) +* [Node serial port logs](./features.md#node-serial-port-logs) + ## Namespaces(Container logs) - +The namespace of Pods to gather container logs. Specify `@managed` to gather logs of system components. + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes container logs](./features.md#kubernetes-container-logs) + ## Pod names(Container logs) - +The substring of Pod name to gather container logs. Specify `@any` to gather logs of all pods. + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes container logs](./features.md#kubernetes-container-logs) + ## Control plane component names + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) + ## Composer Environment Name + +### Features using this parameter + +Following feature tasks are depending on this parameter: + + +* [(Alpha) Composer / Airflow Scheduler](./features.md#alpha-composer--airflow-scheduler) +* [(Alpha) Cloud Composer / Airflow Worker](./features.md#alpha-cloud-composer--airflow-worker) +* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#alpha-composer--airflow-dagprocessormanager) + diff --git a/docs/en/relationships.md b/docs/en/relationships.md index 5704439..e36f10a 100644 --- a/docs/en/relationships.md +++ b/docs/en/relationships.md @@ -106,11 +106,11 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| -|![#997700](https://placehold.co/15x15/997700/997700.png)Waiting for starting container|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is not started yet and waiting for something.(Example: Pulling images, mounting volumes ...etc)| -|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)Container is not ready|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is started but the readiness is not ready.| -|![#007700](https://placehold.co/15x15/007700/007700.png)Container is ready|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is started and the readiness is ready| -|![#113333](https://placehold.co/15x15/113333/113333.png)Container exited with healthy exit code|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is already terminated with successful exit code = 0| -|![#331111](https://placehold.co/15x15/331111/331111.png)Container exited with errornous exit code|![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container|The container is already terminated with errornous exit code != 0| +|![#997700](https://placehold.co/15x15/997700/997700.png)Waiting for starting container|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|The container is not started yet and waiting for something.(Example: Pulling images, mounting volumes ...etc)| +|![#EE4400](https://placehold.co/15x15/EE4400/EE4400.png)Container is not ready|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|The container is started but the readiness is not ready.| +|![#007700](https://placehold.co/15x15/007700/007700.png)Container is ready|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|The container is started and the readiness is ready| +|![#113333](https://placehold.co/15x15/113333/113333.png)Container exited with healthy exit code|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|The container is already terminated with successful exit code = 0| +|![#331111](https://placehold.co/15x15/331111/331111.png)Container exited with errornous exit code|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|The container is already terminated with errornous exit code != 0| diff --git a/docs/template/feature.template.md b/docs/template/feature.template.md index c695be0..7b49d95 100644 --- a/docs/template/feature.template.md +++ b/docs/template/feature.template.md @@ -21,10 +21,12 @@ ### Output timelines -|Timeline type|Short name on chip| -|:-:|:-:| +This feature can generates following timeline relationship of timelines. + +|Timeline relationships|Short name on chip|Description| +|:-:|:-:|:-:| {{- range $index,$timeline := $feature.OutputTimelines}} -|![{{$timeline.RelationshipColorCode}}](https://placehold.co/15x15/{{$timeline.RelationshipColorCode}}/{{$timeline.RelationshipColorCode}}.png)[{{$timeline.LongName}}](./relationships.md#{{$timeline.LongName | anchor}})|{{$timeline.Name}}| +|![{{$timeline.RelationshipColorCode}}](https://placehold.co/15x15/{{$timeline.RelationshipColorCode}}/{{$timeline.RelationshipColorCode}}.png)[{{$timeline.LongName}}](./relationships.md#{{$timeline.LongName | anchor}})|{{$timeline.Label}}|{{$timeline.Description}}| {{- end}} @@ -35,7 +37,7 @@ Sample query: -``` +```ada {{/* "ada" syntax highlighting is good for Cloud Logging filter */}} {{$feature.TargetQueryDependency.SampleQuery}} ``` @@ -50,5 +52,16 @@ Following log queries are used with this feature. {{- end}} {{end}} + +{{with $feature.AvailableInspectionTypes}} + +### Inspection types + +This feature is supported in the following inspection types. +{{range $index,$type := $feature.AvailableInspectionTypes}} +* [{{$type.Name}}](./inspection-type.md#{{$type.Name | anchor}}) +{{- end}} + +{{end}} +{{end}} {{end}} -{{end}} \ No newline at end of file diff --git a/docs/template/form.template.md b/docs/template/form.template.md index 639b5b4..e3c04e5 100644 --- a/docs/template/form.template.md +++ b/docs/template/form.template.md @@ -5,5 +5,18 @@ {{$form.Description}} + +{{with $form.UsedFeatures}} + +### Features using this parameter + +Following feature tasks are depending on this parameter: + +{{range $index,$feature := $form.UsedFeatures }} +* [{{$feature.Name}}](./features.md#{{$feature.Name | anchor}}) +{{- end}} + +{{end}} + {{end}} {{end}} \ No newline at end of file diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index 8840141..0f36442 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -1,6 +1,7 @@ package model import ( + "slices" "strings" "github.com/GoogleCloudPlatform/khi/pkg/inspection" @@ -20,10 +21,11 @@ type FeatureDocumentElement struct { Name string Description string - IndirectQueryDependency []FeatureIndirectDependentQueryElement - TargetQueryDependency FeatureDependentTargetQueryElement - Forms []FeatureDependentFormElement - OutputTimelines []FeatureOutputTimelineElement + IndirectQueryDependency []FeatureIndirectDependentQueryElement + TargetQueryDependency FeatureDependentTargetQueryElement + Forms []FeatureDependentFormElement + OutputTimelines []FeatureOutputTimelineElement + AvailableInspectionTypes []FeatureAvailableInspectionType } type FeatureIndirectDependentQueryElement struct { @@ -49,7 +51,13 @@ type FeatureOutputTimelineElement struct { RelationshipID string RelationshipColorCode string LongName string - Name string + Label string + Description string +} + +type FeatureAvailableInspectionType struct { + ID string + Name string } func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*FeatureDocumentModel, error) { @@ -126,19 +134,21 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat RelationshipID: relationship.EnumKeyName, RelationshipColorCode: strings.TrimLeft(relationship.LabelBackgroundColor, "#"), LongName: relationship.LongName, - Name: relationship.Label, + Label: relationship.Label, + Description: relationship.Description, }) } } result.Features = append(result.Features, FeatureDocumentElement{ - ID: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string), - Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), - Description: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), - IndirectQueryDependency: indirectQueryDependencyElement, - TargetQueryDependency: targetQueryDependencyElement, - Forms: formElements, - OutputTimelines: outputTimelines, + ID: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string), + Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), + Description: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), + IndirectQueryDependency: indirectQueryDependencyElement, + TargetQueryDependency: targetQueryDependencyElement, + Forms: formElements, + OutputTimelines: outputTimelines, + AvailableInspectionTypes: getAvailableInspectionTypes(taskServer, feature), }) } @@ -168,3 +178,23 @@ func getDependentFormTasks(taskServer *inspection.InspectionTaskServer, featureT } return resolved.FilteredSubset(label.TaskLabelKeyIsFormTask, taskfilter.HasTrue, false).GetAll(), nil } + +// getAvailableInspectionTypes returns the list of information about inspection type that supports this feature. +func getAvailableInspectionTypes(taskServer *inspection.InspectionTaskServer, featureTask task.Definition) []FeatureAvailableInspectionType { + result := []FeatureAvailableInspectionType{} + inspectionTypes := taskServer.GetAllInspectionTypes() + for _, inspectionType := range inspectionTypes { + labelsAny, found := featureTask.Labels().Get(inspection_task.LabelKeyInspectionTypes) + labels := []string{inspectionType.Id} + if found { + labels = labelsAny.([]string) + } + if slices.Contains(labels, inspectionType.Id) { + result = append(result, FeatureAvailableInspectionType{ + ID: inspectionType.Id, + Name: inspectionType.Name, + }) + } + } + return result +} diff --git a/pkg/document/model/form.go b/pkg/document/model/form.go index 3acb430..36869ec 100644 --- a/pkg/document/model/form.go +++ b/pkg/document/model/form.go @@ -2,8 +2,10 @@ package model import ( "github.com/GoogleCloudPlatform/khi/pkg/inspection" + inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" "github.com/GoogleCloudPlatform/khi/pkg/inspection/task/label" "github.com/GoogleCloudPlatform/khi/pkg/inspection/taskfilter" + "github.com/GoogleCloudPlatform/khi/pkg/task" ) type FormDocumentModel struct { @@ -14,17 +16,53 @@ type FormDocumentElement struct { ID string Label string Description string + + UsedFeatures []FormUsedFeatureElement +} + +type FormUsedFeatureElement struct { + ID string + Name string } func GetFormDocumentModel(taskServer *inspection.InspectionTaskServer) (*FormDocumentModel, error) { result := FormDocumentModel{} forms := taskServer.RootTaskSet.FilteredSubset(label.TaskLabelKeyIsFormTask, taskfilter.HasTrue, false) for _, form := range forms.GetAll() { + usedFeatures, err := getUsedFeatures(taskServer, form) + if err != nil { + return nil, err + } + usedFeatureElements := []FormUsedFeatureElement{} + for _, feature := range usedFeatures { + usedFeatureElements = append(usedFeatureElements, FormUsedFeatureElement{ + ID: feature.ID().String(), + Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), + }) + } + result.Forms = append(result.Forms, FormDocumentElement{ - ID: form.ID().String(), - Label: form.Labels().GetOrDefault(label.TaskLabelKeyFormFieldLabel, "").(string), - Description: form.Labels().GetOrDefault(label.TaskLabelKeyFormFieldDescription, "").(string), + ID: form.ID().String(), + Label: form.Labels().GetOrDefault(label.TaskLabelKeyFormFieldLabel, "").(string), + Description: form.Labels().GetOrDefault(label.TaskLabelKeyFormFieldDescription, "").(string), + UsedFeatures: usedFeatureElements, }) } return &result, nil } + +func getUsedFeatures(taskServer *inspection.InspectionTaskServer, formTask task.Definition) ([]task.Definition, error) { + var result []task.Definition + features := taskServer.RootTaskSet.FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false) + for _, feature := range features.GetAll() { + hasDependency, err := task.HasDependency(taskServer.RootTaskSet, feature, formTask) + if err != nil { + return nil, err + } + if hasDependency { + result = append(result, feature) + } + + } + return result, nil +} diff --git a/pkg/model/enum/parent_relationship.go b/pkg/model/enum/parent_relationship.go index 53151e9..036eb43 100644 --- a/pkg/model/enum/parent_relationship.go +++ b/pkg/model/enum/parent_relationship.go @@ -43,14 +43,16 @@ type ParentRelationshipFrontendMetadata struct { EnumKeyName string // Label is a short name shown on frontend as the chip on the left of timeline name. Label string - // LongName is a descriptive name of the ralationship. This value is used in the document. - LongName string // Hint explains the meaning of this timeline. This is shown as the tooltip on front end. Hint string LabelColor string LabelBackgroundColor string SortPriority int + // LongName is a descriptive name of the ralationship. This value is used in the document. + LongName string + // Description is a description of this timeline ralationship. This value is used in the document. + Description string // GeneratableEvents contains the list of possible event types put on a timeline with the relationship type. This field is used for document generation. GeneratableEvents []GeneratableEventInfo // GeneratableRevisions contains the list of possible revision types put on a timeline with the relationship type. This field is used for document generation. @@ -81,20 +83,21 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad Visible: false, EnumKeyName: "RelationshipChild", Label: "resource", - LongName: "The default resource timeline", LabelColor: "#000000", LabelBackgroundColor: "#CCCCCC", SortPriority: 1000, + LongName: "The default resource timeline", + Description: "A default timeline recording the history of Kubernetes resources", GeneratableRevisions: []GeneratableRevisionInfo{ { State: RevisionStateInferred, SourceLogType: LogTypeAudit, - Description: "This state indicates the resource exits at the time, but this existence is inferred from the other logs later. The detailed resource information is not available.", + Description: "This state indicates the resource exists at the time, but this existence is inferred from the other logs later. The detailed resource information is not available.", }, { State: RevisionStateExisting, SourceLogType: LogTypeAudit, - Description: "This state indicates the resource exits at the time", + Description: "This state indicates the resource exists at the time", }, { State: RevisionStateDeleted, @@ -148,6 +151,7 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad LabelBackgroundColor: "#4c29e8", Hint: "Resource condition written on .status.conditions", SortPriority: 2000, + Description: "A timeline showing the state changes on `.status.conditions` of the parent resource", GeneratableRevisions: []GeneratableRevisionInfo{ { State: RevisionStateConditionTrue, @@ -175,6 +179,7 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad LabelBackgroundColor: "#000000", Hint: "GCP operations associated with this resource", SortPriority: 3000, + Description: "A timeline showing long running operation status related to the parent resource", GeneratableRevisions: []GeneratableRevisionInfo{ { State: RevisionStateOperationStarted, @@ -237,6 +242,7 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad LabelBackgroundColor: "#008000", Hint: "Pod serving status obtained from endpoint slice", SortPriority: 20000, // later than container + Description: "A timeline indicates the status of endpoint related to the parent resource(Pod or Service)", GeneratableRevisions: []GeneratableRevisionInfo{ { State: RevisionStateEndpointReady, @@ -264,30 +270,31 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad LabelBackgroundColor: "#fe9bab", Hint: "Statuses/logs of a container", SortPriority: 5000, + Description: "A timline of a container included in the parent timeline of a Pod", GeneratableRevisions: []GeneratableRevisionInfo{ { State: RevisionStateContainerWaiting, - SourceLogType: LogTypeContainer, + SourceLogType: LogTypeAudit, Description: "The container is not started yet and waiting for something.(Example: Pulling images, mounting volumes ...etc)", }, { State: RevisionStateContainerRunningNonReady, - SourceLogType: LogTypeContainer, + SourceLogType: LogTypeAudit, Description: "The container is started but the readiness is not ready.", }, { State: RevisionStateContainerRunningReady, - SourceLogType: LogTypeContainer, + SourceLogType: LogTypeAudit, Description: "The container is started and the readiness is ready", }, { State: RevisionStateContainerTerminatedWithSuccess, - SourceLogType: LogTypeContainer, + SourceLogType: LogTypeAudit, Description: "The container is already terminated with successful exit code = 0", }, { State: RevisionStateContainerTerminatedWithError, - SourceLogType: LogTypeContainer, + SourceLogType: LogTypeAudit, Description: "The container is already terminated with errornous exit code != 0", }, }, @@ -311,6 +318,7 @@ var ParentRelationships = map[ParentRelationship]ParentRelationshipFrontendMetad LabelBackgroundColor: "#0077CC", Hint: "Non container resource running on a node", SortPriority: 6000, + Description: "A component running inside of the parent timeline of a Node", GeneratableRevisions: []GeneratableRevisionInfo{ { State: RevisionStateInferred, diff --git a/pkg/source/gcp/task/form.go b/pkg/source/gcp/task/form.go index 9f46a08..be64159 100644 --- a/pkg/source/gcp/task/form.go +++ b/pkg/source/gcp/task/form.go @@ -382,6 +382,7 @@ func getNodeNameSubstringsFromRawInput(value string) []string { var InputNodeNameFilterTask = form.NewInputFormDefinitionBuilder(InputNodeNameFilterTaskID, PriorityForK8sResourceFilterGroup+3000, "Node names"). WithDefaultValueConstant("", true). WithUIDescription("A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster."). + WithDocumentDescription("A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster."). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { nodeNameSubstrings := getNodeNameSubstringsFromRawInput(value) for _, name := range nodeNameSubstrings { diff --git a/pkg/source/gcp/task/gke/k8s_container/form.go b/pkg/source/gcp/task/gke/k8s_container/form.go index 56fed5f..769e6be 100644 --- a/pkg/source/gcp/task/gke/k8s_container/form.go +++ b/pkg/source/gcp/task/gke/k8s_container/form.go @@ -34,6 +34,7 @@ var InputContainerQueryNamespaceFilterTask = form.NewInputFormDefinitionBuilder( WithDefaultValueConstant("@managed", true). WithUIDescription(`Container logs tend to be a lot and take very long time to query. Specify the space splitted namespace lists to query container logs only in the specific namespaces.`). + WithDocumentDescription("The namespace of Pods to gather container logs. Specify `@managed` to gather logs of system components."). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { result, err := queryutil.ParseSetFilter(value, inputNamespacesAliasMap, true, true, true) if err != nil { @@ -62,6 +63,7 @@ var InputContainerQueryPodNamesFilterMask = form.NewInputFormDefinitionBuilder(I WithUIDescription(`Container logs tend to be a lot and take very long time to query. Specify the space splitted pod names lists to query container logs only in the specific pods. This parameter is evaluated as the partial match not the perfect match. You can use the prefix of the pod names.`). + WithDocumentDescription("The substring of Pod name to gather container logs. Specify `@any` to gather logs of all pods."). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { result, err := queryutil.ParseSetFilter(value, inputPodNamesAliasMap, true, true, true) if err != nil { diff --git a/pkg/task/testutil.go b/pkg/task/testutil.go index 1efee37..2bd06d7 100644 --- a/pkg/task/testutil.go +++ b/pkg/task/testutil.go @@ -36,3 +36,22 @@ func newLocalCachedTaskRunnerForSingleTask(target Definition, cache TaskVariable localRunner.WithCacheProvider(cache) return localRunner, nil } + +// HasDependency check if 2 tasks have dependency between them when the task graph was resolved with given task set. +func HasDependency(taskSet *DefinitionSet, dependencyFrom Definition, dependencyTo Definition) (bool, error) { + sourceSet, err := NewSet([]Definition{dependencyFrom}) + if err != nil { + return false, err + } + resolvedSet, err := sourceSet.ResolveTask(taskSet) + if err != nil { + return false, err + } + dependentDefinitions := resolvedSet.GetAll() + for _, definition := range dependentDefinitions { + if definition.ID() == dependencyTo.ID() { + return true, nil + } + } + return false, nil +} From fea187335002b3591bb106fbfd72b4a383cf36e4 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Tue, 11 Feb 2025 15:47:13 +0900 Subject: [PATCH 17/23] Moved folders * Added make commands * Moved files in refenrence folder * Added callouts saying this reference is under contruction --- .../main.go | 13 ++++++++----- docs/en/{ => reference}/features.md | 3 +++ docs/en/{ => reference}/forms.md | 5 +++++ docs/en/{ => reference}/inspection-type.md | 3 +++ docs/en/{ => reference}/relationships.md | 7 +++++-- docs/template/{ => reference}/feature.template.md | 0 docs/template/{ => reference}/form.template.md | 0 .../{ => reference}/inspection-type.template.md | 0 .../{ => reference}/relationship.template.md | 0 scripts/make/codegen.mk | 6 +++++- 10 files changed, 29 insertions(+), 8 deletions(-) rename cmd/{document-generator => reference-generator}/main.go (74%) rename docs/en/{ => reference}/features.md (99%) rename docs/en/{ => reference}/forms.md (99%) rename docs/en/{ => reference}/inspection-type.md (98%) rename docs/en/{ => reference}/relationships.md (98%) rename docs/template/{ => reference}/feature.template.md (100%) rename docs/template/{ => reference}/form.template.md (100%) rename docs/template/{ => reference}/inspection-type.template.md (100%) rename docs/template/{ => reference}/relationship.template.md (100%) diff --git a/cmd/document-generator/main.go b/cmd/reference-generator/main.go similarity index 74% rename from cmd/document-generator/main.go rename to cmd/reference-generator/main.go index f422253..37da33c 100644 --- a/cmd/document-generator/main.go +++ b/cmd/reference-generator/main.go @@ -1,5 +1,8 @@ package main +// cmd/reference-generator/main.go +// Generates KHI reference documents from the task graph or constants defined in code base. + import ( "fmt" "log/slog" @@ -40,24 +43,24 @@ func main() { } } - generator, err := generator.NewDocumentGeneratorFromTemplateFileGlob("./docs/template/*.template.md") + generator, err := generator.NewDocumentGeneratorFromTemplateFileGlob("./docs/template/reference/*.template.md") fatal(err, "failed to load template files") inspectionTypeDocumentModel := model.GetInspectionTypeDocumentModel(inspectionServer) - err = generator.GenerateDocument("./docs/en/inspection-type.md", "inspection-type-template", inspectionTypeDocumentModel, false) + err = generator.GenerateDocument("./docs/en/reference/inspection-type.md", "inspection-type-template", inspectionTypeDocumentModel, false) fatal(err, "failed to generate inspection type document") featureDocumentModel, err := model.GetFeatureDocumentModel(inspectionServer) fatal(err, "failed to generate feature document model") - err = generator.GenerateDocument("./docs/en/features.md", "feature-template", featureDocumentModel, false) + err = generator.GenerateDocument("./docs/en/reference/features.md", "feature-template", featureDocumentModel, false) fatal(err, "failed to generate feature document") formDocumentModel, err := model.GetFormDocumentModel(inspectionServer) fatal(err, "failed to generate form document model") - err = generator.GenerateDocument("./docs/en/forms.md", "form-template", formDocumentModel, false) + err = generator.GenerateDocument("./docs/en/reference/forms.md", "form-template", formDocumentModel, false) fatal(err, "failed to generate form document") relationshipDocumentModel := model.GetRelationshipDocumentModel() - err = generator.GenerateDocument("./docs/en/relationships.md", "relationship-template", relationshipDocumentModel, false) + err = generator.GenerateDocument("./docs/en/reference/relationships.md", "relationship-template", relationshipDocumentModel, false) fatal(err, "failed to generate relationship document") } diff --git a/docs/en/features.md b/docs/en/reference/features.md similarity index 99% rename from docs/en/features.md rename to docs/en/reference/features.md index 606fe7c..8d1bf11 100644 --- a/docs/en/features.md +++ b/docs/en/reference/features.md @@ -1,5 +1,8 @@ # Features +> [!WARNING] +> 🚧 This reference document is under construction. 🚧 + The output timelnes of KHI is formed in the `feature tasks`. A feature may depends on parameters, other log query. User will select features on the 2nd menu of the dialog after clicking `New inspection` button. diff --git a/docs/en/forms.md b/docs/en/reference/forms.md similarity index 99% rename from docs/en/forms.md rename to docs/en/reference/forms.md index 7d7b46b..c3d6f0d 100644 --- a/docs/en/forms.md +++ b/docs/en/reference/forms.md @@ -1,3 +1,8 @@ +# Forms + +> [!WARNING] +> 🚧 This reference document is under construction. 🚧 + ## Project ID diff --git a/docs/en/inspection-type.md b/docs/en/reference/inspection-type.md similarity index 98% rename from docs/en/inspection-type.md rename to docs/en/reference/inspection-type.md index ab7e786..2b87827 100644 --- a/docs/en/inspection-type.md +++ b/docs/en/reference/inspection-type.md @@ -1,5 +1,8 @@ # Inspection types +> [!WARNING] +> 🚧 This reference document is under construction. 🚧 + Log querying and parsing procedures in KHI is done on a DAG based task execution system. Each tasks can have dependency and KHI automatically resolves them and run them parallelly as much as possible. diff --git a/docs/en/relationships.md b/docs/en/reference/relationships.md similarity index 98% rename from docs/en/relationships.md rename to docs/en/reference/relationships.md index e36f10a..e8912c3 100644 --- a/docs/en/relationships.md +++ b/docs/en/reference/relationships.md @@ -1,5 +1,8 @@ # Relationships +> [!WARNING] +> 🚧 This reference document is under construction. 🚧 + KHI timelines are basically placed in the order of `Kind` -> `Namespace` -> `Resource name` -> `Subresource name`. The relationship between its parent and children is usually interpretted as the order of its hierarchy, but some subresources are not actual kubernetes resources and it's associated with the parent timeline for convenience. Each timeline color meanings and type of logs put on them are different by this relationship. @@ -17,8 +20,8 @@ This timeline can have the following revisions. |State|Source log|Description| |---|---|---| -|![#997700](https://placehold.co/15x15/997700/997700.png)Resource may be existing|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource exits at the time, but this existence is inferred from the other logs later. The detailed resource information is not available.| -|![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png)Resource is existing|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource exits at the time| +|![#997700](https://placehold.co/15x15/997700/997700.png)Resource may be existing|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource exists at the time, but this existence is inferred from the other logs later. The detailed resource information is not available.| +|![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png)Resource is existing|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource exists at the time| |![#CC0000](https://placehold.co/15x15/CC0000/CC0000.png)Resource is deleted|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource is deleted at the time.| |![#CC5500](https://placehold.co/15x15/CC5500/CC5500.png)Resource is under deleting with graceful period|![#000000](https://placehold.co/15x15/000000/000000.png)k8s_audit|This state indicates the resource is being deleted with grace period at the time.| |![#4444ff](https://placehold.co/15x15/4444ff/4444ff.png)Resource is being provisioned|![#AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit|This state indicates the resource is being provisioned. Currently this state is only used for cluster/nodepool status only.| diff --git a/docs/template/feature.template.md b/docs/template/reference/feature.template.md similarity index 100% rename from docs/template/feature.template.md rename to docs/template/reference/feature.template.md diff --git a/docs/template/form.template.md b/docs/template/reference/form.template.md similarity index 100% rename from docs/template/form.template.md rename to docs/template/reference/form.template.md diff --git a/docs/template/inspection-type.template.md b/docs/template/reference/inspection-type.template.md similarity index 100% rename from docs/template/inspection-type.template.md rename to docs/template/reference/inspection-type.template.md diff --git a/docs/template/relationship.template.md b/docs/template/reference/relationship.template.md similarity index 100% rename from docs/template/relationship.template.md rename to docs/template/reference/relationship.template.md diff --git a/scripts/make/codegen.mk b/scripts/make/codegen.mk index c5dc598..2845186 100644 --- a/scripts/make/codegen.mk +++ b/scripts/make/codegen.mk @@ -19,4 +19,8 @@ web/src/environments/version.*.ts: VERSION .PHONY=add-licenses add-licenses: - $(GOPATH)/bin/addlicense -c "Google LLC" -l apache . \ No newline at end of file + $(GOPATH)/bin/addlicense -c "Google LLC" -l apache . + +.PHONY=generate-reference +generate-reference: + go run ./cmd/reference-generator/ \ No newline at end of file From 93d6262e57a0896580c0359ee6f538c19510400a Mon Sep 17 00:00:00 2001 From: kyasbal Date: Tue, 11 Feb 2025 16:24:04 +0900 Subject: [PATCH 18/23] Fixed descriptions of feature tasks --- cmd/reference-generator/main.go | 1 + docs/en/reference/features.md | 343 +++++++++--------- docs/en/reference/inspection-type.md | 124 ++++--- .../reference/inspection-type.template.md | 12 +- pkg/document/model/feature.go | 2 +- pkg/document/model/inspection_type.go | 25 +- pkg/inspection/task/label.go | 7 +- pkg/parser/parser.go | 5 +- pkg/server/server_test.go | 8 +- pkg/source/gcp/task/cloud-composer/parser.go | 15 - pkg/source/gcp/task/gke/autoscaler/parser.go | 8 +- pkg/source/gcp/task/gke/compute_api/parser.go | 7 +- pkg/source/gcp/task/gke/gke_audit/parser.go | 7 +- .../gcp/task/gke/k8s_audit/recorder/task.go | 3 +- .../gcp/task/gke/k8s_container/parser.go | 9 +- .../gke/k8s_control_plane_component/parser.go | 7 +- pkg/source/gcp/task/gke/k8s_event/parser.go | 8 +- pkg/source/gcp/task/gke/k8s_node/parser.go | 9 +- pkg/source/gcp/task/gke/network_api/parser.go | 7 +- pkg/source/gcp/task/gke/serialport/parser.go | 7 +- pkg/source/gcp/task/multicloud_api/parser.go | 7 +- pkg/source/gcp/task/onprem_api/parser.go | 7 +- 22 files changed, 278 insertions(+), 350 deletions(-) diff --git a/cmd/reference-generator/main.go b/cmd/reference-generator/main.go index 37da33c..9440a04 100644 --- a/cmd/reference-generator/main.go +++ b/cmd/reference-generator/main.go @@ -46,6 +46,7 @@ func main() { generator, err := generator.NewDocumentGeneratorFromTemplateFileGlob("./docs/template/reference/*.template.md") fatal(err, "failed to load template files") + // Generate the reference for inspection types inspectionTypeDocumentModel := model.GetInspectionTypeDocumentModel(inspectionServer) err = generator.GenerateDocument("./docs/en/reference/inspection-type.md", "inspection-type-template", inspectionTypeDocumentModel, false) fatal(err, "failed to generate inspection type document") diff --git a/docs/en/reference/features.md b/docs/en/reference/features.md index 8d1bf11..c6dc843 100644 --- a/docs/en/reference/features.md +++ b/docs/en/reference/features.md @@ -6,14 +6,13 @@ The output timelnes of KHI is formed in the `feature tasks`. A feature may depends on parameters, other log query. User will select features on the 2nd menu of the dialog after clicking `New inspection` button. - + ## Kubernetes Audit Log -Visualize Kubernetes audit logs in GKE. -This parser reveals how these resources are created,updated or deleted. +Gather kubernetes audit logs and visualize resource modifications. - - + + ### Parameters |Parameter name|Description| @@ -24,8 +23,8 @@ This parser reveals how these resources are created,updated or deleted. |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -39,8 +38,8 @@ This feature can generates following timeline relationship of timelines. |![33DD88](https://placehold.co/15x15/33DD88/33DD88.png)[Owning children timeline](./relationships.md#owning-children-timeline)|owns|| |![FF8855](https://placehold.co/15x15/FF8855/FF8855.png)[Pod binding timeline](./relationships.md#pod-binding-timeline)|binds|| - - + + ### Target log type **![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit** @@ -56,8 +55,8 @@ protoPayload.methodName=~"\.(deployments|replicasets|pods|nodes)\." ``` - - + + ### Inspection types This feature is supported in the following inspection types. @@ -68,15 +67,14 @@ This feature is supported in the following inspection types. * [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) * [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) * [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) - - + + ## Kubernetes Event Logs -Visualize Kubernetes event logs on GKE. -This parser shows events associated to K8s resources +Gather kubernetes event logs and visualize these on the associated resource timeline. - - + + ### Parameters |Parameter name|Description| @@ -86,8 +84,8 @@ This parser shows events associated to K8s resources |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -96,8 +94,8 @@ This feature can generates following timeline relationship of timelines. |:-:|:-:|:-:| |![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| - - + + ### Target log type **![3fb549](https://placehold.co/15x15/3fb549/3fb549.png)k8s_event** @@ -110,8 +108,8 @@ resource.labels.cluster_name="gcp-cluster-name" -- No namespace filter ``` - - + + ### Inspection types This feature is supported in the following inspection types. @@ -122,16 +120,14 @@ This feature is supported in the following inspection types. * [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) * [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) * [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) - - + + ## Kubernetes Node Logs -GKE worker node components logs mainly from kubelet,containerd and dockerd. +Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes. -(WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter. - - - + + ### Parameters |Parameter name|Description| @@ -141,8 +137,8 @@ GKE worker node components logs mainly from kubelet,containerd and dockerd. |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -153,8 +149,8 @@ This feature can generates following timeline relationship of timelines. |![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container|A timline of a container included in the parent timeline of a Pod| |![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)[Node component timeline](./relationships.md#node-component-timeline)|node-component|A component running inside of the parent timeline of a Node| - - + + ### Target log type **![0077CC](https://placehold.co/15x15/0077CC/0077CC.png)k8s_node** @@ -169,8 +165,8 @@ resource.labels.node_name:("gke-test-cluster-node-1" OR "gke-test-cluster-node-2 ``` - - + + ### Inspection types This feature is supported in the following inspection types. @@ -181,16 +177,14 @@ This feature is supported in the following inspection types. * [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) * [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) * [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) - - + + ## Kubernetes container logs -Container logs ingested from stdout/stderr of workload Pods. - -(WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter or target namespace fewer. +Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. - - + + ### Parameters |Parameter name|Description| @@ -201,8 +195,8 @@ Container logs ingested from stdout/stderr of workload Pods. |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -211,8 +205,8 @@ This feature can generates following timeline relationship of timelines. |:-:|:-:|:-:| |![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)[Container timeline](./relationships.md#container-timeline)|container|A timline of a container included in the parent timeline of a Pod| - - + + ### Target log type **![fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)k8s_container** @@ -226,8 +220,8 @@ resource.labels.namespace_name=("default") -resource.labels.pod_name:("nginx-" OR "redis") ``` - - + + ### Inspection types This feature is supported in the following inspection types. @@ -238,14 +232,14 @@ This feature is supported in the following inspection types. * [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) * [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) * [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) - - + + ## GKE Audit logs -GKE audit log including cluster creation,deletion and upgrades. +Gather GKE audit log to show creation/upgrade/deletion of logs cluster/nodepool - - + + ### Parameters |Parameter name|Description| @@ -254,8 +248,8 @@ GKE audit log including cluster creation,deletion and upgrades. |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -265,8 +259,8 @@ This feature can generates following timeline relationship of timelines. |![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| |![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| - - + + ### Target log type **![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)gke_audit** @@ -279,22 +273,22 @@ logName="projects/gcp-project-id/logs/cloudaudit.googleapis.com%2Factivity" resource.labels.cluster_name="gcp-cluster-name" ``` - - + + ### Inspection types This feature is supported in the following inspection types. * [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) * [Cloud Composer](./inspection-type.md#cloud-composer) - - + + ## Compute API Logs -Compute API audit logs used for cluster related logs. This also visualize operations happened during the query time. +Gather Compute API audit logs to show the timings of the provisioning of resources(e.g creating/deleting GCE VM,mounting Persistent Disk...etc) on associated timelines. - - + + ### Parameters |Parameter name|Description| @@ -305,8 +299,8 @@ Compute API audit logs used for cluster related logs. This also visualize operat |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -316,8 +310,8 @@ This feature can generates following timeline relationship of timelines. |![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| |![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| - - + + ### Target log type **![FFCC33](https://placehold.co/15x15/FFCC33/FFCC33.png)compute_api** @@ -331,29 +325,29 @@ protoPayload.resourceName:(instances/gke-test-cluster-node-1 OR instances/gke-te ``` - - + + ### Dependent queries Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit - - + + ### Inspection types This feature is supported in the following inspection types. * [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) * [Cloud Composer](./inspection-type.md#cloud-composer) - - + + ## GCE Network Logs -GCE network API audit log including NEG related audit logs to identify when the associated NEG was attached/detached. +Gather GCE Network API logs to visualize statuses of Network Endpoint Groups(NEG) - - + + ### Parameters |Parameter name|Description| @@ -364,8 +358,8 @@ GCE network API audit log including NEG related audit logs to identify when the |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -375,8 +369,8 @@ This feature can generates following timeline relationship of timelines. |![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| |![A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)[Network Endpoint Group timeline](./relationships.md#network-endpoint-group-timeline)|neg|| - - + + ### Target log type **![33CCFF](https://placehold.co/15x15/33CCFF/33CCFF.png)network_api** @@ -390,29 +384,29 @@ protoPayload.resourceName:(networkEndpointGroups/neg-id-1 OR networkEndpointGrou ``` - - + + ### Dependent queries Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit - - + + ### Inspection types This feature is supported in the following inspection types. * [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) * [Cloud Composer](./inspection-type.md#cloud-composer) - - + + ## MultiCloud API logs -Anthos Multicloud audit log including cluster creation,deletion and upgrades. +Gather Anthos Multicloud audit log including cluster creation,deletion and upgrades. - - + + ### Parameters |Parameter name|Description| @@ -421,8 +415,8 @@ Anthos Multicloud audit log including cluster creation,deletion and upgrades. |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -431,8 +425,8 @@ This feature can generates following timeline relationship of timelines. |:-:|:-:|:-:| |![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| - - + + ### Target log type **![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)multicloud_api** @@ -447,23 +441,22 @@ protoPayload.resourceName:"awsClusters/cluster-foo" ``` - - + + ### Inspection types This feature is supported in the following inspection types. * [GKE on AWS(Anthos on AWS)](./inspection-type.md#gke-on-awsanthos-on-aws) * [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) - - + + ## Autoscaler Logs -Autoscaler logs including decision reasons why they scale up/down or why they didn't. -This log type also includes Node Auto Provisioner logs. +Gather logs related to cluster autoscaler behavior to show them on the timelines of resources related to the autoscaler decision. - - + + ### Parameters |Parameter name|Description| @@ -472,8 +465,8 @@ This log type also includes Node Auto Provisioner logs. |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -483,8 +476,8 @@ This feature can generates following timeline relationship of timelines. |![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| |![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Managed instance group timeline](./relationships.md#managed-instance-group-timeline)|mig|| - - + + ### Target log type **![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)autoscaler** @@ -499,22 +492,22 @@ resource.labels.cluster_name="gcp-cluster-name" logName="projects/gcp-project-id/logs/container.googleapis.com%2Fcluster-autoscaler-visibility" ``` - - + + ### Inspection types This feature is supported in the following inspection types. * [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) * [Cloud Composer](./inspection-type.md#cloud-composer) - - + + ## OnPrem API logs -Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades. +Gather Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades. - - + + ### Parameters |Parameter name|Description| @@ -523,8 +516,8 @@ Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -533,8 +526,8 @@ This feature can generates following timeline relationship of timelines. |:-:|:-:|:-:| |![000000](https://placehold.co/15x15/000000/000000.png)[Operation timeline](./relationships.md#operation-timeline)|operation|A timeline showing long running operation status related to the parent resource| - - + + ### Target log type **![AA00FF](https://placehold.co/15x15/AA00FF/AA00FF.png)onprem_api** @@ -549,22 +542,22 @@ protoPayload.resourceName:"baremetalClusters/my-cluster" ``` - - + + ### Inspection types This feature is supported in the following inspection types. * [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) * [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) - - + + ## Kubernetes Control plane component logs -Visualize Kubernetes control plane component logs on a cluster +Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs - - + + ### Parameters |Parameter name|Description| @@ -574,8 +567,8 @@ Visualize Kubernetes control plane component logs on a cluster |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -585,8 +578,8 @@ This feature can generates following timeline relationship of timelines. |![CCCCCC](https://placehold.co/15x15/CCCCCC/CCCCCC.png)[The default resource timeline](./relationships.md#the-default-resource-timeline)|resource|A default timeline recording the history of Kubernetes resources| |![FF5555](https://placehold.co/15x15/FF5555/FF5555.png)[Control plane component timeline](./relationships.md#control-plane-component-timeline)|controlplane|| - - + + ### Target log type **![FF3333](https://placehold.co/15x15/FF3333/FF3333.png)control_plane_component** @@ -601,8 +594,8 @@ resource.labels.project_id="gcp-project-id" -- No component name filter ``` - - + + ### Inspection types This feature is supported in the following inspection types. @@ -613,14 +606,14 @@ This feature is supported in the following inspection types. * [GKE on Azure(Anthos on Azure)](./inspection-type.md#gke-on-azureanthos-on-azure) * [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](./inspection-type.md#gdcv-for-baremetalgke-on-baremetal-anthos-on-baremetal) * [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](./inspection-type.md#gdcv-for-vmwaregke-on-vmware-anthos-on-vmware) - - + + ## Node serial port logs -Serial port logs of worker nodes. Serial port logging feature must be enabled on instances to query logs correctly. +Gather serialport logs of GKE nodes. This helps detailed investigation on VM bootstrapping issue on GKE node. - - + + ### Parameters |Parameter name|Description| @@ -632,8 +625,8 @@ Serial port logs of worker nodes. Serial port logging feature must be enabled on |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -642,8 +635,8 @@ This feature can generates following timeline relationship of timelines. |:-:|:-:|:-:| |![333333](https://placehold.co/15x15/333333/333333.png)[Serialport log timeline](./relationships.md#serialport-log-timeline)|serialport|| - - + + ### Target log type **![333333](https://placehold.co/15x15/333333/333333.png)serial_port** @@ -661,29 +654,29 @@ labels."compute.googleapis.com/resource_name"=("gke-test-cluster-node-1" OR "gke -- No node name substring filters are specified. ``` - - + + ### Dependent queries Following log queries are used with this feature. * ![000000](https://placehold.co/15x15/000000/000000.png)k8s_audit - - + + ### Inspection types This feature is supported in the following inspection types. * [Google Kubernetes Engine](./inspection-type.md#google-kubernetes-engine) * [Cloud Composer](./inspection-type.md#cloud-composer) - - + + ## (Alpha) Composer / Airflow Scheduler Airflow Scheduler logs contain information related to the scheduling of TaskInstances, making it an ideal source for understanding the lifecycle of TaskInstances. - - + + ### Parameters |Parameter name|Description| @@ -693,8 +686,8 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst |[Composer Environment Name](./forms.md#composer-environment-name)|| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -702,8 +695,8 @@ This feature can generates following timeline relationship of timelines. |Timeline relationships|Short name on chip|Description| |:-:|:-:|:-:| - - + + ### Target log type **![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment** @@ -714,21 +707,21 @@ Sample query: TODO: add sample query ``` - - + + ### Inspection types This feature is supported in the following inspection types. * [Cloud Composer](./inspection-type.md#cloud-composer) - - + + ## (Alpha) Cloud Composer / Airflow Worker Airflow Worker logs contain information related to the execution of TaskInstances. By including these logs, you can gain insights into where and how each TaskInstance was executed. - - + + ### Parameters |Parameter name|Description| @@ -738,8 +731,8 @@ Airflow Worker logs contain information related to the execution of TaskInstance |[Composer Environment Name](./forms.md#composer-environment-name)|| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -747,8 +740,8 @@ This feature can generates following timeline relationship of timelines. |Timeline relationships|Short name on chip|Description| |:-:|:-:|:-:| - - + + ### Target log type **![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment** @@ -759,21 +752,21 @@ Sample query: TODO: add sample query ``` - - + + ### Inspection types This feature is supported in the following inspection types. * [Cloud Composer](./inspection-type.md#cloud-composer) - - + + ## (Alpha) Composer / Airflow DagProcessorManager The DagProcessorManager logs contain information for investigating the number of DAGs included in each Python file and the time it took to parse them. You can get information about missing DAGs and load. - - + + ### Parameters |Parameter name|Description| @@ -783,8 +776,8 @@ The DagProcessorManager logs contain information for investigating the number of |[Composer Environment Name](./forms.md#composer-environment-name)|| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| - - + + ### Output timelines This feature can generates following timeline relationship of timelines. @@ -792,8 +785,8 @@ This feature can generates following timeline relationship of timelines. |Timeline relationships|Short name on chip|Description| |:-:|:-:|:-:| - - + + ### Target log type **![88AA55](https://placehold.co/15x15/88AA55/88AA55.png)composer_environment** @@ -804,11 +797,11 @@ Sample query: TODO: add sample query ``` - - + + ### Inspection types This feature is supported in the following inspection types. * [Cloud Composer](./inspection-type.md#cloud-composer) - + diff --git a/docs/en/reference/inspection-type.md b/docs/en/reference/inspection-type.md index 2b87827..c537282 100644 --- a/docs/en/reference/inspection-type.md +++ b/docs/en/reference/inspection-type.md @@ -12,101 +12,107 @@ KHI filters out unsupported parser for the selected inspection type at first. ## [Google Kubernetes Engine](#gcp-gke) -### Features - +### Features -* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) -* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) -* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) -* [Kubernetes container logs](./features.md#kubernetes-container-logs) -* [GKE Audit logs](./features.md#gke-audit-logs) -* [Compute API Logs](./features.md#compute-api-logs) -* [GCE Network Logs](./features.md#gce-network-logs) -* [Autoscaler Logs](./features.md#autoscaler-logs) -* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) -* [Node serial port logs](./features.md#node-serial-port-logs) +| Feature task name | Description | +| --- | --- | +|[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| +|[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| +|[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[GKE Audit logs](./features.md#gke-audit-logs)|Gather GKE audit log to show creation/upgrade/deletion of logs cluster/nodepool| +|[Compute API Logs](./features.md#compute-api-logs)|Gather Compute API audit logs to show the timings of the provisioning of resources(e.g creating/deleting GCE VM,mounting Persistent Disk...etc) on associated timelines.| +|[GCE Network Logs](./features.md#gce-network-logs)|Gather GCE Network API logs to visualize statuses of Network Endpoint Groups(NEG)| +|[Autoscaler Logs](./features.md#autoscaler-logs)|Gather logs related to cluster autoscaler behavior to show them on the timelines of resources related to the autoscaler decision.| +|[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| +|[Node serial port logs](./features.md#node-serial-port-logs)|Gather serialport logs of GKE nodes. This helps detailed investigation on VM bootstrapping issue on GKE node.| ## [Cloud Composer](#gcp-composer) -### Features - +### Features -* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) -* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) -* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) -* [Kubernetes container logs](./features.md#kubernetes-container-logs) -* [GKE Audit logs](./features.md#gke-audit-logs) -* [Compute API Logs](./features.md#compute-api-logs) -* [GCE Network Logs](./features.md#gce-network-logs) -* [Autoscaler Logs](./features.md#autoscaler-logs) -* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) -* [Node serial port logs](./features.md#node-serial-port-logs) -* [(Alpha) Composer / Airflow Scheduler](./features.md#alpha-composer--airflow-scheduler) -* [(Alpha) Cloud Composer / Airflow Worker](./features.md#alpha-cloud-composer--airflow-worker) -* [(Alpha) Composer / Airflow DagProcessorManager](./features.md#alpha-composer--airflow-dagprocessormanager) +| Feature task name | Description | +| --- | --- | +|[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| +|[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| +|[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[GKE Audit logs](./features.md#gke-audit-logs)|Gather GKE audit log to show creation/upgrade/deletion of logs cluster/nodepool| +|[Compute API Logs](./features.md#compute-api-logs)|Gather Compute API audit logs to show the timings of the provisioning of resources(e.g creating/deleting GCE VM,mounting Persistent Disk...etc) on associated timelines.| +|[GCE Network Logs](./features.md#gce-network-logs)|Gather GCE Network API logs to visualize statuses of Network Endpoint Groups(NEG)| +|[Autoscaler Logs](./features.md#autoscaler-logs)|Gather logs related to cluster autoscaler behavior to show them on the timelines of resources related to the autoscaler decision.| +|[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| +|[Node serial port logs](./features.md#node-serial-port-logs)|Gather serialport logs of GKE nodes. This helps detailed investigation on VM bootstrapping issue on GKE node.| +|[(Alpha) Composer / Airflow Scheduler](./features.md#alpha-composer--airflow-scheduler)|Airflow Scheduler logs contain information related to the scheduling of TaskInstances, making it an ideal source for understanding the lifecycle of TaskInstances.| +|[(Alpha) Cloud Composer / Airflow Worker](./features.md#alpha-cloud-composer--airflow-worker)|Airflow Worker logs contain information related to the execution of TaskInstances. By including these logs, you can gain insights into where and how each TaskInstance was executed.| +|[(Alpha) Composer / Airflow DagProcessorManager](./features.md#alpha-composer--airflow-dagprocessormanager)|The DagProcessorManager logs contain information for investigating the number of DAGs included in each Python file and the time it took to parse them. You can get information about missing DAGs and load.| ## [GKE on AWS(Anthos on AWS)](#gcp-gke-on-aws) -### Features - +### Features -* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) -* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) -* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) -* [Kubernetes container logs](./features.md#kubernetes-container-logs) -* [MultiCloud API logs](./features.md#multicloud-api-logs) -* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +| Feature task name | Description | +| --- | --- | +|[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| +|[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| +|[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[MultiCloud API logs](./features.md#multicloud-api-logs)|Gather Anthos Multicloud audit log including cluster creation,deletion and upgrades.| +|[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| ## [GKE on Azure(Anthos on Azure)](#gcp-gke-on-azure) -### Features - +### Features -* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) -* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) -* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) -* [Kubernetes container logs](./features.md#kubernetes-container-logs) -* [MultiCloud API logs](./features.md#multicloud-api-logs) -* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +| Feature task name | Description | +| --- | --- | +|[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| +|[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| +|[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[MultiCloud API logs](./features.md#multicloud-api-logs)|Gather Anthos Multicloud audit log including cluster creation,deletion and upgrades.| +|[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| ## [GDCV for Baremetal(GKE on Baremetal, Anthos on Baremetal)](#gcp-gdcv-for-baremetal) -### Features - +### Features -* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) -* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) -* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) -* [Kubernetes container logs](./features.md#kubernetes-container-logs) -* [OnPrem API logs](./features.md#onprem-api-logs) -* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +| Feature task name | Description | +| --- | --- | +|[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| +|[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| +|[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[OnPrem API logs](./features.md#onprem-api-logs)|Gather Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades.| +|[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| ## [GDCV for VMWare(GKE on VMWare, Anthos on VMWare)](#gcp-gdcv-for-vmware) -### Features - +### Features -* [Kubernetes Audit Log](./features.md#kubernetes-audit-log) -* [Kubernetes Event Logs](./features.md#kubernetes-event-logs) -* [Kubernetes Node Logs](./features.md#kubernetes-node-logs) -* [Kubernetes container logs](./features.md#kubernetes-container-logs) -* [OnPrem API logs](./features.md#onprem-api-logs) -* [Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs) +| Feature task name | Description | +| --- | --- | +|[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| +|[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| +|[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[OnPrem API logs](./features.md#onprem-api-logs)|Gather Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades.| +|[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| diff --git a/docs/template/reference/inspection-type.template.md b/docs/template/reference/inspection-type.template.md index fef3445..f73cc3a 100644 --- a/docs/template/reference/inspection-type.template.md +++ b/docs/template/reference/inspection-type.template.md @@ -3,14 +3,18 @@ ## [{{$type.Name}}](#{{$type.ID}}) -### Features - +{{with $type.SupportedFeatures}} -{{range $feature := $type.SupportedFeatures}} -* [{{$feature.Name}}](./features.md#{{$feature.Name | anchor }}) +### Features + +| Feature task name | Description | +| --- | --- | +{{- range $feature := $type.SupportedFeatures}} +|[{{$feature.Name}}](./features.md#{{$feature.Name | anchor }})|{{$feature.Description}}| {{- end}} {{end}} +{{end}} {{end}} \ No newline at end of file diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index 0f36442..37999c7 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -141,7 +141,7 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat } result.Features = append(result.Features, FeatureDocumentElement{ - ID: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string), + ID: feature.ID().String(), Name: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), Description: feature.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), IndirectQueryDependency: indirectQueryDependencyElement, diff --git a/pkg/document/model/inspection_type.go b/pkg/document/model/inspection_type.go index 0d19d88..4f6f3be 100644 --- a/pkg/document/model/inspection_type.go +++ b/pkg/document/model/inspection_type.go @@ -1,28 +1,34 @@ package model import ( - "strings" - "github.com/GoogleCloudPlatform/khi/pkg/inspection" inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" "github.com/GoogleCloudPlatform/khi/pkg/inspection/taskfilter" ) -// InspectionTypeDocumentModel is a document model type for generating docs/en/inspection-type.md +// InspectionTypeDocumentModel is a model type for generating document docs/en/reference/inspection-type.md type InspectionTypeDocumentModel struct { + // InspectionTypes are the list of InspectionType defind in KHI. InspectionTypes []InspectionTypeDocumentElement } +// InspectionTypeDocumentElement is a model for a InspectionType used in InspectionTypeDocumentModel. type InspectionTypeDocumentElement struct { - ID string + // ID is the unique name of the InspectionType. + ID string + // Name is the human readable name of the InspectionType. Name string - + // SupportedFeatures is the list of the feature tasks usable for this InspectionType. SupportedFeatures []InspectionTypeDocumentElementFeature } +// InspectionTypeDocumentElementFeature is a model for a feature task used for generatng the list of supported features of a InspectionType. type InspectionTypeDocumentElementFeature struct { - ID string - Name string + // ID is the unique name of the feature task. + ID string + // Name is the human readable name of the feature task. + Name string + // Description is the string exlains the feature task. Description string } @@ -31,18 +37,21 @@ func GetInspectionTypeDocumentModel(taskServer *inspection.InspectionTaskServer) result := InspectionTypeDocumentModel{} inspectionTypes := taskServer.GetAllInspectionTypes() for _, inspectionType := range inspectionTypes { + // Get the list of feature tasks supporting the inspection type. tasks := taskServer.RootTaskSet. FilteredSubset(inspection_task.LabelKeyInspectionTypes, taskfilter.ContainsElement(inspectionType.Id), true). FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false). GetAll() + features := []InspectionTypeDocumentElementFeature{} for _, task := range tasks { features = append(features, InspectionTypeDocumentElementFeature{ - ID: strings.ToLower(task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureDocumentAnchorID, "").(string)), + ID: task.ID().String(), Name: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskTitle, "").(string), Description: task.Labels().GetOrDefault(inspection_task.LabelKeyFeatureTaskDescription, "").(string), }) } + result.InspectionTypes = append(result.InspectionTypes, InspectionTypeDocumentElement{ ID: inspectionType.Id, Name: inspectionType.Name, diff --git a/pkg/inspection/task/label.go b/pkg/inspection/task/label.go index 0d6b031..1b89118 100644 --- a/pkg/inspection/task/label.go +++ b/pkg/inspection/task/label.go @@ -31,8 +31,6 @@ const ( LabelKeyInspectionTypes = InspectionTaskPrefix + "inspection-type" LabelKeyFeatureTaskTitle = InspectionTaskPrefix + "feature/title" LabelKeyFeatureTaskTargetLogType = InspectionTaskPrefix + "feature/log-type" - // LabelKeyFeatureDocumentAnchorID is a key of label for a short length task ID assigned to feature tasks. This is used in link anchors in documents. - LabelKeyFeatureDocumentAnchorID = InspectionTaskPrefix + "feature/short-title" LabelKeyFeatureTaskDescription = InspectionTaskPrefix + "feature/description" @@ -54,7 +52,6 @@ var _ common_task.LabelOpt = (*ProgressReportableTaskLabelOptImpl)(nil) // FeatureTaskLabelImpl is an implementation of task.LabelOpt. // This annotate a task definition to be a feature in inspection. type FeatureTaskLabelImpl struct { - documentAnchorID string title string description string logType enum.LogType @@ -63,7 +60,6 @@ type FeatureTaskLabelImpl struct { func (ftl *FeatureTaskLabelImpl) Write(label *common_task.LabelSet) { label.Set(LabelKeyInspectionFeatureFlag, true) - label.Set(LabelKeyFeatureDocumentAnchorID, ftl.documentAnchorID) label.Set(LabelKeyFeatureTaskTargetLogType, ftl.logType) label.Set(LabelKeyFeatureTaskTitle, ftl.title) label.Set(LabelKeyFeatureTaskDescription, ftl.description) @@ -77,10 +73,9 @@ func (ftl *FeatureTaskLabelImpl) WithDescription(description string) *FeatureTas var _ common_task.LabelOpt = (*FeatureTaskLabelImpl)(nil) -func FeatureTaskLabel(documentAnchorID string, title string, description string, logType enum.LogType, isDefaultFeature bool) *FeatureTaskLabelImpl { +func FeatureTaskLabel(title string, description string, logType enum.LogType, isDefaultFeature bool) *FeatureTaskLabelImpl { return &FeatureTaskLabelImpl{ title: title, - documentAnchorID: documentAnchorID, description: description, logType: logType, isDefaultFeature: isDefaultFeature, diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index ecfc156..07cf30d 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -39,9 +39,6 @@ type Parser interface { // GetParserName Returns it's own parser name. It must be unique by each instances. GetParserName() string - // GetDocumentAnchorID returns a unique ID within feature tasks. This is used as the link anchor ID in document. - GetDocumentAnchorID() string - // TargetLogType returns the log type which this parser should mainly parse and generate revisions or events for. TargetLogType() enum.LogType @@ -178,6 +175,6 @@ func NewParserTaskFromParser(taskId string, parser Parser, isDefaultFeature bool return struct{}{}, nil }, append([]task.LabelOpt{ - inspection_task.FeatureTaskLabel(parser.GetDocumentAnchorID(), parser.GetParserName(), parser.Description(), parser.TargetLogType(), isDefaultFeature), + inspection_task.FeatureTaskLabel(parser.GetParserName(), parser.Description(), parser.TargetLogType(), isDefaultFeature), }, labelOpts...)...) } diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 46f062d..98fa7ef 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -112,16 +112,16 @@ func createTestInspectionServer() (*inspection.InspectionTaskServer, error) { form.NewInputFormDefinitionBuilder("bar-input", 1, "A input field for bar").Build(inspection_task.InspectionTypeLabel("bar")), inspection_task.NewInspectionProcessor("feature-foo1", []string{"foo-input"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-foo1-value", nil - }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo1", "foo feature1", "test-feature", enum.LogTypeAudit, false)), + }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo feature1", "test-feature", enum.LogTypeAudit, false)), inspection_task.NewInspectionProcessor("feature-foo2", []string{"foo-input"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-foo2-value", nil - }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo2", "foo feature2", "test-feature", enum.LogTypeAudit, false)), + }, inspection_task.InspectionTypeLabel("foo"), inspection_task.FeatureTaskLabel("foo feature2", "test-feature", enum.LogTypeAudit, false)), inspection_task.NewInspectionProcessor("feature-bar", []string{"bar-input", "neverend"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-bar1-value", nil - }, inspection_task.InspectionTypeLabel("bar"), inspection_task.FeatureTaskLabel("bar", "bar feature1", "test-feature", enum.LogTypeAudit, false)), + }, inspection_task.InspectionTypeLabel("bar"), inspection_task.FeatureTaskLabel("bar feature1", "test-feature", enum.LogTypeAudit, false)), inspection_task.NewInspectionProcessor("feature-qux", []string{"errorend"}, func(ctx context.Context, taskMode int, v *task.VariableSet, tp *progress.TaskProgress) (any, error) { return "feature-bar1-value", nil - }, inspection_task.InspectionTypeLabel("qux"), inspection_task.FeatureTaskLabel("qux", "qux feature1", "test-feature", enum.LogTypeAudit, false)), + }, inspection_task.InspectionTypeLabel("qux"), inspection_task.FeatureTaskLabel("qux feature1", "test-feature", enum.LogTypeAudit, false)), ioconfig.TestIOConfig, } diff --git a/pkg/source/gcp/task/cloud-composer/parser.go b/pkg/source/gcp/task/cloud-composer/parser.go index d1c7dec..5ef1c19 100644 --- a/pkg/source/gcp/task/cloud-composer/parser.go +++ b/pkg/source/gcp/task/cloud-composer/parser.go @@ -96,11 +96,6 @@ func (t *AirflowSchedulerParser) TargetLogType() enum.LogType { return enum.LogTypeComposerEnvironment } -// GetDocumentAnchorID implements parser.Parser. -func (t *AirflowSchedulerParser) GetDocumentAnchorID() string { - return "airflow_schedule" -} - var _ parser.Parser = &AirflowSchedulerParser{} func (*AirflowSchedulerParser) Dependencies() []string { @@ -231,11 +226,6 @@ func (a *AirflowWorkerParser) TargetLogType() enum.LogType { return enum.LogTypeComposerEnvironment } -// GetDocumentAnchorID implements parser.Parser. -func (a *AirflowWorkerParser) GetDocumentAnchorID() string { - return "airflow_worker" -} - // Dependencies implements parser.Parser. func (*AirflowWorkerParser) Dependencies() []string { return []string{ComposerWorkerLogQueryTaskName} @@ -387,11 +377,6 @@ func (a *AirflowDagProcessorParser) TargetLogType() enum.LogType { return enum.LogTypeComposerEnvironment } -// GetDocumentAnchorID implements parser.Parser. -func (a *AirflowDagProcessorParser) GetDocumentAnchorID() string { - return "airflow_dag_processor" -} - var _ parser.Parser = (*AirflowDagProcessorParser)(nil) func (*AirflowDagProcessorParser) Dependencies() []string { diff --git a/pkg/source/gcp/task/gke/autoscaler/parser.go b/pkg/source/gcp/task/gke/autoscaler/parser.go index 4d9e22f..742133d 100644 --- a/pkg/source/gcp/task/gke/autoscaler/parser.go +++ b/pkg/source/gcp/task/gke/autoscaler/parser.go @@ -41,11 +41,6 @@ func (p *autoscalerLogParser) TargetLogType() enum.LogType { return enum.LogTypeAutoscaler } -// GetDocumentAnchorID implements parser.Parser. -func (p *autoscalerLogParser) GetDocumentAnchorID() string { - return "autoscaler" -} - // Dependencies implements parser.Parser. func (*autoscalerLogParser) Dependencies() []string { return []string{ @@ -55,8 +50,7 @@ func (*autoscalerLogParser) Dependencies() []string { // Description implements parser.Parser. func (*autoscalerLogParser) Description() string { - return `Autoscaler logs including decision reasons why they scale up/down or why they didn't. -This log type also includes Node Auto Provisioner logs.` + return `Gather logs related to cluster autoscaler behavior to show them on the timelines of resources related to the autoscaler decision.` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/gke/compute_api/parser.go b/pkg/source/gcp/task/gke/compute_api/parser.go index 7b5edd2..0896e49 100644 --- a/pkg/source/gcp/task/gke/compute_api/parser.go +++ b/pkg/source/gcp/task/gke/compute_api/parser.go @@ -40,11 +40,6 @@ func (c *computeAPIParser) TargetLogType() enum.LogType { return enum.LogTypeComputeApi } -// GetDocumentAnchorID implements parser.Parser. -func (c *computeAPIParser) GetDocumentAnchorID() string { - return "compute_api" -} - // Dependencies implements parser.Parser. func (*computeAPIParser) Dependencies() []string { return []string{} @@ -52,7 +47,7 @@ func (*computeAPIParser) Dependencies() []string { // Description implements parser.Parser. func (*computeAPIParser) Description() string { - return `Compute API audit logs used for cluster related logs. This also visualize operations happened during the query time.` + return `Gather Compute API audit logs to show the timings of the provisioning of resources(e.g creating/deleting GCE VM,mounting Persistent Disk...etc) on associated timelines.` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/gke/gke_audit/parser.go b/pkg/source/gcp/task/gke/gke_audit/parser.go index 5bb0930..3320877 100644 --- a/pkg/source/gcp/task/gke/gke_audit/parser.go +++ b/pkg/source/gcp/task/gke/gke_audit/parser.go @@ -40,11 +40,6 @@ func (p *gkeAuditLogParser) TargetLogType() enum.LogType { return enum.LogTypeGkeAudit } -// GetDocumentAnchorID implements parser.Parser. -func (p *gkeAuditLogParser) GetDocumentAnchorID() string { - return "gke_audit" -} - // Dependencies implements parser.Parser. func (*gkeAuditLogParser) Dependencies() []string { return []string{} @@ -52,7 +47,7 @@ func (*gkeAuditLogParser) Dependencies() []string { // Description implements parser.Parser. func (*gkeAuditLogParser) Description() string { - return `GKE audit log including cluster creation,deletion and upgrades.` + return `Gather GKE audit log to show creation/upgrade/deletion of logs cluster/nodepool` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go b/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go index d87c32a..37a7487 100644 --- a/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go +++ b/pkg/source/gcp/task/gke/k8s_audit/recorder/task.go @@ -128,8 +128,7 @@ func (r *RecorderTaskManager) Register(server *inspection.InspectionTaskServer) } waiterTask := inspection_task.NewInspectionProcessor(fmt.Sprintf("%s/feature/audit-parser-v2", gcp_task.GCPPrefix), recorderTaskIds, func(ctx context.Context, taskMode int, v *task.VariableSet, progress *progress.TaskProgress) (any, error) { return struct{}{}, nil - }, inspection_task.FeatureTaskLabel("k8s_audit", "Kubernetes Audit Log", `Visualize Kubernetes audit logs in GKE. -This parser reveals how these resources are created,updated or deleted. `, enum.LogTypeAudit, true)) + }, inspection_task.FeatureTaskLabel("Kubernetes Audit Log", `Gather kubernetes audit logs and visualize resource modifications.`, enum.LogTypeAudit, true)) err := server.AddTaskDefinition(waiterTask) return err } diff --git a/pkg/source/gcp/task/gke/k8s_container/parser.go b/pkg/source/gcp/task/gke/k8s_container/parser.go index 55f2295..ea4b438 100644 --- a/pkg/source/gcp/task/gke/k8s_container/parser.go +++ b/pkg/source/gcp/task/gke/k8s_container/parser.go @@ -37,16 +37,9 @@ func (k *k8sContainerParser) TargetLogType() enum.LogType { return enum.LogTypeContainer } -// GetDocumentAnchorID implements parser.Parser. -func (k *k8sContainerParser) GetDocumentAnchorID() string { - return "k8s_container" -} - // Description implements parser.Parser. func (*k8sContainerParser) Description() string { - return `Container logs ingested from stdout/stderr of workload Pods. - -(WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter or target namespace fewer.` + return `Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go b/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go index 26ea83a..aab0c96 100644 --- a/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go +++ b/pkg/source/gcp/task/gke/k8s_control_plane_component/parser.go @@ -35,11 +35,6 @@ func (k *k8sControlPlaneComponentParser) TargetLogType() enum.LogType { return enum.LogTypeControlPlaneComponent } -// GetDocumentAnchorID implements parser.Parser. -func (k *k8sControlPlaneComponentParser) GetDocumentAnchorID() string { - return "k8s_control_plane_component" -} - // Dependencies implements parser.Parser. func (k *k8sControlPlaneComponentParser) Dependencies() []string { return []string{} @@ -47,7 +42,7 @@ func (k *k8sControlPlaneComponentParser) Dependencies() []string { // Description implements parser.Parser. func (k *k8sControlPlaneComponentParser) Description() string { - return `Visualize Kubernetes control plane component logs on a cluster` + return `Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/gke/k8s_event/parser.go b/pkg/source/gcp/task/gke/k8s_event/parser.go index 75ed3ba..b60a8da 100644 --- a/pkg/source/gcp/task/gke/k8s_event/parser.go +++ b/pkg/source/gcp/task/gke/k8s_event/parser.go @@ -39,15 +39,9 @@ func (k *k8sEventParser) TargetLogType() enum.LogType { return enum.LogTypeEvent } -// GetDocumentAnchorID implements parser.Parser. -func (k *k8sEventParser) GetDocumentAnchorID() string { - return "k8s_event" -} - // Description implements parser.Parser. func (*k8sEventParser) Description() string { - return `Visualize Kubernetes event logs on GKE. -This parser shows events associated to K8s resources` + return `Gather kubernetes event logs and visualize these on the associated resource timeline.` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/gke/k8s_node/parser.go b/pkg/source/gcp/task/gke/k8s_node/parser.go index 99b5278..2351c6c 100644 --- a/pkg/source/gcp/task/gke/k8s_node/parser.go +++ b/pkg/source/gcp/task/gke/k8s_node/parser.go @@ -52,16 +52,9 @@ func (p *k8sNodeParser) TargetLogType() enum.LogType { return enum.LogTypeNode } -// GetDocumentAnchorID implements parser.Parser. -func (p *k8sNodeParser) GetDocumentAnchorID() string { - return "k8s_node" -} - // Description implements parser.Parser. func (*k8sNodeParser) Description() string { - return `GKE worker node components logs mainly from kubelet,containerd and dockerd. - -(WARNING)Log volume could be very large for long query duration or big cluster and can lead OOM. Please limit time range shorter.` + return `Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/gke/network_api/parser.go b/pkg/source/gcp/task/gke/network_api/parser.go index 66e1c5d..62d9baa 100644 --- a/pkg/source/gcp/task/gke/network_api/parser.go +++ b/pkg/source/gcp/task/gke/network_api/parser.go @@ -41,11 +41,6 @@ func (g *gceNetworkParser) TargetLogType() enum.LogType { return enum.LogTypeNetworkAPI } -// GetDocumentAnchorID implements parser.Parser. -func (g *gceNetworkParser) GetDocumentAnchorID() string { - return "gce_network" -} - // Dependencies implements parser.Parser. func (*gceNetworkParser) Dependencies() []string { return []string{} @@ -53,7 +48,7 @@ func (*gceNetworkParser) Dependencies() []string { // Description implements parser.Parser. func (*gceNetworkParser) Description() string { - return `GCE network API audit log including NEG related audit logs to identify when the associated NEG was attached/detached.` + return `Gather GCE Network API logs to visualize statuses of Network Endpoint Groups(NEG)` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/gke/serialport/parser.go b/pkg/source/gcp/task/gke/serialport/parser.go index c645c44..3de22b6 100644 --- a/pkg/source/gcp/task/gke/serialport/parser.go +++ b/pkg/source/gcp/task/gke/serialport/parser.go @@ -50,14 +50,9 @@ func (s *SerialPortLogParser) TargetLogType() enum.LogType { return enum.LogTypeSerialPort } -// GetDocumentAnchorID implements parser.Parser. -func (s *SerialPortLogParser) GetDocumentAnchorID() string { - return "serialport" -} - // Description implements parser.Parser. func (*SerialPortLogParser) Description() string { - return `Serial port logs of worker nodes. Serial port logging feature must be enabled on instances to query logs correctly.` + return `Gather serialport logs of GKE nodes. This helps detailed investigation on VM bootstrapping issue on GKE node.` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/multicloud_api/parser.go b/pkg/source/gcp/task/multicloud_api/parser.go index f4f114d..c7abc28 100644 --- a/pkg/source/gcp/task/multicloud_api/parser.go +++ b/pkg/source/gcp/task/multicloud_api/parser.go @@ -41,11 +41,6 @@ func (m *multiCloudAuditLogParser) TargetLogType() enum.LogType { return enum.LogTypeMulticloudAPI } -// GetDocumentAnchorID implements parser.Parser. -func (m *multiCloudAuditLogParser) GetDocumentAnchorID() string { - return "multicloud_api" -} - // Dependencies implements parser.Parser. func (*multiCloudAuditLogParser) Dependencies() []string { return []string{} @@ -53,7 +48,7 @@ func (*multiCloudAuditLogParser) Dependencies() []string { // Description implements parser.Parser. func (*multiCloudAuditLogParser) Description() string { - return `Anthos Multicloud audit log including cluster creation,deletion and upgrades.` + return `Gather Anthos Multicloud audit log including cluster creation,deletion and upgrades.` } // GetParserName implements parser.Parser. diff --git a/pkg/source/gcp/task/onprem_api/parser.go b/pkg/source/gcp/task/onprem_api/parser.go index 69014ea..9e883f9 100644 --- a/pkg/source/gcp/task/onprem_api/parser.go +++ b/pkg/source/gcp/task/onprem_api/parser.go @@ -41,11 +41,6 @@ func (o *onpremCloudAuditLogParser) TargetLogType() enum.LogType { return enum.LogTypeOnPremAPI } -// GetDocumentAnchorID implements parser.Parser. -func (o *onpremCloudAuditLogParser) GetDocumentAnchorID() string { - return "onprem_api" -} - // Dependencies implements parser.Parser. func (*onpremCloudAuditLogParser) Dependencies() []string { return []string{} @@ -53,7 +48,7 @@ func (*onpremCloudAuditLogParser) Dependencies() []string { // Description implements parser.Parser. func (*onpremCloudAuditLogParser) Description() string { - return `Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades.` + return `Gather Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades.` } // GetParserName implements parser.Parser. From f9a203a695971d8a74eff384db411bed055571d6 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Tue, 11 Feb 2025 16:45:10 +0900 Subject: [PATCH 19/23] Added comment on the type of document model for feature tasks --- pkg/document/model/feature.go | 81 +++++++++++++------ pkg/document/model/inspection_type.go | 6 +- .../gcp/task/gke/k8s_container/parser.go | 2 +- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index 37999c7..fdb9484 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -12,54 +12,87 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/task" ) +// FeatureDocumentModel is a model type for generating document docs/en/reference/features.md type FeatureDocumentModel struct { + // Features are the list of feature tasks defined in KHI. Features []FeatureDocumentElement } +// FeatureDocumentElement is a model type for a feature task used in FeatureDocumentModel. type FeatureDocumentElement struct { - ID string - Name string + // ID is the unique name of the feature task. + ID string + // Name is the human readable name of the feature task. + Name string + // Description is the string explain the feature task. Description string - - IndirectQueryDependency []FeatureIndirectDependentQueryElement - TargetQueryDependency FeatureDependentTargetQueryElement - Forms []FeatureDependentFormElement - OutputTimelines []FeatureOutputTimelineElement + // Forms is the list of information about form inputs that is required from the feature task. + Forms []FeatureDependentFormElement + // IndirectQueryDependency is the list of query tasks that is required from this feature task but not the target query task. + IndirectQueryDependency []FeatureIndirectDependentQueryElement + // TargetQUeryDependency is the main query task used in this feature task. + TargetQueryDependency FeatureDependentTargetQueryElement + // OutputTimelines is the list of timelines(=ParentRelationship type) that can be generated by this feature task. + OutputTimelines []FeatureOutputTimelineElement + // AvailableInspctionType is the list of InspectionType that supports this feature task. AvailableInspectionTypes []FeatureAvailableInspectionType } +// FeatureDependentFormElement is a model type for a input form required from a feature task. +type FeatureDependentFormElement struct { + // ID is the unique name of this form element. + ID string + // Label is a human readable short name of this input element. + Label string + // Description is a string explaining this form input. + Description string +} + +// FeatureIndirectDependentQueryElement is a model type for query tasks required from a feature task but not the target query task. type FeatureIndirectDependentQueryElement struct { - ID string - LogTypeLabel string + // ID is the unique name of this query task. + ID string + // LogTypeLabel is a human readable short name of the log type queried by the query task. + LogTypeLabel string + // LogTypeColorCode is the hex color code without the `#` prefix for the log type. LogTypeColorCode string } +// FeatureDependentTargetQueryElement is a model type for a target query task of the feature task. type FeatureDependentTargetQueryElement struct { - ID string - LogTypeLabel string + // ID is the unique name of this query task. + ID string + // LogTypeLabel is a human readable short name of the log type queried by the query task. + LogTypeLabel string + // LogTypeColorCode is the hex color code without the `#` prefix for the log type. LogTypeColorCode string - SampleQuery string -} - -type FeatureDependentFormElement struct { - ID string - Label string - Description string + // SampleQuery is an example query string used in this query task. + SampleQuery string } +// FeatureOutputTimelineElement is a model type for one of relationship type of timelines that can be related to this feature. type FeatureOutputTimelineElement struct { - RelationshipID string + // RelationshipID is the unique name of the relationship type. + RelationshipID string + // RelationshipColorCode is the hex color code without the `#` prefix for the relationship type. RelationshipColorCode string - LongName string - Label string - Description string + // LongName is the human readable name of the relationship + LongName string + // Label is the short name of the timeline. This is also used in the chip on the left side of timelines. + Label string + // Description is the string explains the relationship. + Description string } +// FeatureAvailableInspectionType is a model type for a InspectionType that supports the feature task. type FeatureAvailableInspectionType struct { - ID string + // ID is the unique name of the InspectionType. + ID string + // Name is the human readable name of the InspectionType. Name string } +// GetFeatureDocumentModel returns the document model for feature tasks from the task server. func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*FeatureDocumentModel, error) { result := FeatureDocumentModel{} features := taskServer.RootTaskSet.FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false) @@ -155,6 +188,7 @@ func GetFeatureDocumentModel(taskServer *inspection.InspectionTaskServer) (*Feat return &result, nil } +// getDependentQueryTasks returns the list of query tasks required by the feature task. func getDependentQueryTasks(taskServer *inspection.InspectionTaskServer, featureTask task.Definition) ([]task.Definition, error) { resolveSource, err := task.NewSet([]task.Definition{featureTask}) if err != nil { @@ -167,6 +201,7 @@ func getDependentQueryTasks(taskServer *inspection.InspectionTaskServer, feature return resolved.FilteredSubset(label.TaskLabelKeyIsQueryTask, taskfilter.HasTrue, false).GetAll(), nil } +// getDependentFormTasks returns the list of form tasks required by the feature task. func getDependentFormTasks(taskServer *inspection.InspectionTaskServer, featureTask task.Definition) ([]task.Definition, error) { resolveSource, err := task.NewSet([]task.Definition{featureTask}) if err != nil { diff --git a/pkg/document/model/inspection_type.go b/pkg/document/model/inspection_type.go index 4f6f3be..8cdd31b 100644 --- a/pkg/document/model/inspection_type.go +++ b/pkg/document/model/inspection_type.go @@ -12,7 +12,7 @@ type InspectionTypeDocumentModel struct { InspectionTypes []InspectionTypeDocumentElement } -// InspectionTypeDocumentElement is a model for a InspectionType used in InspectionTypeDocumentModel. +// InspectionTypeDocumentElement is a model type for a InspectionType used in InspectionTypeDocumentModel. type InspectionTypeDocumentElement struct { // ID is the unique name of the InspectionType. ID string @@ -22,7 +22,7 @@ type InspectionTypeDocumentElement struct { SupportedFeatures []InspectionTypeDocumentElementFeature } -// InspectionTypeDocumentElementFeature is a model for a feature task used for generatng the list of supported features of a InspectionType. +// InspectionTypeDocumentElementFeature is a model type for a feature task used for generatng the list of supported features of a InspectionType. type InspectionTypeDocumentElementFeature struct { // ID is the unique name of the feature task. ID string @@ -32,7 +32,7 @@ type InspectionTypeDocumentElementFeature struct { Description string } -// GetInspectionTypeDocumentModel returns the document model from task server. +// GetInspectionTypeDocumentModel returns the document model for inspection types from task server. func GetInspectionTypeDocumentModel(taskServer *inspection.InspectionTaskServer) InspectionTypeDocumentModel { result := InspectionTypeDocumentModel{} inspectionTypes := taskServer.GetAllInspectionTypes() diff --git a/pkg/source/gcp/task/gke/k8s_container/parser.go b/pkg/source/gcp/task/gke/k8s_container/parser.go index ea4b438..8ff343a 100644 --- a/pkg/source/gcp/task/gke/k8s_container/parser.go +++ b/pkg/source/gcp/task/gke/k8s_container/parser.go @@ -39,7 +39,7 @@ func (k *k8sContainerParser) TargetLogType() enum.LogType { // Description implements parser.Parser. func (*k8sContainerParser) Description() string { - return `Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.` + return `Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. Log volume can be huge when the cluster has many Pods.` } // GetParserName implements parser.Parser. From 1b7b4e8ae6375a4acb7cbb36478806bc20774dd4 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Tue, 11 Feb 2025 16:59:27 +0900 Subject: [PATCH 20/23] Added comments in types used for generating document model for relationship --- docs/en/reference/features.md | 2 +- docs/en/reference/inspection-type.md | 12 ++-- docs/en/reference/relationships.md | 33 +++++++++ .../reference/relationship.template.md | 4 ++ pkg/document/model/relationship.go | 70 ++++++++++++++----- 5 files changed, 96 insertions(+), 25 deletions(-) diff --git a/docs/en/reference/features.md b/docs/en/reference/features.md index c6dc843..b02e167 100644 --- a/docs/en/reference/features.md +++ b/docs/en/reference/features.md @@ -181,7 +181,7 @@ This feature is supported in the following inspection types. ## Kubernetes container logs -Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. +Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. Log volume can be huge when the cluster has many Pods. diff --git a/docs/en/reference/inspection-type.md b/docs/en/reference/inspection-type.md index c537282..e67f183 100644 --- a/docs/en/reference/inspection-type.md +++ b/docs/en/reference/inspection-type.md @@ -21,7 +21,7 @@ KHI filters out unsupported parser for the selected inspection type at first. |[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| |[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| |[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| -|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. Log volume can be huge when the cluster has many Pods.| |[GKE Audit logs](./features.md#gke-audit-logs)|Gather GKE audit log to show creation/upgrade/deletion of logs cluster/nodepool| |[Compute API Logs](./features.md#compute-api-logs)|Gather Compute API audit logs to show the timings of the provisioning of resources(e.g creating/deleting GCE VM,mounting Persistent Disk...etc) on associated timelines.| |[GCE Network Logs](./features.md#gce-network-logs)|Gather GCE Network API logs to visualize statuses of Network Endpoint Groups(NEG)| @@ -41,7 +41,7 @@ KHI filters out unsupported parser for the selected inspection type at first. |[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| |[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| |[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| -|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. Log volume can be huge when the cluster has many Pods.| |[GKE Audit logs](./features.md#gke-audit-logs)|Gather GKE audit log to show creation/upgrade/deletion of logs cluster/nodepool| |[Compute API Logs](./features.md#compute-api-logs)|Gather Compute API audit logs to show the timings of the provisioning of resources(e.g creating/deleting GCE VM,mounting Persistent Disk...etc) on associated timelines.| |[GCE Network Logs](./features.md#gce-network-logs)|Gather GCE Network API logs to visualize statuses of Network Endpoint Groups(NEG)| @@ -64,7 +64,7 @@ KHI filters out unsupported parser for the selected inspection type at first. |[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| |[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| |[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| -|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. Log volume can be huge when the cluster has many Pods.| |[MultiCloud API logs](./features.md#multicloud-api-logs)|Gather Anthos Multicloud audit log including cluster creation,deletion and upgrades.| |[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| @@ -80,7 +80,7 @@ KHI filters out unsupported parser for the selected inspection type at first. |[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| |[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| |[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| -|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. Log volume can be huge when the cluster has many Pods.| |[MultiCloud API logs](./features.md#multicloud-api-logs)|Gather Anthos Multicloud audit log including cluster creation,deletion and upgrades.| |[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| @@ -96,7 +96,7 @@ KHI filters out unsupported parser for the selected inspection type at first. |[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| |[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| |[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| -|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. Log volume can be huge when the cluster has many Pods.| |[OnPrem API logs](./features.md#onprem-api-logs)|Gather Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades.| |[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| @@ -112,7 +112,7 @@ KHI filters out unsupported parser for the selected inspection type at first. |[Kubernetes Audit Log](./features.md#kubernetes-audit-log)|Gather kubernetes audit logs and visualize resource modifications.| |[Kubernetes Event Logs](./features.md#kubernetes-event-logs)|Gather kubernetes event logs and visualize these on the associated resource timeline.| |[Kubernetes Node Logs](./features.md#kubernetes-node-logs)|Gather node components(e.g docker/container) logs. Log volume can be huge when the cluster has many nodes.| -|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod.| +|[Kubernetes container logs](./features.md#kubernetes-container-logs)|Gather stdout/stderr logs of containers on the cluster to visualize them on the timeline under an associated Pod. Log volume can be huge when the cluster has many Pods.| |[OnPrem API logs](./features.md#onprem-api-logs)|Gather Anthos OnPrem audit log including cluster creation,deletion,enroll,unenroll and upgrades.| |[Kubernetes Control plane component logs](./features.md#kubernetes-control-plane-component-logs)|Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-manager,api-server) logs| diff --git a/docs/en/reference/relationships.md b/docs/en/reference/relationships.md index e8912c3..ce95e11 100644 --- a/docs/en/reference/relationships.md +++ b/docs/en/reference/relationships.md @@ -45,6 +45,9 @@ This timeline can have the following events. ## ![#4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png)Status condition field timeline + +Timelines of this type have ![#4c29e8](https://placehold.co/15x15/4c29e8/4c29e8.png)`condition` chip on the left side of its timeline name. + ### Revisions @@ -61,6 +64,9 @@ This timeline can have the following revisions. ## ![#000000](https://placehold.co/15x15/000000/000000.png)Operation timeline + +Timelines of this type have ![#000000](https://placehold.co/15x15/000000/000000.png)`operation` chip on the left side of its timeline name. + ### Revisions @@ -84,6 +90,9 @@ This timeline can have the following revisions. ## ![#008000](https://placehold.co/15x15/008000/008000.png)Endpoint serving state timeline + +Timelines of this type have ![#008000](https://placehold.co/15x15/008000/008000.png)`endpointslice` chip on the left side of its timeline name. + ### Revisions @@ -100,6 +109,9 @@ This timeline can have the following revisions. ## ![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)Container timeline + +Timelines of this type have ![#fe9bab](https://placehold.co/15x15/fe9bab/fe9bab.png)`container` chip on the left side of its timeline name. + ### Revisions @@ -135,6 +147,9 @@ This timeline can have the following events. ## ![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)Node component timeline + +Timelines of this type have ![#0077CC](https://placehold.co/15x15/0077CC/0077CC.png)`node-component` chip on the left side of its timeline name. + ### Revisions @@ -162,6 +177,9 @@ This timeline can have the following events. ## ![#33DD88](https://placehold.co/15x15/33DD88/33DD88.png)Owning children timeline + +Timelines of this type have ![#33DD88](https://placehold.co/15x15/33DD88/33DD88.png)`owns` chip on the left side of its timeline name. + ### Aliases @@ -176,6 +194,9 @@ This timeline can have the following aliases. ## ![#FF8855](https://placehold.co/15x15/FF8855/FF8855.png)Pod binding timeline + +Timelines of this type have ![#FF8855](https://placehold.co/15x15/FF8855/FF8855.png)`binds` chip on the left side of its timeline name. + ### Aliases @@ -190,6 +211,9 @@ This timeline can have the following aliases. ## ![#A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)Network Endpoint Group timeline + +Timelines of this type have ![#A52A2A](https://placehold.co/15x15/A52A2A/A52A2A.png)`neg` chip on the left side of its timeline name. + ### Revisions @@ -205,6 +229,9 @@ This timeline can have the following revisions. ## ![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)Managed instance group timeline + +Timelines of this type have ![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)`mig` chip on the left side of its timeline name. + ### Events @@ -219,6 +246,9 @@ This timeline can have the following events. ## ![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)Control plane component timeline + +Timelines of this type have ![#FF5555](https://placehold.co/15x15/FF5555/FF5555.png)`controlplane` chip on the left side of its timeline name. + ### Events @@ -233,6 +263,9 @@ This timeline can have the following events. ## ![#333333](https://placehold.co/15x15/333333/333333.png)Serialport log timeline + +Timelines of this type have ![#333333](https://placehold.co/15x15/333333/333333.png)`serialport` chip on the left side of its timeline name. + ### Events diff --git a/docs/template/reference/relationship.template.md b/docs/template/reference/relationship.template.md index 1c5557f..be87b46 100644 --- a/docs/template/reference/relationship.template.md +++ b/docs/template/reference/relationship.template.md @@ -2,6 +2,10 @@ {{range $index,$relationship := .Relationships }} ## ![#{{$relationship.ColorCode}}](https://placehold.co/15x15/{{$relationship.ColorCode}}/{{$relationship.ColorCode}}.png){{$relationship.LongName}} +{{- with $relationship.HasVisibleChip}} + +Timelines of this type have ![#{{$relationship.ColorCode}}](https://placehold.co/15x15/{{$relationship.ColorCode}}/{{$relationship.ColorCode}}.png)`{{$relationship.Label}}` chip on the left side of its timeline name. +{{end}} {{with $relationship.GeneratableRevisions}} diff --git a/pkg/document/model/relationship.go b/pkg/document/model/relationship.go index 0820a40..c90cc92 100644 --- a/pkg/document/model/relationship.go +++ b/pkg/document/model/relationship.go @@ -6,47 +6,78 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/model/enum" ) +// RelationshipDocumentModel is a model type for generating document docs/en/reference/relationships.md. type RelationshipDocumentModel struct { + // Relationships is a list of relationship document elements. Relationships []RelationshipDocumentElement } +// RelationshipDocumentElement represents a relationship element in the document. type RelationshipDocumentElement struct { - ID string + // ID is the unique identifier of the relationship. + ID string + // HasVisibleChip indicates whether the relationship has a visible chip on the left side of timeline name. HasVisibleChip bool - Label string - LongName string - ColorCode string + // Label is the short label for the relationship. + Label string + // LongName is the descriptive name of the relationship. + LongName string + // ColorCode is the hexadecimal color code for the relationship. + ColorCode string - GeneratableEvents []RelationshipGeneratableEvent + // GeneratableEvents is the list of the generatable events on the timeline of this relationship. + GeneratableEvents []RelationshipGeneratableEvent + // GeneratableRevisions is the list of the generatable revisions on the timeline of this relationship. GeneratableRevisions []RelationshipGeneratableRevisions - GeneratableAliases []RelationshipGeneratableAliases + // GeneratableAliases is the list of the generatable aliases on the timeline of this relationship. + GeneratableAliases []RelationshipGeneratableAliases } +// RelationshipGeneratableEvent represents a generatable event on the timeline of this relationship. type RelationshipGeneratableEvent struct { - ID string + // ID is the unique identifier of the event. + ID string + // SourceLogTypeLabel is the label of the source log type. SourceLogTypeLabel string - ColorCode string - Description string + // ColorCode is the hexadecimal color code for the event without `#` prefix. + ColorCode string + // Description describes the event. + Description string } +// RelationshipGeneratableRevisions represents generatable revision states on the timeline of this relationship. type RelationshipGeneratableRevisions struct { - ID string - SourceLogTypeLabel string + // ID is the unique identifier of the revision state. + ID string + // SourceLogTypeLabel is the label of the source log type. + SourceLogTypeLabel string + // SourceLogTypeColorCode is the hexadecimal color code for the source log type without `#` prefix. SourceLogTypeColorCode string + // RevisionStateColorCode is the hexadecimal color code for the revision state without `#` prefix. RevisionStateColorCode string - RevisionStateLabel string - Description string + // RevisionStateLabel is the label of the revision state. + RevisionStateLabel string + // Description describes the revision state. + Description string } +// RelationshipGeneratableAliases represents generatable aliases on the timeline of this relationship. type RelationshipGeneratableAliases struct { - ID string - AliasedTimelineRelationshipLabel string + // ID is the unique identifier of the alias. + ID string + // AliasedTimelineRelationshipLabel is the label of the aliased timeline relationship. + AliasedTimelineRelationshipLabel string + // AliasedTimelineRelationshipColorCode is the hexadecimal color code for the aliased timeline relationship. AliasedTimelineRelationshipColorCode string - SourceLogTypeLabel string - SourceLogTypeColorCode string - Description string + // SourceLogTypeLabel is the label of the source log type. + SourceLogTypeLabel string + // SourceLogTypeColorCode is the hexadecimal color code for the source log type without `#` prefix. + SourceLogTypeColorCode string + // Description describes the alias. + Description string } +// GetRelationshipDocumentModel returns the document model for relationships. func GetRelationshipDocumentModel() RelationshipDocumentModel { relationships := []RelationshipDocumentElement{} for i := 0; i < int(enum.EnumParentRelationshipLength); i++ { @@ -70,6 +101,7 @@ func GetRelationshipDocumentModel() RelationshipDocumentModel { } } +// getRelationshipGeneratableEvents retrieves generatable events for a given relationship. func getRelationshipGeneratableEvents(reltionship enum.ParentRelationship) []RelationshipGeneratableEvent { result := []RelationshipGeneratableEvent{} relationship := enum.ParentRelationships[reltionship] @@ -85,6 +117,7 @@ func getRelationshipGeneratableEvents(reltionship enum.ParentRelationship) []Rel return result } +// getRelationshipGeneratableRevisions retrieves generatable revisions for a given relationship. func getRelationshipGeneratableRevisions(reltionship enum.ParentRelationship) []RelationshipGeneratableRevisions { result := []RelationshipGeneratableRevisions{} relationship := enum.ParentRelationships[reltionship] @@ -103,6 +136,7 @@ func getRelationshipGeneratableRevisions(reltionship enum.ParentRelationship) [] return result } +// getRelationshipGeneratableAliases retrieves generatable aliases for a given relationship. func getRelationshipGeneratableAliases(reltionship enum.ParentRelationship) []RelationshipGeneratableAliases { result := []RelationshipGeneratableAliases{} relationship := enum.ParentRelationships[reltionship] From 2c0c981df6edfc65be814f5f375d0fd37688a480 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Tue, 11 Feb 2025 17:05:52 +0900 Subject: [PATCH 21/23] Add comments for document model types for form references --- pkg/document/model/form.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pkg/document/model/form.go b/pkg/document/model/form.go index 36869ec..28b071f 100644 --- a/pkg/document/model/form.go +++ b/pkg/document/model/form.go @@ -8,28 +8,38 @@ import ( "github.com/GoogleCloudPlatform/khi/pkg/task" ) +// FormDocumentModel represents the model for generating document docs/en/reference/form.md. type FormDocumentModel struct { + // Forms is a list of form elements for the document. Forms []FormDocumentElement } +// FormDocumentElement represents a single form element in the documentation. type FormDocumentElement struct { - ID string - Label string + // ID is the unique identifier of the form. + ID string + // Label is the display label for the form. + Label string + // Description provides a description of the form. Description string - + // UsedFeatures lists the features requesting this form parameter in their dependency. UsedFeatures []FormUsedFeatureElement } +// FormUsedFeatureElement represents a feature used by a form. type FormUsedFeatureElement struct { - ID string + // ID is the unique identifier of the feature. + ID string + // Name is the human-readable name of the feature. Name string } +// GetFormDocumentModel returns the document model for forms. func GetFormDocumentModel(taskServer *inspection.InspectionTaskServer) (*FormDocumentModel, error) { result := FormDocumentModel{} forms := taskServer.RootTaskSet.FilteredSubset(label.TaskLabelKeyIsFormTask, taskfilter.HasTrue, false) for _, form := range forms.GetAll() { - usedFeatures, err := getUsedFeatures(taskServer, form) + usedFeatures, err := getFeaturesRequestingFormTask(taskServer, form) if err != nil { return nil, err } @@ -51,7 +61,8 @@ func GetFormDocumentModel(taskServer *inspection.InspectionTaskServer) (*FormDoc return &result, nil } -func getUsedFeatures(taskServer *inspection.InspectionTaskServer, formTask task.Definition) ([]task.Definition, error) { +// getFeaturesRequestingFormTask returns the list of feature tasks that depends on the given form task. +func getFeaturesRequestingFormTask(taskServer *inspection.InspectionTaskServer, formTask task.Definition) ([]task.Definition, error) { var result []task.Definition features := taskServer.RootTaskSet.FilteredSubset(inspection_task.LabelKeyInspectionFeatureFlag, taskfilter.HasTrue, false) for _, feature := range features.GetAll() { From 62485dfb928b4e0a5e655aee7c03e0f5097be60b Mon Sep 17 00:00:00 2001 From: kyasbal Date: Tue, 11 Feb 2025 17:20:00 +0900 Subject: [PATCH 22/23] Refactor document related types for review --- cmd/reference-generator/main.go | 14 ++++ pkg/document/generator/generator.go | 39 ++++++---- pkg/document/generator/generator_test.go | 75 +++++++++++-------- .../{splitter => generator}/splitter.go | 39 +++++++--- .../{splitter => generator}/spltter_test.go | 34 ++++++--- pkg/document/generator/util.go | 14 ++++ pkg/document/generator/util_test.go | 14 ++++ pkg/document/model/feature.go | 14 ++++ pkg/document/model/form.go | 14 ++++ pkg/document/model/inspection_type.go | 14 ++++ pkg/document/model/relationship.go | 14 ++++ pkg/inspection/task/label/form.go | 14 ++++ pkg/inspection/task/label/query.go | 14 ++++ pkg/inspection/task/label/query_test.go | 14 ++++ pkg/inspection/taskfilter/filter.go | 14 ++++ pkg/source/gcp/task/form.go | 4 +- pkg/source/gcp/task/form_test.go | 10 +-- 17 files changed, 283 insertions(+), 72 deletions(-) rename pkg/document/{splitter => generator}/splitter.go (71%) rename pkg/document/{splitter => generator}/spltter_test.go (83%) diff --git a/cmd/reference-generator/main.go b/cmd/reference-generator/main.go index 9440a04..054ffde 100644 --- a/cmd/reference-generator/main.go +++ b/cmd/reference-generator/main.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package main // cmd/reference-generator/main.go diff --git a/pkg/document/generator/generator.go b/pkg/document/generator/generator.go index 1f9a241..5e99619 100644 --- a/pkg/document/generator/generator.go +++ b/pkg/document/generator/generator.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package generator import ( @@ -6,8 +20,6 @@ import ( "fmt" "os" "text/template" - - "github.com/GoogleCloudPlatform/khi/pkg/document/splitter" ) var documentFuncs = map[string]any{ @@ -20,6 +32,7 @@ type DocumentGenerator struct { template *template.Template } +// NewDocumentGeneratorFromTemplateFileGlob returns DocumentGenerator with the template files selected by the given glob file path. func NewDocumentGeneratorFromTemplateFileGlob(templateFileGlob string) (*DocumentGenerator, error) { template, err := template.New("").Funcs(documentFuncs).ParseGlob(templateFileGlob) if err != nil { @@ -66,12 +79,12 @@ func (g *DocumentGenerator) generateDocumentString(destinationString string, tem return "", err } outputString := outputBuffer.String() - autoGeneratedSections, err := splitter.SplitToDocumentSections(outputString) + autoGeneratedSections, err := SplitToDocumentSections(outputString) if err != nil { return "", err } - prevGeneratedSections, err := splitter.SplitToDocumentSections(destinationString) + prevGeneratedSections, err := SplitToDocumentSections(destinationString) if err != nil { return "", err } @@ -83,29 +96,29 @@ func (g *DocumentGenerator) generateDocumentString(destinationString string, tem return outputString, nil } -func concatAmendedContents(generated []*splitter.DocumentSection, prev []*splitter.DocumentSection, ignoreNonMatchingGeneratedSection bool) (string, error) { - var resultSections []*splitter.DocumentSection - prevToNextMap := make(map[string]*splitter.DocumentSection) +func concatAmendedContents(generated []*DocumentSection, prev []*DocumentSection, ignoreNonMatchingGeneratedSection bool) (string, error) { + var resultSections []*DocumentSection + prevToNextMap := make(map[string]*DocumentSection) usedPrevIds := make(map[string]interface{}) for index, section := range prev { - if section.Type == splitter.SectionTypeAmend { + if section.Type == SectionTypeAmend { if index == 0 { resultSections = append(resultSections, section) } else { - prevToNextMap[prev[index-1].Id] = section - usedPrevIds[prev[index-1].Id] = struct{}{} + prevToNextMap[prev[index-1].ID] = section + usedPrevIds[prev[index-1].ID] = struct{}{} } } } for _, section := range generated { - if section.Type != splitter.SectionTypeGenerated { + if section.Type != SectionTypeGenerated { continue } resultSections = append(resultSections, section) - if next, ok := prevToNextMap[section.Id]; ok { + if next, ok := prevToNextMap[section.ID]; ok { resultSections = append(resultSections, next) - delete(usedPrevIds, section.Id) + delete(usedPrevIds, section.ID) } } diff --git a/pkg/document/generator/generator_test.go b/pkg/document/generator/generator_test.go index d972b50..41eb963 100644 --- a/pkg/document/generator/generator_test.go +++ b/pkg/document/generator/generator_test.go @@ -1,9 +1,22 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package generator import ( "testing" - "github.com/GoogleCloudPlatform/khi/pkg/document/splitter" "github.com/google/go-cmp/cmp" ) @@ -88,69 +101,69 @@ Generated content 2 func TestConcatAmendedContents(t *testing.T) { testCases := []struct { name string - generated []*splitter.DocumentSection - prev []*splitter.DocumentSection + generated []*DocumentSection + prev []*DocumentSection ignoreNonMatchingGeneratedSection bool wantResult string wantErr string }{ { name: "no amended sections", - generated: []*splitter.DocumentSection{ - {Id: "generated-1", Type: splitter.SectionTypeGenerated, Body: "Generated 1"}, - {Id: "generated-2", Type: splitter.SectionTypeGenerated, Body: "Generated 2"}, + generated: []*DocumentSection{ + {ID: "generated-1", Type: SectionTypeGenerated, Body: "Generated 1"}, + {ID: "generated-2", Type: SectionTypeGenerated, Body: "Generated 2"}, }, - prev: []*splitter.DocumentSection{}, + prev: []*DocumentSection{}, ignoreNonMatchingGeneratedSection: false, wantResult: "Generated 1\nGenerated 2\n", }, { name: "single amended section at beginning", - generated: []*splitter.DocumentSection{ - {Id: "generated-1", Type: splitter.SectionTypeGenerated, Body: "Generated 1"}, + generated: []*DocumentSection{ + {ID: "generated-1", Type: SectionTypeGenerated, Body: "Generated 1"}, }, - prev: []*splitter.DocumentSection{ - {Id: "amended-1", Type: splitter.SectionTypeAmend, Body: "Amended 1"}, + prev: []*DocumentSection{ + {ID: "amended-1", Type: SectionTypeAmend, Body: "Amended 1"}, }, ignoreNonMatchingGeneratedSection: false, wantResult: "Amended 1\nGenerated 1\n", }, { name: "new generated section and a single amended section", - generated: []*splitter.DocumentSection{ - {Id: "generated-1", Type: splitter.SectionTypeGenerated, Body: "Generated 1"}, - {Id: "generated-2", Type: splitter.SectionTypeGenerated, Body: "Generated 2"}, + generated: []*DocumentSection{ + {ID: "generated-1", Type: SectionTypeGenerated, Body: "Generated 1"}, + {ID: "generated-2", Type: SectionTypeGenerated, Body: "Generated 2"}, }, - prev: []*splitter.DocumentSection{ - {Id: "generated-1", Type: splitter.SectionTypeGenerated, Body: "Generated 1"}, - {Id: "amended-1", Type: splitter.SectionTypeAmend, Body: "Amended 1"}, + prev: []*DocumentSection{ + {ID: "generated-1", Type: SectionTypeGenerated, Body: "Generated 1"}, + {ID: "amended-1", Type: SectionTypeAmend, Body: "Amended 1"}, }, ignoreNonMatchingGeneratedSection: false, wantResult: "Generated 1\nAmended 1\nGenerated 2\n", }, { name: "multiple amended sections", - generated: []*splitter.DocumentSection{ - {Id: "generated-1", Body: "Generated 1"}, - {Id: "generated-2", Body: "Generated 2"}, + generated: []*DocumentSection{ + {ID: "generated-1", Body: "Generated 1"}, + {ID: "generated-2", Body: "Generated 2"}, }, - prev: []*splitter.DocumentSection{ - {Id: "amended-1", Type: splitter.SectionTypeAmend, Body: "Amended 1"}, - {Id: "generated-1", Body: "Generated 1"}, - {Id: "amended-2", Type: splitter.SectionTypeAmend, Body: "Amended 2"}, - {Id: "generated-2", Body: "Generated 2"}, - {Id: "amended-3", Type: splitter.SectionTypeAmend, Body: "Amended 3"}, + prev: []*DocumentSection{ + {ID: "amended-1", Type: SectionTypeAmend, Body: "Amended 1"}, + {ID: "generated-1", Body: "Generated 1"}, + {ID: "amended-2", Type: SectionTypeAmend, Body: "Amended 2"}, + {ID: "generated-2", Body: "Generated 2"}, + {ID: "amended-3", Type: SectionTypeAmend, Body: "Amended 3"}, }, ignoreNonMatchingGeneratedSection: false, wantResult: "Amended 1\nGenerated 1\nAmended 2\nGenerated 2\nAmended 3\n", }, { name: "no generated sections", - generated: []*splitter.DocumentSection{}, - prev: []*splitter.DocumentSection{ - {Id: "amended-1", Type: splitter.SectionTypeAmend, Body: "Amended 1"}, - {Id: "generated-1", Body: "Generated 1"}, - {Id: "amended-2", Type: splitter.SectionTypeAmend, Body: "Amended 2"}, + generated: []*DocumentSection{}, + prev: []*DocumentSection{ + {ID: "amended-1", Type: SectionTypeAmend, Body: "Amended 1"}, + {ID: "generated-1", Body: "Generated 1"}, + {ID: "amended-2", Type: SectionTypeAmend, Body: "Amended 2"}, }, ignoreNonMatchingGeneratedSection: false, wantErr: "previous amended sections belongs to other generated sections is not used. Unused ids [generated-1]", diff --git a/pkg/document/splitter/splitter.go b/pkg/document/generator/splitter.go similarity index 71% rename from pkg/document/splitter/splitter.go rename to pkg/document/generator/splitter.go index 3e701df..7b713c5 100644 --- a/pkg/document/splitter/splitter.go +++ b/pkg/document/generator/splitter.go @@ -1,4 +1,18 @@ -package splitter +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package generator import ( "crypto/sha256" @@ -11,12 +25,13 @@ const beginGeneratedSectionPrefix = "" +// SectionType is a enum of the type of a section. type SectionType int const ( // SectionTypeGenerated is the section type indicating the section is generated automatically. SectionTypeGenerated = 0 - // SectionTypeAmend is the section type indicating the section is added after the generation. + // SectionTypeAmend is the section type indicating the section is added by human editor after the generation. SectionTypeAmend = 1 ) @@ -24,11 +39,13 @@ const ( // The splitter read given text and split them in multiple DocumentSection. type DocumentSection struct { Type SectionType - Id string + ID string Body string } // SplitToDocumentSections splits text to array of DocumentSection +// This is for splitting the document with the part automatically generated or the part written by human. +// Generator append the contents written by human in addition to the document generated by the template with keeping its possition. func SplitToDocumentSections(text string) ([]*DocumentSection, error) { lines := strings.Split(text, "\n") var sections []*DocumentSection @@ -46,7 +63,7 @@ func SplitToDocumentSections(text string) ([]*DocumentSection, error) { id := readIdFromGeneratedSectionComment(lineWithoutSpace) currentSection = &DocumentSection{ Type: SectionTypeGenerated, - Id: id, + ID: id, Body: line, } continue @@ -56,8 +73,8 @@ func SplitToDocumentSections(text string) ([]*DocumentSection, error) { if currentSection == nil { return nil, fmt.Errorf("invalid end of section. section id %s ended but not began. line %d", id, lineIndex+1) } - if currentSection.Id != id { - return nil, fmt.Errorf("invalid end of section. section id %s ended but the id is not matching with the previous section id %s. line %d", id, currentSection.Id, lineIndex+1) + if currentSection.ID != id { + return nil, fmt.Errorf("invalid end of section. section id %s ended but the id is not matching with the previous section id %s. line %d", id, currentSection.ID, lineIndex+1) } currentSection.Body += "\n" + line sections = append(sections, currentSection) @@ -68,7 +85,7 @@ func SplitToDocumentSections(text string) ([]*DocumentSection, error) { if currentSection == nil { currentSection = &DocumentSection{ Type: SectionTypeAmend, - Id: "", + ID: "", Body: line, } continue @@ -79,7 +96,7 @@ func SplitToDocumentSections(text string) ([]*DocumentSection, error) { if currentSection != nil { if currentSection.Type == SectionTypeGenerated { - return nil, fmt.Errorf("invalid end of section. section id %s began but not ended", currentSection.Id) + return nil, fmt.Errorf("invalid end of section. section id %s began but not ended", currentSection.ID) } if currentSection.Body != "" { sections = append(sections, currentSection) @@ -87,9 +104,9 @@ func SplitToDocumentSections(text string) ([]*DocumentSection, error) { } for _, section := range sections { - // generate section id for ammended section. This uses hash just because I don't want to use random string to improve testability. - if section.Id == "" { - section.Id = getHashFromText(section.Body) + // generate section id for amended section. This uses hash just because I don't want to use random string to improve testability. + if section.ID == "" { + section.ID = getHashFromText(section.Body) } } return sections, nil diff --git a/pkg/document/splitter/spltter_test.go b/pkg/document/generator/spltter_test.go similarity index 83% rename from pkg/document/splitter/spltter_test.go rename to pkg/document/generator/spltter_test.go index 04a2c61..1c1b330 100644 --- a/pkg/document/splitter/spltter_test.go +++ b/pkg/document/generator/spltter_test.go @@ -1,4 +1,18 @@ -package splitter +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package generator import ( "testing" @@ -23,7 +37,7 @@ Generated content 1 expected: []*DocumentSection{ { Type: SectionTypeGenerated, - Id: "generated-id-1", + ID: "generated-id-1", Body: "\nGenerated content 1\n", }, }, @@ -41,17 +55,17 @@ Generated content 2 expected: []*DocumentSection{ { Type: SectionTypeGenerated, - Id: "generated-id-1", + ID: "generated-id-1", Body: "\nGenerated content 1\n", }, { Type: SectionTypeAmend, - Id: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", // Hash of amend content + ID: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", // Hash of amend content Body: "", }, { Type: SectionTypeGenerated, - Id: "generated-id-2", + ID: "generated-id-2", Body: "\nGenerated content 2\n", }, }, @@ -73,22 +87,22 @@ Amend content 2 expected: []*DocumentSection{ { Type: SectionTypeGenerated, - Id: "generated-id-1", + ID: "generated-id-1", Body: "\nGenerated content 1\n", }, { Type: SectionTypeAmend, - Id: "8425062b6f9c5ce9895ebb6fcd8d3c58c68887c14c8628a33e8f604dac84e919", // Hash of amend content + ID: "8425062b6f9c5ce9895ebb6fcd8d3c58c68887c14c8628a33e8f604dac84e919", // Hash of amend content Body: "\nAmend content 1\n", }, { Type: SectionTypeGenerated, - Id: "generated-id-2", + ID: "generated-id-2", Body: "\nGenerated content 2\n", }, { Type: SectionTypeAmend, - Id: "76b08fe06ecd34111bc58e645360d32593cdfdb797d70f84e7ed0c3cf3103374", // Hash of amend content + ID: "76b08fe06ecd34111bc58e645360d32593cdfdb797d70f84e7ed0c3cf3103374", // Hash of amend content Body: "\nAmend content 2\n", }, }, @@ -120,7 +134,7 @@ Amend content expected: []*DocumentSection{ { Type: SectionTypeAmend, - Id: "1ff547697cd3b7542f7bb024812b201fc77e31bd605f95f247f41b585286c464", + ID: "1ff547697cd3b7542f7bb024812b201fc77e31bd605f95f247f41b585286c464", Body: "\nAmend content\n", }, }, diff --git a/pkg/document/generator/util.go b/pkg/document/generator/util.go index 38adb77..b1d2505 100644 --- a/pkg/document/generator/util.go +++ b/pkg/document/generator/util.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package generator import ( diff --git a/pkg/document/generator/util_test.go b/pkg/document/generator/util_test.go index 90941e9..22f7164 100644 --- a/pkg/document/generator/util_test.go +++ b/pkg/document/generator/util_test.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package generator import ( diff --git a/pkg/document/model/feature.go b/pkg/document/model/feature.go index fdb9484..f442423 100644 --- a/pkg/document/model/feature.go +++ b/pkg/document/model/feature.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package model import ( diff --git a/pkg/document/model/form.go b/pkg/document/model/form.go index 28b071f..bead2f4 100644 --- a/pkg/document/model/form.go +++ b/pkg/document/model/form.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package model import ( diff --git a/pkg/document/model/inspection_type.go b/pkg/document/model/inspection_type.go index 8cdd31b..4f5d951 100644 --- a/pkg/document/model/inspection_type.go +++ b/pkg/document/model/inspection_type.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package model import ( diff --git a/pkg/document/model/relationship.go b/pkg/document/model/relationship.go index c90cc92..b051c8c 100644 --- a/pkg/document/model/relationship.go +++ b/pkg/document/model/relationship.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package model import ( diff --git a/pkg/inspection/task/label/form.go b/pkg/inspection/task/label/form.go index 6dcb40d..a7d7398 100644 --- a/pkg/inspection/task/label/form.go +++ b/pkg/inspection/task/label/form.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package label import ( diff --git a/pkg/inspection/task/label/query.go b/pkg/inspection/task/label/query.go index 46405d8..7f471b0 100644 --- a/pkg/inspection/task/label/query.go +++ b/pkg/inspection/task/label/query.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package label import ( diff --git a/pkg/inspection/task/label/query_test.go b/pkg/inspection/task/label/query_test.go index ad9d9bb..8eeccb9 100644 --- a/pkg/inspection/task/label/query_test.go +++ b/pkg/inspection/task/label/query_test.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package label import ( diff --git a/pkg/inspection/taskfilter/filter.go b/pkg/inspection/taskfilter/filter.go index 4481f35..52aeeb1 100644 --- a/pkg/inspection/taskfilter/filter.go +++ b/pkg/inspection/taskfilter/filter.go @@ -1,3 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package taskfilter // ContainsElement returns a function that represents a condition to filter only tasks that have the specified element in the specified label value. diff --git a/pkg/source/gcp/task/form.go b/pkg/source/gcp/task/form.go index be64159..a9dae52 100644 --- a/pkg/source/gcp/task/form.go +++ b/pkg/source/gcp/task/form.go @@ -43,8 +43,8 @@ const InputProjectIdTaskID = GCPPrefix + "input/project-id" var projectIdValidator = regexp.MustCompile(`^\s*[0-9a-z\.:\-]+\s*$`) var InputProjectIdTask = form.NewInputFormDefinitionBuilder(InputProjectIdTaskID, PriorityForResourceIdentifierGroup+5000, "Project ID"). - WithUIDescription("The project ID containing the logs of cluster to query"). - WithDocumentDescription("The project ID containing the logs of cluster to query"). + WithUIDescription("The project ID containing logs of the cluster to query"). + WithDocumentDescription("The project ID containing logs of the cluster to query"). WithDependencies([]string{}). WithValidator(func(ctx context.Context, value string, variables *task.VariableSet) (string, error) { if !projectIdValidator.Match([]byte(value)) { diff --git a/pkg/source/gcp/task/form_test.go b/pkg/source/gcp/task/form_test.go index 41c6bf9..40c6f1c 100644 --- a/pkg/source/gcp/task/form_test.go +++ b/pkg/source/gcp/task/form_test.go @@ -49,7 +49,7 @@ func TestProjectIdInput(t *testing.T) { Id: GCPPrefix + "input/project-id", Type: "Text", Label: "Project ID", - Description: "A project ID containing the cluster to inspect", + Description: "The project ID containing logs of the cluster to query", HintType: form.HintTypeInfo, AllowEdit: true, }, @@ -66,7 +66,7 @@ func TestProjectIdInput(t *testing.T) { Id: GCPPrefix + "input/project-id", Type: "Text", Label: "Project ID", - Description: "A project ID containing the cluster to inspect", + Description: "The project ID containing logs of the cluster to query", AllowEdit: false, HintType: form.HintTypeInfo, Default: "bar-project", @@ -91,7 +91,7 @@ func TestProjectIdInput(t *testing.T) { Id: GCPPrefix + "input/project-id", Type: "Text", Label: "Project ID", - Description: "A project ID containing the cluster to inspect", + Description: "The project ID containing logs of the cluster to query", AllowEdit: true, HintType: form.HintTypeInfo, ValidationError: "Project ID must match `^*[0-9a-z\\.:\\-]+$`", @@ -109,7 +109,7 @@ func TestProjectIdInput(t *testing.T) { Id: GCPPrefix + "input/project-id", Type: "Text", Label: "Project ID", - Description: "A project ID containing the cluster to inspect", + Description: "The project ID containing logs of the cluster to query", HintType: form.HintTypeInfo, AllowEdit: true, }, @@ -126,7 +126,7 @@ func TestProjectIdInput(t *testing.T) { Id: GCPPrefix + "input/project-id", Type: "Text", Label: "Project ID", - Description: "A project ID containing the cluster to inspect", + Description: "The project ID containing logs of the cluster to query", HintType: form.HintTypeInfo, AllowEdit: true, }, From 3621de61e8a8a132fd3c6f701f2f0907e3c41017 Mon Sep 17 00:00:00 2001 From: kyasbal Date: Tue, 11 Feb 2025 17:23:56 +0900 Subject: [PATCH 23/23] Fixed composer query generator to generate a sample query for document --- docs/en/reference/features.md | 42 ++++++++++++--------- docs/en/reference/forms.md | 2 +- pkg/source/gcp/task/cloud-composer/query.go | 21 ++++++----- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/docs/en/reference/features.md b/docs/en/reference/features.md index b02e167..1eb3972 100644 --- a/docs/en/reference/features.md +++ b/docs/en/reference/features.md @@ -19,7 +19,7 @@ Gather kubernetes audit logs and visualize resource modifications. |:-:|---| |[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| |[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -80,7 +80,7 @@ Gather kubernetes event logs and visualize these on the associated resource time |Parameter name|Description| |:-:|---| |[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -133,7 +133,7 @@ Gather node components(e.g docker/container) logs. Log volume can be huge when t |Parameter name|Description| |:-:|---| |[Node names](./forms.md#node-names)|A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster.| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -191,7 +191,7 @@ Gather stdout/stderr logs of containers on the cluster to visualize them on the |:-:|---| |[Namespaces(Container logs)](./forms.md#namespacescontainer-logs)|The namespace of Pods to gather container logs. Specify `@managed` to gather logs of system components.| |[Pod names(Container logs)](./forms.md#pod-namescontainer-logs)|The substring of Pod name to gather container logs. Specify `@any` to gather logs of all pods.| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -244,7 +244,7 @@ Gather GKE audit log to show creation/upgrade/deletion of logs cluster/nodepool |Parameter name|Description| |:-:|---| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -295,7 +295,7 @@ Gather Compute API audit logs to show the timings of the provisioning of resourc |:-:|---| |[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| |[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -354,7 +354,7 @@ Gather GCE Network API logs to visualize statuses of Network Endpoint Groups(NEG |:-:|---| |[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| |[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -411,7 +411,7 @@ Gather Anthos Multicloud audit log including cluster creation,deletion and upgra |Parameter name|Description| |:-:|---| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -461,7 +461,7 @@ Gather logs related to cluster autoscaler behavior to show them on the timelines |Parameter name|Description| |:-:|---| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -512,7 +512,7 @@ Gather Anthos OnPrem audit log including cluster creation,deletion,enroll,unenro |Parameter name|Description| |:-:|---| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -563,7 +563,7 @@ Gather Kubernetes control plane component(e.g kube-scheduler, kube-controller-ma |Parameter name|Description| |:-:|---| |[Control plane component names](./forms.md#control-plane-component-names)|| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -621,7 +621,7 @@ Gather serialport logs of GKE nodes. This helps detailed investigation on VM boo |[Kind](./forms.md#kind)|The kinds of resources to gather logs. `@default` is a alias of set of kinds that frequently queried. Specify `@any` to query every kinds of resources| |[Namespaces](./forms.md#namespaces)|The namespace of resources to gather logs. Specify `@all_cluster_scoped` to gather logs for all non-namespaced resources. Specify `@all_namespaced` to gather logs for all namespaced resources.| |[Node names](./forms.md#node-names)|A space-separated list of node name substrings used to collect node-related logs. If left blank, KHI gathers logs from all nodes in the cluster.| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Cluster name](./forms.md#cluster-name)|The cluster name to gather logs.| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -682,7 +682,7 @@ Airflow Scheduler logs contain information related to the scheduling of TaskInst |Parameter name|Description| |:-:|---| |[Location](./forms.md#location)|| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Composer Environment Name](./forms.md#composer-environment-name)|| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -704,7 +704,9 @@ This feature can generates following timeline relationship of timelines. Sample query: ```ada -TODO: add sample query +resource.type="cloud_composer_environment" +resource.labels.environment_name="sample-composer-environment" +log_name=projects/test-project/logs/airflow-scheduler ``` @@ -727,7 +729,7 @@ Airflow Worker logs contain information related to the execution of TaskInstance |Parameter name|Description| |:-:|---| |[Location](./forms.md#location)|| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Composer Environment Name](./forms.md#composer-environment-name)|| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -749,7 +751,9 @@ This feature can generates following timeline relationship of timelines. Sample query: ```ada -TODO: add sample query +resource.type="cloud_composer_environment" +resource.labels.environment_name="sample-composer-environment" +log_name=projects/test-project/logs/airflow-worker ``` @@ -772,7 +776,7 @@ The DagProcessorManager logs contain information for investigating the number of |Parameter name|Description| |:-:|---| |[Location](./forms.md#location)|| -|[Project ID](./forms.md#project-id)|The project ID containing the logs of cluster to query| +|[Project ID](./forms.md#project-id)|The project ID containing logs of the cluster to query| |[Composer Environment Name](./forms.md#composer-environment-name)|| |[End time](./forms.md#end-time)|The endtime of the time range to gather logs. The start time of the time range will be this endtime subtracted with the duration parameter.| |[Duration](./forms.md#duration)|The duration of time range to gather logs. Supported time units are `h`,`m` or `s`. (Example: `3h30m`)| @@ -794,7 +798,9 @@ This feature can generates following timeline relationship of timelines. Sample query: ```ada -TODO: add sample query +resource.type="cloud_composer_environment" +resource.labels.environment_name="sample-composer-environment" +log_name=projects/test-project/logs/dag-processor-manager ``` diff --git a/docs/en/reference/forms.md b/docs/en/reference/forms.md index c3d6f0d..c657b5f 100644 --- a/docs/en/reference/forms.md +++ b/docs/en/reference/forms.md @@ -6,7 +6,7 @@ ## Project ID -The project ID containing the logs of cluster to query +The project ID containing logs of the cluster to query ### Features using this parameter diff --git a/pkg/source/gcp/task/cloud-composer/query.go b/pkg/source/gcp/task/cloud-composer/query.go index e38c2d8..6c8947d 100644 --- a/pkg/source/gcp/task/cloud-composer/query.go +++ b/pkg/source/gcp/task/cloud-composer/query.go @@ -40,7 +40,7 @@ var ComposerSchedulerLogQueryTask = query.NewQueryGeneratorTask( InputComposerEnvironmentTaskID, }, createGenerator("airflow-scheduler"), - "TODO: add sample query", + generateQueryForComponent("sample-composer-environment", "test-project", "airflow-scheduler"), ) var ComposerDagProcessorManagerLogQueryTask = query.NewQueryGeneratorTask( @@ -52,7 +52,7 @@ var ComposerDagProcessorManagerLogQueryTask = query.NewQueryGeneratorTask( InputComposerEnvironmentTaskID, }, createGenerator("dag-processor-manager"), - "TODO: add sample query", + generateQueryForComponent("sample-composer-environment", "test-project", "dag-processor-manager"), ) var ComposerMonitoringLogQueryTask = query.NewQueryGeneratorTask( @@ -64,7 +64,7 @@ var ComposerMonitoringLogQueryTask = query.NewQueryGeneratorTask( InputComposerEnvironmentTaskID, }, createGenerator("airflow-monitoring"), - "TODO: add sample query", + generateQueryForComponent("sample-composer-environment", "test-project", "airflow-monitoring"), ) var ComposerWorkerLogQueryTask = query.NewQueryGeneratorTask( @@ -76,7 +76,7 @@ var ComposerWorkerLogQueryTask = query.NewQueryGeneratorTask( InputComposerEnvironmentTaskID, }, createGenerator("airflow-worker"), - "TODO: add sample query", + generateQueryForComponent("sample-composer-environment", "test-project", "airflow-worker"), ) func createGenerator(componentName string) func(ctx context.Context, i int, vs *task.VariableSet) ([]string, error) { @@ -94,14 +94,17 @@ func createGenerator(componentName string) func(ctx context.Context, i int, vs * return []string{}, err } - composerFilter := composerEnvironmentLog(environmentName) - schedulerFilter := logPath(projectId, componentName) - - return []string{fmt.Sprintf(`%s -%s`, composerFilter, schedulerFilter)}, nil + return []string{generateQueryForComponent(environmentName, projectId, componentName)}, nil } } +func generateQueryForComponent(environmentName string, projectId string, componentName string) string { + composerFilter := composerEnvironmentLog(environmentName) + schedulerFilter := logPath(projectId, componentName) + return fmt.Sprintf(`%s +%s`, composerFilter, schedulerFilter) +} + func composerEnvironmentLog(environmentName string) string { return fmt.Sprintf(`resource.type="cloud_composer_environment" resource.labels.environment_name="%s"`, environmentName)