github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/cmd/burrow/commands/vent.go (about) 1 package commands 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "os/signal" 8 "strconv" 9 "strings" 10 "sync" 11 "syscall" 12 "time" 13 14 "github.com/hyperledger/burrow/config/source" 15 "github.com/hyperledger/burrow/crypto" 16 "github.com/hyperledger/burrow/execution/evm/abi" 17 "github.com/hyperledger/burrow/logging/logconfig" 18 "github.com/hyperledger/burrow/vent/config" 19 "github.com/hyperledger/burrow/vent/service" 20 "github.com/hyperledger/burrow/vent/sqldb" 21 "github.com/hyperledger/burrow/vent/sqlsol" 22 "github.com/hyperledger/burrow/vent/types" 23 cli "github.com/jawher/mow.cli" 24 ) 25 26 type LogLevel string 27 28 const ( 29 LogLevelNone LogLevel = "none" 30 LogLevelInfo LogLevel = "info" 31 LogLevelTrace LogLevel = "trace" 32 ) 33 34 func logConfig(level LogLevel) *logconfig.LoggingConfig { 35 logConf := logconfig.New() 36 switch level { 37 case LogLevelNone: 38 return logConf.None() 39 case LogLevelTrace: 40 return logConf.WithTrace() 41 default: 42 return logConf 43 } 44 } 45 46 // Vent consumes EVM events and commits to a DB 47 func Vent(output Output) func(cmd *cli.Cmd) { 48 return func(cmd *cli.Cmd) { 49 50 cmd.Command("start", "Start the Vent consumer service", 51 func(cmd *cli.Cmd) { 52 cfg := config.DefaultVentConfig() 53 54 dbOpts := sqlDBOpts(cmd, cfg) 55 grpcAddrOpt := cmd.StringOpt("chain-addr", cfg.ChainAddress, "Address to connect to the Hyperledger Burrow gRPC server") 56 httpAddrOpt := cmd.StringOpt("http-addr", cfg.HTTPListenAddress, "Address to bind the HTTP server") 57 logLevelOpt := cmd.StringOpt("log-level", string(LogLevelInfo), "Logging level (none, info, trace)") 58 watchAddressesOpt := cmd.StringsOpt("watch", nil, "Add contract address to global watch filter") 59 minimumHeightOpt := cmd.IntOpt("minimum-height", 0, "Only process block greater than or equal to height passed") 60 maxRetriesOpt := cmd.IntOpt("max-retries", int(cfg.BlockConsumerConfig.MaxRetries), "Maximum number of retries when consuming blocks") 61 maxRequestRateOpt := cmd.StringOpt("max-request-rate", "", "Maximum request rate given as (number of requests)/(time base), e.g. 1000/24h for 1000 requests per day") 62 backoffDurationOpt := cmd.StringOpt("backoff", "", 63 "The minimum duration to wait before asking for new blocks - increases exponentially when errors occur. Values like 200ms, 1s, 2m") 64 batchSizeOpt := cmd.IntOpt("batch-size", int(cfg.BlockConsumerConfig.MaxBlockBatchSize), 65 "The maximum number of blocks from which to request events in a single call - will reduce logarithmically to 1 when errors occur.") 66 abiFileOpt := cmd.StringsOpt("abi", cfg.AbiFileOrDirs, "EVM Contract ABI file or folder") 67 specFileOrDirOpt := cmd.StringsOpt("spec", cfg.SpecFileOrDirs, "SQLSol specification file or folder") 68 dbBlockOpt := cmd.BoolOpt("blocks", false, "Create block tables and persist related data") 69 dbTxOpt := cmd.BoolOpt("txs", false, "Create tx tables and persist related data") 70 71 announceEveryOpt := cmd.StringOpt("announce-every", "5s", "Announce vent status every period as a Go duration, e.g. 1ms, 3s, 1h") 72 73 cmd.Before = func() { 74 var err error 75 // Rather annoying boilerplate here... but there is no way to pass mow.cli a pointer for it to fill you value 76 cfg.DBAdapter = *dbOpts.adapter 77 cfg.DBURL = *dbOpts.url 78 cfg.DBSchema = *dbOpts.schema 79 cfg.ChainAddress = *grpcAddrOpt 80 cfg.HTTPListenAddress = *httpAddrOpt 81 cfg.WatchAddresses = make([]crypto.Address, len(*watchAddressesOpt)) 82 cfg.MinimumHeight = uint64(*minimumHeightOpt) 83 cfg.BlockConsumerConfig.MaxRequests, cfg.BlockConsumerConfig.TimeBase, err = parseRequestRate(*maxRequestRateOpt) 84 if err != nil { 85 output.Fatalf("Could not parse max request rate: %w", err) 86 } 87 cfg.BlockConsumerConfig.MaxRetries = uint64(*maxRetriesOpt) 88 cfg.BlockConsumerConfig.BaseBackoffDuration, err = parseDuration(*backoffDurationOpt) 89 if err != nil { 90 output.Fatalf("could not parse backoff duration: %w", err) 91 } 92 cfg.BlockConsumerConfig.MaxBlockBatchSize = uint64(*batchSizeOpt) 93 for i, wa := range *watchAddressesOpt { 94 cfg.WatchAddresses[i], err = crypto.AddressFromHexString(wa) 95 if err != nil { 96 output.Fatalf("could not parse watch address: %w", err) 97 } 98 } 99 cfg.AbiFileOrDirs = *abiFileOpt 100 cfg.SpecFileOrDirs = *specFileOrDirOpt 101 if *dbBlockOpt { 102 cfg.SpecOpt |= sqlsol.Block 103 } 104 if *dbTxOpt { 105 cfg.SpecOpt |= sqlsol.Tx 106 } 107 108 cfg.AnnounceEvery, err = parseDuration(*announceEveryOpt) 109 if err != nil { 110 output.Fatalf("could not parse announce-every duration %s: %v", *announceEveryOpt, err) 111 } 112 } 113 114 cmd.Spec = "--spec=<spec file or dir>... [--abi=<abi file or dir>...] " + 115 "[--watch=<contract address>...] [--minimum-height=<lowest height from which to read>] " + 116 "[--max-retries=<max block request retries>] [--backoff=<minimum backoff duration>] " + 117 "[--max-request-rate=<requests / time base>] [--batch-size=<minimum block batch size>] " + 118 "[--db-adapter] [--db-url] [--db-schema] [--blocks] [--txs] [--chain-addr] [--http-addr] " + 119 "[--log-level] [--announce-every=<duration>]" 120 121 cmd.Action = func() { 122 logger, err := logConfig(LogLevel(*logLevelOpt)).Logger() 123 if err != nil { 124 output.Fatalf("failed to load logger: %v", err) 125 } 126 127 logger = logger.With("service", "vent") 128 consumer := service.NewConsumer(cfg, logger, make(chan types.EventData)) 129 if err != nil { 130 output.Fatalf("Could not create Vent Consumer: %v", err) 131 } 132 server := service.NewServer(cfg, logger, consumer) 133 134 projection, err := sqlsol.SpecLoader(cfg.SpecFileOrDirs, cfg.SpecOpt) 135 if err != nil { 136 output.Fatalf("Spec loader error: %v", err) 137 } 138 139 var wg sync.WaitGroup 140 141 // setup channel for termination signals 142 ch := make(chan os.Signal) 143 144 signal.Notify(ch, syscall.SIGTERM) 145 signal.Notify(ch, syscall.SIGINT) 146 147 // start the events consumer 148 wg.Add(1) 149 150 go func() { 151 if err := consumer.Run(projection, true); err != nil { 152 output.Fatalf("Consumer execution error: %v", err) 153 } 154 155 wg.Done() 156 }() 157 158 // start the http server 159 wg.Add(1) 160 161 go func() { 162 server.Run() 163 wg.Done() 164 }() 165 166 // wait for a termination signal from the OS and 167 // gracefully shutdown the events consumer and the http server 168 go func() { 169 <-ch 170 consumer.Shutdown() 171 server.Shutdown() 172 }() 173 174 // wait until the events consumer and the http server are done 175 wg.Wait() 176 } 177 }) 178 179 cmd.Command("schema", "Print JSONSchema for spec file format to validate table specs", 180 func(cmd *cli.Cmd) { 181 cmd.Action = func() { 182 output.Printf(source.JSONString(types.ProjectionSpecSchema())) 183 } 184 }) 185 186 cmd.Command("spec", "Generate SQLSOL specification from ABIs", 187 func(cmd *cli.Cmd) { 188 abiFileOpt := cmd.StringsOpt("abi", nil, "EVM Contract ABI file or folder") 189 dest := cmd.StringArg("SPEC", "", "Write resulting spec to this json file") 190 191 cmd.Action = func() { 192 abiSpec, err := abi.LoadPath(*abiFileOpt...) 193 if err != nil { 194 output.Fatalf("ABI loader error: %v", err) 195 } 196 197 spec, err := sqlsol.GenerateSpecFromAbis(abiSpec) 198 if err != nil { 199 output.Fatalf("error generating spec: %s\n", err) 200 } 201 202 err = ioutil.WriteFile(*dest, []byte(source.JSONString(spec)), 0644) 203 if err != nil { 204 output.Fatalf("error writing file: %v\n", err) 205 } 206 } 207 }) 208 209 cmd.Command("restore", "Restore the mapped tables from the _vent_log table", 210 func(cmd *cli.Cmd) { 211 const timeLayout = "2006-01-02 15:04:05" 212 213 dbOpts := sqlDBOpts(cmd, config.DefaultVentConfig()) 214 timeOpt := cmd.StringOpt("t time", "", fmt.Sprintf("restore time up to which all "+ 215 "log entries will be applied to restore DB, in the format '%s'- restores all log entries if omitted", 216 timeLayout)) 217 prefixOpt := cmd.StringOpt("p prefix", "", "") 218 219 cmd.Spec = "[--db-adapter] [--db-url] [--db-schema] [--time=<date/time to up to which to restore>] " + 220 "[--prefix=<destination table prefix>]" 221 222 var restoreTime time.Time 223 224 cmd.Before = func() { 225 if *timeOpt != "" { 226 var err error 227 restoreTime, err = time.Parse(timeLayout, *timeOpt) 228 if err != nil { 229 output.Fatalf("Could not parse restore time, should be in the format '%s': %v", 230 timeLayout, err) 231 } 232 } 233 } 234 235 cmd.Action = func() { 236 log, err := logconfig.New().Logger() 237 if err != nil { 238 output.Fatalf("failed to load logger: %v", err) 239 } 240 db, err := sqldb.NewSQLDB(types.SQLConnection{ 241 DBAdapter: *dbOpts.adapter, 242 DBURL: *dbOpts.url, 243 DBSchema: *dbOpts.schema, 244 Log: log.With("service", "vent"), 245 }) 246 if err != nil { 247 output.Fatalf("Could not connect to SQL DB: %v", err) 248 } 249 250 if restoreTime.IsZero() { 251 output.Logf("Restoring DB to state from log") 252 } else { 253 output.Logf("Restoring DB to state from log as of %v", restoreTime) 254 } 255 256 if *prefixOpt == "" { 257 output.Logf("Restoring DB in-place by overwriting any existing tables") 258 } else { 259 output.Logf("Restoring DB to destination tables with prefix '%s'", *prefixOpt) 260 } 261 262 err = db.RestoreDB(restoreTime, *prefixOpt) 263 if err != nil { 264 output.Fatalf("Error restoring DB: %v", err) 265 } 266 output.Logf("Successfully restored DB") 267 } 268 }) 269 } 270 } 271 272 func parseDuration(duration string) (time.Duration, error) { 273 if duration == "" { 274 return 0, nil 275 } 276 return time.ParseDuration(duration) 277 } 278 279 func parseRequestRate(rate string) (int, time.Duration, error) { 280 if rate == "" { 281 return 0, 0, nil 282 } 283 ratio := strings.Split(rate, "/") 284 if len(ratio) != 2 { 285 return 0, 0, fmt.Errorf("expected a ratio string separated by a '/' but got %s", rate) 286 } 287 requests, err := strconv.ParseInt(ratio[0], 10, 0) 288 if err != nil { 289 return 0, 0, fmt.Errorf("could not parse max requests as base 10 integer: %w", err) 290 } 291 timeBase, err := time.ParseDuration(ratio[1]) 292 if err != nil { 293 return 0, 0, fmt.Errorf("could not parse time base: %w", err) 294 } 295 return int(requests), timeBase, nil 296 } 297 298 type dbOpts struct { 299 adapter *string 300 url *string 301 schema *string 302 } 303 304 func sqlDBOpts(cmd *cli.Cmd, cfg *config.VentConfig) dbOpts { 305 return dbOpts{ 306 adapter: cmd.StringOpt("db-adapter", cfg.DBAdapter, "Database adapter, 'postgres' or 'sqlite' (if built with the sqlite tag) are supported"), 307 url: cmd.StringOpt("db-url", cfg.DBURL, "PostgreSQL database URL or SQLite db file path"), 308 schema: cmd.StringOpt("db-schema", cfg.DBSchema, "PostgreSQL database schema (empty for SQLite)"), 309 } 310 }