From 2858e5fac1ffa244a6e468cecd9da59e345b11b8 Mon Sep 17 00:00:00 2001 From: Flynn Date: Mon, 25 Mar 2024 14:56:27 -0400 Subject: [PATCH 1/7] Rationalize color handling: funnel through Color.lookup, and default to blue Signed-off-by: Flynn --- assets/html/index.html | 145 +++++++++++++++++---------------------- faces-chart/values.yaml | 4 +- pkg/faces/colorserver.go | 7 +- pkg/faces/constants.go | 89 +++++++++++++++++++----- pkg/faces/faceserver.go | 4 +- 5 files changed, 145 insertions(+), 104 deletions(-) diff --git a/assets/html/index.html b/assets/html/index.html index b80af38..6cf8def 100644 --- a/assets/html/index.html +++ b/assets/html/index.html @@ -25,22 +25,6 @@ font-family: sans-serif; } - .green { - color: rgb(55 117 59); - } - - .red { - color: rgb(125 42 83); /* dark magenta */ - } - - .blue { - color: rgb(151 202 234); /* light blue */ - } - - .grey { - color: grey; - } - .key { font-family: sans-serif; font-size: 32px; @@ -183,22 +167,6 @@ width: 252px !important; } - .bg-green { - background-color: rgb(55 117 59); - } - - .bg-red { - background-color: rgb(125 42 83); /* dark magenta */ - } - - .bg-blue { - background-color: rgb(151 202 234); /* light blue */ - } - - .bg-grey { - background-color: grey; - } - .inline { display: inline; } @@ -297,7 +265,7 @@

Faces

