<commit_before>package standard

import (
	"fmt"
	G "github.com/ionous/sashimi/game"
	. "github.com/ionous/sashimi/script"
	"github.com/ionous/sashimi/util/lang"
	"strings"
)

// InitStandardLibrary to ensure all standard library scripts are properly created.
func InitStandardLibrary() *Script {
	// the side effect of importing the standard library
	// adds the scripts to the list of those which get initialized.
	return InitScripts()
}

// FIX: there's no error testing here and its definitely possible to screw things up.
func AssignTo(prop G.IObject, rel string, dest G.IObject) {
	// sure hope there's no errors, would relation by value remove the need for transaction?
	if _, parentRel := prop.ParentRelation(); parentRel != "" {
		// note: an object like the fishFood isnt "in the world", and doest have an owner field to clear.
		prop.Set(parentRel, nil)
	}
	prop.Set(rel, dest)
}

//
func init() {
	AddScript(func(s *Script) {
		// FIX: hierarchy is a work in progress.
		// kinds> stories, rooms, objects > actors (> animals),  props(> openers(> doors,containers), supporters, devices)

		// FIX: IObject and Instance, ObjectList are confusing versus kind/object
		// Instance might be better...? or some better name for the class of objects
		// i tried "subjects" but hated it more.
		s.The("kinds",
			// printed plural name (text),
			Have("printed name", "text"),
			Have("indefinite article", "text"), //
			AreEither("singular-named").Or("plural-named"),
			AreEither("common-named").Or("proper-named"), //a name used for an individual person, place, or organization, spelled with initial capital letters, e.g., Larry, Mexico, and Boston Red Sox.
			//not normally preceded by an article or other limiting modifier, as any or some. Nor are they usually pluralized
		)

		// vs. descriptions in "kind"
		// it seems to make sense for now to have two separate description fields.
		// rooms like to say their description, while objects like to say their brief initial appearance ( or name, if there's none. )
		s.The("rooms",
			Have("description", "text"))

		s.The("objects",
			Have("description", "text"),
			Have("brief", "text"))

		// inform's rooms: lighted, dark; unvisited, visited; description, region
		s.The("kinds",
			Called("rooms"),
			AreEither("visited").Or("unvisited").Usually("unvisited"),
		)

		// the class hierarchy means rooms cant contain other rooms.
		s.The("rooms",
			HaveMany("contents", "objects").
				Implying("objects", HaveOne("whereabouts", "room")))

		// things		unlit, lit
		// 	inedible, edible
		//
		// 	unmarked for listing, marked for listing
		// 	described, undescribed : i think, whether to appear in any room descriptions
		// 	mentioned, unmentioned : i think, whether it has appeared in a room description
		// bool	scenery
		// 	wearable
		// 	pushable between rooms
		// 	.handled
		// 	.description (in objects and rooms)
		// 	.initial appearance (brief)
		// 	matching key
		s.The("kinds",
			Called("objects"),
			Exist())

		// hrmmm.... are actors really scenery? handled?
		s.The("objects",
			AreEither("unhandled").Or("handled"),
			AreEither("scenery").Or("not scenery").Usually("not scenery"))

		s.The("openers",
			Called("doors"),
			Exist())

		// CAN WE DEFAULT (USUALLY(X)) DOORS TO fixed-in-place???
		s.The("doors", Before("reporting take").Always(func(g G.Play) {
			g.Say("It is fixed in place.")
			g.StopHere()
		}))

		// nothing special: just a handy name to mirror inform's.
		s.The("actors",
			Called("animals"),
			Exist())

		// hrmm.... implies actors are takeable.
		s.The("objects",
			Called("actors"),
			HaveMany("clothing", "objects").
				Implying("objects", HaveOne("wearer", "actor")),
			HaveMany("inventory", "objects").
				Implying("objects", HaveOne("owner", "actor")))

		// changed: used to have "equipment" for held objects
		// will either implement some sort of "relation with value"
		// or will add a "held","holdable", state to objects.

		s.The("objects",
			Called("props"),
			AreEither("portable").Or("fixed in place"),
		)

		// Usually opaque not transparent, open not closed, unopenable not openable, unlocked not locked.
		// Usually not enterable, lockable.
		// Can have carrying capacity (number)
		s.The("openers",
			Called("containers"),
			HaveMany("contents", "objects").
				Implying("objects", HaveOne("enclosure", "container")),
			AreEither("opaque").Or("transparent"),
			//AreEither("enterable").Or("not enterable"),
			AreEither("lockable").Or("not lockable").Usually("not lockable"),
			AreEither("locked").Or("unlocked").Usually("unlocked"),
		)

		s.The("props",
			Called("supporters"),
			HaveMany("contents", "objects").
				Implying("objects", HaveOne("support", "supporter")))
	})
}

// reflect to the passed action passing the actors's current whereabouts.
// will have to become more sophisticated for being inside a box.
func ReflectToLocation(g G.Play, action string) {
	actor := g.The("actor")
	target := actor.Object("whereabouts")
	//g.Log("reflecting", action, actor, target)
	target.Go(action, actor)
}

// ReflectToTarget runs the passed action, flipping the source and target.
func ReflectToTarget(g G.Play, action string) {
	source := g.The("action.Source")
	target := g.The("action.Target")
	//g.Log("reflecting", action, source, target)
	target.Go(action, source)

}

// ReflectWithContext runs the passed action, shifting to target, context, source.
// FIX: i think it'd be better to first use ReflectToTarget, keeping the context as the third parameter
// and then ReflectToContext, possibly re-swapping source and target.
func ReflectWithContext(g G.Play, action string) {
	source := g.The("action.Source")
	target := g.The("action.Target")
	context := g.The("action.Context")
	//g.Log("reflecting", action, source, target, context)
	target.Go(action, context, source)
}

func init() {
	AddScript(func(s *Script) {

		// one visible thing, and requring light
		s.The("actors",
			Can("look").And("looking").RequiresNothing(),
			To("look",
				// func( g G.Play) { ReflectToLocation(g,"report the view") }
				// reflect to location will send the actor as a parameter,
				// but report the view doesnt expect parameters.
				func(g G.Play) {
					actor := g.The("actor")
					target := actor.Object("whereabouts")
					target.Go("report the view")
				}),
		)

		// one visible thing and requiring light.
		s.The("actors",
			Can("look under it").And("looking under it").RequiresOne("object"),
			To("look under it", func(g G.Play) { ReflectToTarget(g, "report look under") }),
		)

		// FIX: should generate a report/response instead?
		s.The("actors",
			Can("impress").And("impressing").RequiresNothing(),
			To("impress", func(g G.Play) {
				g.Say(lang.Capitalize(ArticleName(g, "actor", nil)), "is unimpressed.")
			}))

		// "taking inventory" in inform
		// again, as with some other actions: for players this happens in carry out, for npcs in report.
		// i'm sure that's useful... somehow....
		s.The("actors",
			Can("report inventory").And("reporting inventory").RequiresNothing(),
			To("report inventory", func(g G.Play) {
				this := g.The("actor")
				source := []string{"Clothing", "Inventory"}
				for _, s := range source {
					contents := this.ObjectList(s)
					if len(contents) > 0 {
						g.Say(s + ":")
						for _, obj := range contents {
							obj.Go("print name")
						}
					} else {
						g.Say(s, "(none).")
					}
				}
			}),
		)

		// FIX: for some reason, the order must be biggest match to smallest, the other way doesnt work.
		s.Execute("report inventory", Matching("inventory|inv|i"))
		s.Execute("look", Matching("look|l"))
		s.Execute("look under it", Matching("look under {{something}}"))
	})
}

//
// when is the right time for functions versus callbacks?
func ListContents(g G.Play, header string, obj G.IObject) (printed bool) {
	// if something described which is not scenery is on the noun and something which is not the player is on the noun:
	// obviously a filterd callback, visitor, would be nice FilterList("contents", func() ... )
	// FIX: if something has scenery objets, they appear as contents,
	// but then the list is empty. ( ex. lab coat, but it might happen elsewhere )
	// we'd maybe need to know if something printed?
	if contents := obj.ObjectList("contents"); len(contents) > 0 {
		g.Say(header, obj.Text("Name"), "is:")
		for _, content := range contents {
			content.Go("print description")
		}
		printed = true
	}
	return printed
}

func NameFullStop(G.IObject) string {
	return ""
}

type NameStatus func(obj G.IObject) string

func ArticleName(g G.Play, which string, status NameStatus) string {
	return articleName(g, which, false, status)
}
func DefiniteName(g G.Play, which string, status NameStatus) string {
	return articleName(g, which, true, status)
}
func articleName(g G.Play, which string, definite bool, status NameStatus) string {
	obj := g.The(which)
	text := obj.Text("Name")
	if obj.Is("proper-named") {
		text = lang.Titleize(text)
	} else {
		article := ""
		if definite {
			article = "the"
		} else {
			article = obj.Text("indefinite article")
			if article == "" {
				if obj.Is("plural-named") {
					article = "some"
				} else if lang.StartsWithVowel(text) {
					article = "an"
				} else {
					article = "a"
				}
			}
		}
		text = strings.Join([]string{article, strings.ToLower(text)}, " ")
	}
	if status != nil {
		if s := status(obj); s != "" {
			text = fmt.Sprintf("%s (%s)", text, s)
		} else {
			text = text + "."
		}
	}
	return text
}

