// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// JSON tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
	// The fields are sorted in increasing index-length order. The winner
	// must therefore be one with the shortest index length. Drop all
	// longer entries, which is easy: just truncate the slice.
	length := len(fields[0].index)
	tagged := -1 // Index of first tagged field.
	for i, f := range fields {
		if len(f.index) > length {
			fields = fields[:i]
			break
		}
		if f.tag {
			if tagged >= 0 {
				// Multiple tagged fields at the same level: conflict.
				// Return no field.
				return field{}, false
			}
			tagged = i
		}
	}
	if tagged >= 0 {
		return fields[tagged], true
	}
	// All remaining fields have the same length. If there's more than one,
	// we have a conflict (two fields named "X" at the same level) and we
	// return no field.
	if len(fields) > 1 {
		return field{}, false
	}
	return fields[0], true
}

var fieldCache struct {
	sync.RWMutex
	m map[reflect.Type][]field
}

// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
	fieldCache.RLock()
	f := fieldCache.m[t]
	fieldCache.RUnlock()
	if f != nil {
		return f
	}

	// Compute fields without lock.
	// Might duplicate effort but won't hold other computations back.
	f = typeFields(t)
	if f == nil {
		f = []field{}
	}

	fieldCache.Lock()
	if fieldCache.m == nil {
		fieldCache.m = map[reflect.Type][]field{}
	}
	fieldCache.m[t] = f
	fieldCache.Unlock()
	return f
}

func isValidTag(s string) bool {
	if s == "" {
		return false
	}
	for _, c := range s {
		switch {
		case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
			// Backslash and quote chars are reserved, but
			// otherwise any punctuation chars are allowed
			// in a tag name.
		default:
			if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
				return false
			}
		}
	}
	return true
}

const (
	caseMask     = ^byte(0x20) // Mask to ignore case in ASCII.
	kelvin       = ''
	smallLongEss = 'ſ'
)

// foldFunc returns one of four different case folding equivalence
// functions, from most general (and slow) to fastest:
//
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
// 3) asciiEqualFold, no special, but includes non-letters (including _)
// 4) simpleLetterEqualFold, no specials, no non-letters.
//
// The letters S and K are special because they map to 3 runes, not just 2:
//   - S maps to s and to U+017F 'ſ' Latin small letter long s
//   - k maps to K and to U+212A 'K' Kelvin sign
//
// See http://play.golang.org/p/tTxjOc0OGo
//
// The returned function is specialized for matching against s and
// should only be given s. It's not curried for performance reasons.
func foldFunc(s []byte) func(s, t []byte) bool {
	nonLetter := false
	special := false // special letter
	for _, b := range s {
		if b >= utf8.RuneSelf {
			return bytes.EqualFold
		}
		upper := b & caseMask
		if upper < 'A' || upper > 'Z' {
			nonLetter = true
		} else if upper == 'K' || upper == 'S' {
			// See above for why these letters are special.
			special = true
		}
	}
	if special {
		return equalFoldRight
	}
	if nonLetter {
		return asciiEqualFold
	}
	return simpleLetterEqualFold
}

// equalFoldRight is a specialization of bytes.EqualFold when s is
// known to be all ASCII (including punctuation), but contains an 's',
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
// See comments on foldFunc.
func equalFoldRight(s, t []byte) bool {
	for _, sb := range s {
		if len(t) == 0 {
			return false
		}
		tb := t[0]
		if tb < utf8.RuneSelf {
			if sb != tb {
				sbUpper := sb & caseMask
				if 'A' <= sbUpper && sbUpper <= 'Z' {
					if sbUpper != tb&caseMask {
						return false
					}
				} else {
					return false
				}
			}
			t = t[1:]
			continue
		}
		// sb is ASCII and t is not. t must be either kelvin
		// sign or long s; sb must be s, S, k, or K.
		tr, size := utf8.DecodeRune(t)
		switch sb {
		case 's', 'S':
			if tr != smallLongEss {
				return false
			}
		case 'k', 'K':
			if tr != kelvin {
				return false
			}
		default:
			return false
		}
		t = t[size:]

	}

	return len(t) <= 0
}

// asciiEqualFold is a specialization of bytes.EqualFold for use when
// s is all ASCII (but may contain non-letters) and contains no
// special-folding letters.
// See comments on foldFunc.
func asciiEqualFold(s, t []byte) bool {
	if len(s) != len(t) {
		return false
	}
	for i, sb := range s {
		tb := t[i]
		if sb == tb {
			continue
		}
		if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
			if sb&caseMask != tb&caseMask {
				return false
			}
		} else {
			return false
		}
	}
	return true
}

// simpleLetterEqualFold is a specialization of bytes.EqualFold for
// use when s is all ASCII letters (no underscores, etc) and also
// doesn't contain 'k', 'K', 's', or 'S'.
// See comments on foldFunc.
func simpleLetterEqualFold(s, t []byte) bool {
	if len(s) != len(t) {
		return false
	}
	for i, b := range s {
		if b&caseMask != t[i]&caseMask {
			return false
		}
	}
	return true
}

// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string

// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
	if idx := strings.Index(tag, ","); idx != -1 {
		return tag[:idx], tagOptions(tag[idx+1:])
	}
	return tag, tagOptions("")
}

// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
	if len(o) == 0 {
		return false
	}
	s := string(o)
	for s != "" {
		var next string
		i := strings.Index(s, ",")
		if i >= 0 {
			s, next = s[:i], s[i+1:]
		}
		if s == optionName {
			return true
		}
		s = next
	}
	return false
}



package kyaml

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	yaml "go.yaml.in/yaml/v3"
)

// Encoder formats objects or YAML data (JSON is valid YAML) into KYAML. KYAML
// is halfway between YAML and JSON, but is a strict subset of YAML, so it
// should should be readable by any YAML parser. It is designed to be explicit
// and unambiguous, and eschews significant whitespace.
type Encoder struct {
	// Compact tells the encoder to use compact formatting. This puts all the
	// data on one line, with no extra newlines, no comments, and no multi-line
	// formatting.
	Compact bool
}

// FromYAML renders a KYAML (multi-)document from YAML bytes (JSON is YAML),
// including the KYAML header. The result always has a trailing newline.
func (ky *Encoder) FromYAML(in io.Reader, out io.Writer) error {
	// We need a YAML decoder to handle multi-document streams.
	dec := yaml.NewDecoder(in)

	// Process each document in the stream.
	for {
		var doc yaml.Node
		err := dec.Decode(&doc)
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("error decoding: %v", err)
		}
		if doc.Kind != yaml.DocumentNode {
			return fmt.Errorf("kyaml internal error: line %d: expected a document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
		}

		// Always emit a document separator, which helps disambiguate between YAML
		// and JSON.
		if _, err := fmt.Fprintln(out, "---"); err != nil {
			return err
		}

		if err := ky.renderDocument(&doc, 0, ky.flags(), out); err != nil {
			return err
		}
		fmt.Fprintf(out, "
")
	}

	return nil
}

// FromObject renders a KYAML document from a Go object, including the KYAML
// header. The result always has a trailing newline.
func (ky *Encoder) FromObject(obj any, out io.Writer) error {
	jb, err := json.Marshal(obj)
	if err != nil {
		return fmt.Errorf("error marshaling to JSON: %v", err)
	}
	// JSON is YAML.
	return ky.FromYAML(bytes.NewReader(jb), out)
}

// Marshal renders a single Go object as KYAML, without the header or trailing
// newline.
func (ky *Encoder) Marshal(obj any) ([]byte, error) {
	// Convert the object to JSON bytes to take advantage of all the JSON tag
	// handling and things like that.
	jb, err := json.Marshal(obj)
	if err != nil {
		return nil, fmt.Errorf("error marshaling to JSON: %v", err)
	}

	buf := &bytes.Buffer{}
	// JSON is YAML.
	if err := ky.fromObjectYAML(bytes.NewReader(jb), buf); err != nil {
		return nil, fmt.Errorf("error rendering object: %v", err)
	}
	return buf.Bytes(), nil
}

func (ky *Encoder) fromObjectYAML(in io.Reader, out io.Writer) error {
	yb, err := io.ReadAll(in)
	if err != nil {
		return err
	}

	var doc yaml.Node
	if err := yaml.Unmarshal(yb, &doc); err != nil {
		return fmt.Errorf("error decoding: %v", err)
	}
	if doc.Kind != yaml.DocumentNode {
		return fmt.Errorf("kyaml internal error: line %d: expected document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
	}

	if err := ky.renderNode(&doc, 0, ky.flags(), out); err != nil {
		return fmt.Errorf("error rendering document: %v", err)
	}

	return nil
}

// From the YAML spec.
const (
	intTag       = "!!int"
	floatTag     = "!!float"
	boolTag      = "!!bool"
	strTag       = "!!str"
	timestampTag = "!!timestamp"
	seqTag       = "!!seq"
	mapTag       = "!!map"
	nullTag      = "!!null"
	binaryTag    = "!!binary"
	mergeTag     = "!!merge"
)

type flagMask uint64

const (
	flagsNone     flagMask = 0
	flagLazyQuote flagMask = 0x01
	flagCompact   flagMask = 0x02
)

// flags returns a flagMask representing the current encoding options. It can
// be used directly or OR'ed with another mask.
func (ky *Encoder) flags() flagMask {
	flags := flagsNone
	if ky.Compact {
		flags |= flagCompact
	}
	return flags
}

// renderNode processes a YAML node, calling the appropriate render function
// for its type.  Each render function should assume that the output "cursor"
// is positioned at the start of the node and should not emit a final newline.
// If a render function needs to linewrap or indent (e.g. a struct), it should
// assume the indent level is currently correct for the node type itself, and
// may need to indent more.
func (ky *Encoder) renderNode(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	if node == nil {
		return nil
	}

	switch node.Kind {
	case yaml.DocumentNode:
		return ky.renderDocument(node, indent, flags, out)
	case yaml.ScalarNode:
		return ky.renderScalar(node, indent, flags, out)
	case yaml.SequenceNode:
		return ky.renderSequence(node, indent, flags, out)
	case yaml.MappingNode:
		return ky.renderMapping(node, indent, flags, out)
	case yaml.AliasNode:
		return ky.renderAlias(node, indent, flags, out)
	}
	return fmt.Errorf("kyaml internal error: line %d: unknown node kind %v", node.Line, node.Kind)
}

// renderDocument processes a YAML document node, rendering it to the output.
// This function assumes that the output "cursor" is positioned at the start of
// the document. This does not emit a final newline.
func (ky *Encoder) renderDocument(doc *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	if len(doc.Content) == 0 {
		return fmt.Errorf("kyaml internal error: line %d: document has no content node (%d)", doc.Line, len(doc.Content))
	}
	if len(doc.Content) > 1 {
		return fmt.Errorf("kyaml internal error: line %d: document has more than one content node (%d)", doc.Line, len(doc.Content))
	}
	if indent != 0 {
		return fmt.Errorf("kyaml internal error: line %d: document non-zero indent (%d)", doc.Line, indent)
	}

	compact := flags&flagCompact != 0

	// For document nodes, the cursor is assumed to be ready to render.
	child := doc.Content[0]
	if !compact {
		if len(doc.HeadComment) > 0 {
			ky.renderComments(doc.HeadComment, indent, out)
			fmt.Fprint(out, "
")
		}
		if len(child.HeadComment) > 0 {
			ky.renderComments(child.HeadComment, indent, out)
			fmt.Fprint(out, "
")
		}
	}
	if err := ky.renderNode(child, indent, flags, out); err != nil {
		return err
	}
	if !compact {
		if len(child.LineComment) > 0 {
			ky.renderComments(" "+child.LineComment, 0, out)
		}
		if len(child.FootComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(child.FootComment, indent, out)
		}
		if len(doc.LineComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(" "+doc.LineComment, 0, out)
		}
		if len(doc.FootComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(doc.FootComment, indent, out)
		}
	}
	return nil
}

// renderScalar processes a YAML scalar node, rendering it to the output.  This
// DOES NOT render a trailing newline or head/line/foot comments, as those
// require the parent context.
func (ky *Encoder) renderScalar(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	switch node.Tag {
	case intTag, floatTag, boolTag, nullTag:
		fmt.Fprint(out, node.Value)
	case strTag, timestampTag:
		return ky.renderString(node.Value, indent+1, flags, out)
	default:
		return fmt.Errorf("kyaml internal error: line %d: unknown tag %q on scalar node %q", node.Line, node.Tag, node.Value)
	}
	return nil
}

const kyamlFoldStr = "\
"

var regularEscapeMap = map[rune]string{
	'
': "\n" + kyamlFoldStr, // use YAML's line folding to make the output more readable
	'	': "	",                 // literal tab
}
var compactEscapeMap = map[rune]string{
	'
': "\n",
	'	': "\t",
}
Back to Blog

Building a Chaos Machine in Go

Michael Duren
Building a Chaos Machine in Go
#Go #Chaos Engineering #DevOps

Building a Chaos Machine in Go

Chaos engineering is the discipline of experimenting on a system in order to build confidence in the system’s capability to withstand turbulent conditions in production. Today, I’m going to share how I built a simple generic chaos machine using Go.

What is Chaos Engineering?

Chaos engineering isn’t just about breaking things. It’s about breaking things on purpose to learn how to build more resilient systems.

Why Go?

Go is perfect for this kind of tool because of its:

  • Concurrency primitives: Goroutines make it easy to simulate concurrent load.
  • Performance: Go is fast and efficient.
  • Simplicity: The language is easy to read and write.

The Design

The chaos machine consists of three main components:

  1. Orchestrator: Manages the chaos experiments.
  2. Agents: processes that run on target nodes and inject failures.
  3. Reporter: Collects metrics and logs.
chaos-agent.go
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    fmt.Println("Starting Chaos Machine...")
    
    // Simulate randomness
    rand.Seed(time.Now().UnixNano())
    
    if rand.Intn(10) > 5 {
        fmt.Println("Injecting latency...")
        time.Sleep(2 * time.Second)
    } else {
        fmt.Println("System operating normally.")
    }
}

Implementation Details

We used the os/exec package to run system commands that simulate failures, such as network partitions using iptables or CPU spikes using simple loops.

Network Latency

To simulate network latency, we can use tc (Traffic Control) on Linux.

bash
# Inject 100ms delay
$ tc qdisc add dev eth0 root netem delay 100ms

# Verify the delay
$ ping google.com
PING google.com (142.250.72.14) 56(84) bytes of data.
64 bytes from google.com: icmp_seq=1 ttl=115 time=102 ms

Production Warning

Be very careful when running tc commands in production! Make sure you have a way to rollback changes if you lose connectivity.

Conclusion

Building a chaos machine in Go was a fun project that taught me a lot about system resilience. Give it a try!