From d56565555b828cdc4ab9f9a3b6a913334943524a Mon Sep 17 00:00:00 2001 From: jkoberg Date: Tue, 7 Feb 2023 14:57:33 +0100 Subject: [PATCH] introduce userlog service Signed-off-by: jkoberg --- Makefile | 1 + ocis-pkg/config/config.go | 2 + ocis-pkg/config/defaultconfig.go | 2 + ocis/pkg/runtime/service/service.go | 2 + .../pkg/config/defaults/defaultconfig.go | 4 + services/userlog/Makefile | 37 +++++ services/userlog/README.md | 11 ++ services/userlog/cmd/userlog/main.go | 14 ++ services/userlog/pkg/command/health.go | 18 ++ services/userlog/pkg/command/root.go | 59 +++++++ services/userlog/pkg/command/server.go | 101 ++++++++++++ services/userlog/pkg/command/version.go | 19 +++ services/userlog/pkg/config/config.go | 58 +++++++ services/userlog/pkg/config/debug.go | 9 + .../pkg/config/defaults/defaultconfig.go | 78 +++++++++ services/userlog/pkg/config/log.go | 9 + services/userlog/pkg/config/parser/parse.go | 38 +++++ services/userlog/pkg/config/service.go | 6 + services/userlog/pkg/logging/logging.go | 17 ++ services/userlog/pkg/metrics/metrics.go | 35 ++++ services/userlog/pkg/server/http/option.go | 102 ++++++++++++ services/userlog/pkg/server/http/server.go | 65 ++++++++ services/userlog/pkg/service/service.go | 155 ++++++++++++++++++ services/userlog/pkg/service/service_test.go | 3 + 24 files changed, 845 insertions(+) create mode 100644 services/userlog/Makefile create mode 100644 services/userlog/README.md create mode 100644 services/userlog/cmd/userlog/main.go create mode 100644 services/userlog/pkg/command/health.go create mode 100644 services/userlog/pkg/command/root.go create mode 100644 services/userlog/pkg/command/server.go create mode 100644 services/userlog/pkg/command/version.go create mode 100644 services/userlog/pkg/config/config.go create mode 100644 services/userlog/pkg/config/debug.go create mode 100644 services/userlog/pkg/config/defaults/defaultconfig.go create mode 100644 services/userlog/pkg/config/log.go create mode 100644 services/userlog/pkg/config/parser/parse.go create mode 100644 services/userlog/pkg/config/service.go create mode 100644 services/userlog/pkg/logging/logging.go create mode 100644 services/userlog/pkg/metrics/metrics.go create mode 100644 services/userlog/pkg/server/http/option.go create mode 100644 services/userlog/pkg/server/http/server.go create mode 100644 services/userlog/pkg/service/service.go create mode 100644 services/userlog/pkg/service/service_test.go diff --git a/Makefile b/Makefile index b32ec49a0..7e5a0a220 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ OCIS_MODULES = \ services/storage-users \ services/store \ services/thumbnails \ + services/userlog \ services/users \ services/web \ services/webdav\ diff --git a/ocis-pkg/config/config.go b/ocis-pkg/config/config.go index 76ba265f0..a9ab12f6c 100644 --- a/ocis-pkg/config/config.go +++ b/ocis-pkg/config/config.go @@ -31,6 +31,7 @@ import ( storageusers "github.com/owncloud/ocis/v2/services/storage-users/pkg/config" store "github.com/owncloud/ocis/v2/services/store/pkg/config" thumbnails "github.com/owncloud/ocis/v2/services/thumbnails/pkg/config" + userlog "github.com/owncloud/ocis/v2/services/userlog/pkg/config" users "github.com/owncloud/ocis/v2/services/users/pkg/config" web "github.com/owncloud/ocis/v2/services/web/pkg/config" webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/config" @@ -106,6 +107,7 @@ type Config struct { StorageUsers *storageusers.Config `yaml:"storage_users"` Store *store.Config `yaml:"store"` Thumbnails *thumbnails.Config `yaml:"thumbnails"` + Userlog *userlog.Config `yaml:"userlog"` Users *users.Config `yaml:"users"` Web *web.Config `yaml:"web"` WebDAV *webdav.Config `yaml:"webdav"` diff --git a/ocis-pkg/config/defaultconfig.go b/ocis-pkg/config/defaultconfig.go index 67d056eaf..434c42e13 100644 --- a/ocis-pkg/config/defaultconfig.go +++ b/ocis-pkg/config/defaultconfig.go @@ -29,6 +29,7 @@ import ( storageusers "github.com/owncloud/ocis/v2/services/storage-users/pkg/config/defaults" store "github.com/owncloud/ocis/v2/services/store/pkg/config/defaults" thumbnails "github.com/owncloud/ocis/v2/services/thumbnails/pkg/config/defaults" + userlog "github.com/owncloud/ocis/v2/services/userlog/pkg/config/defaults" users "github.com/owncloud/ocis/v2/services/users/pkg/config/defaults" web "github.com/owncloud/ocis/v2/services/web/pkg/config/defaults" webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/config/defaults" @@ -71,6 +72,7 @@ func DefaultConfig() *Config { StorageUsers: storageusers.DefaultConfig(), Store: store.DefaultConfig(), Thumbnails: thumbnails.DefaultConfig(), + Userlog: userlog.DefaultConfig(), Users: users.DefaultConfig(), Web: web.DefaultConfig(), WebDAV: webdav.DefaultConfig(), diff --git a/ocis/pkg/runtime/service/service.go b/ocis/pkg/runtime/service/service.go index 517d6418d..fe7060c05 100644 --- a/ocis/pkg/runtime/service/service.go +++ b/ocis/pkg/runtime/service/service.go @@ -46,6 +46,7 @@ import ( storageusers "github.com/owncloud/ocis/v2/services/storage-users/pkg/command" store "github.com/owncloud/ocis/v2/services/store/pkg/command" thumbnails "github.com/owncloud/ocis/v2/services/thumbnails/pkg/command" + userlog "github.com/owncloud/ocis/v2/services/userlog/pkg/command" users "github.com/owncloud/ocis/v2/services/users/pkg/command" web "github.com/owncloud/ocis/v2/services/web/pkg/command" webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/command" @@ -131,6 +132,7 @@ func NewService(options ...Option) (*Service, error) { s.ServicesRegistry[opts.Config.Search.Service.Name] = search.NewSutureService s.ServicesRegistry[opts.Config.Postprocessing.Service.Name] = postprocessing.NewSutureService s.ServicesRegistry[opts.Config.EventHistory.Service.Name] = eventhistory.NewSutureService + s.ServicesRegistry[opts.Config.Userlog.Service.Name] = userlog.NewSutureService // populate delayed services s.Delayed[opts.Config.Sharing.Service.Name] = sharing.NewSutureService diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index af5a71204..140a57d87 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -202,6 +202,10 @@ func DefaultPolicies() []config.Policy { Endpoint: "/api/v0/settings", Service: "com.owncloud.web.settings", }, + { + Endpoint: "/api/v0/activities", + Service: "com.owncloud.userlog.userlog", + }, }, }, } diff --git a/services/userlog/Makefile b/services/userlog/Makefile new file mode 100644 index 000000000..b1f19d550 --- /dev/null +++ b/services/userlog/Makefile @@ -0,0 +1,37 @@ +SHELL := bash +NAME := userlog + +include ../../.make/recursion.mk + +############ tooling ############ +ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI +include ../../.bingo/Variables.mk +endif + +############ go tooling ############ +include ../../.make/go.mk + +############ release ############ +include ../../.make/release.mk + +############ docs generate ############ +include ../../.make/docs.mk + +.PHONY: docs-generate +docs-generate: config-docs-generate + +############ generate ############ +include ../../.make/generate.mk + +.PHONY: ci-go-generate +ci-go-generate: # CI runs ci-node-generate automatically before this target + +.PHONY: ci-node-generate +ci-node-generate: + +############ licenses ############ +.PHONY: ci-node-check-licenses +ci-node-check-licenses: + +.PHONY: ci-node-save-licenses +ci-node-save-licenses: diff --git a/services/userlog/README.md b/services/userlog/README.md new file mode 100644 index 000000000..612af80c3 --- /dev/null +++ b/services/userlog/README.md @@ -0,0 +1,11 @@ +# Userlog service + +The `userlog` service provides a way to configure which events a user wants to be informed about and an API to retrieve them. + +## Configuring + +The `userlog` service has hardcoded configuration for now. + +## Retrieving + +The `userlog` service provides an API to retrieve configured events. diff --git a/services/userlog/cmd/userlog/main.go b/services/userlog/cmd/userlog/main.go new file mode 100644 index 000000000..efdb7ae4b --- /dev/null +++ b/services/userlog/cmd/userlog/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "github.com/owncloud/ocis/v2/services/userlog/pkg/command" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config/defaults" +) + +func main() { + if err := command.Execute(defaults.DefaultConfig()); err != nil { + os.Exit(1) + } +} diff --git a/services/userlog/pkg/command/health.go b/services/userlog/pkg/command/health.go new file mode 100644 index 000000000..6ef365346 --- /dev/null +++ b/services/userlog/pkg/command/health.go @@ -0,0 +1,18 @@ +package command + +import ( + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" + "github.com/urfave/cli/v2" +) + +// Health is the entrypoint for the health command. +func Health(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "health", + Usage: "Check health status", + Action: func(c *cli.Context) error { + // Not implemented + return nil + }, + } +} diff --git a/services/userlog/pkg/command/root.go b/services/userlog/pkg/command/root.go new file mode 100644 index 000000000..c9fb4b39b --- /dev/null +++ b/services/userlog/pkg/command/root.go @@ -0,0 +1,59 @@ +package command + +import ( + "context" + "os" + + "github.com/owncloud/ocis/v2/ocis-pkg/clihelper" + ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" + "github.com/thejerf/suture/v4" + "github.com/urfave/cli/v2" +) + +// GetCommands provides all commands for this service +func GetCommands(cfg *config.Config) cli.Commands { + return []*cli.Command{ + // start this service + Server(cfg), + + // interaction with this service + + // infos about this service + Health(cfg), + Version(cfg), + } +} + +// Execute is the entry point for the userlog command. +func Execute(cfg *config.Config) error { + app := clihelper.DefaultApp(&cli.App{ + Name: "userlog", + Usage: "starts userlog service", + Commands: GetCommands(cfg), + }) + + return app.Run(os.Args) +} + +// SutureService allows for the userlog command to be embedded and supervised by a suture supervisor tree. +type SutureService struct { + cfg *config.Config +} + +// NewSutureService creates a new userlog.SutureService +func NewSutureService(cfg *ociscfg.Config) suture.Service { + cfg.Notifications.Commons = cfg.Commons + return SutureService{ + cfg: cfg.Userlog, + } +} + +func (s SutureService) Serve(ctx context.Context) error { + s.cfg.Context = ctx + if err := Execute(s.cfg); err != nil { + return err + } + + return nil +} diff --git a/services/userlog/pkg/command/server.go b/services/userlog/pkg/command/server.go new file mode 100644 index 000000000..076eed54e --- /dev/null +++ b/services/userlog/pkg/command/server.go @@ -0,0 +1,101 @@ +package command + +import ( + "context" + "fmt" + + "github.com/cs3org/reva/v2/pkg/events" + "github.com/cs3org/reva/v2/pkg/events/stream" + "github.com/oklog/run" + "github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" + ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config/parser" + "github.com/owncloud/ocis/v2/services/userlog/pkg/logging" + "github.com/owncloud/ocis/v2/services/userlog/pkg/metrics" + "github.com/owncloud/ocis/v2/services/userlog/pkg/server/http" + "github.com/urfave/cli/v2" + "go-micro.dev/v4/store" +) + +// all events we care about +var _registeredEvents = []events.Unmarshaller{ + events.UploadReady{}, +} + +// Server is the entrypoint for the server command. +func Server(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "server", + Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name), + Category: "server", + Before: func(c *cli.Context) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + logger := logging.Configure(cfg.Service.Name, cfg.Log) + + err := ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...) + if err != nil { + return err + } + + gr := run.Group{} + ctx, cancel := func() (context.Context, context.CancelFunc) { + if cfg.Context == nil { + return context.WithCancel(context.Background()) + } + return context.WithCancel(cfg.Context) + }() + mtrcs := metrics.New() + + defer cancel() + + consumer, err := stream.NatsFromConfig(stream.NatsConfig(cfg.Events)) + if err != nil { + return err + } + + var st store.Store + switch cfg.Store.Type { + case "inmemory": + st = store.NewMemoryStore() + default: + return fmt.Errorf("unknown store '%s' configured", cfg.Store.Type) + } + + mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1) + + { + server, err := http.Server( + http.Logger(logger), + http.Context(ctx), + http.Config(cfg), + http.Metrics(mtrcs), + http.Store(st), + http.Consumer(consumer), + http.RegisteredEvents(_registeredEvents), + ) + + if err != nil { + logger.Info().Err(err).Str("transport", "http").Msg("Failed to initialize server") + return err + } + + gr.Add(func() error { + return server.Run() + }, func(err error) { + logger.Error(). + Str("transport", "http"). + Err(err). + Msg("Shutting down server") + + cancel() + }) + } + + return gr.Run() + }, + } +} diff --git a/services/userlog/pkg/command/version.go b/services/userlog/pkg/command/version.go new file mode 100644 index 000000000..fbee1e06e --- /dev/null +++ b/services/userlog/pkg/command/version.go @@ -0,0 +1,19 @@ +package command + +import ( + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" + "github.com/urfave/cli/v2" +) + +// Version prints the service versions of all running instances. +func Version(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "version", + Usage: "print the version of this binary and the running service instances", + Category: "info", + Action: func(c *cli.Context) error { + // not implemented + return nil + }, + } +} diff --git a/services/userlog/pkg/config/config.go b/services/userlog/pkg/config/config.go new file mode 100644 index 000000000..874c63fa7 --- /dev/null +++ b/services/userlog/pkg/config/config.go @@ -0,0 +1,58 @@ +package config + +import ( + "context" + "time" + + "github.com/owncloud/ocis/v2/ocis-pkg/shared" +) + +// Config combines all available configuration parts. +type Config struct { + Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service + + Service Service `yaml:"-"` + + Log *Log `yaml:"log"` + Debug Debug `yaml:"debug"` + + HTTP HTTP `yaml:"http"` + GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` + + Events Events `yaml:"events"` + Store Store `yaml:"store"` + + Context context.Context `yaml:"-"` +} + +// Store configures the store to use +type Store struct { + Type string `yaml:"type" env:"USERLOG_STORE_TYPE" desc:"The type of the store. Supported is inmemory"` + RecordExpiry time.Duration `yaml:"record_expiry" env:"USERLOG_RECORD_EXPIRY" desc:"time to life for events in the store"` +} + +// Events combines the configuration options for the event bus. +type Events struct { + Endpoint string `yaml:"endpoint" env:"USERLOG_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture."` + Cluster string `yaml:"cluster" env:"USERLOG_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system."` + TLSInsecure bool `yaml:"tls_insecure" env:"OCIS_INSECURE;USERLOG_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates."` + TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"USERLOG_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided NOTIFICATIONS_EVENTS_TLS_INSECURE will be seen as false."` + EnableTLS bool `yaml:"enable_tls" env:"OCIS_EVENTS_ENABLE_TLS;USERLOG_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the ocis service which receives and delivers events between the services.."` +} + +// CORS defines the available cors configuration. +type CORS struct { + AllowedOrigins []string `yaml:"allow_origins" env:"OCIS_CORS_ALLOW_ORIGINS;USERLOG_CORS_ALLOW_ORIGINS" desc:"A comma-separated list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin"` + AllowedMethods []string `yaml:"allow_methods" env:"OCIS_CORS_ALLOW_METHODS;USERLOG_CORS_ALLOW_METHODS" desc:"A comma-separated list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method"` + AllowedHeaders []string `yaml:"allow_headers" env:"OCIS_CORS_ALLOW_HEADERS;USERLOG_CORS_ALLOW_HEADERS" desc:"A comma-separated list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers."` + AllowCredentials bool `yaml:"allow_credentials" env:"OCIS_CORS_ALLOW_CREDENTIALS;USERLOG_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials."` +} + +// HTTP defines the available http configuration. +type HTTP struct { + Addr string `yaml:"addr" env:"USERLOG_HTTP_ADDR" desc:"The bind address of the HTTP service."` + Namespace string `yaml:"-"` + Root string `yaml:"root" env:"USERLOG_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service."` + CORS CORS `yaml:"cors"` + TLS shared.HTTPServiceTLS `yaml:"tls"` +} diff --git a/services/userlog/pkg/config/debug.go b/services/userlog/pkg/config/debug.go new file mode 100644 index 000000000..d29ac4916 --- /dev/null +++ b/services/userlog/pkg/config/debug.go @@ -0,0 +1,9 @@ +package config + +// Debug defines the available debug configuration. +type Debug struct { + Addr string `yaml:"addr" env:"USERLOG_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."` + Token string `yaml:"token" env:"USERLOG_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint."` + Pprof bool `yaml:"pprof" env:"USERLOG_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling."` + Zpages bool `yaml:"zpages" env:"USERLOG_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."` +} diff --git a/services/userlog/pkg/config/defaults/defaultconfig.go b/services/userlog/pkg/config/defaults/defaultconfig.go new file mode 100644 index 000000000..faae9fdde --- /dev/null +++ b/services/userlog/pkg/config/defaults/defaultconfig.go @@ -0,0 +1,78 @@ +package defaults + +import ( + "strings" + + "github.com/owncloud/ocis/v2/ocis-pkg/shared" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" +) + +// FullDefaultConfig returns the full default config +func FullDefaultConfig() *config.Config { + cfg := DefaultConfig() + EnsureDefaults(cfg) + Sanitize(cfg) + return cfg +} + +// DefaultConfig return the default configuration +func DefaultConfig() *config.Config { + return &config.Config{ + Service: config.Service{ + Name: "userlog", + }, + Events: config.Events{ + Endpoint: "127.0.0.1:9233", + Cluster: "ocis-cluster", + EnableTLS: false, + }, + Store: config.Store{ + Type: "inmemory", + }, + HTTP: config.HTTP{ + Addr: "127.0.0.1:0", + Root: "/", + Namespace: "com.owncloud.userlog", + CORS: config.CORS{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET"}, + AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With"}, + AllowCredentials: true, + }, + }, + } +} + +// EnsureDefaults ensures the config contains default values +func EnsureDefaults(cfg *config.Config) { + // provide with defaults for shared logging, since we need a valid destination address for "envdecode". + if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil { + cfg.Log = &config.Log{ + Level: cfg.Commons.Log.Level, + Pretty: cfg.Commons.Log.Pretty, + Color: cfg.Commons.Log.Color, + File: cfg.Commons.Log.File, + } + } else if cfg.Log == nil { + cfg.Log = &config.Log{} + } + + if cfg.GRPCClientTLS == nil { + cfg.GRPCClientTLS = &shared.GRPCClientTLS{} + if cfg.Commons != nil && cfg.Commons.GRPCClientTLS != nil { + cfg.GRPCClientTLS = cfg.Commons.GRPCClientTLS + } + } + + if cfg.Commons != nil { + cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS + } +} + +// Sanitize sanitizes the config +func Sanitize(cfg *config.Config) { + // sanitize config + if cfg.HTTP.Root != "/" { + cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/") + } +} diff --git a/services/userlog/pkg/config/log.go b/services/userlog/pkg/config/log.go new file mode 100644 index 000000000..9e9098aa5 --- /dev/null +++ b/services/userlog/pkg/config/log.go @@ -0,0 +1,9 @@ +package config + +// Log defines the available log configuration. +type Log struct { + Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;USERLOG_LOG_LEVEL" desc:"The log level. Valid values are: \"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"."` + Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;USERLOG_LOG_PRETTY" desc:"Activates pretty log output."` + Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;USERLOG_LOG_COLOR" desc:"Activates colorized log output."` + File string `mapstructure:"file" env:"OCIS_LOG_FILE;USERLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."` +} diff --git a/services/userlog/pkg/config/parser/parse.go b/services/userlog/pkg/config/parser/parse.go new file mode 100644 index 000000000..254d4667c --- /dev/null +++ b/services/userlog/pkg/config/parser/parse.go @@ -0,0 +1,38 @@ +package parser + +import ( + "errors" + + ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config/defaults" + + "github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode" +) + +// ParseConfig loads configuration from known paths. +func ParseConfig(cfg *config.Config) error { + _, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg) + if err != nil { + return err + } + + defaults.EnsureDefaults(cfg) + + // load all env variables relevant to the config in the current context. + if err := envdecode.Decode(cfg); err != nil { + // no environment variable set for this config is an expected "error" + if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) { + return err + } + } + + defaults.Sanitize(cfg) + + return Validate(cfg) +} + +// Validate validates the config +func Validate(cfg *config.Config) error { + return nil +} diff --git a/services/userlog/pkg/config/service.go b/services/userlog/pkg/config/service.go new file mode 100644 index 000000000..d1eac383f --- /dev/null +++ b/services/userlog/pkg/config/service.go @@ -0,0 +1,6 @@ +package config + +// Service defines the available service configuration. +type Service struct { + Name string `yaml:"-"` +} diff --git a/services/userlog/pkg/logging/logging.go b/services/userlog/pkg/logging/logging.go new file mode 100644 index 000000000..691170093 --- /dev/null +++ b/services/userlog/pkg/logging/logging.go @@ -0,0 +1,17 @@ +package logging + +import ( + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" +) + +// LoggerFromConfig initializes a service-specific logger instance. +func Configure(name string, cfg *config.Log) log.Logger { + return log.NewLogger( + log.Name(name), + log.Level(cfg.Level), + log.Pretty(cfg.Pretty), + log.Color(cfg.Color), + log.File(cfg.File), + ) +} diff --git a/services/userlog/pkg/metrics/metrics.go b/services/userlog/pkg/metrics/metrics.go new file mode 100644 index 000000000..1c34c6f9f --- /dev/null +++ b/services/userlog/pkg/metrics/metrics.go @@ -0,0 +1,35 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +var ( + // Namespace defines the namespace for the defines metrics. + Namespace = "ocis" + + // Subsystem defines the subsystem for the defines metrics. + Subsystem = "userlog" +) + +// Metrics defines the available metrics of this service. +type Metrics struct { + BuildInfo *prometheus.GaugeVec +} + +// New initializes the available metrics. +func New() *Metrics { + m := &Metrics{ + BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "build_info", + Help: "Build information", + }, []string{"version"}), + } + + _ = prometheus.Register( + m.BuildInfo, + ) + + // TODO: implement metrics + return m +} diff --git a/services/userlog/pkg/server/http/option.go b/services/userlog/pkg/server/http/option.go new file mode 100644 index 000000000..905621f56 --- /dev/null +++ b/services/userlog/pkg/server/http/option.go @@ -0,0 +1,102 @@ +package http + +import ( + "context" + + "github.com/cs3org/reva/v2/pkg/events" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" + "github.com/owncloud/ocis/v2/services/userlog/pkg/metrics" + "github.com/urfave/cli/v2" + "go-micro.dev/v4/store" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Logger log.Logger + Context context.Context + Config *config.Config + Metrics *metrics.Metrics + Flags []cli.Flag + Namespace string + Store store.Store + Consumer events.Consumer + RegisteredEvents []events.Unmarshaller +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} + +// Context provides a function to set the context option. +func Context(val context.Context) Option { + return func(o *Options) { + o.Context = val + } +} + +// Config provides a function to set the config option. +func Config(val *config.Config) Option { + return func(o *Options) { + o.Config = val + } +} + +// Metrics provides a function to set the metrics option. +func Metrics(val *metrics.Metrics) Option { + return func(o *Options) { + o.Metrics = val + } +} + +// Flags provides a function to set the flags option. +func Flags(val []cli.Flag) Option { + return func(o *Options) { + o.Flags = append(o.Flags, val...) + } +} + +// Namespace provides a function to set the Namespace option. +func Namespace(val string) Option { + return func(o *Options) { + o.Namespace = val + } +} + +// Store provides a function to configure the store +func Store(store store.Store) Option { + return func(o *Options) { + o.Store = store + } +} + +// Consumer provides a function to configure the consumer +func Consumer(consumer events.Consumer) Option { + return func(o *Options) { + o.Consumer = consumer + } +} + +// RegisteredEvents provides a function to register events +func RegisteredEvents(evs []events.Unmarshaller) Option { + return func(o *Options) { + o.RegisteredEvents = evs + } +} diff --git a/services/userlog/pkg/server/http/server.go b/services/userlog/pkg/server/http/server.go new file mode 100644 index 000000000..38b5db78a --- /dev/null +++ b/services/userlog/pkg/server/http/server.go @@ -0,0 +1,65 @@ +package http + +import ( + "fmt" + + "github.com/owncloud/ocis/v2/ocis-pkg/service/http" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + svc "github.com/owncloud/ocis/v2/services/userlog/pkg/service" + "go-micro.dev/v4" +) + +// Service is the service interface +type Service interface { +} + +// Server initializes the http service and server. +func Server(opts ...Option) (http.Service, error) { + options := newOptions(opts...) + + service, err := http.NewService( + http.TLSConfig(options.Config.HTTP.TLS), + http.Logger(options.Logger), + http.Namespace(options.Config.HTTP.Namespace), + http.Name("userlog"), + http.Version(version.GetString()), + http.Address(options.Config.HTTP.Addr), + http.Context(options.Context), + http.Flags(options.Flags...), + ) + if err != nil { + options.Logger.Error(). + Err(err). + Msg("Error initializing http service") + return http.Service{}, fmt.Errorf("could not initialize http service: %w", err) + } + + //middlewares := []func(stdhttp.Handler) stdhttp.Handler{ + //middleware.TraceContext, + //chimiddleware.RequestID, + //middleware.Version( + //"userlog", + //version.GetString(), + //), + //middleware.Logger( + //options.Logger, + //), + //} + + handle, err := svc.NewUserlogService(options.Config, options.Consumer, options.Store, options.RegisteredEvents) + if err != nil { + return http.Service{}, err + } + + { + //handle = svc.NewInstrument(handle, options.Metrics) + //handle = svc.NewLogging(handle, options.Logger) + //handle = svc.NewTracing(handle) + } + + if err := micro.RegisterHandler(service.Server(), handle); err != nil { + return http.Service{}, err + } + + return service, nil +} diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go new file mode 100644 index 000000000..06240d3f7 --- /dev/null +++ b/services/userlog/pkg/service/service.go @@ -0,0 +1,155 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "reflect" + + "github.com/cs3org/reva/v2/pkg/events" + "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" + ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" + "github.com/owncloud/ocis/v2/services/userlog/pkg/config" + "go-micro.dev/v4/store" +) + +// Comment when you read this on review +var _adminid = "2502d8b8-a5e7-4ab3-b858-5aafae4a64a2" + +// UserlogService is the service responsible for user activities +type UserlogService struct { + ch <-chan events.Event + store store.Store + cfg *config.Config + historyClient ehsvc.EventHistoryService + registeredEvents map[string]events.Unmarshaller +} + +// NewUserlogService returns an EventHistory service +func NewUserlogService(cfg *config.Config, consumer events.Consumer, store store.Store, registeredEvents []events.Unmarshaller) (*UserlogService, error) { + if consumer == nil || store == nil { + return nil, fmt.Errorf("Need non nil consumer (%v) and store (%v) to work properly", consumer, store) + } + + ch, err := events.Consume(consumer, "userlog", registeredEvents...) + if err != nil { + return nil, err + } + + grpcClient := grpc.DefaultClient() + grpcClient.Options() + c := ehsvc.NewEventHistoryService("com.owncloud.api.eventhistory", grpcClient) + + ul := &UserlogService{ch: ch, store: store, cfg: cfg, historyClient: c, registeredEvents: make(map[string]events.Unmarshaller)} + + for _, e := range registeredEvents { + typ := reflect.TypeOf(e) + ul.registeredEvents[typ.String()] = e + } + + go ul.MemorizeEvents() + + return ul, nil +} + +// MemorizeEvents stores eventIDs a user wants to receive +func (ul *UserlogService) MemorizeEvents() { + for event := range ul.ch { + switch event.Event.(type) { + default: + // for each event type we need to: + + // I) find users eligible to receive the event + + // II) filter users who want to receive the event + + // III) store the eventID for each user + + // TEMP TESTING CODE + if err := ul.addEventToUser(_adminid, event.ID); err != nil { + continue + } + } + } +} + +// GetEvents allows to retrieve events from the eventhistory by userid +func (ul *UserlogService) GetEvents(ctx context.Context, userid string) ([]interface{}, error) { + rec, err := ul.store.Read(userid) + if err != nil { + return nil, err + } + + if len(rec) == 0 { + // no events available + return []interface{}{}, nil + } + + var eventIDs []string + if err := json.Unmarshal(rec[0].Value, &eventIDs); err != nil { + // this should never happen + return nil, err + } + + resp, err := ul.historyClient.GetEvents(ctx, &ehsvc.GetEventsRequest{Ids: eventIDs}) + if err != nil { + return nil, err + } + + var events []interface{} + for _, e := range resp.Events { + ev, ok := ul.registeredEvents[e.Type] + if !ok { + // this should not happen but we handle it anyway + continue + } + + event, err := ev.Unmarshal(e.Event) + if err != nil { + // this shouldn't happen either + continue + } + + events = append(events, event) + } + + return events, nil +} + +func (ul *UserlogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { + evs, err := ul.GetEvents(r.Context(), _adminid) + if err != nil { + return + } + + // TODO: format response + b, _ := json.Marshal(evs) + w.Write(b) +} + +func (ul *UserlogService) addEventToUser(userid string, eventid string) error { + recs, err := ul.store.Read(userid) + if err != nil && err != store.ErrNotFound { + return err + } + + var ids []string + if len(recs) > 0 { + if err := json.Unmarshal(recs[0].Value, &ids); err != nil { + return err + } + } + + ids = append(ids, eventid) + + b, err := json.Marshal(ids) + if err != nil { + return err + } + + return ul.store.Write(&store.Record{ + Key: userid, + Value: b, + }) +} diff --git a/services/userlog/pkg/service/service_test.go b/services/userlog/pkg/service/service_test.go new file mode 100644 index 000000000..944a1f69f --- /dev/null +++ b/services/userlog/pkg/service/service_test.go @@ -0,0 +1,3 @@ +package service_test + +// tests here