//
// System actions
func init() {
	AddScript(func(s *Script) {

		// inform has two entries for some actions (looking under as an example, jumping as a counter example):
		// 1. carry out an actor looking under: if the player
		// 2. report an actor looking under: if not the player
		// its not exactly clear to me why, the docs give guidelines for this, but not rationale.
		// it might be interesting to queue says, if they need to be cancelled or held back.
		// keep in mind, most of these really want to be animations, and only sometimes voice.
		s.The("objects",
			Can("report look under").And("reporting look under").RequiresOne("actor"),
			To("report look under", func(g G.Play) {
				source, actor := g.The("action.Source"), g.The("action.Target")
				if g.The("player") == actor {
					g.Say("You find nothing of interest.")
				} else {
					g.Say(actor.Text("Name"), "looks under the", source.Text("Name"), ".")
				}
			}))

		s.The("objects",
			Can("print name").And("printing name text").RequiresNothing(),
			To("print name", func(g G.Play) {
				if text := ArticleName(g, "object", NameFullStop); len(text) > 0 {
					text = lang.Capitalize(text)
					g.Say(text)
				}
			}))

		s.The("containers",
			When("printing name text").
				Always(func(g G.Play) {
				// FIX: conditional return instead of Always?
				// or some way ( dependency injection ) to get at the event object
				// of course, rules producing values and stacks might work too.
				// FIX: a container is an opener... where do we print the opener status name
				// put this on doors for now.
				text := ArticleName(g, "container", func(obj G.IObject) (status string) {
					if obj.Is("closed") {
						if obj.Is("hinged") {
							status = "closed"
						}
					} else if list := obj.ObjectList("contents"); len(list) == 0 {
						if obj.Is("transparent") || obj.Is("open") {
							status = "empty"
						}
					}
					return status
				})
				text = lang.Capitalize(text)
				g.Say(text)
				g.StopHere()
			}))

		s.The("doors",
			When("printing name text").
				Always(func(g G.Play) {
				text := ArticleName(g, "door", func(obj G.IObject) (status string) {
					if obj.Is("hinged") {
						if obj.Is("open") {
							status = "open"
						} else {
							status = "closed"
						}
					}
					return status
				})
				text = lang.Capitalize(text)
				g.Say(text)
				g.StopHere()
			}))

		s.The("rooms",
			Can("report the view").And("reporting the view").RequiresNothing(),
			To("report the view", func(g G.Play) {
				room := g.The("room")
				// sometines a blank like is printed without this
				// (maybe certain directions? or going through doors?)
				// not sure why, so leaving this for consistency
				g.Say(Lines("", room.Text("Name")))
				g.Say(Lines(room.Text("description"), ""))
				// FIX? uses 1 to exclude the player....
				// again, this happens because we dont know if print description actually did anything (re:scenery, etc.)
				if contents := room.ObjectList("contents"); len(contents) > 1 {
					for _, obj := range contents {
						obj.Go("print description")
						g.Say("")
					}
				}
				// FIX: duplicated in stories describe the first room
				room.IsNow("visited")
				g.The("status bar").SetText("left", lang.Titleize(room.Text("Name")))
			}))
	})
}
<commit_msg>tweak is visited for alice-app<commit_after>package standard

import (
	"fmt"
	G "github.com/ionous/sashimi/game"
	. "github.com/ionous/sashimi/script"
	"github.com/ionous/sashimi/util/lang"
	"strings"
)

// InitStandardLibrary to ensure all standard library scripts are properly created.
func InitStandardLibrary() *Script {
	// the side effect of importing the standard library
	// adds the scripts to the list of those which get initialized.
	return InitScripts()
}

// FIX: there's no error testing here and its definitely possible to screw things up.
func AssignTo(prop G.IObject, rel string, dest G.IObject) {
	// sure hope there's no errors, would relation by value remove the need for transaction?
	if _, parentRel := prop.ParentRelation(); parentRel != "" {
		// note: an object like the fishFood isnt "in the world", and doest have an owner field to clear.
		prop.Set(parentRel, nil)
	}
	prop.Set(rel, dest)
}

//
func init() {
	AddScript(func(s *Script) {
		// FIX: hierarchy is a work in progress.
		// kinds> stories, rooms, objects > actors (> animals),  props(> openers(> doors,containers), supporters, devices)

		// FIX: IObject and Instance, ObjectList are confusing versus kind/object
		// Instance might be better...? or some better name for the class of objects
		// i tried "subjects" but hated it more.
		s.The("kinds",
			// printed plural name (text),
			Have("printed name", "text"),
			Have("indefinite article", "text"), //
			AreEither("singular-named").Or("plural-named"),
			AreEither("common-named").Or("proper-named"), //a name used for an individual person, place, or organization, spelled with initial capital letters, e.g., Larry, Mexico, and Boston Red Sox.
			//not normally preceded by an article or other limiting modifier, as any or some. Nor are they usually pluralized
		)

		// vs. descriptions in "kind"
		// it seems to make sense for now to have two separate description fields.
		// rooms like to say their description, while objects like to say their brief initial appearance ( or name, if there's none. )
		s.The("rooms",
			Have("description", "text"))

		s.The("objects",
			Have("description", "text"),
			Have("brief", "text"))

		// inform's rooms: lighted, dark; unvisited, visited; description, region
		s.The("kinds",
			Called("rooms"),
			AreEither("visited").Or("unvisited").Usually("unvisited"),
		)

		// the class hierarchy means rooms cant contain other rooms.
		s.The("rooms",
			HaveMany("contents", "objects").
				Implying("objects", HaveOne("whereabouts", "room")))

		// things		unlit, lit
		// 	inedible, edible
		//
		// 	unmarked for listing, marked for listing
		// 	described, undescribed : i think, whether to appear in any room descriptions
		// 	mentioned, unmentioned : i think, whether it has appeared in a room description
		// bool	scenery
		// 	wearable
		// 	pushable between rooms
		// 	.handled
		// 	.description (in objects and rooms)
		// 	.initial appearance (brief)
		// 	matching key
		s.The("kinds",
			Called("objects"),
			Exist())

		// hrmmm.... are actors really scenery? handled?
		s.The("objects",
			AreEither("unhandled").Or("handled"),
			AreEither("scenery").Or("not scenery").Usually("not scenery"))

		s.The("openers",
			Called("doors"),
			Exist())

		// CAN WE DEFAULT (USUALLY(X)) DOORS TO fixed-in-place???
		s.The("doors", Before("reporting take").Always(func(g G.Play) {
			g.Say("It is fixed in place.")
			g.StopHere()
		}))

		// nothing special: just a handy name to mirror inform's.
		s.The("actors",
			Called("animals"),
			Exist())

		// hrmm.... implies actors are takeable.
		s.The("objects",
			Called("actors"),
			HaveMany("clothing", "objects").
				Implying("objects", HaveOne("wearer", "actor")),
			HaveMany("inventory", "objects").
				Implying("objects", HaveOne("owner", "actor")))

		// changed: used to have "equipment" for held objects
		// will either implement some sort of "relation with value"
		// or will add a "held","holdable", state to objects.

		s.The("objects",
			Called("props"),
			AreEither("portable").Or("fixed in place"),
		)

		// Usually opaque not transparent, open not closed, unopenable not openable, unlocked not locked.
		// Usually not enterable, lockable.
		// Can have carrying capacity (number)
		s.The("openers",
			Called("containers"),
			HaveMany("contents", "objects").
				Implying("objects", HaveOne("enclosure", "container")),
			AreEither("opaque").Or("transparent"),
			//AreEither("enterable").Or("not enterable"),
			AreEither("lockable").Or("not lockable").Usually("not lockable"),
			AreEither("locked").Or("unlocked").Usually("unlocked"),
		)

		s.The("props",
			Called("supporters"),
			HaveMany("contents", "objects").
				Implying("objects", HaveOne("support", "supporter")))
	})
}

// reflect to the passed action passing the actors's current whereabouts.
// will have to become more sophisticated for being inside a box.
func ReflectToLocation(g G.Play, action string) {
	actor := g.The("actor")
	target := actor.Object("whereabouts")
	//g.Log("reflecting", action, actor, target)
	target.Go(action, actor)
}

// ReflectToTarget runs the passed action, flipping the source and target.
func ReflectToTarget(g G.Play, action string) {
	source := g.The("action.Source")
	target := g.The("action.Target")
	//g.Log("reflecting", action, source, target)
	target.Go(action, source)

}

// ReflectWithContext runs the passed action, shifting to target, context, source.
// FIX: i think it'd be better to first use ReflectToTarget, keeping the context as the third parameter
// and then ReflectToContext, possibly re-swapping source and target.
func ReflectWithContext(g G.Play, action string) {
	source := g.The("action.Source")
	target := g.The("action.Target")
	context := g.The("action.Context")
	//g.Log("reflecting", action, source, target, context)
	target.Go(action, context, source)
}

func init() {
	AddScript(func(s *Script) {

		// one visible thing, and requring light
		s.The("actors",
			Can("look").And("looking").RequiresNothing(),
			To("look",
				// func( g G.Play) { ReflectToLocation(g,"report the view") }
				// reflect to location will send the actor as a parameter,
				// but report the view doesnt expect parameters.
				func(g G.Play) {
					actor := g.The("actor")
					target := actor.Object("whereabouts")
					target.Go("report the view")
				}),
		)

		// one visible thing and requiring light.
		s.The("actors",
			Can("look under it").And("looking under it").RequiresOne("object"),
			To("look under it", func(g G.Play) { ReflectToTarget(g, "report look under") }),
		)

		// FIX: should generate a report/response instead?
		s.The("actors",
			Can("impress").And("impressing").RequiresNothing(),
			To("impress", func(g G.Play) {
				g.Say(lang.Capitalize(ArticleName(g, "actor", nil)), "is unimpressed.")
			}))

		// "taking inventory" in inform
		// again, as with some other actions: for players this happens in carry out, for npcs in report.
		// i'm sure that's useful... somehow....
		s.The("actors",
			Can("report inventory").And("reporting inventory").RequiresNothing(),
			To("report inventory", func(g G.Play) {
				this := g.The("actor")
				source := []string{"Clothing", "Inventory"}
				for _, s := range source {
					contents := this.ObjectList(s)
					if len(contents) > 0 {
						g.Say(s + ":")
						for _, obj := range contents {
							obj.Go("print name")
						}
					} else {
						g.Say(s, "(none).")
					}
				}
			}),
		)

		// FIX: for some reason, the order must be biggest match to smallest, the other way doesnt work.
		s.Execute("report inventory", Matching("inventory|inv|i"))
		s.Execute("look", Matching("look|l"))
		s.Execute("look under it", Matching("look under {{something}}"))
	})
}

//
// when is the right time for functions versus callbacks?
func ListContents(g G.Play, header string, obj G.IObject) (printed bool) {
	// if something described which is not scenery is on the noun and something which is not the player is on the noun:
	// obviously a filterd callback, visitor, would be nice FilterList("contents", func() ... )
	// FIX: if something has scenery objets, they appear as contents,
	// but then the list is empty. ( ex. lab coat, but it might happen elsewhere )
	// we'd maybe need to know if something printed?
	if contents := obj.ObjectList("contents"); len(contents) > 0 {
		g.Say(header, obj.Text("Name"), "is:")
		for _, content := range contents {
			content.Go("print description")
		}
		printed = true
	}
	return printed
}

func NameFullStop(G.IObject) string {
	return ""
}

type NameStatus func(obj G.IObject) string

func ArticleName(g G.Play, which string, status NameStatus) string {
	return articleName(g, which, false, status)
}
func DefiniteName(g G.Play, which string, status NameStatus) string {
	return articleName(g, which, true, status)
}
func articleName(g G.Play, which string, definite bool, status NameStatus) string {
	obj := g.The(which)
	text := obj.Text("Name")
	if obj.Is("proper-named") {
		text = lang.Titleize(text)
	} else {
		article := ""
		if definite {
			article = "the"
		} else {
			article = obj.Text("indefinite article")
			if article == "" {
				if obj.Is("plural-named") {
					article = "some"
				} else if lang.StartsWithVowel(text) {
					article = "an"
				} else {
					article = "a"
				}
			}
		}
		text = strings.Join([]string{article, strings.ToLower(text)}, " ")
	}
	if status != nil {
		if s := status(obj); s != "" {
			text = fmt.Sprintf("%s (%s)", text, s)
		} else {
			text = text + "."
		}
	}
	return text
}

