33package dials
44
55import (
6+ "fmt"
7+ "net/http"
68 "sync/atomic"
9+
10+ "golang.org/x/net/html"
11+ "golang.org/x/net/html/atom"
12+
13+ statuspage "github.com/vimeo/go-status-page"
714)
815
916// Dials is the main access point for your configuration.
@@ -13,6 +20,12 @@ type Dials[T any] struct {
1320 params Params [T ]
1421 cbch chan <- userCallbackEvent
1522 monCtl chan <- verifyEnable [T ]
23+ dumpStack chan <- dumpSourceStack [T ]
24+
25+ // sourceVals and defVal are only present if the status page is enabled
26+ // and there are no watching sources. (or the monitor has exited)
27+ sourceVals atomic.Pointer [[]sourceValue ]
28+ defVal * T
1629}
1730
1831// View returns the configuration struct populated.
@@ -23,11 +36,145 @@ func (d *Dials[T]) View() *T {
2336 return versioned .cfg
2437}
2538
26- // View returns the configuration struct populated, and an opaque token.
39+ // ViewVersion returns the configuration struct populated, and an opaque token.
2740func (d * Dials [T ]) ViewVersion () (* T , CfgSerial [T ]) {
2841 versioned := d .value .Load ()
2942 // v cannot be nil because we initialize this value immediately after
3043 // creating the the Dials object
3144 return versioned .cfg , CfgSerial [T ]{s : versioned .serial , cfg : versioned .cfg }
45+ }
46+
47+ // ServeHTTP is only active if [Params.EnableStatusPage] was set to true when creating the dials instance.
48+ //
49+ // This is experimental and may panic while serving, buyer beware!!!
50+ //
51+ // It is heavily recommended that users of this functionality set
52+ // `statuspage:"-"` tags on any sensitive fields with secrets/credentials, etc.
53+ func (d * Dials [T ]) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
54+ if ! d .params .EnableStatusPage {
55+ http .Error (w , "Dials status page not enabled. EnableStatusPage must be set in dials.Params." , http .StatusNotFound )
56+ return
57+ }
58+ srcs := d .sourceVals .Load ()
59+ _ , serial := d .ViewVersion ()
60+ if srcs == nil {
61+ // Make the response channel size-1 so the monitor doesn't block on the response
62+ respCh := make (chan dumpSourceStackResponse [T ], 1 )
63+ // ask the monitor for the stack
64+ select {
65+ case d .dumpStack <- dumpSourceStack [T ]{resp : respCh }:
66+ case <- r .Context ().Done ():
67+ // not worth trying to resolve the race around when the monitor shuts down for a status page.
68+ // just return a 500.
69+ http .Error (w , "context expired while attempting to request the current source-stack; please try again." ,
70+ http .StatusInternalServerError )
71+ return
72+ }
73+ select {
74+ case v := <- respCh :
75+ srcs = & v .stack
76+ serial = v .serial
77+ case <- r .Context ().Done ():
78+ // not worth trying to resolve the race around when the monitor shuts down for a status page.
79+ // just return a 500.
80+ http .Error (w , "context expired while attempting to acquire the current stacks; please try again." ,
81+ http .StatusInternalServerError )
82+ return
83+ }
84+ }
85+
86+ root := html.Node {Type : html .DocumentNode }
87+ root .AppendChild (& html.Node {
88+ Type : html .DoctypeNode ,
89+ DataAtom : atom .Html ,
90+ Data : atom .Html .String (),
91+ })
92+ htmlElem := createElemAtom (atom .Html )
93+ root .AppendChild (htmlElem )
94+
95+ head := createElemAtom (atom .Head )
96+
97+ htmlElem .AppendChild (head )
98+ title := createElemAtom (atom .Title )
99+ title .AppendChild (textNode ("Dials Status" ))
100+ head .AppendChild (title )
101+ header := createElemAtom (atom .H1 )
102+ header .AppendChild (textNode ("Dials Status" ))
103+ head .AppendChild (header )
104+
105+ body := createElemAtom (atom .Body )
106+ htmlElem .AppendChild (body )
107+
108+ curCfg , genCfgErr := statuspage .GenHTMLNodes (serial .cfg )
109+ if genCfgErr != nil {
110+ http .Error (w , fmt .Sprintf ("failed to render status page for current config of type %T: %s." , serial .cfg , genCfgErr ),
111+ http .StatusInternalServerError )
112+ return
113+ }
114+ curStatusH2 := createElemAtom (atom .H2 )
115+ curStatusH2 .AppendChild (textNode ("current configuration" ))
116+ body .AppendChild (curStatusH2 )
117+ curStatusVers := createElemAtom (atom .P )
118+ curStatusVers .AppendChild (textNode (fmt .Sprintf ("Current Serial: %d" , serial .s )))
119+ body .AppendChild (curStatusVers )
120+
121+ for _ , cfgNode := range curCfg {
122+ // add a horizontal rule to separate sections
123+ body .AppendChild (createElemAtom (atom .Hr ))
124+ body .AppendChild (cfgNode )
125+ }
126+ defCfgH2 := createElemAtom (atom .H2 )
127+ defCfgH2 .AppendChild (textNode ("Default Configuration" ))
128+ body .AppendChild (defCfgH2 )
129+ defCfgNodes , defCfgErr := statuspage .GenHTMLNodes (d .defVal )
130+ if defCfgErr != nil {
131+ http .Error (w , fmt .Sprintf ("failed to render status page for default config of type %T: %s." ,
132+ serial .cfg , defCfgErr ),
133+ http .StatusInternalServerError )
134+ return
135+ }
136+
137+ for _ , cfgNode := range defCfgNodes {
138+ // add a horizontal rule to separate sections
139+ body .AppendChild (createElemAtom (atom .Hr ))
140+ body .AppendChild (cfgNode )
141+ }
142+
143+ for srcIdx , srcVal := range * srcs {
144+ body .AppendChild (createElemAtom (atom .Hr ))
145+ srcSectionHeader := createElemAtom (atom .H2 )
146+ srcSectionHeader .AppendChild (textNode (fmt .Sprintf ("Source %d of type %T (watching %t)" , srcIdx , srcVal .source , srcVal .watching )))
147+ body .AppendChild (srcSectionHeader )
148+ srcBodyNodes , srcBodyGenErr := statuspage .GenHTMLNodes (srcVal .value .Interface ())
149+ if srcBodyGenErr != nil {
150+ http .Error (w , fmt .Sprintf ("failed to render status page for config of type %T on source %d: %s." , serial .cfg , srcIdx , srcBodyGenErr ),
151+ http .StatusInternalServerError )
152+ return
153+ }
154+ for _ , bn := range srcBodyNodes {
155+ // add a horizontal rule to separate sections
156+ body .AppendChild (createElemAtom (atom .Hr ))
157+ body .AppendChild (bn )
158+ }
159+ }
160+
161+ if renderErr := html .Render (w , htmlElem ); renderErr != nil {
162+ http .Error (w , fmt .Sprintf ("failed to render status page into html for config of type %T : %s." , serial .cfg , renderErr ),
163+ http .StatusInternalServerError )
164+ }
165+ }
166+
167+ func createElemAtom (d atom.Atom ) * html.Node {
168+ n := & html.Node {Type : html .ElementNode , DataAtom : d , Data : d .String ()}
169+ if n .DataAtom == atom .Table {
170+ n .Attr = append (n .Attr , html.Attribute {
171+ Key : "style" ,
172+ Val : "border: 1px solid; min-width: 100px" ,
173+ })
174+ }
175+ return n
176+ }
32177
178+ func textNode (d string ) * html.Node {
179+ return & html.Node {Type : html .TextNode , Data : d }
33180}
0 commit comments