1
2 package cmd
3
4 import (
5 "context"
6 "encoding/json"
7 "errors"
8 "expvar"
9 "fmt"
10 "io"
11 "log"
12 "log/syslog"
13 "net/http"
14 "net/http/pprof"
15 "os"
16 "os/signal"
17 "runtime"
18 "runtime/debug"
19 "strings"
20 "syscall"
21 "time"
22
23 "github.com/go-logr/stdr"
24 "github.com/go-sql-driver/mysql"
25 "github.com/prometheus/client_golang/prometheus"
26 "github.com/prometheus/client_golang/prometheus/collectors"
27 "github.com/prometheus/client_golang/prometheus/promhttp"
28 "github.com/redis/go-redis/v9"
29 "go.opentelemetry.io/otel"
30 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
31 "go.opentelemetry.io/otel/propagation"
32 "go.opentelemetry.io/otel/sdk/resource"
33 "go.opentelemetry.io/otel/sdk/trace"
34 semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
35 "google.golang.org/grpc/grpclog"
36
37 "github.com/letsencrypt/boulder/core"
38 blog "github.com/letsencrypt/boulder/log"
39 "github.com/letsencrypt/boulder/strictyaml"
40 "github.com/letsencrypt/validator/v10"
41 )
42
43
44
45
46 func init() {
47 for _, v := range os.Args {
48 if v == "--version" || v == "-version" {
49 fmt.Println(VersionString())
50 os.Exit(0)
51 }
52 }
53 }
54
55
56 type mysqlLogger struct {
57 blog.Logger
58 }
59
60 func (m mysqlLogger) Print(v ...interface{}) {
61 m.AuditErrf("[mysql] %s", fmt.Sprint(v...))
62 }
63
64
65 type grpcLogger struct {
66 blog.Logger
67 }
68
69
70
71 func (log grpcLogger) Fatal(args ...interface{}) {
72 log.Error(args...)
73 os.Exit(1)
74 }
75 func (log grpcLogger) Fatalf(format string, args ...interface{}) {
76 log.Errorf(format, args...)
77 os.Exit(1)
78 }
79 func (log grpcLogger) Fatalln(args ...interface{}) {
80 log.Errorln(args...)
81 os.Exit(1)
82 }
83
84
85 func (log grpcLogger) Error(args ...interface{}) {
86 log.Logger.AuditErr(fmt.Sprint(args...))
87 }
88 func (log grpcLogger) Errorf(format string, args ...interface{}) {
89 log.Logger.AuditErrf(format, args...)
90 }
91 func (log grpcLogger) Errorln(args ...interface{}) {
92 log.Logger.AuditErr(fmt.Sprintln(args...))
93 }
94
95
96 func (log grpcLogger) Warning(args ...interface{}) {
97 log.Logger.Warning(fmt.Sprint(args...))
98 }
99 func (log grpcLogger) Warningf(format string, args ...interface{}) {
100 log.Logger.Warningf(format, args...)
101 }
102 func (log grpcLogger) Warningln(args ...interface{}) {
103 msg := fmt.Sprintln(args...)
104
105 if strings.Contains(msg, `ccResolverWrapper: error parsing service config: no JSON service config provided`) {
106 return
107 }
108
109 if strings.Contains(msg, `Server.processUnaryRPC failed to write status: connection error: desc = "transport is closing"`) {
110 return
111 }
112
113 log.Logger.Warning(msg)
114 }
115
116
117
118 func (log grpcLogger) Info(args ...interface{}) {}
119 func (log grpcLogger) Infof(format string, args ...interface{}) {}
120 func (log grpcLogger) Infoln(args ...interface{}) {}
121
122
123
124 func (log grpcLogger) V(l int) bool {
125
126
127
128
129 return false
130 }
131
132
133 type promLogger struct {
134 blog.Logger
135 }
136
137 func (log promLogger) Println(args ...interface{}) {
138 log.AuditErr(fmt.Sprint(args...))
139 }
140
141 type redisLogger struct {
142 blog.Logger
143 }
144
145 func (rl redisLogger) Printf(ctx context.Context, format string, v ...interface{}) {
146 rl.Infof(format, v...)
147 }
148
149
150 type logWriter struct {
151 blog.Logger
152 }
153
154 func (lw logWriter) Write(p []byte) (n int, err error) {
155
156 lw.Logger.Info(strings.Trim(string(p), "\n"))
157 return
158 }
159
160
161 type logOutput struct {
162 blog.Logger
163 }
164
165 func (l logOutput) Output(calldepth int, logline string) error {
166 l.Logger.Info(logline)
167 return nil
168 }
169
170
171
172
173
174
175
176
177
178
179
180
181
182 func StatsAndLogging(logConf SyslogConfig, otConf OpenTelemetryConfig, addr string) (prometheus.Registerer, blog.Logger, func(context.Context)) {
183 logger := NewLogger(logConf)
184
185 shutdown := NewOpenTelemetry(otConf, logger)
186
187 return newStatsRegistry(addr, logger), logger, shutdown
188 }
189
190
191
192
193
194
195 func NewLogger(logConf SyslogConfig) blog.Logger {
196 var logger blog.Logger
197 if logConf.SyslogLevel >= 0 {
198 syslogger, err := syslog.Dial(
199 "",
200 "",
201 syslog.LOG_INFO,
202 core.Command())
203 FailOnError(err, "Could not connect to Syslog")
204 syslogLevel := int(syslog.LOG_INFO)
205 if logConf.SyslogLevel != 0 {
206 syslogLevel = logConf.SyslogLevel
207 }
208 logger, err = blog.New(syslogger, logConf.StdoutLevel, syslogLevel)
209 FailOnError(err, "Could not connect to Syslog")
210 } else {
211 logger = blog.StdoutLogger(logConf.StdoutLevel)
212 }
213
214 _ = blog.Set(logger)
215 _ = mysql.SetLogger(mysqlLogger{logger})
216 grpclog.SetLoggerV2(grpcLogger{logger})
217 log.SetOutput(logWriter{logger})
218 redis.SetLogger(redisLogger{logger})
219
220
221
222 go func() {
223 for {
224 time.Sleep(time.Minute)
225 logger.Info(fmt.Sprintf("time=%s", time.Now().Format(time.RFC3339Nano)))
226 }
227 }()
228 return logger
229 }
230
231 func newVersionCollector() prometheus.Collector {
232 buildTime := core.Unspecified
233 if core.GetBuildTime() != core.Unspecified {
234
235
236 bt, err := time.Parse(time.UnixDate, core.BuildTime)
237 if err != nil {
238
239 buildTime = "Unparsable"
240 } else {
241 buildTime = bt.Format(time.RFC3339)
242 }
243 }
244 return prometheus.NewGaugeFunc(
245 prometheus.GaugeOpts{
246 Name: "version",
247 Help: fmt.Sprintf(
248 "A metric with a constant value of '1' labeled by the short commit-id (buildId), build timestamp in RFC3339 format (buildTime), and Go release tag like 'go1.3' (goVersion) from which %s was built.",
249 core.Command(),
250 ),
251 ConstLabels: prometheus.Labels{
252 "buildId": core.GetBuildID(),
253 "buildTime": buildTime,
254 "goVersion": runtime.Version(),
255 },
256 },
257 func() float64 { return 1 },
258 )
259 }
260
261 func newStatsRegistry(addr string, logger blog.Logger) prometheus.Registerer {
262 registry := prometheus.NewRegistry()
263 registry.MustRegister(collectors.NewGoCollector())
264 registry.MustRegister(collectors.NewProcessCollector(
265 collectors.ProcessCollectorOpts{}))
266 registry.MustRegister(newVersionCollector())
267
268 mux := http.NewServeMux()
269
270
271
272 mux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
273 mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
274 mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
275 mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
276
277
278 mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
279 mux.Handle("/debug/pprof/block", pprof.Handler("block"))
280 mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
281 mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
282 mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
283
284 mux.Handle("/debug/vars", expvar.Handler())
285 mux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{
286 ErrorLog: promLogger{logger},
287 }))
288
289 server := http.Server{
290 Addr: addr,
291 Handler: mux,
292 ReadTimeout: time.Minute,
293 }
294 go func() {
295 err := server.ListenAndServe()
296 if err != nil {
297 logger.Errf("unable to boot debug server on %s: %v", addr, err)
298 os.Exit(1)
299 }
300 }()
301 return registry
302 }
303
304
305
306 func NewOpenTelemetry(config OpenTelemetryConfig, logger blog.Logger) func(ctx context.Context) {
307 otel.SetLogger(stdr.New(logOutput{logger}))
308 otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { logger.Errf("OpenTelemetry error: %v", err) }))
309
310 r, err := resource.Merge(
311 resource.Default(),
312 resource.NewWithAttributes(
313 semconv.SchemaURL,
314 semconv.ServiceNameKey.String(core.Command()),
315 semconv.ServiceVersionKey.String(core.GetBuildID()),
316 ),
317 )
318 if err != nil {
319 FailOnError(err, "Could not create OpenTelemetry resource")
320 }
321
322 opts := []trace.TracerProviderOption{
323 trace.WithResource(r),
324
325
326 trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(config.SampleRatio))),
327 }
328
329 if config.Endpoint != "" {
330 exporter, err := otlptracegrpc.New(
331 context.Background(),
332 otlptracegrpc.WithInsecure(),
333 otlptracegrpc.WithEndpoint(config.Endpoint))
334 if err != nil {
335 FailOnError(err, "Could not create OpenTelemetry OTLP exporter")
336 }
337
338 opts = append(opts, trace.WithBatcher(exporter))
339 }
340
341 tracerProvider := trace.NewTracerProvider(opts...)
342 otel.SetTracerProvider(tracerProvider)
343 otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
344
345 return func(ctx context.Context) {
346 err := tracerProvider.Shutdown(ctx)
347 if err != nil {
348 logger.Errf("Error while shutting down OpenTelemetry: %v", err)
349 }
350 }
351 }
352
353
354
355 func AuditPanic() {
356 err := recover()
357
358 if err == nil {
359 return
360 }
361
362
363
364 log := blog.Get()
365
366 fail, ok := err.(failure)
367 if ok {
368 log.AuditErr(fail.msg)
369 } else {
370
371 log.AuditErrf("Panic caused by err: %s", err)
372
373 log.AuditErrf("Stack Trace (Current goroutine) %s", debug.Stack())
374 }
375
376
377 os.Exit(1)
378 }
379
380
381
382 type failure struct {
383 msg string
384 }
385
386 func (f failure) String() string {
387 return f.msg
388 }
389
390
391
392 func Fail(msg string) {
393 panic(failure{msg})
394 }
395
396
397
398
399 func FailOnError(err error, msg string) {
400 if err == nil {
401 return
402 }
403 if msg == "" {
404 Fail(err.Error())
405 } else {
406 Fail(fmt.Sprintf("%s: %s", msg, err))
407 }
408 }
409
410 func decodeJSONStrict(in io.Reader, out interface{}) error {
411 decoder := json.NewDecoder(in)
412 decoder.DisallowUnknownFields()
413
414 return decoder.Decode(out)
415 }
416
417
418
419
420
421
422 func ReadConfigFile(filename string, out interface{}) error {
423 file, err := os.Open(filename)
424 if err != nil {
425 return err
426 }
427 defer file.Close()
428
429 return decodeJSONStrict(file, out)
430 }
431
432
433
434
435
436
437
438 func ValidateJSONConfig(cv *ConfigValidator, in io.Reader) error {
439 if cv == nil {
440 return errors.New("config validator cannot be nil")
441 }
442
443
444 validate := validator.New()
445 for tag, v := range cv.Validators {
446 err := validate.RegisterValidation(tag, v)
447 if err != nil {
448 return err
449 }
450 }
451
452 err := decodeJSONStrict(in, cv.Config)
453 if err != nil {
454 return err
455 }
456 err = validate.Struct(cv.Config)
457 if err != nil {
458 errs, ok := err.(validator.ValidationErrors)
459 if !ok {
460
461 return err
462 }
463 if len(errs) > 0 {
464 allErrs := []string{}
465 for _, e := range errs {
466 allErrs = append(allErrs, e.Error())
467 }
468 return errors.New(strings.Join(allErrs, ", "))
469 }
470 }
471 return nil
472 }
473
474
475
476
477
478
479
480 func ValidateYAMLConfig(cv *ConfigValidator, in io.Reader) error {
481 if cv == nil {
482 return errors.New("config validator cannot be nil")
483 }
484
485
486 validate := validator.New()
487 for tag, v := range cv.Validators {
488 err := validate.RegisterValidation(tag, v)
489 if err != nil {
490 return err
491 }
492 }
493
494 inBytes, err := io.ReadAll(in)
495 if err != nil {
496 return err
497 }
498 err = strictyaml.Unmarshal(inBytes, cv.Config)
499 if err != nil {
500 return err
501 }
502 err = validate.Struct(cv.Config)
503 if err != nil {
504 errs, ok := err.(validator.ValidationErrors)
505 if !ok {
506
507 return err
508 }
509 if len(errs) > 0 {
510 allErrs := []string{}
511 for _, e := range errs {
512 allErrs = append(allErrs, e.Error())
513 }
514 return errors.New(strings.Join(allErrs, ", "))
515 }
516 }
517 return nil
518 }
519
520
521 func VersionString() string {
522 return fmt.Sprintf("Versions: %s=(%s %s) Golang=(%s) BuildHost=(%s)", core.Command(), core.GetBuildID(), core.GetBuildTime(), runtime.Version(), core.GetBuildHost())
523 }
524
525
526
527
528
529
530
531 func CatchSignals(callback func()) {
532 WaitForSignal()
533 callback()
534 }
535
536
537
538
539
540
541 func WaitForSignal() {
542 sigChan := make(chan os.Signal, 1)
543 signal.Notify(sigChan, syscall.SIGTERM)
544 signal.Notify(sigChan, syscall.SIGINT)
545 signal.Notify(sigChan, syscall.SIGHUP)
546 <-sigChan
547 }
548
View as plain text