//
// System actions
func init() {
	AddScript(func(s *Script) {

		// inform has two entries for some actions (looking under as an example, jumping as a counter example):
		// 1. carry out an actor looking under: if the player
		// 2. report an actor looking under: if not the player
		// its not exactly clear to me why, the docs give guidelines for this, but not rationale.
		// it might be interesting to queue says, if they need to be cancelled or held back.
		// keep in mind, most of these really want to be animations, and only sometimes voice.
		s.The("objects",
			Can("report look under").And("reporting look under").RequiresOne("actor"),
			To("report look under", func(g G.Play) {
				source, actor := g.The("action.Source"), g.The("action.Target")
				if g.The("player") == actor {
					g.Say("You find nothing of interest.")
				} else {
					g.Say(actor.Text("Name"), "looks under the", source.Text("Name"), ".")
				}
			}))

		s.The("objects",
			Can("print name").And("printing name text").RequiresNothing(),
			To("print name", func(g G.Play) {
				if text := ArticleName(g, "object", NameFullStop); len(text) > 0 {
					text = lang.Capitalize(text)
					g.Say(text)
				}
			}))

		s.The("containers",
			When("printing name text").
				Always(func(g G.Play) {
				// FIX: conditional return instead of Always?
				// or some way ( dependency injection ) to get at the event object
				// of course, rules producing values and stacks might work too.
				// FIX: a container is an opener... where do we print the opener status name
				// put this on doors for now.
				text := ArticleName(g, "container", func(obj G.IObject) (status string) {
					if obj.Is("closed") {
						if obj.Is("hinged") {
							status = "closed"
						}
					} else if list := obj.ObjectList("contents"); len(list) == 0 {
						if obj.Is("transparent") || obj.Is("open") {
							status = "empty"
						}
					}
					return status
				})
				text = lang.Capitalize(text)
				g.Say(text)
				g.StopHere()
			}))

		s.The("doors",
			When("printing name text").
				Always(func(g G.Play) {
				text := ArticleName(g, "door", func(obj G.IObject) (status string) {
					if obj.Is("hinged") {
						if obj.Is("open") {
							status = "open"
						} else {
							status = "closed"
						}
					}
					return status
				})
				text = lang.Capitalize(text)
				g.Say(text)
				g.StopHere()
			}))

		s.The("rooms",
			Can("report the view").And("reporting the view").RequiresNothing(),
			After("reporting the view").Always(func(g G.Play) {
				room := g.The("room")
				room.IsNow("visited")
			}),
			To("report the view", func(g G.Play) {
				room := g.The("room")
				// sometines a blank like is printed without this
				// (maybe certain directions? or going through doors?)
				// not sure why, so leaving this for consistency
				g.Say(Lines("", room.Text("Name")))
				g.Say(Lines(room.Text("description"), ""))
				// FIX? uses 1 to exclude the player....
				// again, this happens because we dont know if print description actually did anything (re:scenery, etc.)
				if contents := room.ObjectList("contents"); len(contents) > 1 {
					for _, obj := range contents {
						obj.Go("print description")
						g.Say("")
					}
				}
				// FIX: duplicated in stories describe the first room
				g.The("status bar").SetText("left", lang.Titleize(room.Text("Name")))
			}))
	})
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"github.com/dghubble/sling"
	"net/http"
	"strings"
	"io/ioutil"
	"errors"
)

type SpectoLabSimulation struct {
	Vendor      string `json:"vendor"`
	Api         string `json:"api"`
	Version     string `json:"version"`
	Name        string `json:"name"`
	Description string `json:"description"`
}

type SpectoLab struct {
	Host   string
	Port   string
	ApiKey string
}

func (s *SpectoLab) SimulationIsPresent(simulation Simulation) (bool, error) {
	url := s.buildUrl(fmt.Sprintf("/api/v1/users/%v/vendors/%v/apis/%v/versions/%v/%v", simulation.Vendor, simulation.Vendor, "build-pipeline", simulation.Version, simulation.Name))

	request, err := sling.New().Get(url).Add("Authorization", s.buildAuthorizationHeaderValue()).Request()
	if err != nil {
		return false, err
	}

	response, _ := http.DefaultClient.Do(request)
	defer response.Body.Close()

	return response.StatusCode == 200, nil
}

func (s *SpectoLab) CreateSimulation(simulationName Simulation) int {
	simulation := SpectoLabSimulation{Vendor: simulationName.Vendor, Api: "build-pipeline", Version: simulationName.Version, Name: simulationName.Name, Description: "test"}

	url := s.buildUrl("/api/v1/simulations")

	request, _ := sling.New().Post(url).Add("Authorization", s.buildAuthorizationHeaderValue()).BodyJSON(simulation).Request()
	response, _ := http.DefaultClient.Do(request)
	defer response.Body.Close()
	return response.StatusCode
}

func (s *SpectoLab) UploadSimulation(simulation Simulation, data []byte) (int, error) {
	simulationExists, _ := s.SimulationIsPresent(simulation)
	if !simulationExists {
		postStatusCode := s.CreateSimulation(simulation)
		if postStatusCode != 201 {
			return 0, errors.New("Failed to create a new simulation on the Specto Lab")
		}
	}

	url := s.buildUrl(fmt.Sprintf("/api/v1/users/%v/vendors/%v/apis/%v/versions/%v/%v/data", simulation.Vendor, simulation.Vendor, "build-pipeline", simulation.Version, simulation.Name))

	request, _ := sling.New().Put(url).Add("Authorization", s.buildAuthorizationHeaderValue()).Add("Content-Type", "application/json").Body(strings.NewReader(string(data))).Request()
	response, _ := http.DefaultClient.Do(request)
	defer response.Body.Close()

	return response.StatusCode, nil
}

func (s *SpectoLab) GetSimulation(simulation Simulation, overrideHost string) []byte {
	var url string

	if len(overrideHost) > 0 {
		url = s.buildUrl(fmt.Sprintf("/api/v1/users/%v/vendors/%v/apis/%v/versions/%v/%v/data?override-host=%v", simulation.Vendor, simulation.Vendor, "build-pipeline", simulation.Version, simulation.Name, overrideHost))
	} else {
		url = s.buildUrl(fmt.Sprintf("/api/v1/users/%v/vendors/%v/apis/%v/versions/%v/%v/data", simulation.Vendor, simulation.Vendor, "build-pipeline", simulation.Version, simulation.Name))
	}

	request, _ := sling.New().Get(url).Add("Authorization", s.buildAuthorizationHeaderValue()).Add("Content-Type", "application/json").Request()
	response, _ := http.DefaultClient.Do(request)
	defer response.Body.Close()

	body, _ := ioutil.ReadAll(response.Body)

	return body
}

func (s *SpectoLab) buildUrl(endpoint string) string {
	return fmt.Sprintf("%v%v", s.buildBaseUrl(), endpoint)
}

func (s *SpectoLab) buildBaseUrl() string {
	return fmt.Sprintf("http://%v:%v", s.Host, s.Port)
}

func (s *SpectoLab) buildAuthorizationHeaderValue() string {
	return fmt.Sprintf("Bearer %v", s.ApiKey)
}

<commit_msg>Updated to use the new Specto Lab urls<commit_after>package main

import (
	"fmt"
	"github.com/dghubble/sling"
	"net/http"
	"strings"
	"io/ioutil"
	"errors"
)

type SpectoLabSimulation struct {
	Version     string `json:"version"`
	Name        string `json:"name"`
	Description string `json:"description"`
}

type SpectoLab struct {
	Host   string
	Port   string
	ApiKey string
}

func (s *SpectoLab) SimulationIsPresent(simulation Simulation) (bool, error) {
	url := s.buildUrl(fmt.Sprintf("/api/v1/users/%v/simulations/%v/versions/%v", simulation.Vendor, simulation.Name, simulation.Version))

	request, err := sling.New().Get(url).Add("Authorization", s.buildAuthorizationHeaderValue()).Request()

	if err != nil {
		return false, err
	}

	response, err := http.DefaultClient.Do(request)

	if err != nil {
		return false, err
	}

	defer response.Body.Close()
	return response.StatusCode == 200, nil
}

func (s *SpectoLab) CreateSimulation(simulationName Simulation) (int, error) {
	simulation := SpectoLabSimulation{Version: simulationName.Version, Name: simulationName.Name, Description: "A description could go here"}

	url := s.buildUrl("/api/v1/simulations")
	request, err := sling.New().Post(url).BodyJSON(simulation).Add("Authorization", s.buildAuthorizationHeaderValue()).Request()

	if err != nil {
		return 0, err
	}

	response, err := http.DefaultClient.Do(request)
	defer response.Body.Close()

	if err != nil {
		return 0, err
	}

	return response.StatusCode, nil
}

func (s *SpectoLab) UploadSimulation(simulation Simulation, data []byte) (int, error) {
	simulationExists, err := s.SimulationIsPresent(simulation)

	if err != nil {
		return 0, err
	}

	if !simulationExists {
		postStatusCode, err := s.CreateSimulation(simulation)

		if err != nil {
			return 0, err
		}
		if postStatusCode != 201 {
			return 0, errors.New("Failed to create a new simulation on the Specto Lab")
		}
	}

	url := s.buildUrl(fmt.Sprintf("/api/v1/users/%v/simulations/%v/versions/%v/data", simulation.Vendor,  simulation.Name, simulation.Version))

	request, _ := sling.New().Put(url).Add("Authorization", s.buildAuthorizationHeaderValue()).Add("Content-Type", "application/json").Body(strings.NewReader(string(data))).Request()
	response, _ := http.DefaultClient.Do(request)
	defer response.Body.Close()

	return response.StatusCode, nil
}

func (s *SpectoLab) GetSimulation(simulation Simulation, overrideHost string) []byte {
	var url string
	if len(overrideHost) > 0 {
		url = s.buildUrl(fmt.Sprintf("/api/v1/users/%v/simulations/%v/versions/%v/data?override-host=%v", simulation.Vendor, simulation.Name, simulation.Version, overrideHost))
	} else {
		url = s.buildUrl(fmt.Sprintf("/api/v1/users/%v/simulations/%v/versions/%v/data", simulation.Vendor, simulation.Name, simulation.Version))
	}

	request, _ := sling.New().Get(url).Add("Authorization", s.buildAuthorizationHeaderValue()).Add("Content-Type", "application/json").Request()
	response, _ := http.DefaultClient.Do(request)
	defer response.Body.Close()

	body, _ := ioutil.ReadAll(response.Body)

	return body
}

func (s *SpectoLab) buildUrl(endpoint string) string {
	return fmt.Sprintf("%v%v", s.buildBaseUrl(), endpoint)
}

func (s *SpectoLab) buildBaseUrl() string {
	return fmt.Sprintf("http://%v:%v", s.Host, s.Port)
}