logmsg(color, msg) { let now = new Date().toISOString() console.log(`${now} ${msg}`) - // this.logdiv.innerHTML = `${now}: ${msg}
` + this.logdiv.innerHTML + // this.logdiv.innerHTML = `${now}: ${msg}
` + this.logdiv.innerHTML } // success, fail, and info are wrappers around logmsg to avoid @@ -308,7 +276,7 @@

Faces

} fail(msg) { - this.logmsg(Cell.colors.red, msg) + this.logmsg(Cell.colors.purple, msg) } info(msg) { @@ -369,7 +337,7 @@

Faces

} start() { - this.userDiv.innerHTML = `User: ${this.user}` + this.userDiv.innerHTML = `User: ${this.user}` } // stop() { @@ -435,12 +403,12 @@

Faces

if (status == 599) { // Latched error state. smiley = Cell.smilies.neutral - bgColor = Cell.colors.hotpink + bgColor = Cell.colors.yellow bumpCounter = true } else if (status == 429) { smiley = Cell.smilies.sleeping - bgColor = Cell.colors.pink + bgColor = Cell.colors.red bumpCounter = true } else if (status == 200) { @@ -449,7 +417,7 @@

Faces

} if (bgColor == "504") { - bgColor = Cell.colors.pink + bgColor = Cell.colors.red } this.countDiv.innerHTML = 0 @@ -457,9 +425,9 @@

Faces

} else { // This should probably never happen. - borderColor = Cell.colors.red + borderColor = Cell.colors.purple smiley = Cell.smilies.upset - bgColor = Cell.colors.red + bgColor = Cell.colors.purple bumpCounter = true } @@ -570,16 +538,19 @@

Faces

"yay": "🎉", }; + // There are many many notes about these colors in pkg/faces/constants.go. + // Go read that: the short version is that colorblindness is a thing, so + // don't muck with these too much. static colors = { - "grey": "grey", - "purple": "rgb(48 34 130)", - "green": "rgb(55 117 59)", - "cyan": "rgb(55 117 59)", // too similar to pink and grey, avoid - "blue": "rgb(151 202 234)", // light blue - "yellow": "rgb(218 204 130)", - "pink": "rgb(191 108 120)", - "hotpink": "rgb(158 75 149)", - "red": "rgb(125 42 83)", // dark magenta + "grey": "#BBBBBB", + "black": "#000000", + "white": "#FFFFFF", + "darkblue": "#4477AA", + "blue": "#66CCEE", + "green": "#228833", + "yellow": "#CCBB44", + "red": "#EE6677", + "purple": "#AA3377", } constructor(logger, sw, podSet, fetchURL, enclosingDiv, row, col) { @@ -596,7 +567,7 @@

Faces

let cellDiv = document.createElement("div") cellDiv.id = `cell-${row}-${col}` cellDiv.className = "cell" - cellDiv.style.background = "grey" + cellDiv.style.background = Cell.colors.grey cellDiv.style.opacity = 0.5 let smileySpan = document.createElement("span") @@ -672,7 +643,10 @@

Faces

// ...then figure out what we got. let { curStatus, anyTimeouts, - smiley, bgColor, borderColor } = this.parseResults(xhr); + smiley, bgColor, borderColor, errors } = this.parseResults(xhr); + + // let msg = `[${xhrName}] (${latency}ms): ${smiley} ${bgColor} ${borderColor} -- ${errors}` + // this.success(msg); // Update the pod, if we can... let pod = xhr.getResponseHeader("x-faces-pod"); @@ -721,10 +695,6 @@

Faces

$(`cell-count-${this.row}-${this.col}`).innerHTML = "" } - // FINALLY: show 'em what we got. - // let msg = `[${xhrName}] XHR result (${latency}ms): ${errors} -- ${text}` - // this.success(msg); - if ((smiley != undefined) || (bgColor != undefined)) { $(`cell-${this.row}-${this.col}`).style.opacity = 0.0 @@ -767,7 +737,7 @@

Faces

setTimeout(() => { $(`smiley-${this.row}-${this.col}`).innerHTML = Cell.smilies.confused $(`cell-${this.row}-${this.col}`).style.opacity = 1.0 - $(`cell-${this.row}-${this.col}`).style.background = Cell.colors.red + $(`cell-${this.row}-${this.col}`).style.background = Cell.colors.purple $(`cell-${this.row}-${this.col}`).style.borderColor = "grey" }, 50) @@ -818,9 +788,9 @@

Faces

smiley = obj.smiley; bgColor = obj.color; - if (obj.errors != undefined) { + if ((obj.errors != undefined) && (obj.errors.length > 0)) { errors = obj.errors.join(","); - borderColor = Cell.colors.red + borderColor = Cell.colors.purple } else { errors = "success!"; @@ -836,9 +806,9 @@

Faces

catch (e) { // Whoops, something went wrong. If it's a SyntaxError, that // probably means we got bad JSON. Otherwise, it's... who knows? - borderColor = Cell.colors.red + borderColor = Cell.colors.purple smiley = Cell.smilies.confused; - bgColor = Cell.colors.red; + bgColor = Cell.colors.purple; if (e instanceof SyntaxError) { errors = "parse error"; @@ -860,16 +830,16 @@

Faces

} else if (curStatus == 599) { // This is our latched-error status. Show it as a neutral face - // on a hot pink background. + // on a yellow background. smiley = Cell.smilies.neutral; - bgColor = Cell.colors.hotpink; + bgColor = Cell.colors.yellow; } else if (Math.floor(curStatus / 100) == 5) { // Some other 5yz, so an unknown kind of server error. (In // practice, this is probably a 503 because there's some kind // of connectivity error, but whatever, we don't care. smiley = Cell.smilies.confused; - bgColor = Cell.colors.red; + bgColor = Cell.colors.purple; errors = "server error"; } @@ -881,24 +851,37 @@

Faces

bgColor = Cell.colors.purple } - return { curStatus, anyTimeouts, smiley, bgColor, borderColor }; + return { curStatus, anyTimeouts, smiley, bgColor, borderColor, errors }; } } class Key { constructor(keyDiv) { let keyEntries = [ - [ "smiling", Cell.colors.blue, Cell.colors.grey, "24px", "Success!" ], - [ "confused", Cell.colors.red, Cell.colors.grey, "", "Face service error" ], - [ "sleeping", Cell.colors.pink, Cell.colors.grey, "", "Timeout" ], - [ "kaboom", Cell.colors.red, Cell.colors.grey, "24px", "Service overwhelmed" ], - [ "smiling", Cell.colors.grey, "transparent", "", "Color service error" ], - [ "", Cell.colors.blue, Cell.colors.red, "24px", "Smiley service error" ], - [ "-", "-", "-", "", "Slow service" ] + [ "Success!", + "grinning", Cell.colors.blue, Cell.colors.grey, "24px" ], + + [ "Face service error", + "confused", Cell.colors.purple, Cell.colors.grey, "" ], + + [ "Timeout", + "sleeping", Cell.colors.red, Cell.colors.grey, "" ], + + [ "Service overwhelmed", + "kaboom", Cell.colors.yellow, Cell.colors.purple, "24px" ], + + [ "Color service error", + "grinning", Cell.colors.grey, Cell.colors.purple, "" ], + + [ "Smiley service error", + "", Cell.colors.blue, Cell.colors.purple, "24px" ], + + [ "Slow service", + "-", "-", "-", "" ] ] for (let i = 0; i < keyEntries.length; i++) { - let [ smileyName, bgColor, borderColor, margin, text ] = keyEntries[i] + let [ text, smileyName, bgColor, borderColor, margin ] = keyEntries[i] if (smileyName != "-") { let smiley = "" @@ -998,11 +981,11 @@

Faces

if (cellCount > 4) { let smiley = Cell.smilies.sleeping - let bgColor = Cell.colors.pink + let bgColor = Cell.colors.red if (cell.lastStatus == 599) { - smiley = Cell.smilies.screaming - bgColor = Cell.colors.hotpink + smiley = Cell.smilies.neutral + bgColor = Cell.colors.yellow } $(`smiley-${cell.row}-${cell.col}`).innerHTML = smiley @@ -1044,7 +1027,7 @@

Faces

mark(shape, fgColor, bgColor) { this.markerdiv.innerHTML += ` -
+
@@ -1114,7 +1097,7 @@

Faces

else if (xhr.status != 200) { text = `Unknown status ${xhr.status} after ${latency}ms` shape = "19.000,49.000 31.000,49.000 31.000,1.000 19.000,1.000" - fgColor = Cell.colors.red + fgColor = Cell.colors.purple } else { // Parse JSON! @@ -1130,12 +1113,12 @@

Faces

if (e instanceof SyntaxError) { text = `Could not parse QotM: ${e.message}\n${xhr.responseText}` shape = "19.000,49.000 31.000,49.000 31.000,1.000 19.000,1.000" - fgColor = Cell.colors.red + fgColor = Cell.colors.purple } else { text = `Missing QotM? ${e.message}` shape = "19.000,49.000 31.000,49.000 31.000,1.000 19.000,1.000" - fgColor = Cell.colors.red + fgColor = Cell.colors.purple } } } @@ -1160,7 +1143,7 @@

