diff --git a/fxcore/README.md b/fxcore/README.md index b39b7536..76fe1539 100644 --- a/fxcore/README.md +++ b/fxcore/README.md @@ -134,6 +134,9 @@ modules: liveness: expose: true # to expose health check liveness route, disabled by default path: /livez # health check liveness route path (default /livez) + tasks: + expose: true # to expose tasks route, disabled by default + path: /tasks/:name # tasks route path (default /tasks/:name) debug: config: expose: true # to expose debug config route diff --git a/fxcore/go.mod b/fxcore/go.mod index 5d7cd354..e82ddac9 100644 --- a/fxcore/go.mod +++ b/fxcore/go.mod @@ -14,7 +14,7 @@ require ( github.com/ankorstore/yokai/healthcheck v1.1.0 github.com/ankorstore/yokai/httpserver v1.6.0 github.com/ankorstore/yokai/log v1.2.0 - github.com/ankorstore/yokai/trace v1.3.0 + github.com/ankorstore/yokai/trace v1.4.0 github.com/arl/statsviz v0.6.0 github.com/labstack/echo/v4 v4.13.3 github.com/labstack/gommon v0.4.2 diff --git a/fxcore/go.sum b/fxcore/go.sum index ec0a18cb..8df59b53 100644 --- a/fxcore/go.sum +++ b/fxcore/go.sum @@ -22,6 +22,8 @@ github.com/ankorstore/yokai/log v1.2.0 h1:jiuDiC0dtqIGIOsFQslUHYoFJ1qjI+rOMa6dI1 github.com/ankorstore/yokai/log v1.2.0/go.mod h1:MVvUcms1AYGo0BT6l88B9KJdvtK6/qGKdgyKVXfbmyc= github.com/ankorstore/yokai/trace v1.3.0 h1:0ji32oymIcxTmH5h6GRWLo5ypwBbWrZkXRf9rWF9070= github.com/ankorstore/yokai/trace v1.3.0/go.mod h1:m7EL2MRBilgCtrly5gA4F0jkGSXR2EbG6LsotbTJ4nA= +github.com/ankorstore/yokai/trace v1.4.0 h1:AdEQs/4TEuqOJ9p/EfsQmrtmkSG3pcmE7r/l+FQFxY8= +github.com/ankorstore/yokai/trace v1.4.0/go.mod h1:m7EL2MRBilgCtrly5gA4F0jkGSXR2EbG6LsotbTJ4nA= github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE= github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/fxcore/info.go b/fxcore/info.go index 320cd7cb..d773f964 100644 --- a/fxcore/info.go +++ b/fxcore/info.go @@ -1,6 +1,9 @@ package fxcore import ( + "fmt" + "sort" + "github.com/ankorstore/yokai/config" "github.com/ankorstore/yokai/log" "github.com/ankorstore/yokai/trace" @@ -129,3 +132,57 @@ func (i *FxCoreModuleInfo) Data() map[string]interface{} { "extra": i.ExtraInfos, } } + +// FxModuleInfoRegistry is the registry collecting info about registered modules. +type FxModuleInfoRegistry struct { + infos map[string]FxModuleInfo +} + +// FxModuleInfoRegistryParam allows injection of the required dependencies in [NewFxModuleInfoRegistry]. +type FxModuleInfoRegistryParam struct { + fx.In + Infos []any `group:"core-module-infos"` +} + +// NewFxModuleInfoRegistry returns a new [FxModuleInfoRegistry]. +func NewFxModuleInfoRegistry(p FxModuleInfoRegistryParam) *FxModuleInfoRegistry { + infos := make(map[string]FxModuleInfo) + + for _, info := range p.Infos { + if castInfo, ok := info.(FxModuleInfo); ok { + infos[castInfo.Name()] = castInfo + } + } + + return &FxModuleInfoRegistry{ + infos: infos, + } +} + +func (r *FxModuleInfoRegistry) Names() []string { + names := make([]string, len(r.infos)) + + i := 0 + for name := range r.infos { + names[i] = name + i++ + } + + sort.Strings(names) + + return names +} + +// All returns a map of all registered [FxModuleInfo]. +func (r *FxModuleInfoRegistry) All() map[string]FxModuleInfo { + return r.infos +} + +// Find returns a [FxModuleInfo] by name. +func (r *FxModuleInfoRegistry) Find(name string) (FxModuleInfo, error) { + if info, ok := r.infos[name]; ok { + return info, nil + } + + return nil, fmt.Errorf("fx module info with name %s was not found", name) +} diff --git a/fxcore/info_test.go b/fxcore/info_test.go index b1ed0f3f..1a9539e9 100644 --- a/fxcore/info_test.go +++ b/fxcore/info_test.go @@ -8,7 +8,17 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewFxCoreModuleInfo(t *testing.T) { +type testModuleInfo struct{} + +func (i *testModuleInfo) Name() string { + return "test" +} + +func (i *testModuleInfo) Data() map[string]interface{} { + return map[string]interface{}{} +} + +func TestFxCoreModuleInfo(t *testing.T) { t.Setenv("APP_ENV", "test") cfg, err := config.NewDefaultConfigFactory().Create( @@ -54,3 +64,70 @@ func TestNewFxCoreModuleInfo(t *testing.T) { info.Data(), ) } + +func TestFxModuleInfoRegistry(t *testing.T) { + t.Parallel() + + createRegistry := func(tb testing.TB) *fxcore.FxModuleInfoRegistry { + tb.Helper() + + cfg, err := config.NewDefaultConfigFactory().Create( + config.WithFilePaths("./testdata/config"), + ) + assert.NoError(tb, err) + + return fxcore.NewFxModuleInfoRegistry(fxcore.FxModuleInfoRegistryParam{ + Infos: []interface{}{ + &testModuleInfo{}, + fxcore.NewFxCoreModuleInfo(fxcore.FxCoreModuleInfoParam{ + Config: cfg, + ExtraInfos: []fxcore.FxExtraInfo{}, + }), + "invalid", + }, + }) + } + + t.Run("test type", func(t *testing.T) { + t.Parallel() + + registry := createRegistry(t) + + assert.IsType(t, &fxcore.FxModuleInfoRegistry{}, registry) + }) + + t.Run("test all", func(t *testing.T) { + t.Parallel() + + registry := createRegistry(t) + + assert.Len(t, registry.All(), 2) + }) + + t.Run("test names", func(t *testing.T) { + t.Parallel() + + registry := createRegistry(t) + + assert.Equal(t, []string{fxcore.ModuleName, "test"}, registry.Names()) + }) + + t.Run("test find", func(t *testing.T) { + t.Parallel() + + registry := createRegistry(t) + + testInfo, err := registry.Find("test") + assert.NoError(t, err) + assert.Equal(t, "test", testInfo.Name()) + + coreInfo, err := registry.Find(fxcore.ModuleName) + assert.NoError(t, err) + assert.Equal(t, fxcore.ModuleName, coreInfo.Name()) + + invalidInfo, err := registry.Find("invalid") + assert.Error(t, err) + assert.Equal(t, "fx module info with name invalid was not found", err.Error()) + assert.Nil(t, invalidInfo) + }) +} diff --git a/fxcore/module.go b/fxcore/module.go index 98a9c981..8f86bbe9 100644 --- a/fxcore/module.go +++ b/fxcore/module.go @@ -4,6 +4,7 @@ import ( "context" "embed" "fmt" + "io" "net/http" "strconv" @@ -37,6 +38,7 @@ const ( DefaultHealthCheckStartupPath = "/healthz" DefaultHealthCheckLivenessPath = "/livez" DefaultHealthCheckReadinessPath = "/readyz" + DefaultTasksPath = "/tasks" DefaultDebugConfigPath = "/debug/config" DefaultDebugPProfPath = "/debug/pprof" DefaultDebugBuildPath = "/debug/build" @@ -63,6 +65,7 @@ var FxCoreModule = fx.Module( fxhealthcheck.FxHealthcheckModule, fx.Provide( NewFxModuleInfoRegistry, + NewTaskRegistry, NewFxCore, fx.Annotate( NewFxCoreModuleInfo, @@ -92,7 +95,8 @@ type FxCoreParam struct { Checker *healthcheck.Checker Config *config.Config Logger *log.Logger - Registry *FxModuleInfoRegistry + InfoRegistry *FxModuleInfoRegistry + TaskRegistry *TaskRegistry MetricsRegistry *prometheus.Registry } @@ -232,7 +236,7 @@ func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) { dashboardEnabled := p.Config.GetBool("modules.core.server.dashboard.enabled") // dashboard overview - overviewInfo, err := p.Registry.Find(ModuleName) + overviewInfo, err := p.InfoRegistry.Find(ModuleName) if err != nil { return nil, err } @@ -248,6 +252,7 @@ func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) { overviewTraceProcessorExpose := p.Config.GetBool("modules.core.server.dashboard.overview.trace_processor") // template expositions + tasksExpose := p.Config.GetBool("modules.core.server.tasks.expose") metricsExpose := p.Config.GetBool("modules.core.server.metrics.expose") startupExpose := p.Config.GetBool("modules.core.server.healthcheck.startup.expose") livenessExpose := p.Config.GetBool("modules.core.server.healthcheck.liveness.expose") @@ -260,6 +265,7 @@ func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) { modulesExpose := p.Config.GetBool("modules.core.server.debug.modules.expose") // template paths + tasksPath := p.Config.GetString("modules.core.server.tasks.path") metricsPath := p.Config.GetString("modules.core.server.metrics.path") startupPath := p.Config.GetString("modules.core.server.healthcheck.startup.path") livenessPath := p.Config.GetString("modules.core.server.healthcheck.liveness.path") @@ -271,6 +277,48 @@ func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) { buildPath := p.Config.GetString("modules.core.server.debug.build.path") modulesPath := p.Config.GetString("modules.core.server.debug.modules.path") + // tasks + if tasksExpose { + if tasksPath == "" { + tasksPath = DefaultTasksPath + } + + coreServer.POST(fmt.Sprintf("%s/:name", tasksPath), func(c echo.Context) error { + ctx := c.Request().Context() + + logger := log.CtxLogger(ctx) + + name := c.Param("name") + + input, err := io.ReadAll(c.Request().Body) + if err != nil { + logger.Error().Err(err).Str("task", name).Msg("request body read error") + + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("cannot read request body: %v", err.Error())) + } + + err = c.Request().Body.Close() + if err != nil { + logger.Error().Err(err).Str("task", name).Msg("request body close error") + + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("cannot close request body: %v", err.Error())) + } + + res := p.TaskRegistry.Run(ctx, name, input) + if !res.Success { + logger.Error().Err(err).Str("task", name).Msg("task execution error") + + return c.JSON(http.StatusInternalServerError, res) + } + + logger.Info().Str("task", name).Msg("task execution success") + + return c.JSON(http.StatusOK, res) + }) + + coreServer.Logger.Debug("registered tasks handler") + } + // metrics if metricsExpose { if metricsPath == "" { @@ -393,14 +441,14 @@ func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) { coreServer.Logger.Debug("registered debug build handler") } - // debug modules + // modules if modulesExpose || appDebug { if modulesPath == "" { modulesPath = DefaultDebugModulesPath } coreServer.GET(fmt.Sprintf("%s/:name", modulesPath), func(c echo.Context) error { - info, err := p.Registry.Find(c.Param("name")) + info, err := p.InfoRegistry.Find(c.Param("name")) if err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } @@ -466,6 +514,9 @@ func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) { "overviewLogOutputExpose": overviewLogOutputExpose, "overviewTraceSamplerExpose": overviewTraceSamplerExpose, "overviewTraceProcessorExpose": overviewTraceProcessorExpose, + "tasksExpose": tasksExpose, + "tasksPath": tasksPath, + "tasksNames": p.TaskRegistry.Names(), "metricsExpose": metricsExpose, "metricsPath": metricsPath, "startupExpose": startupExpose, @@ -486,7 +537,7 @@ func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) { "buildPath": buildPath, "modulesExpose": modulesExpose || appDebug, "modulesPath": modulesPath, - "modulesNames": p.Registry.Names(), + "modulesNames": p.InfoRegistry.Names(), "theme": theme, }) }) diff --git a/fxcore/module_test.go b/fxcore/module_test.go index 3401bac8..a7b579a3 100644 --- a/fxcore/module_test.go +++ b/fxcore/module_test.go @@ -2,6 +2,7 @@ package fxcore_test import ( "bytes" + "encoding/json" "net/http" "net/http/httptest" "strings" @@ -9,6 +10,7 @@ import ( "github.com/ankorstore/yokai/fxcore" "github.com/ankorstore/yokai/fxcore/testdata/probes" + "github.com/ankorstore/yokai/fxcore/testdata/tasks" "github.com/ankorstore/yokai/fxhealthcheck" "github.com/ankorstore/yokai/healthcheck" "github.com/ankorstore/yokai/log/logtest" @@ -1053,3 +1055,71 @@ func TestModuleDashboardTheme(t *testing.T) { } } } + +func TestModuleDashboardTasks(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("TASKS_ENABLED", "true") + + var core *fxcore.Core + + fxcore.NewBootstrapper().RunTestApp( + t, + fxcore.AsTasks( + tasks.NewErrorTask, + tasks.NewSuccessTask, + ), + fx.Populate(&core), + ) + + // [GET] /tasks/success + req := httptest.NewRequest(http.MethodPost, "/tasks/success", bytes.NewBuffer([]byte("test input"))) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + + res := fxcore.TaskResult{} + err := json.Unmarshal(rec.Body.Bytes(), &res) + assert.NoError(t, err) + + assert.True(t, res.Success) + assert.Equal(t, "task success", res.Message) + assert.Equal( + t, + map[string]any{ + "app": "core-app", + "input": "test input", + }, + res.Details, + ) + + // [GET] /tasks/error + req = httptest.NewRequest(http.MethodPost, "/tasks/error", bytes.NewBuffer([]byte("test input"))) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusInternalServerError, rec.Code) + + res = fxcore.TaskResult{} + err = json.Unmarshal(rec.Body.Bytes(), &res) + assert.NoError(t, err) + + assert.False(t, res.Success) + assert.Equal(t, "task error", res.Message) + assert.Nil(t, res.Details) + + // [GET] /tasks/invalid + req = httptest.NewRequest(http.MethodPost, "/tasks/invalid", bytes.NewBuffer([]byte("test input"))) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusInternalServerError, rec.Code) + + res = fxcore.TaskResult{} + err = json.Unmarshal(rec.Body.Bytes(), &res) + assert.NoError(t, err) + + assert.False(t, res.Success) + assert.Equal(t, "task invalid not found", res.Message) + assert.Nil(t, res.Details) +} diff --git a/fxcore/register.go b/fxcore/register.go index 065d29e5..cbd4758f 100644 --- a/fxcore/register.go +++ b/fxcore/register.go @@ -14,3 +14,25 @@ func AsCoreExtraInfo(name string, value string) fx.Option { ), ) } + +// AsTask registers a task in the core. +func AsTask(constructor any) fx.Option { + return fx.Provide( + fx.Annotate( + constructor, + fx.As(new(Task)), + fx.ResultTags(`group:"core-tasks"`), + ), + ) +} + +// AsTasks registers several tasks in the core. +func AsTasks(constructors ...any) fx.Option { + options := []fx.Option{} + + for _, constructor := range constructors { + options = append(options, AsTask(constructor)) + } + + return fx.Options(options...) +} diff --git a/fxcore/register_test.go b/fxcore/register_test.go index 23d84e3f..cb274cad 100644 --- a/fxcore/register_test.go +++ b/fxcore/register_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ankorstore/yokai/fxcore" + "github.com/ankorstore/yokai/fxcore/testdata/tasks" "github.com/stretchr/testify/assert" ) @@ -15,3 +16,19 @@ func TestAsCoreExtraInfo(t *testing.T) { assert.Equal(t, "fx.supplyOption", fmt.Sprintf("%T", result)) } + +func TestAsTask(t *testing.T) { + t.Parallel() + + result := fxcore.AsTask(tasks.NewErrorTask) + + assert.Equal(t, "fx.provideOption", fmt.Sprintf("%T", result)) +} + +func TestAsTasks(t *testing.T) { + t.Parallel() + + result := fxcore.AsTasks(tasks.NewErrorTask) + + assert.Equal(t, "fx.optionGroup", fmt.Sprintf("%T", result)) +} diff --git a/fxcore/registry.go b/fxcore/registry.go deleted file mode 100644 index 29a8c67c..00000000 --- a/fxcore/registry.go +++ /dev/null @@ -1,62 +0,0 @@ -package fxcore - -import ( - "fmt" - "sort" - - "go.uber.org/fx" -) - -// FxModuleInfoRegistry is the registry collecting info about registered modules. -type FxModuleInfoRegistry struct { - infos map[string]FxModuleInfo -} - -// FxModuleInfoRegistryParam allows injection of the required dependencies in [NewFxModuleInfoRegistry]. -type FxModuleInfoRegistryParam struct { - fx.In - Infos []any `group:"core-module-infos"` -} - -// NewFxModuleInfoRegistry returns a new [FxModuleInfoRegistry]. -func NewFxModuleInfoRegistry(p FxModuleInfoRegistryParam) *FxModuleInfoRegistry { - infos := make(map[string]FxModuleInfo) - - for _, info := range p.Infos { - if castInfo, ok := info.(FxModuleInfo); ok { - infos[castInfo.Name()] = castInfo - } - } - - return &FxModuleInfoRegistry{ - infos: infos, - } -} - -func (r *FxModuleInfoRegistry) Names() []string { - names := make([]string, len(r.infos)) - - i := 0 - for name := range r.infos { - names[i] = name - i++ - } - - sort.Strings(names) - - return names -} - -// All returns a map of all registered [FxModuleInfo]. -func (r *FxModuleInfoRegistry) All() map[string]FxModuleInfo { - return r.infos -} - -// Find returns a [FxModuleInfo] by name. -func (r *FxModuleInfoRegistry) Find(name string) (FxModuleInfo, error) { - if info, ok := r.infos[name]; ok { - return info, nil - } - - return nil, fmt.Errorf("fx module info with name %s was not found", name) -} diff --git a/fxcore/registry_test.go b/fxcore/registry_test.go deleted file mode 100644 index 80d794e8..00000000 --- a/fxcore/registry_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package fxcore_test - -import ( - "testing" - - "github.com/ankorstore/yokai/config" - "github.com/ankorstore/yokai/fxcore" - "github.com/stretchr/testify/assert" -) - -type testModuleInfo struct{} - -func (i *testModuleInfo) Name() string { - return "test" -} - -func (i *testModuleInfo) Data() map[string]interface{} { - return map[string]interface{}{} -} - -func TestNewFxModuleInfoRegistry(t *testing.T) { - t.Parallel() - - registry, err := prepareTestFxModuleInfoRegistry() - assert.NoError(t, err) - - assert.IsType(t, &fxcore.FxModuleInfoRegistry{}, registry) -} - -func TestAll(t *testing.T) { - t.Parallel() - - registry, err := prepareTestFxModuleInfoRegistry() - assert.NoError(t, err) - - assert.Len(t, registry.All(), 2) -} - -func TestNames(t *testing.T) { - t.Parallel() - - registry, err := prepareTestFxModuleInfoRegistry() - assert.NoError(t, err) - - assert.Equal(t, []string{fxcore.ModuleName, "test"}, registry.Names()) -} - -func TestFind(t *testing.T) { - t.Parallel() - - registry, err := prepareTestFxModuleInfoRegistry() - assert.NoError(t, err) - - testInfo, err := registry.Find("test") - assert.NoError(t, err) - assert.Equal(t, "test", testInfo.Name()) - - coreInfo, err := registry.Find(fxcore.ModuleName) - assert.NoError(t, err) - assert.Equal(t, fxcore.ModuleName, coreInfo.Name()) - - invalidInfo, err := registry.Find("invalid") - assert.Error(t, err) - assert.Equal(t, "fx module info with name invalid was not found", err.Error()) - assert.Nil(t, invalidInfo) -} - -func prepareTestFxModuleInfoRegistry() (*fxcore.FxModuleInfoRegistry, error) { - cfg, err := config.NewDefaultConfigFactory().Create( - config.WithFilePaths("./testdata/config"), - ) - if err != nil { - return nil, err - } - - return fxcore.NewFxModuleInfoRegistry(fxcore.FxModuleInfoRegistryParam{ - Infos: []interface{}{ - &testModuleInfo{}, - fxcore.NewFxCoreModuleInfo(fxcore.FxCoreModuleInfoParam{ - Config: cfg, - ExtraInfos: []fxcore.FxExtraInfo{}, - }), - "invalid", - }, - }), nil -} diff --git a/fxcore/task.go b/fxcore/task.go new file mode 100644 index 00000000..2d9f62f4 --- /dev/null +++ b/fxcore/task.go @@ -0,0 +1,72 @@ +package fxcore + +import ( + "context" + "fmt" + "sort" + + "go.uber.org/fx" +) + +// TaskResult is a Task execution result. +type TaskResult struct { + Success bool `json:"success"` + Message string `json:"message"` + Details map[string]any `json:"details,omitempty"` +} + +// Task is an interface for tasks implementations. +type Task interface { + Name() string + Run(ctx context.Context, input []byte) TaskResult +} + +// TaskRegistry is a registry of Task implementations. +type TaskRegistry struct { + tasks map[string]Task +} + +// TaskRegistryParams is used to inject dependencies in NewTaskRegistry. +type TaskRegistryParams struct { + fx.In + Tasks []Task `group:"core-tasks"` +} + +// NewTaskRegistry returns a new TaskRegistry instance. +func NewTaskRegistry(p TaskRegistryParams) *TaskRegistry { + tasks := make(map[string]Task) + + for _, task := range p.Tasks { + tasks[task.Name()] = task + } + + return &TaskRegistry{ + tasks: tasks, + } +} + +// Names returns all registered Task names. +func (r *TaskRegistry) Names() []string { + var names []string + + for name := range r.tasks { + names = append(names, name) + } + + sort.Strings(names) + + return names +} + +// Run runs a specific Task. +func (r *TaskRegistry) Run(ctx context.Context, name string, input []byte) TaskResult { + task, ok := r.tasks[name] + if !ok { + return TaskResult{ + Success: false, + Message: fmt.Sprintf("task %s not found", name), + } + } + + return task.Run(ctx, input) +} diff --git a/fxcore/task_test.go b/fxcore/task_test.go new file mode 100644 index 00000000..7094abb6 --- /dev/null +++ b/fxcore/task_test.go @@ -0,0 +1,82 @@ +package fxcore_test + +import ( + "context" + "testing" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxcore" + "github.com/ankorstore/yokai/fxcore/testdata/tasks" + "github.com/stretchr/testify/assert" +) + +func TestTaskRegistry(t *testing.T) { + t.Parallel() + + createRegistry := func(tb testing.TB) *fxcore.TaskRegistry { + tb.Helper() + + cfg, err := config.NewDefaultConfigFactory().Create( + config.WithFilePaths("./testdata/config"), + ) + assert.NoError(tb, err) + + return fxcore.NewTaskRegistry(fxcore.TaskRegistryParams{ + Tasks: []fxcore.Task{ + tasks.NewSuccessTask(cfg), + tasks.NewErrorTask(), + }, + }) + } + + t.Run("test names", func(t *testing.T) { + t.Parallel() + + registry := createRegistry(t) + + assert.Equal(t, []string{"error", "success"}, registry.Names()) + }) + + t.Run("test run with success task", func(t *testing.T) { + t.Parallel() + + registry := createRegistry(t) + + res := registry.Run(context.Background(), "success", []byte("test input")) + + assert.True(t, res.Success) + assert.Equal(t, "task success", res.Message) + assert.Equal( + t, + map[string]any{ + "app": "core-app", + "input": "test input", + }, + res.Details, + ) + }) + + t.Run("test run with error task", func(t *testing.T) { + t.Parallel() + + registry := createRegistry(t) + + res := registry.Run(context.Background(), "error", []byte("test input")) + + assert.False(t, res.Success) + assert.Equal(t, "task error", res.Message) + assert.Nil(t, res.Details) + }) + + t.Run("test run with invalid task", func(t *testing.T) { + t.Parallel() + + registry := createRegistry(t) + + res := registry.Run(context.Background(), "invalid", []byte("test input")) + + assert.False(t, res.Success) + assert.Equal(t, "task invalid not found", res.Message) + assert.Nil(t, res.Details) + }) +} diff --git a/fxcore/templates/dashboard.html b/fxcore/templates/dashboard.html index e61eb4fb..85022ce4 100644 --- a/fxcore/templates/dashboard.html +++ b/fxcore/templates/dashboard.html @@ -1,3 +1,4 @@ +
- {{ .overviewInfo.AppName }} -
-Description | -{{ .overviewInfo.AppDescription }} |
-
Env | -{{ .overviewInfo.AppEnv }} |
-
Debug | -{{ .overviewInfo.AppDebug }} |
-
Version | -{{ .overviewInfo.AppVersion }} |
-
+ {{ .overviewInfo.AppName }} +
+Description | +{{ .overviewInfo.AppDescription }} |
+
Env | +{{ .overviewInfo.AppEnv }} |
+
Debug | +{{ .overviewInfo.AppDebug }} |
+
Version | +{{ .overviewInfo.AppVersion }} |
+
- Logs -
-Level | -{{ .overviewInfo.LogLevel }} |
-
Output | -{{ .overviewInfo.LogOutput }} |
-
+ Logs +
+Level | +{{ .overviewInfo.LogLevel }} |
+
Output | +{{ .overviewInfo.LogOutput }} |
+
- Traces -
-Sampler | -{{ .overviewInfo.TraceSampler }} |
-
Processor | -{{ .overviewInfo.TraceProcessor }} |
-
+ Traces +
+Sampler | +{{ .overviewInfo.TraceSampler }} |
+
Processor | +{{ .overviewInfo.TraceProcessor }} |
+
- Extra information -
-{{ $infoName }} | -{{ $infoValue }} |
-
+ Extra information +
+{{ $infoName }} | +{{ $infoValue }} |
+
{% taskResultMessage %}
+ +{% taskResultMessage %}
+ +' + this.content + '
';
@@ -345,10 +465,17 @@
return '' + JSON.stringify(this.content, null, 2) + '
';
}
}
+ },
+ computedTaskResultDetails() {
+ if (this.taskResultDetails === undefined) {
+ return '';
+ }
+
+ return '' + JSON.stringify(this.taskResultDetails, null, 2) + '
';
}
},
}).mount('#app')
- document.dispatchEvent(new KeyboardEvent('keydown', {}))
+ document.dispatchEvent(new KeyboardEvent('keydown', {}));
\ No newline at end of file
diff --git a/fxcore/testdata/config/config.yaml b/fxcore/testdata/config/config.yaml
index db70eb5b..188ace50 100644
--- a/fxcore/testdata/config/config.yaml
+++ b/fxcore/testdata/config/config.yaml
@@ -47,6 +47,8 @@ modules:
expose: ${READINESS_ENABLED}
liveness:
expose: ${LIVENESS_ENABLED}
+ tasks:
+ expose: ${TASKS_ENABLED}
debug:
config:
expose: ${CONFIG_ENABLED}
diff --git a/fxcore/testdata/tasks/error.go b/fxcore/testdata/tasks/error.go
new file mode 100644
index 00000000..5dbeb4a8
--- /dev/null
+++ b/fxcore/testdata/tasks/error.go
@@ -0,0 +1,26 @@
+package tasks
+
+import (
+ "context"
+
+ "github.com/ankorstore/yokai/fxcore"
+)
+
+var _ fxcore.Task = (*ErrorTask)(nil)
+
+type ErrorTask struct{}
+
+func NewErrorTask() *ErrorTask {
+ return &ErrorTask{}
+}
+
+func (t *ErrorTask) Name() string {
+ return "error"
+}
+
+func (t *ErrorTask) Run(context.Context, []byte) fxcore.TaskResult {
+ return fxcore.TaskResult{
+ Success: false,
+ Message: "task error",
+ }
+}
diff --git a/fxcore/testdata/tasks/success.go b/fxcore/testdata/tasks/success.go
new file mode 100644
index 00000000..182aab0d
--- /dev/null
+++ b/fxcore/testdata/tasks/success.go
@@ -0,0 +1,35 @@
+package tasks
+
+import (
+ "context"
+
+ "github.com/ankorstore/yokai/config"
+ "github.com/ankorstore/yokai/fxcore"
+)
+
+var _ fxcore.Task = (*SuccessTask)(nil)
+
+type SuccessTask struct {
+ config *config.Config
+}
+
+func NewSuccessTask(config *config.Config) *SuccessTask {
+ return &SuccessTask{
+ config: config,
+ }
+}
+
+func (t *SuccessTask) Name() string {
+ return "success"
+}
+
+func (t *SuccessTask) Run(ctx context.Context, input []byte) fxcore.TaskResult {
+ return fxcore.TaskResult{
+ Success: true,
+ Message: "task success",
+ Details: map[string]any{
+ "app": t.config.AppName(),
+ "input": string(input),
+ },
+ }
+}