func (s *SpectoLab) buildAuthorizationHeaderValue() string {
	return fmt.Sprintf("Bearer %v", s.ApiKey)
}

<|endoftext|>


<commit_before>package static

import (
	"bytes"
	"compress/gzip"
	"encoding/base64"
	"errors"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"time"
)

// DirFile contains the static directory and file content info
type DirFile struct {
	Path       string
	Name       string
	Size       int64
	Mode       os.FileMode
	ModTime    int64
	IsDir      bool
	Compressed string
	Files      []*DirFile
}

// Files contains a full instance of a static file collection
type Files struct {
	dir Dir
}

// File contains the static FileInfo
type File struct {
	data         []byte
	path         string
	name         string
	size         int64
	mode         os.FileMode
	modTime      int64
	isDir        bool
	files        []*File
	lastDirIndex int
}

// Dir implements the FileSystem interface
type Dir struct {
	name             string
	isProductionMode bool
	files            map[string]*File
}

type httpFile struct {
	*bytes.Reader
	*File
}

// Config contains information about how extracting the data should behave
type Config struct {
	IsProductionMode bool
	Name             string
}

// Open returns the FileSystem DIR
func (dir Dir) Open(name string) (http.File, error) {

	if dir.isProductionMode {
		f, found := dir.files[path.Clean(name)]
		if !found {
			return nil, os.ErrNotExist
		}

		return f.File()
	}

	return os.Open(name)
}

// File returns an http.File or error
func (f File) File() (http.File, error) {

	// if production read filesystem file
	return &httpFile{
		Reader: bytes.NewReader(f.data),
		File:   &f,
	}, nil
}

// Close closes the File, rendering it unusable for I/O. It returns an error, if any.
func (f File) Close() error {
	return nil
}

// Readdir returns nil fileinfo and an error because the static FileSystem does not store directories
func (f File) Readdir(count int) ([]os.FileInfo, error) {

	if !f.IsDir() {
		return nil, errors.New("not a directory")
	}

	var files []os.FileInfo

	if count <= 0 {
		files = make([]os.FileInfo, len(f.files))
		count = len(f.files)
		f.lastDirIndex = 0
	} else {
		files = make([]os.FileInfo, count)
	}

	if f.lastDirIndex >= len(f.files) {
		return nil, io.EOF
	}

	if count+f.lastDirIndex >= len(f.files) {
		count = len(f.files)
	}

	for i := f.lastDirIndex; i < count; i++ {
		files = append(files, *f.files[i])
	}

	return files, nil
}

// Stat returns the FileInfo structure describing file. If there is an error, it will be of type *PathError.
func (f File) Stat() (os.FileInfo, error) {
	return f, nil
}

// Name returns the name of the file as presented to Open.
func (f File) Name() string {
	return f.name
}

// Size length in bytes for regular files; system-dependent for others
func (f File) Size() int64 {
	return f.size
}

// Mode returns file mode bits
func (f File) Mode() os.FileMode {
	mode := os.FileMode(0644)
	if f.IsDir() {
		return mode | os.ModeDir
	}
	return mode
}

// ModTime returns the files modification time
func (f File) ModTime() time.Time {
	return time.Unix(f.modTime, 0)
}

// IsDir reports whether f describes a directory.
func (f File) IsDir() bool {
	return f.isDir
}

// Sys returns the underlying data source (can return nil)
func (f File) Sys() interface{} {
	return f
}

// New create a new static file instance.
func New(config *Config, file *DirFile) (*Files, error) {
	files := map[string]*File{}

	if config.IsProductionMode {
		processFiles(files, file)
	}

	return &Files{
		dir: Dir{
			name:             config.Name,
			isProductionMode: config.IsProductionMode,
			files:            files,
		},
	}, nil
}

func processFiles(files map[string]*File, file *DirFile) *File {

	f := &File{
		path:    file.Path,
		name:    file.Name,
		size:    file.Size,
		mode:    file.Mode,
		modTime: file.ModTime,
		isDir:   file.IsDir,
		files:   []*File{},
	}

	files[f.path] = f

	if file.IsDir {
		for _, dirFile := range file.Files {
			resultFile := processFiles(files, dirFile)
			f.files = append(f.files, resultFile)
		}

		return f
	}

	// decompress file contents
	b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(file.Compressed))
	reader, err := gzip.NewReader(b64)
	if err != nil {
		log.Fatal(err)
	}

	f.data, err = ioutil.ReadAll(reader)
	if err != nil {
		log.Fatal(err)
	}

	return f
}

// FS returns an http.FileSystem object for serving files over http
func (s *Files) FS() http.FileSystem {
	return s.dir
}

// GetFile returns an http.File object
func (s *Files) GetFile(name string) (http.File, error) {
	return s.dir.Open(name)
}

// GetFileBytes return a files contents as []byte from the filesystem, static or local
func (s *Files) GetFileBytes(name string) ([]byte, error) {
	f, err := s.dir.Open(name)
	if err != nil {
		return nil, err
	}

	return ioutil.ReadAll(f)
}

// GetFileString return a files contents as string from the filesystem, static or local
func (s *Files) GetFileString(name string) (string, error) {
	f, err := s.dir.Open(name)
	if err != nil {
		return "", err
	}

	b, err := ioutil.ReadAll(f)
	if err != nil {
		return "", err
	}

	return string(b), nil
}
<commit_msg>Cleanup some code<commit_after>package static

import (
	"bytes"
	"compress/gzip"
	"encoding/base64"
	"errors"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"time"
)

// DirFile contains the static directory and file content info
type DirFile struct {
	Path       string
	Name       string
	Size       int64
	Mode       os.FileMode
	ModTime    int64
	IsDir      bool
	Compressed string
	Files      []*DirFile
}

// Files contains a full instance of a static file collection
type Files struct {
	dir Dir
}

// File contains the static FileInfo
type File struct {
	data         []byte
	path         string
	name         string
	size         int64
	mode         os.FileMode
	modTime      int64
	isDir        bool
	files        []*File
	lastDirIndex int
}

// Dir implements the FileSystem interface
type Dir struct {
	name             string
	isProductionMode bool
	files            map[string]*File
}

type httpFile struct {
	*bytes.Reader
	*File
}

// Config contains information about how extracting the data should behave
type Config struct {
	IsProductionMode bool
	Name             string
}

// Open returns the FileSystem DIR
func (dir Dir) Open(name string) (http.File, error) {

	if dir.isProductionMode {
		f, found := dir.files[path.Clean(name)]
		if !found {
			return nil, os.ErrNotExist
		}

		return f.File()
	}

	return os.Open(name)
}

// File returns an http.File or error
func (f File) File() (http.File, error) {

	// if production read filesystem file
	return &httpFile{
		Reader: bytes.NewReader(f.data),
		File:   &f,
	}, nil
}

// Close closes the File, rendering it unusable for I/O. It returns an error, if any.
func (f File) Close() error {
	return nil
}

// Readdir returns nil fileinfo and an error because the static FileSystem does not store directories
func (f File) Readdir(count int) ([]os.FileInfo, error) {

	if !f.IsDir() {
		return nil, errors.New("not a directory")
	}

	var files []os.FileInfo

	if count <= 0 {
		files = make([]os.FileInfo, len(f.files))
		count = len(f.files)
		f.lastDirIndex = 0
	} else {
		files = make([]os.FileInfo, count)
	}

	if f.lastDirIndex >= len(f.files) {
		return nil, io.EOF
	}

	if count+f.lastDirIndex >= len(f.files) {
		count = len(f.files)
	}

	for i := f.lastDirIndex; i < count; i++ {
		files = append(files, *f.files[i])
	}

	return files, nil
}

// Stat returns the FileInfo structure describing file. If there is an error, it will be of type *PathError.
func (f File) Stat() (os.FileInfo, error) {
	return f, nil
}

// Name returns the name of the file as presented to Open.
func (f File) Name() string {
	return f.name
}

// Size length in bytes for regular files; system-dependent for others
func (f File) Size() int64 {
	return f.size
}

// Mode returns file mode bits
func (f File) Mode() os.FileMode {
	mode := os.FileMode(0644)
	if f.IsDir() {
		return mode | os.ModeDir
	}
	return mode
}

// ModTime returns the files modification time
func (f File) ModTime() time.Time {
	return time.Unix(f.modTime, 0)
}

// IsDir reports whether f describes a directory.
func (f File) IsDir() bool {
	return f.isDir
}

// Sys returns the underlying data source (can return nil)
func (f File) Sys() interface{} {
	return f
}

// New create a new static file instance.
func New(config *Config, file *DirFile) (*Files, error) {
	files := map[string]*File{}

	if config.IsProductionMode {
		processFiles(files, file)
	}

	return &Files{
		dir: Dir{
			name:             config.Name,
			isProductionMode: config.IsProductionMode,
			files:            files,
		},
	}, nil
}

func processFiles(files map[string]*File, file *DirFile) *File {

	f := &File{
		path:    file.Path,
		name:    file.Name,
		size:    file.Size,
		mode:    file.Mode,
		modTime: file.ModTime,
		isDir:   file.IsDir,
		files:   []*File{},
	}

	files[f.path] = f

	if file.IsDir {
		for _, dirFile := range file.Files {
			resultFile := processFiles(files, dirFile)
			f.files = append(f.files, resultFile)
		}

		return f
	}

	// decompress file contents
	b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(file.Compressed))
	reader, err := gzip.NewReader(b64)
	if err != nil {
		log.Fatal(err)
	}

	f.data, err = ioutil.ReadAll(reader)
	if err != nil {
		log.Fatal(err)
	}

	return f
}

// FS returns an http.FileSystem object for serving files over http
func (s *Files) FS() http.FileSystem {
	return s.dir
}

// GetFile returns an http.File object
func (s *Files) GetFile(name string) (http.File, error) {
	return s.dir.Open(name)
}

// GetFileBytes return a files contents as []byte from the filesystem, static or local
func (s *Files) GetFileBytes(name string) ([]byte, error) {
	f, err := s.dir.Open(name)
	if err != nil {
		return nil, err
	}

	return ioutil.ReadAll(f)
}

// GetFileString return a files contents as string from the filesystem, static or local
func (s *Files) GetFileString(name string) (string, error) {

	b, err := s.GetFileBytes(name)
	if err != nil {
		return "", err
	}

	return string(b), nil
}
<|endoftext|>


<commit_before>package server