Faces

// this.success(msg); let nowISO = now.toISOString() - this.xhrdiv.innerHTML = `

${nowISO}: ${msg}

` + this.xhrdiv.innerHTML = `

${nowISO}: ${msg}

` this.markers.mark(shape, fgColor, bgColor) }) @@ -1178,7 +1161,7 @@

Faces

// this.fail(msg); let nowISO = now.toISOString() - this.xhrdiv.innerHTML = `

${nowISO}: Failed!

` + this.xhrdiv.innerHTML = `

${nowISO}: Failed!

` // FIXME: this looks like the wrong arguments for calling this.markers.mark this.markers.mark("red") diff --git a/faces-chart/values.yaml b/faces-chart/values.yaml index c5c2ca7..6ce074d 100644 --- a/faces-chart/values.yaml +++ b/faces-chart/values.yaml @@ -58,7 +58,7 @@ color: imagePullPolicy: "" # If not set, uses backend.imagePullPolicy errorFraction: "" # If not set, uses backend.errorFraction delayBuckets: "" # If not set, uses backend.delayBuckets - color: "rgb(151 202 234)" # Override if desired, defaults to colorblind-friendly light blue from the Tol palette + color: "blue" # Override if desired, defaults to colorblind-friendly light blue from the Tol palette color2: enabled: False # If set to True, enables the second color workload @@ -68,4 +68,4 @@ color2: imagePullPolicy: "" # If not set, uses backend.imagePullPolicy errorFraction: "" # If not set, uses backend.errorFraction delayBuckets: "" # If not set, uses backend.delayBuckets - color: "rgb(55 117 59)" # Override if desired, defaults to colorblind-friendly green from the Tol palette + color: "green" # Override if desired, defaults to colorblind-friendly green from the Tol palette diff --git a/pkg/faces/colorserver.go b/pkg/faces/colorserver.go index 6b7f613..d59806c 100644 --- a/pkg/faces/colorserver.go +++ b/pkg/faces/colorserver.go @@ -48,9 +48,10 @@ func NewColorServer(serverName string) *ColorServer { func (srv *ColorServer) SetupFromEnvironment() { srv.BaseServer.SetupFromEnvironment() - srv.color = utils.StringFromEnv("COLOR", "green") + colorName := utils.StringFromEnv("COLOR", "blue") + srv.color = Colors.Lookup(colorName) - fmt.Printf("%s %s: color %s\n", time.Now().Format(time.RFC3339), srv.Name, srv.color) + fmt.Printf("%s %s: color %s => %s\n", time.Now().Format(time.RFC3339), srv.Name, colorName, srv.color) } func (srv *ColorServer) colorGetHandler(r *http.Request, rstat *BaseRequestStatus) *BaseServerResponse { @@ -61,7 +62,7 @@ func (srv *ColorServer) colorGetHandler(r *http.Request, rstat *BaseRequestStatu return &BaseServerResponse{ StatusCode: http.StatusTooManyRequests, Data: map[string]interface{}{ - "color": Defaults["color-ratelimit"], + "color": Colors.Lookup(Defaults["color-ratelimit"]), "rate": fmt.Sprintf("%.1f RPS", srv.CurrentRate()), "errors": []string{errstr}, }, diff --git a/pkg/faces/constants.go b/pkg/faces/constants.go index b55a5b8..327202b 100644 --- a/pkg/faces/constants.go +++ b/pkg/faces/constants.go @@ -28,31 +28,88 @@ var Smileys = map[string]string{ "Screaming": "😱", } -// colorblind-friendly colors from the Tol palette -var Colors = map[string]string{ - "grey": "grey", - "purple": "rgb(48 34 130)", - "green": "rgb(55 117 59)", - "cyan": "rgb(55 117 59)", // too similar to pink and grey, avoid - "blue": "rgb(151 202 234)", // light blue - "yellow": "rgb(218 204 130)", - "pink": "rgb(191 108 120)", - "hotpink": "rgb(158 75 149)", - "red": "rgb(125 42 83)", // dark magenta +type Palette struct { + colors map[string]string +} + +// These colors are from the "Bright" color scheme shown in the "Qualitative +// Color Schemes" section of https://personal.sron.nl/~pault/. The notes about +// color pairs to avoid are from using https://davidmathlogic.com/colorblind/ +// and from using the Python colorspacious module to compute distances between +// color pairs. +// +// The color names are from https://personal.sron.nl/~pault _except_ that I'm +// using "darkblue" for 4477AA, and "blue" for 66CCEE, because overall 4477AA +// turns out to cause more trouble for colorblind folks than 66CCEE. +// +// Specific problematic pairs: +// +// Protanopia/deuteranopia: darkblue/purple, green/red, grey/red, and maybe +// blue/grey (the math says it's a problem, looking at davidmathlogic seems +// like probably not?) +// +// Deuteranopia: yellow/red might be a problem, according to the math +// +// Tritanopia: this is more rare than the others, but darkblue and green are +// almost identical to this crowd, and the yellow/grey pair is troubling too. +// +// So, by default, Faces uses: +// +// blue (66CCEE) for color workload success +// grey (BBBBBB) for color workload error +// purple (AA3377) for when the face workload can't talk to the color +// workload at all +// red (EE6677) for a color timeout and +// yellow (CCBB44) for a latched error state +// +// and, hopefully, that's a decent compromise. + +var Colors = Palette{ + colors: map[string]string{ + // Include grey/black/white because they're sometimes convenient. + "grey": "#BBBBBB", + "black": "#000000", + "white": "#FFFFFF", + + // See lots of notes above. + "darkblue": "#4477AA", + "blue": "#66CCEE", + "green": "#228833", + "yellow": "#CCBB44", + "red": "#EE6677", + "purple": "#AA3377", + }, +} + +func (p *Palette) Lookup(name string) string { + if color, ok := p.colors[name]; ok { + return color + } + + // If the color starts with '#', assume it's a hex color code and + // return it as-is. + + if name[0] == '#' { + return name + } + + // It doesn't look like a hex code and it's not a color code we know, + // so just return yellow as a fallback. + return p.colors["yellow"] } var Defaults = map[string]string{ // Default to grey background, cursing face. - "color": Colors["grey"], + "color": "grey", "smiley": Smileys["Cursing"], // 504 errors (GatewayTimeout) from the face workload will get handled in // the GUI, but from the color & smiley workloads, they should get - // translated to a pink color and a sleeping face. - "color-504": Colors["pink"], + // translated to a red color and a sleeping face. + "color-504": "red", "smiley-504": Smileys["Sleeping"], - // Ratelimits are pink with an exploding head. - "color-ratelimit": Colors["pink"], + // Ratelimits are yellow with an exploding head. + "color-ratelimit": "yellow", "smiley-ratelimit": Smileys["Kaboom"], } diff --git a/pkg/faces/faceserver.go b/pkg/faces/faceserver.go index 01f833e..ddcfd38 100644 --- a/pkg/faces/faceserver.go +++ b/pkg/faces/faceserver.go @@ -187,7 +187,7 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) if rstat.IsRateLimited() { errors = append(errors, rstat.Message()) smiley = Defaults["smiley-ratelimit"] - color = Defaults["color-ratelimit"] + color = Colors.Lookup(Defaults["color-ratelimit"]) } else { user := r.Header.Get("X-Faces-User") @@ -227,7 +227,7 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) if colorResp.statusCode != http.StatusOK { errors = append(errors, fmt.Sprintf("color: %s", colorResp.data)) - color = mapStatus("color", colorResp.statusCode) + color = Colors.Lookup(mapStatus("color", colorResp.statusCode)) } else { color = colorResp.data } From 5a9d388d6ef0f345decdf3ac9eb16f2d2bcf7335 Mon Sep 17 00:00:00 2001 From: Flynn Date: Mon, 25 Mar 2024 15:06:47 -0400 Subject: [PATCH 2/7] Update logo-128.png with new color Signed-off-by: Flynn --- assets/logo-128.png | Bin 17951 -> 18035 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/logo-128.png b/assets/logo-128.png index 70f696eff4097bfe5e1dd234583d4f4ce7c15fd9..46445f840586cb786d4295df51b1829157a2aa06 100644 GIT binary patch delta 17715 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 mep(9bFu Date: Mon, 25 Mar 2024 18:19:58 -0400 Subject: [PATCH 3/7] Rationalize smiley handling, too, funnelling through Smileys.Lookup Signed-off-by: Flynn --- pkg/faces/constants.go | 41 +++++++++++++++++++++++++++------------ pkg/faces/faceserver.go | 17 ++++++++++++++-- pkg/faces/smileyserver.go | 15 ++++++++++---- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/pkg/faces/constants.go b/pkg/faces/constants.go index 327202b..f7c2c79 100644 --- a/pkg/faces/constants.go +++ b/pkg/faces/constants.go @@ -17,15 +17,32 @@ package faces -var Smileys = map[string]string{ - "Smiling": "😃", - "Sleeping": "😴", - "Cursing": "🤬", - "Kaboom": "🤯", - "HeartEyes": "😍", - "Neutral": "😐", - "RollingEyes": "🙄", - "Screaming": "😱", +type SmileyMap struct { + smileys map[string]string +} + +var Smileys = SmileyMap{ + smileys: map[string]string{ + "Smiling": "😃", + "Sleeping": "😴", + "Cursing": "🤬", + "Kaboom": "🤯", + "HeartEyes": "😍", + "Neutral": "😐", + "RollingEyes": "🙄", + "Screaming": "😱", + "Vomiting": "🤮", + }, +} + +// Lookup a smiley by name. If found, return the HTML entity for the smiley +// and true; if not found, return an empty string and false. +func (sm *SmileyMap) Lookup(name string) (string, bool) { + if smiley, ok := sm.smileys[name]; ok { + return smiley, true + } + + return "", false } type Palette struct { @@ -101,15 +118,15 @@ func (p *Palette) Lookup(name string) string { var Defaults = map[string]string{ // Default to grey background, cursing face. "color": "grey", - "smiley": Smileys["Cursing"], + "smiley": "Cursing", // 504 errors (GatewayTimeout) from the face workload will get handled in // the GUI, but from the color & smiley workloads, they should get // translated to a red color and a sleeping face. "color-504": "red", - "smiley-504": Smileys["Sleeping"], + "smiley-504": "Sleeping", // Ratelimits are yellow with an exploding head. "color-ratelimit": "yellow", - "smiley-ratelimit": Smileys["Kaboom"], + "smiley-ratelimit": "Kaboom", } diff --git a/pkg/faces/faceserver.go b/pkg/faces/faceserver.go index ddcfd38..8980e5f 100644 --- a/pkg/faces/faceserver.go +++ b/pkg/faces/faceserver.go @@ -181,12 +181,13 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) errors := []string{} var smiley string var color string + var smileyOK bool rateStr := fmt.Sprintf("%.1f RPS", srv.CurrentRate()) if rstat.IsRateLimited() { errors = append(errors, rstat.Message()) - smiley = Defaults["smiley-ratelimit"] + smiley, smileyOK = Smileys.Lookup(Defaults["smiley-ratelimit"]) color = Colors.Lookup(Defaults["color-ratelimit"]) } else { user := r.Header.Get("X-Faces-User") @@ -218,9 +219,16 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) if smileyResp.statusCode != http.StatusOK { errors = append(errors, fmt.Sprintf("smiley: %s", smileyResp.data)) - smiley = mapStatus("smiley", smileyResp.statusCode) + mapped := mapStatus("smiley", smileyResp.statusCode) + smiley, smileyOK = Smileys.Lookup(mapped) + + if srv.debugEnabled { + fmt.Printf("%s %s: mapped smiley %d to %s (%s, %v)\n", + time.Now().Format(time.RFC3339), srv.Name, smileyResp.statusCode, mapped, smiley, smileyOK) + } } else { smiley = smileyResp.data + smileyOK = true } colorResp := <-colorCh @@ -233,6 +241,11 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) } } + if !smileyOK { + // Something bizarre happened with the smiley lookup? + smiley, _ = Smileys.Lookup("Vomiting") + } + end := time.Now() latency := end.Sub(start) diff --git a/pkg/faces/smileyserver.go b/pkg/faces/smileyserver.go index d8b0aac..1eaebb3 100644 --- a/pkg/faces/smileyserver.go +++ b/pkg/faces/smileyserver.go @@ -50,26 +50,33 @@ func (srv *SmileyServer) SetupFromEnvironment() { smileyKey := utils.StringFromEnv("SMILEY", "Smiling") - smiley, ok := Smileys[smileyKey] + smiley, ok := Smileys.Lookup(smileyKey) if !ok { smileyKey = "Neutral" - smiley = Smileys[smileyKey] + smiley, _ = Smileys.Lookup(smileyKey) } srv.smiley = smiley - fmt.Printf("%s %s: smiley %s\n", time.Now().Format(time.RFC3339), srv.Name, smileyKey) + fmt.Printf("%s %s: smiley %s (%s)\n", time.Now().Format(time.RFC3339), srv.Name, smileyKey, srv.smiley) } func (srv *SmileyServer) smileyGetHandler(r *http.Request, rstat *BaseRequestStatus) *BaseServerResponse { // The only error we need to handle here is the internal rate limiter. if rstat.ratelimited { + smiley, ok := Smileys.Lookup(Defaults["smiley-ratelimit"]) + + if !ok { + // This isn't good. + smiley, _ = Smileys.Lookup("Vomiting") + } + errstr := fmt.Sprintf("Rate limited (%.1f RPS > max %.1f RPS)", srv.CurrentRate(), srv.maxRate) return &BaseServerResponse{ StatusCode: http.StatusTooManyRequests, Data: map[string]interface{}{ - "smiley": Defaults["smiley-ratelimit"], + "smiley": smiley, "rate": fmt.Sprintf("%.1f RPS", srv.CurrentRate()), "errors": []string{errstr}, }, From a886945124dc145e9272280244fe83c60b2297a3 Mon Sep 17 00:00:00 2001 From: Flynn Date: Tue, 26 Mar 2024 12:39:58 -0400 Subject: [PATCH 4/7] Switch "smiling" to "grinning" because that's the actual name of the smiley. Signed-off-by: Flynn --- assets/html/index.html | 2 +- pkg/faces/constants.go | 2 +- pkg/faces/smileyserver.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/html/index.html b/assets/html/index.html index 6cf8def..f1f3864 100644 --- a/assets/html/index.html +++ b/assets/html/index.html @@ -531,7 +531,7 @@

Faces

"neutral": "😐", "screaming": "😱", "sleeping": "😴", - "smiling": "😃", + "grinning": "😃", "thinking": "🤔", "tongue": "😛", "upset": "😬", diff --git a/pkg/faces/constants.go b/pkg/faces/constants.go index f7c2c79..a02772f 100644 --- a/pkg/faces/constants.go +++ b/pkg/faces/constants.go @@ -23,7 +23,7 @@ type SmileyMap struct { var Smileys = SmileyMap{ smileys: map[string]string{ - "Smiling": "😃", + "Grinning": "😃", "Sleeping": "😴", "Cursing": "🤬", "Kaboom": "🤯", diff --git a/pkg/faces/smileyserver.go b/pkg/faces/smileyserver.go index 1eaebb3..3993660 100644 --- a/pkg/faces/smileyserver.go +++ b/pkg/faces/smileyserver.go @@ -48,7 +48,7 @@ func NewSmileyServer(serverName string) *SmileyServer { func (srv *SmileyServer) SetupFromEnvironment() { srv.BaseServer.SetupFromEnvironment() - smileyKey := utils.StringFromEnv("SMILEY", "Smiling") + smileyKey := utils.StringFromEnv("SMILEY", "Grinning") smiley, ok := Smileys.Lookup(smileyKey) From 47ee8d3bd73fedb90f05633cb949b4a4f41fafbc Mon Sep 17 00:00:00 2001 From: Flynn Date: Mon, 25 Mar 2024 18:22:10 -0400 Subject: [PATCH 5/7] Clean up chart to allow color and smiley to not unnecessarily duplicate setting defaults Signed-off-by: Flynn --- faces-chart/templates/color.yaml | 2 ++ faces-chart/templates/color2.yaml | 2 ++ faces-chart/templates/smiley.yaml | 2 ++ faces-chart/templates/smiley2.yaml | 2 ++ faces-chart/values.yaml | 4 ++-- 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/faces-chart/templates/color.yaml b/faces-chart/templates/color.yaml index 1b6ae5c..a91d436 100644 --- a/faces-chart/templates/color.yaml +++ b/faces-chart/templates/color.yaml @@ -37,8 +37,10 @@ spec: env: - name: FACES_SERVICE value: "color" + {{- if .Values.color.color }} - name: COLOR value: {{ .Values.color.color }} + {{- end -}} {{- include "partials.color-errorFraction" . }} {{- include "partials.color-delayBuckets" . }} resources: diff --git a/faces-chart/templates/color2.yaml b/faces-chart/templates/color2.yaml index f7a76af..95c83ea 100644 --- a/faces-chart/templates/color2.yaml +++ b/faces-chart/templates/color2.yaml @@ -38,8 +38,10 @@ spec: env: - name: FACES_SERVICE value: "color" + {{- if .Values.color2.color }} - name: COLOR value: {{ .Values.color2.color }} + {{- end -}} {{- include "partials.color2-errorFraction" . }} {{- include "partials.color2-delayBuckets" . }} resources: diff --git a/faces-chart/templates/smiley.yaml b/faces-chart/templates/smiley.yaml index 37ba530..ae5b66e 100644 --- a/faces-chart/templates/smiley.yaml +++ b/faces-chart/templates/smiley.yaml @@ -37,8 +37,10 @@ spec: env: - name: FACES_SERVICE value: "smiley" + {{- if .Values.smiley.smiley }} - name: SMILEY value: {{ .Values.smiley.smiley }} + {{- end -}} {{- include "partials.smiley-errorFraction" . }} {{- include "partials.smiley-delayBuckets" . }} resources: diff --git a/faces-chart/templates/smiley2.yaml b/faces-chart/templates/smiley2.yaml index 771127d..f61477c 100644 --- a/faces-chart/templates/smiley2.yaml +++ b/faces-chart/templates/smiley2.yaml @@ -38,8 +38,10 @@ spec: env: - name: FACES_SERVICE value: "smiley" + {{- if .Values.smiley2.smiley }} - name: SMILEY value: {{ .Values.smiley2.smiley }} + {{- end -}} {{- include "partials.smiley2-errorFraction" . }} {{- include "partials.smiley2-delayBuckets" . }} resources: diff --git a/faces-chart/values.yaml b/faces-chart/values.yaml index 6ce074d..feeeda0 100644 --- a/faces-chart/values.yaml +++ b/faces-chart/values.yaml @@ -39,7 +39,7 @@ smiley: imagePullPolicy: "" # If not set, uses backend.imagePullPolicy errorFraction: "" # If not set, uses backend.errorFraction delayBuckets: "" # If not set, uses backend.delayBuckets - smiley: "Smiling" # Override if desired + smiley: "" # Override if desired smiley2: enabled: False # If set to True, enables the second smiley workload @@ -58,7 +58,7 @@ color: imagePullPolicy: "" # If not set, uses backend.imagePullPolicy errorFraction: "" # If not set, uses backend.errorFraction delayBuckets: "" # If not set, uses backend.delayBuckets - color: "blue" # Override if desired, defaults to colorblind-friendly light blue from the Tol palette + color: "" # Override if desired, defaults to colorblind-friendly light blue from the Tol palette color2: enabled: False # If set to True, enables the second color workload From c6327d6bf17058a7368b84b51155c8a4198a6b02 Mon Sep 17 00:00:00 2001 From: Flynn Date: Tue, 26 Mar 2024 12:49:34 -0400 Subject: [PATCH 6/7] Clean up README and chart description Signed-off-by: Flynn --- README.md | 43 +++++++++++++++++++++++++++++++++++++----- faces-chart/Chart.yaml | 4 ++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ef03fec..a1376f2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Faces Demo This is the Faces demo application. It has a single-page web GUI that presents -a grid of cells, each of which _should_ show a smiling face on a green +a grid of cells, each of which _should_ show a grinning face on a light blue background. Spoiler alert: installed exactly as committed to this repo, that isn't what you'll get -- many, many things can go wrong, and will. The point of the demo is let you try to fix things. @@ -16,10 +16,14 @@ In here you will find: - These things are installed in a demo configuration: read and think **carefully** before using this demo as background for a production installation! In particular: - - We use `sed` to force everything to just one replica when installing - Emissary -- **DON'T** do that in production. - - We only configure HTTP, not HTTPS. Again, **DON'T** do this in - production. + + - We deploy Emissary with only one replica of everything, using a + currently-unofficial chart to also skip support for `v1` and `v2` + Emissary CRDs. + + - We only configure HTTP, not HTTPS. + + These are likely both bad ideas for a production installation. - `DEMO.md`, a Markdown file for the resilience demo presented live for a couple of events. The easiest way to use `DEMO.md` is to run it with @@ -53,6 +57,35 @@ In here you will find: - To run the demo as we've given it before, check out [DEMO.md]. The easiest way to use that is to run it with [demosh]. +## Architecture + +The Faces architecture is fairly simple: + +- The `faces-gui` workload, reached on the `/faces/` path, just returns the + HTML and Javascript for the GUI. The GUI is a single-page webapp that + displays a grid of cells: for each cell, the GUI calls the `face` workload. + +- The `face` workload, reached on the `/face/` path, calls the `smiley` + workload to get a smiley face and the `color` workload to get a color. It + then composes the responses together and returns the smiley/color + combination to the GUI for display. + +- The `smiley` workload returns a smiley face. By default, this is a grinning + smiley, U+1F603, but you can set the `SMILEY` environment variable to any + key in the `Smileys` map from `constants.go` to get a different smiley. + +- The `color` workload returns a color. By default, this is a light blue, but + you can set the `COLOR` environment variable to any key in the `Colors` map + from `constants.go` to get a different color, or to any arbitrary hex color + code (e.g. `#ff0000` for bright red). + + The named colors in the `Colors` map are meant to work for normal color + vision and for various kinds of colorblindness, and are taken from the + "Bright" color scheme shown in the "Qualitative // Color Schemes" section of + https://personal.sron.nl/~pault/. For (much) more information, read the + comments in `pkg/faces/constants.go`. Feedback here is welcome, since the + Faces authors have normal color vision... + [Linkerd]: https://linkerd.io [Emissary-ingress]: https://www.getambassador.io/docs/emissary/ [DEMO.md]: DEMO.md diff --git a/faces-chart/Chart.yaml b/faces-chart/Chart.yaml index 11a64a9..e6eeadf 100644 --- a/faces-chart/Chart.yaml +++ b/faces-chart/Chart.yaml @@ -3,8 +3,8 @@ apiVersion: "v2" appVersion: %VERSION% description: | This is the Faces demo application. It has a single-page web GUI that - presents a grid of cells, each of which _should_ show a smiling face on - a green background. Spoiler alert: installed exactly as committed to this + presents a grid of cells, each of which _should_ show a grinning face on a + light blue background. Spoiler alert: installed exactly as committed to this repo, that isn't what you'll get -- many, many things can go wrong, and will. The point of the demo is let you try to fix things. type: application From fe7e8e345acc90ff78c1cd3cd9e468032c546efa Mon Sep 17 00:00:00 2001 From: Flynn Date: Tue, 26 Mar 2024 22:38:07 -0400 Subject: [PATCH 7/7] Minor GUI tweaks Signed-off-by: Flynn --- assets/html/index.html | 22 +++++++++++----------- pkg/faces/guiserver.go | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/assets/html/index.html b/assets/html/index.html index f1f3864..710f123 100644 --- a/assets/html/index.html +++ b/assets/html/index.html @@ -58,7 +58,7 @@ .cell { display: inline-block; - border: 1px solid grey; + border: 2px solid grey; border-radius: 16px; height: 120px; width: 120px; @@ -80,16 +80,16 @@ } .cell-pod-info { - height: 120px; - width: 120px; - border-radius: 16px; + display: inline-block; + position: relative; + text-align: center; } .cell-pod-id { position: absolute; - top: 1px; - left: 1px; - width: 118px; + top: 4px; + left: 4px; + width: 120px; border-radius: 14px 14px 0px 0px; font-family: sans-serif; background: white; @@ -163,8 +163,8 @@ border-right: 2px solid grey; padding-left: .5em; padding-right: .5em; - max-width: 252px !important; - width: 252px !important; + max-width: 258px !important; + width: 258px !important; } .inline { @@ -478,7 +478,7 @@

