diff --git a/.gitignore b/.gitignore index 4c49bd7..6816e68 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +bin/* .env diff --git a/Makefile b/Makefile index 655d2f5..e2c3c7b 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,23 @@ -APP_NAME := eia-api-go -SCHEMA_JSON := schema/eia-api-oapi.json -SCHEMA_DIR := schema -GENERATED_DIR := generated +# Variables +CLIENT_PKG := ./cmd/eia-client -# Default rule -all: build +.PHONY: all generate build install clean -# Retrieve and prepare the schema -schema: - mkdir -p $(SCHEMA_DIR) - # if [ -f $(SCHEMA_JSON) ]; then rm -f $(SCHEMA_JSON); fi - # curl -o $(SCHEMA_DIR)/eia-api-swagger.zip https://www.eia.gov/opendata/eia-api-swagger.zip - # unzip -o $(SCHEMA_DIR)/eia-api-swagger.zip -d $(SCHEMA_DIR) +# Default target +all: generate build -# Generate the Go client using Swagger Codegen -generate: schema - mkdir -p $(GENERATED_DIR) - docker run --rm \ - -v ${PWD}:/local \ - swaggerapi/swagger-codegen-cli-v3:latest generate \ - -i /local/$(SCHEMA_JSON) \ - -l go \ - -o /local/$(GENERATED_DIR) +# Generate code +generate: + go generate ./... -# Build the application -build: - go build -o $(APP_NAME) +# Build the client command binary +build: generate + go build -o bin/eia-client $(CLIENT_PKG) -# Install the application -install: - go install ./... +# Install the client command binary +install: generate + go install $(CLIENT_PKG) -# Clean generated code +# Clean up generated files and build artifacts clean: - rm -rf $(GENERATED_DIR) - rm -rf $(SCHEMA_DIR) - -.PHONY: all schema generate build install clean + rm -rf bin/eia-client diff --git a/cmd/client/cmd/list/list_series.go b/cmd/client/cmd/list/list_series.go deleted file mode 100644 index 75ef2e3..0000000 --- a/cmd/client/cmd/list/list_series.go +++ /dev/null @@ -1,42 +0,0 @@ -package list - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - eiaapi "gitea.libretechconsulting.com/50W/eia-api-go/api" -) - -var ListCmd = &cobra.Command{ - Use: "list ", - Args: cobra.RangeArgs(0, 1), - Short: "List Available EIA Series", - Run: runListCmd, -} - -func runListCmd(cmd *cobra.Command, _ []string) { - client, err := eiaapi.NewClient("https://api.eia.gov") - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - - resp, err := client.GetV2(cmd.Context()) - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - - defer resp.Body.Close() - response := make([]byte, 0) - - _, err = resp.Body.Read(response) - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - - fmt.Printf("%s\n", response) -} diff --git a/cmd/client/LICENSE b/cmd/eia-client/LICENSE similarity index 100% rename from cmd/client/LICENSE rename to cmd/eia-client/LICENSE diff --git a/cmd/eia-client/cmd/list/list_series.go b/cmd/eia-client/cmd/list/list_series.go new file mode 100644 index 0000000..4738cee --- /dev/null +++ b/cmd/eia-client/cmd/list/list_series.go @@ -0,0 +1,32 @@ +package list + +import ( + "github.com/spf13/cobra" + + "gitea.libretechconsulting.com/50W/eia-api-go/cmd/eia-client/internal/util" +) + +var ListCmd = &cobra.Command{ + Use: "list ", + Args: cobra.RangeArgs(0, 1), + Short: "List Available EIA Series", + PreRun: util.SetClient, + Run: runListCmd, +} + +func runListCmd(cmd *cobra.Command, _ []string) { + logger := util.Logger(cmd) + client, err := util.Client(cmd) + if err != nil { + logger.Fatal().Err(err).Send() + } + + ctx, cncl := util.RequestCtx(cmd) + defer cncl() + resp, err := client.GetV2(ctx) + if err != nil { + logger.Err(err).Send() + } + + logger.Debug().Any("resp", resp).Send() +} diff --git a/cmd/client/cmd/root.go b/cmd/eia-client/cmd/root.go similarity index 57% rename from cmd/client/cmd/root.go rename to cmd/eia-client/cmd/root.go index 6117370..e9b06c7 100644 --- a/cmd/client/cmd/root.go +++ b/cmd/eia-client/cmd/root.go @@ -25,15 +25,32 @@ import ( "context" "os" "os/signal" + "time" - "gitea.libretechconsulting.com/50W/eia-api-go/cmd/client/cmd/list" + "github.com/rs/zerolog" "github.com/spf13/cobra" + + "gitea.libretechconsulting.com/50W/eia-api-go/cmd/eia-client/cmd/list" + "gitea.libretechconsulting.com/50W/eia-api-go/cmd/eia-client/internal/util" ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "eia-client", - Short: "Useful utilities for EIA API v2", + Use: "eia-client", + Short: "Useful utilities for EIA API v2", + PersistentPreRun: PreRun, +} + +func PreRun(cmd *cobra.Command, _ []string) { + // Set up logging + console := zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC1123, + } + logger := zerolog.New(console).With(). + Timestamp().Logger().Level(*util.GetLogLevel(cmd)) + + util.SetLogger(cmd, &logger) } // Execute adds all child commands to the root command and sets flags appropriately. @@ -49,15 +66,21 @@ func Execute() { } func init() { - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. + // Enable PreRun for all commands + cobra.EnableTraverseRunHooks = true - // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cmd.yaml)") + // Handle command flags + rootCmd.PersistentFlags().StringP(util.FLAG_LOG_LEVEL, "l", util.DefaultLogLevel.String(), + "Log Level, or set "+util.ENV_LOG_LEVEL+" in environment") - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + rootCmd.PersistentFlags().StringP(util.FLAG_API_LOG_LEVEL, "L", util.DefAPILogLevel.String(), + "Log Level for EIA API Middleware, or set "+util.ENV_API_LOG_LEVEL+" in environment") + + rootCmd.PersistentFlags().StringP(util.FLAG_APIKEY, + "a", "", "API Key, or set "+util.ENV_API+" in environment") + + rootCmd.PersistentFlags().DurationP(util.FLAG_TMOUT, "t", util.DefRequestTimeout, + "Request timeout, or set "+util.ENV_TMOUT+" in environment") // Subcommands rootCmd.AddCommand(list.ListCmd) diff --git a/cmd/eia-client/internal/util/util.go b/cmd/eia-client/internal/util/util.go new file mode 100644 index 0000000..454c4c4 --- /dev/null +++ b/cmd/eia-client/internal/util/util.go @@ -0,0 +1,28 @@ +package util + +// These constants contain typed values +// for safely setting and retrieving flags +// from the cobra command +const ( + FLAG_APIKEY = "apiKey" + ENV_API = "EIA_API_KEY" + + FLAG_TMOUT = "timeout" + ENV_TMOUT = "EIA_API_TMOUT" + + FLAG_API_LOG_LEVEL = "apiLogLevel" + ENV_API_LOG_LEVEL = "EIA_API_LOG_LEVEL" + + FLAG_LOG_LEVEL = "logLevel" + ENV_LOG_LEVEL = "LOG_LEVEL" +) + +// Type for context data +type eiaCtxField uint8 + +// These constants contain fields to safely set +// or get fields from the command context +const ( + CTX_EIA_CLIENT eiaCtxField = iota + CTX_EIA_TMOUT +) diff --git a/cmd/eia-client/internal/util/util_api.go b/cmd/eia-client/internal/util/util_api.go new file mode 100644 index 0000000..cb73e56 --- /dev/null +++ b/cmd/eia-client/internal/util/util_api.go @@ -0,0 +1,67 @@ +package util + +import ( + "context" + "errors" + "os" + "time" + + "github.com/rs/zerolog" + "github.com/spf13/cobra" + + "gitea.libretechconsulting.com/50W/eia-api-go/pkg/eia" +) + +var ( + DefRequestTimeout = 5 * time.Second + DefAPILogLevel = zerolog.DebugLevel +) + +func RequestCtx(cmd *cobra.Command) (context.Context, context.CancelFunc) { + return context.WithTimeout(cmd.Context(), GetRequestTmout(cmd)) +} + +func GetRequestTmout(cmd *cobra.Command) time.Duration { + tmout, ok := cmd.Context().Value(CTX_EIA_TMOUT).(time.Duration) + if !ok || tmout == 0 { + return DefRequestTimeout + } + return tmout +} + +func SetRequestTmout(cmd *cobra.Command, tmout time.Duration) { + cmd.SetContext(context.WithValue(cmd.Context(), + CTX_EIA_TMOUT, tmout)) +} + +func Client(cmd *cobra.Command) (*eia.Client, error) { + client, ok := cmd.Context().Value(CTX_EIA_CLIENT).(*eia.Client) + if !ok { + return nil, errors.New("command context lacks api client") + } + return client, nil +} + +func SetClient(cmd *cobra.Command, _ []string) { + client, err := eia.NewClient(&eia.ClientOpts{ + Context: cmd.Context(), + APIKey: GetAPIKey(cmd), + Logger: Logger(cmd), + LogLevel: GetAPILogLevel(cmd), + }) + if err != nil { + Logger(cmd).Fatal().Err(err).Send() + } + + cmd.SetContext(context.WithValue(cmd.Context(), + CTX_EIA_CLIENT, client)) +} + +func GetAPIKey(cmd *cobra.Command) string { + if key := os.Getenv(ENV_API); key != "" { + return key + } + + key, _ := cmd.Flags().GetString(FLAG_APIKEY) + return key +} diff --git a/cmd/eia-client/internal/util/util_logging.go b/cmd/eia-client/internal/util/util_logging.go new file mode 100644 index 0000000..ac2400a --- /dev/null +++ b/cmd/eia-client/internal/util/util_logging.go @@ -0,0 +1,74 @@ +package util + +import ( + "os" + + "github.com/rs/zerolog" + "github.com/spf13/cobra" +) + +var DefaultLogLevel = zerolog.InfoLevel + +func Logger(cmd *cobra.Command) *zerolog.Logger { + return zerolog.Ctx(cmd.Context()) +} + +func SetLogger(cmd *cobra.Command, logger *zerolog.Logger) { + cmd.SetContext(logger.WithContext(cmd.Context())) +} + +func GetLogLevel(cmd *cobra.Command) *zerolog.Level { + var level zerolog.Level + var err error + if envLevel := os.Getenv(ENV_LOG_LEVEL); envLevel != "" { + level, err = zerolog.ParseLevel(envLevel) + if err != nil { + Logger(cmd).Err(err). + Str("envLogLevel", envLevel). + Str("envVar", ENV_LOG_LEVEL). + Str("defaultLevel", DefaultLogLevel.String()). + Msg("Invalid log level in environment, using default") + level = DefaultLogLevel + } + } + + if flagLevel, err := cmd.Flags().GetString(FLAG_LOG_LEVEL); err == nil { + level, err = zerolog.ParseLevel(flagLevel) + if err != nil { + Logger(cmd).Err(err). + Str("flagLogLevel", flagLevel). + Str("defaultLevel", DefaultLogLevel.String()). + Msg("Invalid log level in command flag, using default") + level = DefaultLogLevel + } + } + return &level +} + +func GetAPILogLevel(cmd *cobra.Command) *zerolog.Level { + var level zerolog.Level + var err error + if envLevel := os.Getenv(ENV_API_LOG_LEVEL); envLevel != "" { + level, err = zerolog.ParseLevel(envLevel) + if err != nil { + Logger(cmd).Err(err). + Str("envLogLevel", envLevel). + Str("envVar", ENV_API_LOG_LEVEL). + Str("defaultLevel", DefAPILogLevel.String()). + Msg("Invalid API log level in environment, using default") + level = DefAPILogLevel + } + } + + if flagLevel, err := cmd.Flags().GetString(FLAG_API_LOG_LEVEL); err == nil { + level, err = zerolog.ParseLevel(flagLevel) + if err != nil { + Logger(cmd).Err(err). + Str("flagLogLevel", flagLevel). + Str("defaultLevel", DefAPILogLevel.String()). + Msg("Invalid API log level in command flag, using default") + level = DefAPILogLevel + } + } + return &level +} diff --git a/cmd/client/main.go b/cmd/eia-client/main.go similarity index 94% rename from cmd/client/main.go rename to cmd/eia-client/main.go index 6a883c0..491a42e 100644 --- a/cmd/client/main.go +++ b/cmd/eia-client/main.go @@ -21,7 +21,7 @@ THE SOFTWARE. */ package main -import "gitea.libretechconsulting.com/50W/eia-api-go/cmd/client/cmd" +import "gitea.libretechconsulting.com/50W/eia-api-go/cmd/eia-client/cmd" func main() { cmd.Execute() diff --git a/go.mod b/go.mod index d7f720b..b4eef08 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module gitea.libretechconsulting.com/50W/eia-api-go go 1.23.3 require ( + github.com/deepmap/oapi-codegen v1.16.3 github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 github.com/oapi-codegen/runtime v1.1.1 + github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 ) @@ -19,6 +21,8 @@ require ( github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect @@ -26,6 +30,7 @@ require ( github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect golang.org/x/tools v0.27.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index ed392e3..cf8d7af 100644 --- a/go.sum +++ b/go.sum @@ -5,10 +5,13 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deepmap/oapi-codegen v1.16.3 h1:GT9G86SbQtT1r8ZB+4Cybi9VGdu1P5ieNvNdEoCSbrA= +github.com/deepmap/oapi-codegen v1.16.3/go.mod h1:JD6ErqeX0nYnhdciLc61Konj3NBASREMlkHOgHn8WAM= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= github.com/dprotaso/go-yit v0.0.0-20240618133044-5a0af90af097 h1:f5nA5Ys8RXqFXtKc0XofVRiuwNTuJzPIwTmbjLz9vj8= github.com/dprotaso/go-yit v0.0.0-20240618133044-5a0af90af097/go.mod h1:FTAVyH6t+SlS97rv6EXRVuBDLkQqcIe/xQw9f4IFUI4= @@ -24,6 +27,7 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -60,6 +64,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -84,10 +94,14 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -143,6 +157,9 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/pkg/eia/eia.go b/pkg/eia/eia.go new file mode 100644 index 0000000..161734e --- /dev/null +++ b/pkg/eia/eia.go @@ -0,0 +1,87 @@ +package eia + +import ( + "context" + "net/url" + "slices" + "time" + + "github.com/deepmap/oapi-codegen/pkg/securityprovider" + "github.com/rs/zerolog" + + eiaapi "gitea.libretechconsulting.com/50W/eia-api-go/api" +) + +const ( + defaultBaseURL = "https://api.eia.gov" + defaultPingTimeout = 10 * time.Second + defaultLogLevel = zerolog.DebugLevel +) + +// Simple wrapper around generated EIA API client +// that embeds a client with a bearer auth interceptor +// and optional logging middleware +// +// Performs a basic availability check against the API +// when creating a new client, and exposes a Ping() method +// for health / readiness probes +type Client struct { + ctx context.Context + apiKey string + healthCheckTimeout time.Duration + *eiaapi.Client +} + +type ClientOpts struct { + Context context.Context // Base context for requests + APIKey string // API Key [EIA Opendata API v2](https://www.eia.gov/opendata/index.php) + Logger *zerolog.Logger // Optional logger, if set is injected into logging middleware + LogLevel *zerolog.Level // Optional log level, default is zerolog.DebugLevel + BaseURL *url.URL // Optional, default is https://api.eia.gov + HealthCheckTimeout *time.Duration // Timeout for Ping() function, default is 10s +} + +func NewClient(opts *ClientOpts) (*Client, error) { + baseURL := defaultBaseURL + if opts.BaseURL != nil { + baseURL = opts.BaseURL.String() + } + + hcTimeout := defaultPingTimeout + if opts.HealthCheckTimeout != nil && *opts.HealthCheckTimeout > time.Duration(0) { + hcTimeout = *opts.HealthCheckTimeout + } + + middlewares := make([]eiaapi.ClientOption, 0, 2) + + // Injects Authorization: Bearer header into + // outbound API calls + basicAuth, err := securityprovider.NewSecurityProviderBearerToken(opts.APIKey) + if err != nil { + return nil, err + } + middlewares = append(middlewares, eiaapi.WithRequestEditorFn(basicAuth.Intercept)) + + // Logging middleware, if logger is given + if opts.Logger != nil { + logLevel := defaultLogLevel + if opts.LogLevel != nil { + logLevel = *opts.LogLevel + } + + middlewares = append(middlewares, + eiaapi.WithRequestEditorFn(newLoggingMiddleware(opts.Logger, logLevel))) + } + + client, err := eiaapi.NewClient(baseURL, slices.Clip(middlewares)...) + if err != nil { + return nil, err + } + + return &Client{ + apiKey: opts.APIKey, + ctx: opts.Context, + healthCheckTimeout: hcTimeout, + Client: client, + }, nil +} diff --git a/pkg/eia/eia_health.go b/pkg/eia/eia_health.go new file mode 100644 index 0000000..4161352 --- /dev/null +++ b/pkg/eia/eia_health.go @@ -0,0 +1,20 @@ +package eia + +import ( + "context" + "fmt" +) + +func (c *Client) Ping() error { + ctx, cncl := context.WithTimeout(c.ctx, c.healthCheckTimeout) + defer cncl() + + resp, err := c.GetV2(ctx) + if err != nil { + return err + } else if resp.StatusCode != 200 { + return fmt.Errorf("non-200 response [%s] from eia api", resp.Status) + } + + return nil +} diff --git a/pkg/eia/eia_logging.go b/pkg/eia/eia_logging.go new file mode 100644 index 0000000..d8608f7 --- /dev/null +++ b/pkg/eia/eia_logging.go @@ -0,0 +1,24 @@ +package eia + +import ( + "context" + "net/http" + "time" + + "github.com/rs/zerolog" + + eiaapi "gitea.libretechconsulting.com/50W/eia-api-go/api" +) + +func newLoggingMiddleware(logger *zerolog.Logger, level zerolog.Level) eiaapi.RequestEditorFn { + return func(_ context.Context, req *http.Request) error { + logger.WithLevel(level). + Str("method", req.Method). + Str("host", req.URL.Host). + Str("path", req.URL.Path). + Str("query", req.Form.Encode()). + Time("timestamp", time.Now()). + Send() + return nil + } +}