import (
	"fmt"
	"os"
	"runtime"
	"runtime/pprof"
	"strings"
	"time"

	"github.com/Comcast/webpa-common/logging"
	"github.com/Comcast/webpa-common/xmetrics"
	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

const (
	// DefaultPrimaryAddress is the bind address of the primary server (e.g. talaria, petasos, etc)
	DefaultPrimaryAddress = ":8080"

	// DefaultHealthAddress is the bind address of the health check server
	DefaultHealthAddress = ":8081"

	// DefaultMetricsAddress is the bind address of the metrics server
	DefaultMetricsAddress = ":8082"

	// DefaultHealthLogInterval is the interval at which health statistics are emitted
	// when a non-positive log interval is specified
	DefaultHealthLogInterval time.Duration = time.Duration(60 * time.Second)

	// DefaultLogConnectionState is the default setting for logging connection state messages.  This
	// value is primarily used when a *WebPA value is nil.
	DefaultLogConnectionState = false

	// AlternateSuffix is the suffix appended to the server name, along with a period (.), for
	// logging information pertinent to the alternate server.
	AlternateSuffix = "alternate"

	// DefaultProject is used as a metrics namespace when one is not defined.
	DefaultProject = "xmidt"

	// HealthSuffix is the suffix appended to the server name, along with a period (.), for
	// logging information pertinent to the health server.
	HealthSuffix = "health"

	// PprofSuffix is the suffix appended to the server name, along with a period (.), for
	// logging information pertinent to the pprof server.
	PprofSuffix = "pprof"

	// MetricsSuffix is the suffix appended to the server name, along with a period (.), for
	// logging information pertinent to the metrics server.
	MetricsSuffix = "metrics"

	// FileFlagName is the name of the command-line flag for specifying an alternate
	// configuration file for Viper to hunt for.
	FileFlagName = "file"

	// FileFlagShorthand is the command-line shortcut flag for FileFlagName
	FileFlagShorthand = "f"

	// CPUProfileFlagName is the command-line flag for creating a cpuprofile on the server
	CPUProfileFlagName = "cpuprofile"

	// CPUProfileShortHand is the command-line shortcut for creating cpushorthand on the server
	CPUProfileShorthand = "c"

	// MemProfileFlagName is the command-line flag for creating memprofile on the server
	MemProfileFlagName = "memprofile"

	// MemProfileShortHand is the command-line shortcut for creating memprofile on the server
	MemProfileShorthand = "m"
)

// ConfigureFlagSet adds the standard set of WebPA flags to the supplied FlagSet.  Use of this function
// is optional, and necessary only if the standard flags should be supported.  However, this is highly desirable,
// as ConfigureViper can make use of the standard flags to tailor how configuration is loaded or if gathering cpuprofile
// or memprofile data is needed.
func ConfigureFlagSet(applicationName string, f *pflag.FlagSet) {
	f.StringP(FileFlagName, FileFlagShorthand, applicationName, "base name of the configuration file")
	f.StringP(CPUProfileFlagName, CPUProfileShorthand, "cpuprofile", "base name of the cpuprofile file")
	f.StringP(MemProfileFlagName, MemProfileShorthand, "memprofile", "base name of the memprofile file")
}

// create CPUProfileFiles creates a cpu profile of the server, its triggered by the optional flag cpuprofile
//
// the CPU profile is created on the server's start
func CreateCPUProfileFile(v *viper.Viper, fp *pflag.FlagSet, l log.Logger) {
	flag := fp.Lookup("cpuprofile")
	if flag == nil {
		return
	}

	f, err := os.Create(flag.Value.String())
	if err != nil {
		l.Log("could not create CPU profile: ", err)
	}
	defer f.Close()

	if err := pprof.StartCPUProfile(f); err != nil {
		l.Log("could not start CPU profile: ", err)
	}

	defer pprof.StopCPUProfile()
}

// Create CPUProfileFiles creates a memory profile of the server, its triggered by the optional flag memprofile
//
// the memory profile is created on the server's exit.
// this function should be used within the application.
func CreateMemoryProfileFile(v *viper.Viper, fp *pflag.FlagSet, l log.Logger) {
	flag := fp.Lookup("memprofile")
	if flag == nil {
		return
	}

	f, err := os.Create(flag.Value.String())
	if err != nil {
		l.Log("could not create memory profile: ", err)
	}

	defer f.Close()
	runtime.GC()
	if err := pprof.WriteHeapProfile(f); err != nil {
		l.Log("could not write memory profile: ", err)
	}
}

// ConfigureViper configures a Viper instances using the opinionated WebPA settings.  All WebPA servers should
// use this function.
//
// The flagSet is optional.  If supplied, it will be bound to the given Viper instance.  Additionally, if the
// flagSet has a FileFlagName flag, it will be used as the configuration name to hunt for instead of the
// application name.
func ConfigureViper(applicationName string, f *pflag.FlagSet, v *viper.Viper) (err error) {
	v.AddConfigPath(fmt.Sprintf("/etc/%s", applicationName))
	v.AddConfigPath(fmt.Sprintf("$HOME/.%s", applicationName))
	v.AddConfigPath(".")

	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.SetEnvPrefix(applicationName)
	v.AutomaticEnv()

	v.SetDefault("primary.name", applicationName)
	v.SetDefault("primary.address", DefaultPrimaryAddress)
	v.SetDefault("primary.logConnectionState", DefaultLogConnectionState)

	v.SetDefault("alternate.name", fmt.Sprintf("%s.%s", applicationName, AlternateSuffix))

	v.SetDefault("health.name", fmt.Sprintf("%s.%s", applicationName, HealthSuffix))
	v.SetDefault("health.address", DefaultHealthAddress)
	v.SetDefault("health.logInterval", DefaultHealthLogInterval)
	v.SetDefault("health.logConnectionState", DefaultLogConnectionState)

	v.SetDefault("pprof.name", fmt.Sprintf("%s.%s", applicationName, PprofSuffix))
	v.SetDefault("pprof.logConnectionState", DefaultLogConnectionState)

	v.SetDefault("metric.name", fmt.Sprintf("%s.%s", applicationName, MetricsSuffix))
	v.SetDefault("metric.address", DefaultMetricsAddress)

	v.SetDefault("project", DefaultProject)

	configName := applicationName
	if f != nil {
		if fileFlag := f.Lookup(FileFlagName); fileFlag != nil {
			// use the command-line to specify the base name of the file to be searched for
			configName = fileFlag.Value.String()
		}

		err = v.BindPFlags(f)
	}

	v.SetConfigName(configName)
	return
}

/*
Configure is a one-stop shopping function for preparing WebPA configuration.  This function
does not itself read in configuration from the Viper environment.  Typical usage is:

    var (
      f = pflag.NewFlagSet()
      v = viper.New()
    )

    if err := server.Configure("petasos", os.Args, f, v); err != nil {
      // deal with the error, possibly just exiting
    }

    // further customizations to the Viper instance can be done here

    if err := v.ReadInConfig(); err != nil {
      // more error handling
    }

Usage of this function is only necessary if custom configuration is needed.  Normally,
using New will suffice.
*/
func Configure(applicationName string, arguments []string, f *pflag.FlagSet, v *viper.Viper) (err error) {
	if f != nil {
		ConfigureFlagSet(applicationName, f)
		err = f.Parse(arguments)
		if err != nil {
			return
		}
	}

	err = ConfigureViper(applicationName, f, v)
	return
}

/*
Initialize handles the bootstrapping of the server code for a WebPA node.  It configures Viper,
reads configuration, and unmarshals the appropriate objects.  This function is typically all that's
needed to fully instantiate a WebPA server.  Typical usage:

    var (
      f = pflag.NewFlagSet()
      v = viper.New()

      // can customize both the FlagSet and the Viper before invoking New
      logger, registry, webPA, err = server.Initialize("petasos", os.Args, f, v)
    )

    if err != nil {
      // deal with the error, possibly just exiting
    }

Note that the FlagSet is optional but highly encouraged.  If not supplied, then no command-line binding
is done for the unmarshalled configuration.

This function always returns a logger, regardless of any errors.  This allows clients to use the returned
logger when reporting errors.  This function falls back to a logger that writes to os.Stdout if it cannot
create a logger from the Viper environment.
*/
func Initialize(applicationName string, arguments []string, f *pflag.FlagSet, v *viper.Viper, modules ...xmetrics.Module) (logger log.Logger, registry xmetrics.Registry, webPA *WebPA, err error) {
	defer func() {
		if err != nil {
			// never return a WebPA in the presence of an error, to
			// avoid an ambiguous API
			webPA = nil

			// Make sure there's at least a default logger for the caller to use
			logger = logging.DefaultLogger()
		}
	}()

	if err = Configure(applicationName, arguments, f, v); err != nil {
		return
	}

	if err = v.ReadInConfig(); err != nil {
		return
	}

	webPA = &WebPA{
		ApplicationName: applicationName,
	}

	err = v.Unmarshal(webPA)
	if err != nil {
		return
	}

	logger = logging.New(webPA.Log)
	logger.Log(level.Key(), level.InfoValue(), logging.MessageKey(), "initialized Viper environment", "configurationFile", v.ConfigFileUsed())

	if len(webPA.Metric.MetricsOptions.Namespace) == 0 {
		webPA.Metric.MetricsOptions.Namespace = applicationName
	}

	if len(webPA.Metric.MetricsOptions.Subsystem) == 0 {
		webPA.Metric.MetricsOptions.Subsystem = applicationName
	}

	webPA.Metric.MetricsOptions.Logger = logger
	registry, err = webPA.Metric.NewRegistry(modules...)
	if err != nil {
		return
	}

	// CreateCPUProfileFile(v, f, logger)
	return
}
<commit_msg>server: include case with no fp provided<commit_after>package server

import (
	"fmt"
	"os"
	"runtime"
	"runtime/pprof"
	"strings"
	"time"

	"github.com/Comcast/webpa-common/logging"
	"github.com/Comcast/webpa-common/xmetrics"
	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

const (
	// DefaultPrimaryAddress is the bind address of the primary server (e.g. talaria, petasos, etc)
	DefaultPrimaryAddress = ":8080"

	// DefaultHealthAddress is the bind address of the health check server
	DefaultHealthAddress = ":8081"

	// DefaultMetricsAddress is the bind address of the metrics server
	DefaultMetricsAddress = ":8082"

	// DefaultHealthLogInterval is the interval at which health statistics are emitted
	// when a non-positive log interval is specified
	DefaultHealthLogInterval time.Duration = time.Duration(60 * time.Second)

	// DefaultLogConnectionState is the default setting for logging connection state messages.  This
	// value is primarily used when a *WebPA value is nil.
	DefaultLogConnectionState = false

	// AlternateSuffix is the suffix appended to the server name, along with a period (.), for
	// logging information pertinent to the alternate server.
	AlternateSuffix = "alternate"

	// DefaultProject is used as a metrics namespace when one is not defined.
	DefaultProject = "xmidt"

	// HealthSuffix is the suffix appended to the server name, along with a period (.), for
	// logging information pertinent to the health server.
	HealthSuffix = "health"

	// PprofSuffix is the suffix appended to the server name, along with a period (.), for
	// logging information pertinent to the pprof server.
	PprofSuffix = "pprof"

	// MetricsSuffix is the suffix appended to the server name, along with a period (.), for
	// logging information pertinent to the metrics server.
	MetricsSuffix = "metrics"

	// FileFlagName is the name of the command-line flag for specifying an alternate
	// configuration file for Viper to hunt for.
	FileFlagName = "file"

	// FileFlagShorthand is the command-line shortcut flag for FileFlagName
	FileFlagShorthand = "f"

	// CPUProfileFlagName is the command-line flag for creating a cpuprofile on the server
	CPUProfileFlagName = "cpuprofile"

	// CPUProfileShortHand is the command-line shortcut for creating cpushorthand on the server
	CPUProfileShorthand = "c"

	// MemProfileFlagName is the command-line flag for creating memprofile on the server
	MemProfileFlagName = "memprofile"

	// MemProfileShortHand is the command-line shortcut for creating memprofile on the server
	MemProfileShorthand = "m"
)

// ConfigureFlagSet adds the standard set of WebPA flags to the supplied FlagSet.  Use of this function
// is optional, and necessary only if the standard flags should be supported.  However, this is highly desirable,
// as ConfigureViper can make use of the standard flags to tailor how configuration is loaded or if gathering cpuprofile
// or memprofile data is needed.
func ConfigureFlagSet(applicationName string, f *pflag.FlagSet) {
	f.StringP(FileFlagName, FileFlagShorthand, applicationName, "base name of the configuration file")
	f.StringP(CPUProfileFlagName, CPUProfileShorthand, "cpuprofile", "base name of the cpuprofile file")
	f.StringP(MemProfileFlagName, MemProfileShorthand, "memprofile", "base name of the memprofile file")
}

// create CPUProfileFiles creates a cpu profile of the server, its triggered by the optional flag cpuprofile
//
// the CPU profile is created on the server's start
func CreateCPUProfileFile(v *viper.Viper, fp *pflag.FlagSet, l log.Logger) {
	if fp == nil {
		return
	}

	flag := fp.Lookup("cpuprofile")
	if flag == nil {
		return
	}

	f, err := os.Create(flag.Value.String())
	if err != nil {
		l.Log("could not create CPU profile: ", err)
	}
	defer f.Close()

	if err := pprof.StartCPUProfile(f); err != nil {
		l.Log("could not start CPU profile: ", err)
	}

	defer pprof.StopCPUProfile()
}

// Create CPUProfileFiles creates a memory profile of the server, its triggered by the optional flag memprofile
//
// the memory profile is created on the server's exit.
// this function should be used within the application.
func CreateMemoryProfileFile(v *viper.Viper, fp *pflag.FlagSet, l log.Logger) {
	if fp == nil {
		return
	}

	flag := fp.Lookup("memprofile")
	if flag == nil {
		return
	}

	f, err := os.Create(flag.Value.String())
	if err != nil {
		l.Log("could not create memory profile: ", err)
	}

	defer f.Close()
	runtime.GC()
	if err := pprof.WriteHeapProfile(f); err != nil {
		l.Log("could not write memory profile: ", err)
	}
}

// ConfigureViper configures a Viper instances using the opinionated WebPA settings.  All WebPA servers should
// use this function.
//
// The flagSet is optional.  If supplied, it will be bound to the given Viper instance.  Additionally, if the
// flagSet has a FileFlagName flag, it will be used as the configuration name to hunt for instead of the
// application name.
func ConfigureViper(applicationName string, f *pflag.FlagSet, v *viper.Viper) (err error) {
	v.AddConfigPath(fmt.Sprintf("/etc/%s", applicationName))
	v.AddConfigPath(fmt.Sprintf("$HOME/.%s", applicationName))
	v.AddConfigPath(".")

	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.SetEnvPrefix(applicationName)
	v.AutomaticEnv()

	v.SetDefault("primary.name", applicationName)
	v.SetDefault("primary.address", DefaultPrimaryAddress)
	v.SetDefault("primary.logConnectionState", DefaultLogConnectionState)

	v.SetDefault("alternate.name", fmt.Sprintf("%s.%s", applicationName, AlternateSuffix))

	v.SetDefault("health.name", fmt.Sprintf("%s.%s", applicationName, HealthSuffix))
	v.SetDefault("health.address", DefaultHealthAddress)
	v.SetDefault("health.logInterval", DefaultHealthLogInterval)
	v.SetDefault("health.logConnectionState", DefaultLogConnectionState)

	v.SetDefault("pprof.name", fmt.Sprintf("%s.%s", applicationName, PprofSuffix))
	v.SetDefault("pprof.logConnectionState", DefaultLogConnectionState)

	v.SetDefault("metric.name", fmt.Sprintf("%s.%s", applicationName, MetricsSuffix))
	v.SetDefault("metric.address", DefaultMetricsAddress)

	v.SetDefault("project", DefaultProject)

	configName := applicationName
	if f != nil {
		if fileFlag := f.Lookup(FileFlagName); fileFlag != nil {
			// use the command-line to specify the base name of the file to be searched for
			configName = fileFlag.Value.String()
		}

		err = v.BindPFlags(f)
	}

	v.SetConfigName(configName)
	return
}

/*
Configure is a one-stop shopping function for preparing WebPA configuration.  This function
does not itself read in configuration from the Viper environment.  Typical usage is:

    var (
      f = pflag.NewFlagSet()
      v = viper.New()
    )

    if err := server.Configure("petasos", os.Args, f, v); err != nil {
      // deal with the error, possibly just exiting
    }

    // further customizations to the Viper instance can be done here

    if err := v.ReadInConfig(); err != nil {
      // more error handling
    }

Usage of this function is only necessary if custom configuration is needed.  Normally,
using New will suffice.
*/
func Configure(applicationName string, arguments []string, f *pflag.FlagSet, v *viper.Viper) (err error) {
	if f != nil {
		ConfigureFlagSet(applicationName, f)
		err = f.Parse(arguments)
		if err != nil {
			return
		}
	}

	err = ConfigureViper(applicationName, f, v)
	return
}

/*
Initialize handles the bootstrapping of the server code for a WebPA node.  It configures Viper,
reads configuration, and unmarshals the appropriate objects.  This function is typically all that's
needed to fully instantiate a WebPA server.  Typical usage:

    var (
      f = pflag.NewFlagSet()
      v = viper.New()

      // can customize both the FlagSet and the Viper before invoking New
      logger, registry, webPA, err = server.Initialize("petasos", os.Args, f, v)
    )

    if err != nil {
      // deal with the error, possibly just exiting
    }

Note that the FlagSet is optional but highly encouraged.  If not supplied, then no command-line binding
is done for the unmarshalled configuration.

This function always returns a logger, regardless of any errors.  This allows clients to use the returned
logger when reporting errors.  This function falls back to a logger that writes to os.Stdout if it cannot
create a logger from the Viper environment.
*/
func Initialize(applicationName string, arguments []string, f *pflag.FlagSet, v *viper.Viper, modules ...xmetrics.Module) (logger log.Logger, registry xmetrics.Registry, webPA *WebPA, err error) {
	defer func() {
		if err != nil {
			// never return a WebPA in the presence of an error, to
			// avoid an ambiguous API
			webPA = nil

			// Make sure there's at least a default logger for the caller to use
			logger = logging.DefaultLogger()
		}
	}()

	if err = Configure(applicationName, arguments, f, v); err != nil {
		return
	}

	if err = v.ReadInConfig(); err != nil {
		return
	}

	webPA = &WebPA{
		ApplicationName: applicationName,
	}

	err = v.Unmarshal(webPA)
	if err != nil {
		return
	}

	logger = logging.New(webPA.Log)
	logger.Log(level.Key(), level.InfoValue(), logging.MessageKey(), "initialized Viper environment", "configurationFile", v.ConfigFileUsed())

	if len(webPA.Metric.MetricsOptions.Namespace) == 0 {
		webPA.Metric.MetricsOptions.Namespace = applicationName
	}

	if len(webPA.Metric.MetricsOptions.Subsystem) == 0 {
		webPA.Metric.MetricsOptions.Subsystem = applicationName
	}

	webPA.Metric.MetricsOptions.Logger = logger
	registry, err = webPA.Metric.NewRegistry(modules...)
	if err != nil {
		return
	}

	CreateCPUProfileFile(v, f, logger)

	return
}
<|endoftext|>


<commit_before>package server

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"io/ioutil"
	"net/http"
	"sync"
	"time"

	"github.com/Comcast/webpa-common/concurrent"
	"github.com/Comcast/webpa-common/health"
	"github.com/Comcast/webpa-common/logging"
	"github.com/Comcast/webpa-common/xmetrics"
	"github.com/go-kit/kit/log"
	stdprometheus "github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	// ErrorNoPrimaryAddress is the error returned when no primary address is specified in a WebPA instance
	ErrorNoPrimaryAddress = errors.New("No primary address configured")
)

// executor is an internal type used to start an HTTP server.  *http.Server implements
// this interface.  It can be mocked for testing.
type executor interface {
	ListenAndServe() error
	ListenAndServeTLS(certificateFile, keyFile string) error
}

// Secure exposes the optional certificate information to be used when starting an HTTP server.
type Secure interface {
	// Certificate returns the certificate information associated with this Secure instance.
	// BOTH the returned file paths must be non-empty if a TLS server is desired.
	Certificate() (certificateFile, keyFile string)
}

// ListenAndServe invokes the appropriate server method based on the secure information.
// If Secure.Certificate() returns both a certificateFile and a keyFile, e.ListenAndServeTLS()
// is called to start the server.  Otherwise, e.ListenAndServe() is used.
func ListenAndServe(logger log.Logger, s Secure, e executor) {
	certificateFile, keyFile := s.Certificate()
	if len(certificateFile) > 0 && len(keyFile) > 0 {

		go func() {
			logging.Error(logger).Log(
				logging.ErrorKey(), e.ListenAndServeTLS(certificateFile, keyFile),
			)
		}()
	} else {
		go func() {
			logging.Error(logger).Log(
				logging.ErrorKey(), e.ListenAndServe(),
			)
		}()
	}
}

// Basic describes a simple HTTP server.  Typically, this struct has its values
// injected via Viper.  See the New function in this package.
type Basic struct {
	Name               string
	Address            string
	CertificateFile    string
	KeyFile            string
	ClientCACertFile   string
	LogConnectionState bool
}

func (b *Basic) Certificate() (certificateFile, keyFile string) {
	return b.CertificateFile, b.KeyFile
}

// New creates an http.Server using this instance's configuration.  The given logger is required,
// but the handler may be nil.  If the handler is nil, http.DefaultServeMux is used, which matches
// the behavior of http.Server.
//
// This method returns nil if the configured address is empty, effectively disabling
// this server from startup.
func (b *Basic) New(logger log.Logger, handler http.Handler) *http.Server {
	if len(b.Address) == 0 {
		return nil
	}

	// Adding MTLS support using client CA cert pool
	var tlsConfig *tls.Config
	certificateFile, keyFile := b.Certificate()
	// Only when HTTPS i.e. cert & key present, check for client CA and set TLS config for MTLS
	if len(certificateFile) > 0 && len(keyFile) > 0 && len(b.ClientCACertFile) > 0 {

		caCert, err := ioutil.ReadFile(b.ClientCACertFile)
		if err != nil {
			logging.Error(logger).Log(logging.MessageKey(), "Error in reading ClientCACertFile ",
				logging.ErrorKey(), err)
		} else {
			caCertPool := x509.NewCertPool()
			caCertPool.AppendCertsFromPEM(caCert)
			tlsConfig = &tls.Config{
				ClientCAs:  caCertPool,
				ClientAuth: tls.RequireAndVerifyClientCert,
			}
			tlsConfig.BuildNameToCertificate()
		}
	}

	server := &http.Server{
		Addr:      b.Address,
		Handler:   handler,
		ErrorLog:  NewErrorLog(b.Name, logger),
		TLSConfig: tlsConfig,
	}

	if b.LogConnectionState {
		server.ConnState = NewConnectionStateLogger(b.Name, logger)
	}

	return server
}

// Metric is the configurable factory for a metrics server.
type Metric struct {
	Name               string
	Address            string
	CertificateFile    string
	KeyFile            string
	LogConnectionState bool
	HandlerOptions     promhttp.HandlerOpts
	MetricsOptions     xmetrics.Options
}

func (m *Metric) Certificate() (certificateFile, keyFile string) {
	return m.CertificateFile, m.KeyFile
}

func (m *Metric) NewRegistry(modules ...xmetrics.Module) (xmetrics.Registry, error) {
	// always append the builtin server metrics, which can be overridden in configuration
	modules = append(modules, Metrics)
	return xmetrics.NewRegistry(&m.MetricsOptions, modules...)
}

func (m *Metric) New(logger log.Logger, gatherer stdprometheus.Gatherer) *http.Server {
	if len(m.Address) == 0 {
		return nil
	}

	mux := http.NewServeMux()
	mux.Handle("/metrics", promhttp.HandlerFor(gatherer, m.HandlerOptions))

	server := &http.Server{
		Addr:     m.Address,
		Handler:  mux,
		ErrorLog: NewErrorLog(m.Name, logger),
	}

	if m.LogConnectionState {
		server.ConnState = NewConnectionStateLogger(m.Name, logger)
	}

	return server
}

// Health represents a configurable factory for a Health server.  As with the Basic type,
// if the Address is not specified, health is considered to be disabled.
//
// Due to a limitation of Viper, this struct does not use an embedded Basic
// instance.  Rather, it duplicates the fields so that Viper can inject them.
type Health struct {
	Name               string
	Address            string
	CertificateFile    string
	KeyFile            string
	LogConnectionState bool
	LogInterval        time.Duration
	Options            []string
}

func (h *Health) Certificate() (certificateFile, keyFile string) {
	return h.CertificateFile, h.KeyFile
}

// NewHealth creates a Health instance from this instance's configuration.  If the Address
// field is not supplied, this method returns nil.
func (h *Health) NewHealth(logger log.Logger, options ...health.Option) *health.Health {
	if len(h.Address) == 0 {
		return nil
	}

	for _, value := range h.Options {
		options = append(options, health.Stat(value))
	}

	return health.New(
		h.LogInterval,
		logger,
		options...,
	)
}

// New creates an HTTP server instance for serving health statistics.  If the health parameter
// is nil, then h.NewHealth is used to create a Health instance.  Otherwise, the health parameter
// is returned as is.
//
// If the Address option is not supplied, the health module is considered to be disabled.  In that
// case, this method simply returns the health parameter as the monitor and a nil server instance.
func (h *Health) New(logger log.Logger, health *health.Health) (*health.Health, *http.Server) {
	if len(h.Address) == 0 {
		// health is disabled
		return nil, nil
	}

	if health == nil {
		if health = h.NewHealth(logger); health == nil {
			// should never hit this case, since NewHealth performs the same
			// Address field check as this method.  but, just to be safe ...
			return nil, nil
		}
	}

	mux := http.NewServeMux()
	mux.Handle("/health", health)

	server := &http.Server{
		Addr:     h.Address,
		Handler:  mux,
		ErrorLog: NewErrorLog(h.Name, logger),
	}

	if h.LogConnectionState {
		server.ConnState = NewConnectionStateLogger(h.Name, logger)
	}

	return health, server
}

// WebPA represents a server component within the WebPA cluster.  It is used for both
// primary servers (e.g. petasos) and supporting, embedded servers such as pprof.
type WebPA struct {
	// Primary is the main server for this application, e.g. petasos.
	Primary Basic

	// Alternate is an alternate server which serves the primary application logic.
	// Used to have the same API served on more than one port and possibly more than
	// one protocol, e.g. HTTP and HTTPS.
	Alternate Basic

	// Health describes the health server for this application.  Note that if the Address
	// is empty, no health server is started.
	Health Health

	// Pprof describes the pprof server for this application.  Note that if the Address
	// is empty, no pprof server is started.
	Pprof Basic

	// Metric describes the metrics provider server for this application
	Metric Metric

	// Log is the logging configuration for this application.
	Log *logging.Options
}

// Prepare gets a WebPA server ready for execution.  This method does not return errors, but the returned
// Runnable may return an error.  The supplied logger will usually come from the New function, but the
// WebPA.Log object can be used to create a different logger if desired.
//
// The caller may pass an arbitrary Health instance.  If this parameter is nil, this method will attempt to
// create one using Health.NewHealth.  In either case, if Health.Address is not supplied, no health server
// will be instantiated.
//
// The caller may also pass a gatherer type. If it is not provided, the default provided by prometheus is used.
//
// The supplied http.Handler is used for the primary server.  If the alternate server has an address,
// it will also be used for that server.  The health server uses an internally create handler, while pprof and metrics
// servers use http.DefaultServeMux.  The health Monitor created from configuration is returned so that other
// infrastructure can make use of it.
func (w *WebPA) Prepare(logger log.Logger, health *health.Health, registry xmetrics.Registry, primaryHandler http.Handler) (health.Monitor, concurrent.Runnable) {
	// allow the health instance to be non-nil, in which case it will be used in favor of
	// the WebPA-configured instance.
	healthHandler, healthServer := w.Health.New(logger, health)
	infoLog := logging.Info(logger)

	return healthHandler, concurrent.RunnableFunc(func(waitGroup *sync.WaitGroup, shutdown <-chan struct{}) error {
		if healthHandler != nil && healthServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Health.Name, "address", w.Health.Address)
			ListenAndServe(logger, &w.Health, healthServer)
			healthHandler.Run(waitGroup, shutdown)

			// wrap the primary handler in the RequestTracker decorator
			primaryHandler = healthHandler.RequestTracker(primaryHandler)
		}

		if pprofServer := w.Pprof.New(logger, nil); pprofServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Pprof.Name, "address", w.Pprof.Address)
			ListenAndServe(logger, &w.Pprof, pprofServer)
		}

		//wrap common metrics around both server handler
		primaryHandler = w.decorateWithBasicMetrics(registry, primaryHandler)

		if primaryServer := w.Primary.New(logger, primaryHandler); primaryServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Primary.Name, "address", w.Primary.Address)
			ListenAndServe(logger, &w.Primary, primaryServer)
		} else {
			return ErrorNoPrimaryAddress
		}

		if alternateServer := w.Alternate.New(logger, primaryHandler); alternateServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Alternate.Name, "address", w.Alternate.Address)
			ListenAndServe(logger, &w.Alternate, alternateServer)
		}

		if metricsServer := w.Metric.New(logger, registry); metricsServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Metric.Name, "address", w.Metric.Address)
			ListenAndServe(logger, &w.Metric, metricsServer)
		}

		return nil
	})
}