Faces

// New pod! let podDiv = document.createElement("div") podDiv.id = `cell-pod-${shortName}` - podDiv.className = "cell" + podDiv.className = "cell-pod-info" let podIDDiv = document.createElement("div") podIDDiv.id = `cell-pod-id-${shortName}` @@ -488,7 +488,7 @@

Faces

let podInfoDiv = document.createElement("div") podInfoDiv.id = `cell-pod-info-${shortName}` - podInfoDiv.className = "cell-pod-info" + podInfoDiv.className = "cell" podDiv.appendChild(podInfoDiv) let podSmileySpan = document.createElement("span") diff --git a/pkg/faces/guiserver.go b/pkg/faces/guiserver.go index 6c927cb..e1dfa03 100644 --- a/pkg/faces/guiserver.go +++ b/pkg/faces/guiserver.go @@ -118,7 +118,7 @@ func (srv *GUIServer) guiGetHandler(w http.ResponseWriter, r *http.Request) { rtext = strings.ReplaceAll(rtext, "%%{hide_key}", fmt.Sprintf("%v", srv.hideKey)) rtext = strings.ReplaceAll(rtext, "%%{show_pods}", fmt.Sprintf("%v", srv.showPods)) rtext = strings.ReplaceAll(rtext, "%%{user}", user) - rtext = strings.ReplaceAll(rtext, "%%{user_Agent}", userAgent) + rtext = strings.ReplaceAll(rtext, "%%{user_agent}", userAgent) } } else if strings.HasPrefix(r.URL.Path, "/face/") { // /face/ is a special case: we forward it to the face workload. This is