From 10296fb4dd40fe4d461d825a26c6c174e1dadf83 Mon Sep 17 00:00:00 2001 From: Tigor Hutasuhut Date: Thu, 22 Aug 2024 09:18:44 +0700 Subject: [PATCH] initial commit --- .envrc | 1 + .gitignore | 2 + cmd/zen/main.go | 4 + flake.lock | 26 ++++ flake.nix | 31 +++++ go.mod | 3 + schemas/proto/buf.gen.yaml | 24 ++++ schemas/proto/buf.lock | 2 + schemas/proto/buf.yaml | 4 + schemas/proto/notify/v1/notify.proto | 19 +++ .../proto/notify/v1/send_notification.proto | 79 +++++++++++ schemas/proto/notify/v1/types.proto | 23 +++ zcore/internal/server/server.go | 1 + zcore/zerr/zerr.go | 33 +++++ zcore/zlog/zlog.go | 1 + zcore/zoptions/notify.go | 131 ++++++++++++++++++ 16 files changed, 384 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 cmd/zen/main.go create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 schemas/proto/buf.gen.yaml create mode 100644 schemas/proto/buf.lock create mode 100644 schemas/proto/buf.yaml create mode 100644 schemas/proto/notify/v1/notify.proto create mode 100644 schemas/proto/notify/v1/send_notification.proto create mode 100644 schemas/proto/notify/v1/types.proto create mode 100644 zcore/internal/server/server.go create mode 100644 zcore/zerr/zerr.go create mode 100644 zcore/zlog/zlog.go create mode 100644 zcore/zoptions/notify.go diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d37cea --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.direnv +gen diff --git a/cmd/zen/main.go b/cmd/zen/main.go new file mode 100644 index 0000000..fb56211 --- /dev/null +++ b/cmd/zen/main.go @@ -0,0 +1,4 @@ +package main + +// This is the main entry point for CLI interface. +func main() {} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8ebe1d5 --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1724177844, + "narHash": "sha256-G7Mf9uN9m8FimeP3eMHu/dOC4QS8QAzo0h4ZIlDHcCA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d13fa5a45a34e7c8be33474f58003914430bdc5a", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixpkgs-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b5e3927 --- /dev/null +++ b/flake.nix @@ -0,0 +1,31 @@ +{ + inputs = { + nixpkgs.url = "nixpkgs/nixpkgs-unstable"; + }; + + outputs = { nixpkgs, ... }: + let + system = "x86_64-linux"; + + pkgs = import nixpkgs { inherit system; }; + in + { + devShell.${system} = pkgs.mkShell { + name = "zen-shell"; + buildInputs = with pkgs; [ + go + nodePackages_latest.nodejs + goose + air + upx + buf + buf-language-server + protoc-gen-go + protoc-gen-connect-go + protoc-gen-validate + air + gopls + ]; + }; + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..000d784 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitlab.bareksa.com/backend/zen + +go 1.22.5 diff --git a/schemas/proto/buf.gen.yaml b/schemas/proto/buf.gen.yaml new file mode 100644 index 0000000..4e69c53 --- /dev/null +++ b/schemas/proto/buf.gen.yaml @@ -0,0 +1,24 @@ +version: v2 +managed: + enabled: true + disable: + - module: buf.build/bufbuild/protovalidate + - module: buf.build/protocolbuffers/wellknowntypes + + override: + - file_option: go_package_prefix + value: gitlab.bareksa.com/backend/zen/zcore/internal/gen/proto + +plugins: + - remote: buf.build/protocolbuffers/go:v1.34.2 + out: ../../zcore/internal/gen/proto + opt: + - paths=source_relative + + - remote: buf.build/connectrpc/go + out: ../../zcore/internal/gen/proto + opt: + - paths=source_relative + + - remote: buf.build/community/pseudomuto-doc:v1.5.1 + out: ../../zcore/internal/gen/proto diff --git a/schemas/proto/buf.lock b/schemas/proto/buf.lock new file mode 100644 index 0000000..4f98143 --- /dev/null +++ b/schemas/proto/buf.lock @@ -0,0 +1,2 @@ +# Generated by buf. DO NOT EDIT. +version: v2 diff --git a/schemas/proto/buf.yaml b/schemas/proto/buf.yaml new file mode 100644 index 0000000..bf7f2bb --- /dev/null +++ b/schemas/proto/buf.yaml @@ -0,0 +1,4 @@ +version: v2 +deps: + - buf.build/bufbuild/protovalidate + - buf.build/protocolbuffers/wellknowntypes diff --git a/schemas/proto/notify/v1/notify.proto b/schemas/proto/notify/v1/notify.proto new file mode 100644 index 0000000..083c884 --- /dev/null +++ b/schemas/proto/notify/v1/notify.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package notify.v1; + +import "notify/v1/send_notification.proto"; + +service NotifyService { + rpc SendNotification(SendNotificationRequest) returns (SendNotificationResponse); + rpc SendAttachment(stream SendAttachmentRequest) returns (SendAttachmentResponse); +} + +message SendAttachmentRequest { + string notification_id = 1; + string name = 2; + string content_type = 3; + bytes chunk = 4; +} + +message SendAttachmentResponse {} diff --git a/schemas/proto/notify/v1/send_notification.proto b/schemas/proto/notify/v1/send_notification.proto new file mode 100644 index 0000000..df2957f --- /dev/null +++ b/schemas/proto/notify/v1/send_notification.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package notify.v1; + +import "notify/v1/types.proto"; + +message SendNotificationRequest { + Payload payload = 1; + Service service = 2; +} + +// SendAttachmentResponse is sent when message is successful. +message SendNotificationResponse { + // notification_id is the snowflake id of the message. + // + // Guaranteed to be unique even on distributed environment. + // + // notification_id can be used in `SendAttachment` rpc to + // attach binary data to the message. + string notification_id = 1; +} + +message Payload { + string message = 1; + Level level = 2; + optional int64 code = 3; + // details adds context to the message. + // + // like timestamps, invalid inputs, + // request payloads, backend response, etc. + // + // Anything that can enrich why this message + // appears will help. + // + // DO NOT INCLUDE BINARIES LIKE IMAGES, PDFS, DOCS, IN + // THIS PAYLOAD. USE `SendAttachment` rpc + // to attach binary values instead since they are designed + // for streaming. Server and Client RAM + // can be eaten alive if you failed to do so since GRPC + // handles via whole messages. + oneof details { + // Sends JSON as details. + bytes d_json = 4; + string d_text = 5; + } + oneof error { + bytes e_json = 6; + string e_text = 7; + } +} + +message Service { + // The name of the service that is sending the notification. + // + // This is used to identify the service that is sending the notification. + // + // The value of name should be related to the product or service. + // e.g. `sbn-frontend`, `sbn-cron-job`, `payment-frontend`, `robo-frontend`. + // + // `name`, `type`, and `environment` combination are used to select which channel the + // discord notification is sent. + string name = 1; + string type = 2; + Environment environment = 3; + optional string version = 4; + // domain specifies whose message this belongs to. + // + // It's not required to be set, but setting this field + // will help zen to categorize messages when building + // reports. + ServiceDomain domain = 5; + repeated Attribute attributes = 6; +} + +enum ServiceDomain { + SERVICE_DOMAIN_UNSPECIFIED = 0; + SERVICE_DOMAIN_FRONTEND = 1; + SERVICE_DOMAIN_BACKEND = 2; +} diff --git a/schemas/proto/notify/v1/types.proto b/schemas/proto/notify/v1/types.proto new file mode 100644 index 0000000..23bab5d --- /dev/null +++ b/schemas/proto/notify/v1/types.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package notify.v1; + +enum Level { + LEVEL_UNSPECIFIED = 0; + LEVEL_DEBUG = 1; + LEVEL_INFO = 2; + LEVEL_WARN = 3; + LEVEL_ERROR = 4; +} + +enum Environment { + ENVIRONMENT_UNSPECIFIED = 0; + ENVIRONMENT_DEVELOPMENT = 1; + ENVIRONMENT_STAGING = 2; + ENVIRONMENT_PRODUCTION = 3; +} + +message Attribute { + string key = 1; + string value = 2; +} diff --git a/zcore/internal/server/server.go b/zcore/internal/server/server.go new file mode 100644 index 0000000..abb4e43 --- /dev/null +++ b/zcore/internal/server/server.go @@ -0,0 +1 @@ +package server diff --git a/zcore/zerr/zerr.go b/zcore/zerr/zerr.go new file mode 100644 index 0000000..07b86ba --- /dev/null +++ b/zcore/zerr/zerr.go @@ -0,0 +1,33 @@ +package zerr + +import ( + "context" + "time" + + "gitlab.bareksa.com/backend/zen/zcore/zoptions" +) + +type Error interface { + error + + Code(code int) Error + GetCode() int + + Message(msg string, args ...any) Error + GetMessage() string + + PublicMessage(msg string, args ...any) Error + GetPublicMessage() string + + Caller(pc uintptr) Error + GetCaller() uintptr + + Time(t time.Time) Error + GetTime() time.Time + + Key(msg string, args ...any) Error + GetKey() string + + Log(ctx context.Context) Error + Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error +} diff --git a/zcore/zlog/zlog.go b/zcore/zlog/zlog.go new file mode 100644 index 0000000..d3e6811 --- /dev/null +++ b/zcore/zlog/zlog.go @@ -0,0 +1 @@ +package zlog diff --git a/zcore/zoptions/notify.go b/zcore/zoptions/notify.go new file mode 100644 index 0000000..9026744 --- /dev/null +++ b/zcore/zoptions/notify.go @@ -0,0 +1,131 @@ +package zoptions + +import "time" + +type NotifyParameters struct { + // AlwaysSend makes sure this message + // is always sent no matter the condition. + // + // It will cause `zen` to ignore every other + // parameters and just send the message. + AlwaysSend bool + + // InitialBackoff is the starting backoff. + // + // If not specified, the default duration is time.Minute. + InitialBackoff time.Duration + // MaxBackoff is the maximum backoff. If unset, the default value + // is time.Hour * 24. + MaxBackoff time.Duration + + // BackoffFormula is a string that represents the formula to calculate the backoff time + // when multiple message with the same key is fired off repeatedly. + // + // It uses CEL (Common Expression Language) syntax. See: https://github.com/google/cel-go + // for the language spec. + // + // Expression must return a duration type. + // + // If invalid cel expression, empty string, or wrong return value, default formula will be used. + // + // Available variables: + // + // - `repeat`: the number of times the notification has been sent. type: int. + // - `last`: the last time the notification was sent. type: timestamp. + // - `prev_backoff`: the backoff value from previous evaluation. 0 if first seen. Type: duration. + // - `initial_backoff`: the initial_backoff value. Type: duration + // - `max_backoff`: the maximum backoff value. Type: duration + // + // Available Non-Standard functions: + // + // // pow multiplies the base with the exponent + // pow(base: double, exp: double) -> double + // + // // mult_dur_double multiplies the duration with the double value + // mult_dur_double(dur: duration, mult: double) -> duration + // + // + // Example (and also default formula): + // + // mult_dur_double(initial_backoff, pow(1.5, double(repeat))) > max_backoff + // ? max_backoff + // : mult_dur_double(initial_backoff, pow(1.5, double(repeat))) + BackoffFormula string +} + +type NotifyOption interface { + Apply(parameters *NotifyParameters) +} + +type NotifyOptionFunc func(parameters *NotifyParameters) + +func (f NotifyOptionFunc) Apply(parameters *NotifyParameters) { + f(parameters) +} + +type NotifiyOptionBuilder []NotifyOption + +func (no NotifiyOptionBuilder) Apply(parameters *NotifyParameters) { + for _, opt := range no { + opt.Apply(parameters) + } +} + +func (no NotifiyOptionBuilder) AlwaysSend(alwaysSend bool) NotifiyOptionBuilder { + return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { + parameters.AlwaysSend = alwaysSend + })) +} + +func (no NotifiyOptionBuilder) InitialBackoff(backoff time.Duration) NotifiyOptionBuilder { + return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { + parameters.InitialBackoff = backoff + })) +} + +func (no NotifiyOptionBuilder) MaxBackoff(maxBackoff time.Duration) NotifiyOptionBuilder { + return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { + parameters.MaxBackoff = maxBackoff + })) +} + +// BackoffFormula is a string that represents the formula to calculate the backoff time +// when multiple message with the same key is fired off repeatedly. +// +// It uses CEL (Common Expression Language) syntax. See: https://github.com/google/cel-go +// for the language spec. +// +// Expression must return a duration type. +// +// If invalid cel expression, empty string, or wrong return value, default formula will be used. +// +// Available variables: +// +// - `repeat`: the number of times the notification has been sent. type: int. +// - `last`: the last time the notification was sent. type: timestamp. +// - `prev_backoff`: the backoff value from previous evaluation. 0 if this message is first seen. Type: duration. +// - `initial_backoff`: the initial_backoff value. Type: duration +// - `max_backoff`: the maximum backoff value. Type: duration +// +// Available Non-Standard functions: +// +// // pow multiplies the base with the exponent +// pow(base: double, exp: double) -> double +// +// // mult_dur_double multiplies the duration with the double value +// mult_dur_double(dur: duration, mult: double) -> duration +// +// Example (and also default formula): +// +// mult_dur_double(initial_backoff, pow(1.5, double(repeat))) > max_backoff +// ? max_backoff +// : mult_dur_double(initial_backoff, pow(1.5, double(repeat))) +func (no NotifiyOptionBuilder) BackoffFormula(formula string) NotifiyOptionBuilder { + return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { + parameters.BackoffFormula = formula + })) +} + +func Notify() NotifiyOptionBuilder { + return NotifiyOptionBuilder{} +}