//decorateWithBasicMetrics wraps a WebPA server handler with basic instrumentation metrics
func (w *WebPA) decorateWithBasicMetrics(p xmetrics.PrometheusProvider, next http.Handler) http.Handler {
	var (
		requestCounterVec    = p.NewCounterVec("api_requests_total")
		inFlightGauge        = p.NewGaugeVec("in_flight_requests").WithLabelValues()
		requestDurationVec   = p.NewHistogramVec("request_duration_seconds")
		requestSizeVec       = p.NewHistogramVec("request_size_bytes")
		responseSizeVec      = p.NewHistogramVec("response_size_bytes")
		timeToWriteHeaderVec = p.NewHistogramVec("time_writing_header_seconds")
	)

	//todo: Example documentation does something interesting with /pull vs. /push endpoints
	//https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp#InstrumentHandlerDuration
	//for now, let's keep it simple so /metrics only

	return promhttp.InstrumentHandlerInFlight(inFlightGauge,
		promhttp.InstrumentHandlerCounter(requestCounterVec,
			promhttp.InstrumentHandlerDuration(requestDurationVec,
				promhttp.InstrumentHandlerResponseSize(responseSizeVec,
					promhttp.InstrumentHandlerRequestSize(requestSizeVec,
						promhttp.InstrumentHandlerTimeToWriteHeader(timeToWriteHeaderVec, next))),
			),
		),
	)
}
<commit_msg>Inject static response headers (which can include the version) into all responses from the primary and metrics handlers<commit_after>package server

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"io/ioutil"
	"net/http"
	"sync"
	"time"

	"github.com/Comcast/webpa-common/concurrent"
	"github.com/Comcast/webpa-common/health"
	"github.com/Comcast/webpa-common/logging"
	"github.com/Comcast/webpa-common/xhttp"
	"github.com/Comcast/webpa-common/xmetrics"
	"github.com/go-kit/kit/log"
	"github.com/justinas/alice"
	stdprometheus "github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	// ErrorNoPrimaryAddress is the error returned when no primary address is specified in a WebPA instance
	ErrorNoPrimaryAddress = errors.New("No primary address configured")
)

