/
app.go
334 lines (306 loc) · 16.3 KB
/
app.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
// Copyright (c) 2015, Alexander Cherniuk <ts33kr@gmail.com>
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package boot
import "os"
import "time"
import "os/signal"
import "net/http"
import "path/filepath"
import "strings"
import "regexp"
import "sync"
import "fmt"
import "github.com/naoina/denco"
import "github.com/pelletier/go-toml"
import "github.com/renstrom/shortuuid"
import "github.com/Sirupsen/logrus"
import "github.com/blang/semver"
import "github.com/robfig/cron"
// Create and initialize a new application. This is a front gate for
// the framework, since you should start by creating a new app struct.
// Every application should have a valid name (tag) and a version. So
// this function makes sure they have been passed and are all valid.
// Generally, you should not be creating more than one application.
func New (name, version string) *App {
var room = make(map[string] interface {})
const url = "https://github.com/ts33kr/boot"
const ename = "name is not of correct format"
const eversion = "version is not valid semver"
pattern := regexp.MustCompile("^[a-zA-Z0-9-_]+$")
var parsed semver.Version = semver.MustParse(version)
if !pattern.MatchString(name) { panic(ename) }
if parsed.Validate() != nil { panic(eversion) }
application := &App { Name: name, Version: parsed }
application.Storage = Storage { Container: room }
application.CronEngine = cron.New() // create CRON
application.Servers = make(map[string]*http.Server)
application.Reference = shortuuid.New() // V4
application.Providers = make([]*Provider, 0)
application.Services = make([]*Service, 0)
application.TimeLayout = time.RFC850
application.Namespace = url // set
return application // prepared app
}
// Erect the application. Once completed, the application should have
// all the services installed and all the necessary configurations done
// before invoking the deploy sequence. Basically, this method will do
// everything to get the application configured and be ready to launch.
// Method itself however will not launch the app; see Deploy for that.
func (app *App) Boot(env, level, root string) {
const eenv = "environment name must be 1 word"
const estat = "could not open the specified root"
pattern := regexp.MustCompile("^[a-zA-Z0-9]+$")
parsedLevel, err := logrus.ParseLevel(level)
if err != nil { panic("wrong logging level") }
if !pattern.MatchString(env) { panic(eenv) }
if _, e := os.Stat(root); e != nil { panic(estat) }
app.RootDirectory = filepath.Clean(root)
app.Journal = app.makeJournal(parsedLevel)
app.Env = strings.ToLower(strings.TrimSpace(env))
app.Config = app.loadConfig(app.Env, "config")
app.Booted = time.Now() // mark app as booted
for _, p := range app.Providers { // setups
if p.Available[env] { // env available?
p.Invoked = time.Now(); p.Setup(app)
} // setup provider only if availale
} // all the providers have been invoked
for _, s := range app.Services { s.Up(app) }
log := app.Journal.WithField("env", app.Env)
log = log.WithField("root", app.RootDirectory)
log = log.WithField("level", parsedLevel)
log.Info("application has been booted")
app.CronEngine.Start() // launch CRON
app.routers = app.assembleRouters()
}
// Deploy the application. Spawn one or more of HTTP(s) servers, as
// defined in the loaded config, and make them listen on respective
// addresses and ports. Every server will have this application set as
// the HTTP requests handler. Method will block until all servers are
// stopped. See boot.App and this method implementation for details.
func (app *App) Deploy(sv Supervisor) {
var volume int = len(app.Services) // size
log := app.Journal.WithField("name", app.Name)
log = log.WithField("version", app.Version)
log = log.WithField("ref", app.Reference) // UID
log.Infof("deploying app with %v services", volume)
cancelled := make(chan os.Signal, 1) // killed
signal.Notify(cancelled, os.Interrupt, os.Kill)
app.Supervisor = sv // install app-wide supervisor
app.unfoldHttpsServers() // spawn HTTPS and listen
app.unfoldHttpServers() // spawn HTTP and listen
go func() { // this runs in the background
_ = <- cancelled // waiting for signal
signal.Stop(cancelled) // stop monitoring
fmt.Fprintln(app.Journal.Out) // write ^C\n
moment := time.Now().Format(app.TimeLayout)
uptime := time.Now().Sub(app.Booted) // calc
for _, s := range app.Services { s.Down(app) }
for _, p := range app.Providers { // cleanups
if !p.Invoked.IsZero() { // was invoked?
p.Cleanup(app); // run the cleanup
} // only cleanup the setup-ed ones
} // all the provider have been cleaned up
log := app.Journal.WithField("time", moment)
log = log.WithField("uptime", uptime.String())
log.Warn("shutting the application down")
app.CronEngine.Stop() // stop CRON engine
os.Exit(2) // emulate Ctrl-C exit code
}() // run go-routine & wait to finish
app.finish.Wait()
}
// Load config file that contains the configuration data for the app
// instance. Config file should be a valid TOML file that has a bare
// minimum data to make it a valid config. Method will panic in case if
// there is an error loading the config or interpreting data inside.
// Must have the app.name and app.version fields defined correctly.
// Refer to implementation code for more details on the loading.
func (app *App) loadConfig(name, base string) *toml.TomlTree {
const eload = "failed to load TOML config\n %v"
const estat = "could not open config file at %v"
const ever = "app does not satifsy config version"
const eforeign = "config is from different app"
var root string = app.RootDirectory // root dir
var fileName string = fmt.Sprintf("%s.toml", name)
resolved := filepath.Join(root, base, fileName)
var clean string = filepath.Clean(resolved)
log := app.Journal.WithField("file", clean)
log.Info("loading application config file")
_, err := os.Stat(clean) // check if file exists
if err != nil { panic(fmt.Errorf(estat, clean)) }
tree, err := toml.LoadFile(clean) // load config up!
if err != nil { panic(fmt.Errorf(eload, err.Error())) }
req, ok := tree.Get("app.require").(*toml.TomlTree)
if ok && req != nil { // check app requirements
var avr string = app.Version.String()
name := req.GetDefault("name", app.Name)
version := req.GetDefault("version", avr)
vr, _ := semver.ParseRange(version.(string))
if vr == nil || !vr(app.Version) { panic(ever) }
if name != app.Name { panic(eforeign) }
} // assume requirements are satisfied
return tree // config tree is ready
}
// Build an adequate instance of the structured logger for this
// application instance. The journal builder may draw data from the
// app instance to configure the journal correctly. This method only
// instantiates a very basic journal; anything more complicated than
// that should be implementing using a boot.Provider to do it.
func (app *App) makeJournal(level logrus.Level) *logrus.Logger {
const m = "begin writing application journal"
var journal *logrus.Logger = &logrus.Logger {}
formatter := new(logrus.TextFormatter) // std
journal.Level = level // use requested level
journal.Out = os.Stdout // all goes to stdout
journal.Hooks = make(logrus.LevelHooks) // empty
journal.Formatter = formatter // set formatter
formatter.ForceColors = false // act smart
formatter.DisableColors = false // make pretty
formatter.DisableTimestamp = false // is useful
formatter.FullTimestamp = false // numbers
formatter.TimestampFormat = time.StampMilli
formatter.DisableSorting = false // order!
moment := time.Now().Format(app.TimeLayout)
journal.WithField("time", moment).Info(m)
return journal // is ready to use
}
// Core data structure of the framework; represents a web application
// built with the framework. Contains all the necessary API to create
// and launch the application, as well as to maintain its lifecyle and
// the operational business logic. Please refer to the fields of the
// structure as well as the methods for a detailed information.
type App struct {
// Syncronization primitive that should be used to lock on when
// performing any changes to application instance. Especially it
// must be used when modifying the values of structure fields of
// application. Therefore, all write-access to application should
// be made mutually exclusive, using this embedded mutex.
sync.Mutex
// Name is a short string token that identifies the application.
// It is advised to keep it machine & human readable: in a form of
// of a slug - no spaces, all lower case, et cetera. The framework
// itself, as well as any other code could use this variable to
// unique identify an instance of the specific application.
Name string
// Complement the application name; represents a version of the
// running application instance. The version format should conform
// to the semver (Semantical Versioning) format. Typically, version
// looks like 0.0.1, according to the semver formatting. Refer to
// the Semver package for more info on how to work with versions.
Version semver.Version
// A path within the local file system where an instance of the
// running application should be residing. The framework will use
// this path to lookup configuration directories, optional static
// assets and a number of other things it may need. By default, it
// will be set to the CWD directory that the app was launched in.
RootDirectory string
// Default time layout (formatting template) to be used by the
// framework and application code, when it needs to output or send
// some dates & times that have no specific formatting requirement.
// This is prefferable way of doing it, rather than have different,
// inconsistent formatting in different portions of the code.
TimeLayout string
// Short identifier of the logical environment that this instance
// of the application is running in, such as: production, staging,
// development and a number of any other possible environments that
// could be defined and used by the application creators. It should
// be kept as short, prererrably a 1-word ID, for convenience.
Env string
// Default application namespace that will be used to generate
// UUID identificators of v5. This may potentially used by other
// consumers that need namespace declaration. Typically, value is
// going to be a stringified URL that designates some namespace.
// This field will be set by the framework automatically mostly.
Namespace string
// Unique identifier of the application instance, conforming to a
// version 5 of the commonly known UUID standards. Every time an
// application is launched - it gets a new UUID identifier that
// uniquely represents the specific instance of the application.
// So every time you start your application, it gets a new ID.
Reference string
// Root level logger, as configured by the framework, according to
// the application and environment settings. Since the framework
// makes extensive use of a structured logger, this field contains
// a pre-configured root logging structure, with no fields set yet.
// Please refer to the Logrus package for more information on it.
Journal *logrus.Logger
// General purpose storage for keeping key/value records per the
// application instance. The storage may be used by the framework
// as well as the application code, to store and retrieve any sort
// of values that may be required by the application logic or the
// framework logic. Beware, values are empty-interface typed.
Storage
// Supervisor instance to use with this application instance. A
// supervisor is responsible for handling issues that might occur
// during the normal operation mode. Please refer to Supervisor
// interface definition & documentation for more information.
// Normally, a default supervisor should be used, as it is.
Supervisor Supervisor
// HTTP request routers that the app will use to match incoming
// requests against the registered endpoints that must handle the
// requests. The framework will build and maintain the routers
// automatically; normally you should not be refering to this
// field directly. See Denco library docs for more details.
routers map[string] *denco.Router
// Configuration data for the application instance. This will be
// populated by the framework, when the app is being launched. It
// will locate the necessary TOML configuration file, based on the
// environment configured, load it and make it availale to the app.
// Please refer to the corresponding method for more details.
Config *toml.TomlTree
// Instant in time when the application was booted. A nil value
// should indicate that the application instance has not yet been
// booted up. This value is used internally by the framework in a
// multiple of ways; and may also be used by whoever is interested
// the time of when exactly the application was launched.
Booted time.Time
// The CRON engine that will be employed by the framework and an
// application to implement and run the peridoic jobs. Please see
// the Aux structure and its CronExpression field for usage. As
// well please refer to the github.com/robfig/cron packaged used
// for detailed information on the implemented employed.
CronEngine *cron.Cron
// Map of HTTP servers that will be used to server application
// instance. Servers are automatically created by the framework
// for every corresponding section in the config file. This is
// needed for applications that must be served on multiple ports
// or network interfaces at the same time, within one process.
Servers map[string] *http.Server
// Application wide stop signal, implement as a wait group. After
// the app is being booted the caller should wait on this group to
// be resumed once the application has been gracefully stopped. Do
// prefer this construct instead of abruptly terminating the app
// using other, likely more destructive, ways of terminating it.
finish sync.WaitGroup
// Slice of providers installed within this application. Provider
// is an entity, with a piece of code attached, that provides some
// kind of functionality for the application, such as: a database
// connection, etc. Providers will be invoked when the application
// is being launched. Refer to Provider for more information.
Providers []*Provider
// Slice of services mounted in the application instance. Service
// is a collection of endpoints (HTTP request handlers), amongst
// other things. This slice should not be manipulated directly;
// but rather through the provided API to manage services within
// an application instance; please refer to it for details.
Services []*Service
}