Skip to content

Commit

Permalink
[ASCII-1025] Render check output using status component (#21881)
Browse files Browse the repository at this point in the history
[ASCII-1025] Render check output using status component
  • Loading branch information
GustavoCaso authored Jan 6, 2024
1 parent e5675a8 commit 5c918b0
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 388 deletions.
59 changes: 2 additions & 57 deletions cmd/agent/gui/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,14 @@ import (
"expvar"
"html/template"
"io"
"strings"

"github.com/DataDog/datadog-agent/comp/core/status"
"github.com/DataDog/datadog-agent/pkg/autodiscovery"
"github.com/DataDog/datadog-agent/pkg/collector"
"github.com/DataDog/datadog-agent/pkg/collector/check/stats"
"github.com/DataDog/datadog-agent/pkg/status/render"
)

var fmap = render.Fmap()

func init() {
fmap["lastErrorTraceback"] = lastErrorTraceback
fmap["lastErrorMessage"] = lastErrorMessage
fmap["pythonLoaderError"] = pythonLoaderError
fmap["status"] = displayStatus
}
var fmap = status.HTMLFmap()

// Data is a struct used for filling templates
type Data struct {
Expand Down Expand Up @@ -110,50 +102,3 @@ func fillTemplate(w io.Writer, data Data, request string) error {
e = t.Execute(w, data)
return e
}

/****** Helper functions for the template formatting ******/

func pythonLoaderError(value string) template.HTML {
value = template.HTMLEscapeString(value)

value = strings.Replace(value, "\n", "<br>", -1)
value = strings.Replace(value, " ", "&nbsp;&nbsp;&nbsp;", -1)
return template.HTML(value)
}

func lastErrorTraceback(value string) template.HTML {
var lastErrorArray []map[string]string

err := json.Unmarshal([]byte(value), &lastErrorArray)
if err != nil || len(lastErrorArray) == 0 {
return template.HTML("No traceback")
}

traceback := template.HTMLEscapeString(lastErrorArray[0]["traceback"])

traceback = strings.Replace(traceback, "\n", "<br>", -1)
traceback = strings.Replace(traceback, " ", "&nbsp;&nbsp;&nbsp;", -1)

return template.HTML(traceback)
}

func lastErrorMessage(value string) template.HTML {
var lastErrorArray []map[string]string
err := json.Unmarshal([]byte(value), &lastErrorArray)
if err == nil && len(lastErrorArray) > 0 {
if msg, ok := lastErrorArray[0]["message"]; ok {
value = msg
}
}
return template.HTML(template.HTMLEscapeString(value))
}

func displayStatus(check map[string]interface{}) template.HTML {
if check["LastError"].(string) != "" {
return template.HTML("[<span class=\"error\">ERROR</span>]")
}
if len(check["LastWarnings"].([]interface{})) != 0 {
return template.HTML("[<span class=\"warning\">WARNING</span>]")
}
return template.HTML("[<span class=\"ok\">OK</span>]")
}
3 changes: 2 additions & 1 deletion cmd/process-agent/subcommands/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/DataDog/datadog-agent/comp/core"
"github.com/DataDog/datadog-agent/comp/core/config"
"github.com/DataDog/datadog-agent/comp/core/log"
compStatus "github.com/DataDog/datadog-agent/comp/core/status"
"github.com/DataDog/datadog-agent/comp/process"
apiutil "github.com/DataDog/datadog-agent/pkg/api/util"
ddconfig "github.com/DataDog/datadog-agent/pkg/config"
Expand Down Expand Up @@ -94,7 +95,7 @@ func writeNotRunning(log log.Component, w io.Writer) {
}

func writeError(log log.Component, w io.Writer, e error) {
tpl, err := template.New("").Funcs(render.Textfmap()).Parse(errorMessage)
tpl, err := template.New("").Funcs(compStatus.TextFmap()).Parse(errorMessage)
if err != nil {
_ = log.Error(err)
}
Expand Down
88 changes: 73 additions & 15 deletions comp/core/status/render_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ func HTMLFmap() htemplate.FuncMap {
htmlFuncMap = htemplate.FuncMap{
"doNotEscape": doNotEscape,
"lastError": lastError,
"lastErrorTraceback": func(s string) htemplate.HTML { return doNotEscape(lastErrorTraceback(s)) },
"lastErrorMessage": func(s string) htemplate.HTML { return doNotEscape(lastErrorMessage(s)) },
"configError": configError,
"printDashes": PrintDashes,
"formatUnixTime": formatUnixTime,
Expand All @@ -47,14 +45,17 @@ func HTMLFmap() htemplate.FuncMap {
"toUnsortedList": toUnsortedList,
"formatTitle": formatTitle,
"add": add,
"status": status,
"redText": redText,
"yellowText": yellowText,
"greenText": greenText,
"ntpWarning": ntpWarning,
"version": getVersion,
"percent": func(v float64) string { return fmt.Sprintf("%02.1f", v*100) },
"complianceResult": complianceResult,
"lastErrorTraceback": lastErrorTracebackHTML,
"lastErrorMessage": lastErrorMessageHTML,
"pythonLoaderError": pythonLoaderErrorHTML,
"status": statusHTML,
}
})
return htmlFuncMap
Expand Down Expand Up @@ -126,22 +127,34 @@ func lastErrorMessage(value string) string {
}

// formatUnixTime formats the unix time to make it more readable
func formatUnixTime(unixTime int64) string {
func formatUnixTime(unixTime any) string {
// Initially treat given unixTime is in nanoseconds
t := time.Unix(0, int64(unixTime))
// If year returned 1970, assume unixTime actually in seconds
if t.Year() == time.Unix(0, 0).Year() {
t = time.Unix(int64(unixTime), 0)
parseFunction := func(value int64) string {
t := time.Unix(0, value)
// If year returned 1970, assume unixTime actually in seconds
if t.Year() == time.Unix(0, 0).Year() {
t = time.Unix(value, 0)
}

_, tzoffset := t.Zone()
result := t.Format(timeFormat)
if tzoffset != 0 {
result += " / " + t.UTC().Format(timeFormat)
}
msec := t.UnixNano() / int64(time.Millisecond)
result += " (" + strconv.Itoa(int(msec)) + ")"

return result
}

_, tzoffset := t.Zone()
result := t.Format(timeFormat)
if tzoffset != 0 {
result += " / " + t.UTC().Format(timeFormat)
switch v := unixTime.(type) {
case int64:
return parseFunction(v)
case float64:
return parseFunction(int64(v))
default:
return fmt.Sprintf("Invalid time parameter %T", v)
}
msec := t.UnixNano() / int64(time.Millisecond)
result += " (" + strconv.Itoa(int(msec)) + ")"
return result
}

// PrintDashes repeats the pattern (dash) for the length of s
Expand Down Expand Up @@ -284,3 +297,48 @@ func getVersion(instances map[string]interface{}) string {
}
return ""
}

func pythonLoaderErrorHTML(value string) htemplate.HTML {
value = htemplate.HTMLEscapeString(value)

value = strings.Replace(value, "\n", "<br>", -1)
value = strings.Replace(value, " ", "&nbsp;&nbsp;&nbsp;", -1)
return htemplate.HTML(value)
}

func lastErrorTracebackHTML(value string) htemplate.HTML {
var lastErrorArray []map[string]string

err := json.Unmarshal([]byte(value), &lastErrorArray)
if err != nil || len(lastErrorArray) == 0 {
return htemplate.HTML("No traceback")
}

traceback := htemplate.HTMLEscapeString(lastErrorArray[0]["traceback"])

traceback = strings.Replace(traceback, "\n", "<br>", -1)
traceback = strings.Replace(traceback, " ", "&nbsp;&nbsp;&nbsp;", -1)

return htemplate.HTML(traceback)
}

func lastErrorMessageHTML(value string) htemplate.HTML {
var lastErrorArray []map[string]string
err := json.Unmarshal([]byte(value), &lastErrorArray)
if err == nil && len(lastErrorArray) > 0 {
if msg, ok := lastErrorArray[0]["message"]; ok {
value = msg
}
}
return htemplate.HTML(htemplate.HTMLEscapeString(value))
}

func statusHTML(check map[string]interface{}) htemplate.HTML {
if check["LastError"].(string) != "" {
return htemplate.HTML("[<span class=\"error\">ERROR</span>]")
}
if len(check["LastWarnings"].([]interface{})) != 0 {
return htemplate.HTML("[<span class=\"warning\">WARNING</span>]")
}
return htemplate.HTML("[<span class=\"ok\">OK</span>]")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package render
package status

import (
"testing"
Expand Down
52 changes: 44 additions & 8 deletions pkg/cli/subcommands/check/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
"github.com/DataDog/datadog-agent/comp/core/flare"
"github.com/DataDog/datadog-agent/comp/core/log/logimpl"
"github.com/DataDog/datadog-agent/comp/core/secrets"
"github.com/DataDog/datadog-agent/comp/core/status"
"github.com/DataDog/datadog-agent/comp/core/status/statusimpl"
"github.com/DataDog/datadog-agent/comp/core/sysprobeconfig/sysprobeconfigimpl"
"github.com/DataDog/datadog-agent/comp/core/workloadmeta"
"github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors"
Expand All @@ -57,13 +59,11 @@ import (
"github.com/DataDog/datadog-agent/pkg/cli/standalone"
"github.com/DataDog/datadog-agent/pkg/collector"
"github.com/DataDog/datadog-agent/pkg/collector/check"
checkid "github.com/DataDog/datadog-agent/pkg/collector/check/id"
"github.com/DataDog/datadog-agent/pkg/collector/check/stats"
pkgconfig "github.com/DataDog/datadog-agent/pkg/config"
"github.com/DataDog/datadog-agent/pkg/config/model"
"github.com/DataDog/datadog-agent/pkg/serializer"
statuscollector "github.com/DataDog/datadog-agent/pkg/status/collector"
"github.com/DataDog/datadog-agent/pkg/status/render"
"github.com/DataDog/datadog-agent/pkg/util/fxutil"
"github.com/DataDog/datadog-agent/pkg/util/optional"
"github.com/DataDog/datadog-agent/pkg/util/scrubber"
Expand Down Expand Up @@ -176,6 +176,11 @@ func MakeCommand(globalParamsGetter func() GlobalParams) *cobra.Command {
return params
}),

fx.Supply(
status.NewInformationProvider(statuscollector.Provider{}),
),
statusimpl.Module(),

// TODO(components): this is a temporary hack as the StartServer() method of the API package was previously called with nil arguments
// This highlights the fact that the API Server created by JMX (through ExecJmx... function) should be different from the ones created
// in others commands such as run.
Expand Down Expand Up @@ -234,6 +239,7 @@ func run(
secretResolver secrets.Component,
agentAPI internalAPI.Component,
invChecks inventorychecks.Component,
statusComponent status.Component,
) error {
previousIntegrationTracing := false
previousIntegrationTracingExhaustive := false
Expand Down Expand Up @@ -428,12 +434,35 @@ func run(
var checkFileOutput bytes.Buffer
var instancesData []interface{}
printer := aggregator.AgentDemultiplexerPrinter{DemultiplexerWithAggregator: demultiplexer}
collectorData := statuscollector.GetStatusInfo()
data, err := statusComponent.GetStatusBySection(status.CollectorSection, "json", false)

if err != nil {
return err
}

collectorData := map[string]interface{}{}
err = json.Unmarshal(data, &collectorData)

if err != nil {
return err
}

checkRuns := collectorData["runnerStats"].(map[string]interface{})["Checks"].(map[string]interface{})
for _, c := range cs {
s := runCheck(cliParams, c, printer)
checkMap := make(map[checkid.ID]*stats.Stats)
checkMap[c.ID()] = s
resultBytes, err := json.Marshal(s)
if err != nil {
return err
}

var checkResult map[string]interface{}
err = json.Unmarshal(resultBytes, &checkResult)
if err != nil {
return err
}

checkMap := make(map[string]interface{})
checkMap[string(c.ID())] = checkResult
checkRuns[c.String()] = checkMap

// Sleep for a while to allow the aggregator to finish ingesting all the metrics/events/sc
Expand Down Expand Up @@ -510,9 +539,16 @@ func run(
checkFileOutput.WriteString(data + "\n")
}

statusJSON, _ := json.Marshal(collectorData)
checkStatus, _ := render.FormatCheckStats(statusJSON)
p(checkStatus)
// workaround for this one use case of the status component
// we want to render the collector text format with custom data
collectorProvider := statuscollector.Provider{}
buffer := new(bytes.Buffer)
err := collectorProvider.TextWithData(buffer, collectorData)
if err != nil {
return err
}

p(buffer.String())

p(" Metadata\n ========")

Expand Down
40 changes: 40 additions & 0 deletions pkg/status/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ package collector
import (
"encoding/json"
"expvar"
"io"

"github.com/DataDog/datadog-agent/comp/core/status"
"github.com/DataDog/datadog-agent/pkg/status/render"
)

// GetStatusInfo retrives collector information
Expand Down Expand Up @@ -88,3 +92,39 @@ func PopulateStatus(stats map[string]interface{}) {
}
stats["inventories"] = checkMetadata
}

// Provider provides the functionality to populate the status output with the collector information
type Provider struct{}

// Name returns the name
func (Provider) Name() string {
return "Collector"
}

// Section return the section
func (Provider) Section() string {
return status.CollectorSection
}

// JSON populates the status map
func (Provider) JSON(stats map[string]interface{}) error {
PopulateStatus(stats)

return nil
}

// Text populates the status buffer with the human readbable version
func (Provider) Text(buffer io.Writer) error {
return render.ParseTemplate(buffer, "/collector.tmpl", GetStatusInfo())
}

// HTML populates the status buffer with the HTML version
func (Provider) HTML(buffer io.Writer) error {
return render.ParseHTMLTemplate(buffer, "/collectorHTML.tmpl", GetStatusInfo())
}

// TextWithData allows to render the human reaadable version with custom data
// This is a hack only needed for the agent check subcommand
func (Provider) TextWithData(buffer io.Writer, data any) error {
return render.ParseTemplate(buffer, "/collector.tmpl", data)
}
Loading

0 comments on commit 5c918b0

Please sign in to comment.