// executor is an internal type used to start an HTTP server.  *http.Server implements
// this interface.  It can be mocked for testing.
type executor interface {
	ListenAndServe() error
	ListenAndServeTLS(certificateFile, keyFile string) error
}

// Secure exposes the optional certificate information to be used when starting an HTTP server.
type Secure interface {
	// Certificate returns the certificate information associated with this Secure instance.
	// BOTH the returned file paths must be non-empty if a TLS server is desired.
	Certificate() (certificateFile, keyFile string)
}

// ListenAndServe invokes the appropriate server method based on the secure information.
// If Secure.Certificate() returns both a certificateFile and a keyFile, e.ListenAndServeTLS()
// is called to start the server.  Otherwise, e.ListenAndServe() is used.
func ListenAndServe(logger log.Logger, s Secure, e executor) {
	certificateFile, keyFile := s.Certificate()
	if len(certificateFile) > 0 && len(keyFile) > 0 {

		go func() {
			logging.Error(logger).Log(
				logging.ErrorKey(), e.ListenAndServeTLS(certificateFile, keyFile),
			)
		}()
	} else {
		go func() {
			logging.Error(logger).Log(
				logging.ErrorKey(), e.ListenAndServe(),
			)
		}()
	}
}

// Basic describes a simple HTTP server.  Typically, this struct has its values
// injected via Viper.  See the New function in this package.
type Basic struct {
	Name               string
	Address            string
	CertificateFile    string
	KeyFile            string
	ClientCACertFile   string
	LogConnectionState bool
}

func (b *Basic) Certificate() (certificateFile, keyFile string) {
	return b.CertificateFile, b.KeyFile
}

// New creates an http.Server using this instance's configuration.  The given logger is required,
// but the handler may be nil.  If the handler is nil, http.DefaultServeMux is used, which matches
// the behavior of http.Server.
//
// This method returns nil if the configured address is empty, effectively disabling
// this server from startup.
func (b *Basic) New(logger log.Logger, handler http.Handler) *http.Server {
	if len(b.Address) == 0 {
		return nil
	}

	// Adding MTLS support using client CA cert pool
	var tlsConfig *tls.Config
	certificateFile, keyFile := b.Certificate()
	// Only when HTTPS i.e. cert & key present, check for client CA and set TLS config for MTLS
	if len(certificateFile) > 0 && len(keyFile) > 0 && len(b.ClientCACertFile) > 0 {

		caCert, err := ioutil.ReadFile(b.ClientCACertFile)
		if err != nil {
			logging.Error(logger).Log(logging.MessageKey(), "Error in reading ClientCACertFile ",
				logging.ErrorKey(), err)
		} else {
			caCertPool := x509.NewCertPool()
			caCertPool.AppendCertsFromPEM(caCert)
			tlsConfig = &tls.Config{
				ClientCAs:  caCertPool,
				ClientAuth: tls.RequireAndVerifyClientCert,
			}
			tlsConfig.BuildNameToCertificate()
		}
	}

	server := &http.Server{
		Addr:      b.Address,
		Handler:   handler,
		ErrorLog:  NewErrorLog(b.Name, logger),
		TLSConfig: tlsConfig,
	}

	if b.LogConnectionState {
		server.ConnState = NewConnectionStateLogger(b.Name, logger)
	}

	return server
}

// Metric is the configurable factory for a metrics server.
type Metric struct {
	Name               string
	Address            string
	CertificateFile    string
	KeyFile            string
	LogConnectionState bool
	HandlerOptions     promhttp.HandlerOpts
	MetricsOptions     xmetrics.Options
}

func (m *Metric) Certificate() (certificateFile, keyFile string) {
	return m.CertificateFile, m.KeyFile
}

func (m *Metric) NewRegistry(modules ...xmetrics.Module) (xmetrics.Registry, error) {
	// always append the builtin server metrics, which can be overridden in configuration
	modules = append(modules, Metrics)
	return xmetrics.NewRegistry(&m.MetricsOptions, modules...)
}

func (m *Metric) New(logger log.Logger, chain alice.Chain, gatherer stdprometheus.Gatherer) *http.Server {
	if len(m.Address) == 0 {
		return nil
	}

	mux := http.NewServeMux()
	mux.Handle("/metrics", chain.Then(promhttp.HandlerFor(gatherer, m.HandlerOptions)))

	server := &http.Server{
		Addr:     m.Address,
		Handler:  mux,
		ErrorLog: NewErrorLog(m.Name, logger),
	}

	if m.LogConnectionState {
		server.ConnState = NewConnectionStateLogger(m.Name, logger)
	}

	return server
}

// Health represents a configurable factory for a Health server.  As with the Basic type,
// if the Address is not specified, health is considered to be disabled.
//
// Due to a limitation of Viper, this struct does not use an embedded Basic
// instance.  Rather, it duplicates the fields so that Viper can inject them.
type Health struct {
	Name               string
	Address            string
	CertificateFile    string
	KeyFile            string
	LogConnectionState bool
	LogInterval        time.Duration
	Options            []string
}

func (h *Health) Certificate() (certificateFile, keyFile string) {
	return h.CertificateFile, h.KeyFile
}

// NewHealth creates a Health instance from this instance's configuration.  If the Address
// field is not supplied, this method returns nil.
func (h *Health) NewHealth(logger log.Logger, options ...health.Option) *health.Health {
	if len(h.Address) == 0 {
		return nil
	}

	for _, value := range h.Options {
		options = append(options, health.Stat(value))
	}

	return health.New(
		h.LogInterval,
		logger,
		options...,
	)
}

// New creates an HTTP server instance for serving health statistics.  If the health parameter
// is nil, then h.NewHealth is used to create a Health instance.  Otherwise, the health parameter
// is returned as is.
//
// If the Address option is not supplied, the health module is considered to be disabled.  In that
// case, this method simply returns the health parameter as the monitor and a nil server instance.
func (h *Health) New(logger log.Logger, health *health.Health) (*health.Health, *http.Server) {
	if len(h.Address) == 0 {
		// health is disabled
		return nil, nil
	}

	if health == nil {
		if health = h.NewHealth(logger); health == nil {
			// should never hit this case, since NewHealth performs the same
			// Address field check as this method.  but, just to be safe ...
			return nil, nil
		}
	}

	mux := http.NewServeMux()
	mux.Handle("/health", health)

	server := &http.Server{
		Addr:     h.Address,
		Handler:  mux,
		ErrorLog: NewErrorLog(h.Name, logger),
	}

	if h.LogConnectionState {
		server.ConnState = NewConnectionStateLogger(h.Name, logger)
	}

	return health, server
}

// WebPA represents a server component within the WebPA cluster.  It is used for both
// primary servers (e.g. petasos) and supporting, embedded servers such as pprof.
type WebPA struct {
	// Primary is the main server for this application, e.g. petasos.
	Primary Basic

	// Alternate is an alternate server which serves the primary application logic.
	// Used to have the same API served on more than one port and possibly more than
	// one protocol, e.g. HTTP and HTTPS.
	Alternate Basic

	// Health describes the health server for this application.  Note that if the Address
	// is empty, no health server is started.
	Health Health

	// Pprof describes the pprof server for this application.  Note that if the Address
	// is empty, no pprof server is started.
	Pprof Basic

	// Metric describes the metrics provider server for this application
	Metric Metric

	// ResponseHeaders hold custom, static response headers that are emitted on every response.
	// Version headers are a primary use case for this configuration options.
	ResponseHeaders http.Header

	// Log is the logging configuration for this application.
	Log *logging.Options
}

// Prepare gets a WebPA server ready for execution.  This method does not return errors, but the returned
// Runnable may return an error.  The supplied logger will usually come from the New function, but the
// WebPA.Log object can be used to create a different logger if desired.
//
// The caller may pass an arbitrary Health instance.  If this parameter is nil, this method will attempt to
// create one using Health.NewHealth.  In either case, if Health.Address is not supplied, no health server
// will be instantiated.
//
// The caller may also pass a gatherer type. If it is not provided, the default provided by prometheus is used.
//
// The supplied http.Handler is used for the primary server.  If the alternate server has an address,
// it will also be used for that server.  The health server uses an internally create handler, while pprof and metrics
// servers use http.DefaultServeMux.  The health Monitor created from configuration is returned so that other
// infrastructure can make use of it.
func (w *WebPA) Prepare(logger log.Logger, health *health.Health, registry xmetrics.Registry, primaryHandler http.Handler) (health.Monitor, concurrent.Runnable) {
	// allow the health instance to be non-nil, in which case it will be used in favor of
	// the WebPA-configured instance.
	var (
		healthHandler, healthServer = w.Health.New(logger, health)
		infoLog                     = logging.Info(logger)
		staticHeaders               = xhttp.StaticHeaders(w.ResponseHeaders)
	)

	return healthHandler, concurrent.RunnableFunc(func(waitGroup *sync.WaitGroup, shutdown <-chan struct{}) error {
		if healthHandler != nil && healthServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Health.Name, "address", w.Health.Address)
			ListenAndServe(logger, &w.Health, healthServer)
			healthHandler.Run(waitGroup, shutdown)

			// wrap the primary handler in the RequestTracker decorator
			primaryHandler = healthHandler.RequestTracker(primaryHandler)
		}

		if pprofServer := w.Pprof.New(logger, nil); pprofServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Pprof.Name, "address", w.Pprof.Address)
			ListenAndServe(logger, &w.Pprof, pprofServer)
		}

		//wrap common metrics around both server handler
		primaryHandler = staticHeaders(w.decorateWithBasicMetrics(registry, primaryHandler))

		if primaryServer := w.Primary.New(logger, primaryHandler); primaryServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Primary.Name, "address", w.Primary.Address)
			ListenAndServe(logger, &w.Primary, primaryServer)
		} else {
			return ErrorNoPrimaryAddress
		}

		if alternateServer := w.Alternate.New(logger, primaryHandler); alternateServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Alternate.Name, "address", w.Alternate.Address)
			ListenAndServe(logger, &w.Alternate, alternateServer)
		}

		if metricsServer := w.Metric.New(logger, alice.New(staticHeaders), registry); metricsServer != nil {
			infoLog.Log(logging.MessageKey(), "starting server", "name", w.Metric.Name, "address", w.Metric.Address)
			ListenAndServe(logger, &w.Metric, metricsServer)
		}

		return nil
	})
}

//decorateWithBasicMetrics wraps a WebPA server handler with basic instrumentation metrics
func (w *WebPA) decorateWithBasicMetrics(p xmetrics.PrometheusProvider, next http.Handler) http.Handler {
	var (
		requestCounterVec    = p.NewCounterVec("api_requests_total")
		inFlightGauge        = p.NewGaugeVec("in_flight_requests").WithLabelValues()
		requestDurationVec   = p.NewHistogramVec("request_duration_seconds")
		requestSizeVec       = p.NewHistogramVec("request_size_bytes")
		responseSizeVec      = p.NewHistogramVec("response_size_bytes")
		timeToWriteHeaderVec = p.NewHistogramVec("time_writing_header_seconds")
	)

	//todo: Example documentation does something interesting with /pull vs. /push endpoints
	//https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp#InstrumentHandlerDuration
	//for now, let's keep it simple so /metrics only

	return promhttp.InstrumentHandlerInFlight(inFlightGauge,
		promhttp.InstrumentHandlerCounter(requestCounterVec,
			promhttp.InstrumentHandlerDuration(requestDurationVec,
				promhttp.InstrumentHandlerResponseSize(responseSizeVec,
					promhttp.InstrumentHandlerRequestSize(requestSizeVec,
						promhttp.InstrumentHandlerTimeToWriteHeader(timeToWriteHeaderVec, next))),
			),
		),
	)
}
<|endoftext|>