Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ jobs:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '1.20.6'
go-version: '1.21'

- name: Vet
run: go vet ./...
Expand Down
46 changes: 24 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
# slacklogger

This client is used to send messages using a slack webhook url.
This lib can be used to easily send messages to slack channels via the Slack Web API.

## Installation

```shell
go get github.com/Clarilab/slacklogger/v2
go get github.com/Clarilab/slacklogger/v3
```

## Importing

```go
import "github.com/Clarilab/slacklogger/v2"
import "github.com/Clarilab/slacklogger/v3"
```

## Examples

### Logging with an instanced logger
## Requirements
A [**Slack-App**](https://api.slack.com/docs/apps) installed to the desired **Slack-Workspace** with correct permissions/scopes set.

```go
webhookURL := "https://hooks.slack.com/..."
environment := "development"
isDebug := false
Minimum required permission/scope is: **chat:write**

slacker := slacklogger.NewSlackLogger(webhookURL, environment, isDebug)
For more information checkout the [Slack Apps Quickstart Guide](https://api.slack.com/quickstart).

slacker.Log("Something weird")
## Authorization
When using a proxy like [**Slack-Proxy**](https://github.com/fortio/slack-proxy), the authorization needs to be setup in the proxy and is not needed here.

// this will result in: env=development Something weird
```
Otherwise the [**Slack-Apps**](https://api.slack.com/docs/apps) **OAuth-Token** needs to be provided via the WithAuthorization() option.

### Logging without an instanced logger
## Examples

```go
webhookURL := "https://hooks.slack.com/..."
environment := ""
isDebug := false
message := "Hello World!"
url := "https://<workspace-name>.slack.com/api/chat.postMessage"
token := "slack-token"
channel := "log-channel"
environment := "development"



slacklogger.LogWithURL(message, webhookURL, environment, isDebug)
slacker := slacklogger.NewSlackLogger(url, channel, slacklogger.WithEnvironment(environment), slacklogger.WithAuthorization(token))

slacker.Log("Something weird")

// this will result in: Hello World!
// this will result in:
// environment: development
//
// Something weird
```

If isDebug is set to true, it will print to stdout instead.
If the UseDebug option is used, it will log to stdout instead using the log/slog logger.
98 changes: 36 additions & 62 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,58 @@ package slacklogger

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)

type Field struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}

type Action struct {
Type string `json:"type"`
Text string `json:"text"`
Url string `json:"url"`
Style string `json:"style"`
}

type Attachment struct {
Fallback *string `json:"fallback"`
Color *string `json:"color"`
PreText *string `json:"pretext"`
AuthorName *string `json:"author_name"`
AuthorLink *string `json:"author_link"`
AuthorIcon *string `json:"author_icon"`
Title *string `json:"title"`
TitleLink *string `json:"title_link"`
Text *string `json:"text"`
ImageUrl *string `json:"image_url"`
Fields []*Field `json:"fields"`
Footer *string `json:"footer"`
FooterIcon *string `json:"footer_icon"`
Timestamp *int64 `json:"ts"`
MarkdownIn *[]string `json:"mrkdwn_in"`
Actions []*Action `json:"actions"`
CallbackID *string `json:"callback_id"`
ThumbnailUrl *string `json:"thumb_url"`
}

// Payload is the payload send to slack.
type Payload struct {
Parse string `json:"parse,omitempty"`
Username string `json:"username,omitempty"`
IconUrl string `json:"icon_url,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
LinkNames string `json:"link_names,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
UnfurlLinks bool `json:"unfurl_links,omitempty"`
UnfurlMedia bool `json:"unfurl_media,omitempty"`
Markdown bool `json:"mrkdwn,omitempty"`
}

func (attachment *Attachment) AddField(field Field) *Attachment {
attachment.Fields = append(attachment.Fields, &field)
return attachment
Channel string `json:"channel"` // required
Text string `json:"text"`
AsUser bool `json:"as_user"`
Username string `json:"username,omitempty"`
IconURL string `json:"icon_url,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
ThreadTS string `json:"thread_ts,omitempty"`
Parse string `json:"parse,omitempty"`
LinkNames bool `json:"link_names,omitempty"`
Blocks json.RawMessage `json:"blocks,omitempty"` // JSON serialized array of blocks
}

func (attachment *Attachment) AddAction(action Action) *Attachment {
attachment.Actions = append(attachment.Actions, &action)
return attachment
}
func (l *SlackLogger) send(ctx context.Context, payload *Payload) error {
const (
errMessage = "failed to send to slack: %w"
headerContentType = "Content-Type"
headerAuthorization = "Authorization"
mimeJSON = "application/json; charset=utf-8"
tokenPrefix = "Bearer "
)

var ErrFailedToMarshalJSON = fmt.Errorf("failed to marshal payload")
payloadBytes, err := json.Marshal(&payload)
if err != nil {
return fmt.Errorf(errMessage, err)
}

func Send(webhookUrl string, payload Payload) error {
jsonBytes, err := json.Marshal(&payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, l.url, bytes.NewBuffer(payloadBytes))
if err != nil {
return ErrFailedToMarshalJSON
return fmt.Errorf(errMessage, err)
}

req.Header.Set(headerContentType, mimeJSON)

if l.token != "" {
req.Header.Set(headerAuthorization, tokenPrefix+l.token)
}

resp, err := http.Post(webhookUrl, "application/json", bytes.NewBuffer(jsonBytes))
resp, err := l.client.Do(req)
if err != nil {
return err
return fmt.Errorf(errMessage, err)
}

if resp.StatusCode >= 400 {
return error(fmt.Errorf("Error sending msg. Status: %v", resp.Status))
if resp.StatusCode >= http.StatusBadRequest {
return fmt.Errorf(errMessage, newSlackError(resp.Status))
}

return nil
Expand Down
15 changes: 15 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package slacklogger

func newSlackError(status string) error {
return &SlackError{status: status}
}

// SlackError occurs when slack responded with an error status code.
type SlackError struct {
status string
}

// Error implements the error interface.
func (e *SlackError) Error() string {
return "error sending to slack. status: " + e.status
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/Clarilab/slacklogger/v2
module github.com/Clarilab/slacklogger/v3

go 1.20
go 1.21
28 changes: 28 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package slacklogger

// Option is an option func for creating a new SlackLogger.
type Option func(*SlackLogger)

// WithAuthorization sets the authorization token.
func WithAuthorization(token string) Option {
return func(sl *SlackLogger) {
sl.token = token
}
}

// UseDebug is an option for creating a new SlackLogger.
// When used the logger is NOT logging to slack, instead its logging to stdout using log/slog.
// Can be useful for tests.
func UseDebug() Option {
return func(sl *SlackLogger) {
sl.isDebug = true
}
}

// WithEnvironment is an option for creating a new SlackLogger.
// When used the given environment is added before the message.
func WithEnvironment(env string) Option {
return func(sl *SlackLogger) {
sl.environment = env
}
}
Comment on lines +22 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will man das hier vielleicht einfach zu einem "WithPrefix" oder so machen? 😄
Dann kann man auch evtl multiple prefixes machen

78 changes: 59 additions & 19 deletions slacklogger.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,87 @@
package slacklogger

import (
"context"
"fmt"
"net/http"

"log/slog"
)

// SlackLogger provides functionality to send messages to slack.
type SlackLogger struct {
webhookURL string
client *http.Client
url string
token string
channel string
environment string
isDebug bool
}

// NewSlackLogger returns a new instance of SlackLogger
func NewSlackLogger(webhookURL, environment string, isDebug bool) *SlackLogger {
return &SlackLogger{
webhookURL: webhookURL,
environment: environment,
isDebug: isDebug,
// NewSlackLogger creates a new instance of SlackLogger.
func NewSlackLogger(url, channel string, options ...Option) *SlackLogger {
sl := SlackLogger{
client: new(http.Client),
url: url,
channel: channel,
}

for i := range options {
options[i](&sl)
}

return &sl
}

func (logger *SlackLogger) Log(message string) {
LogWithURL(message, logger.webhookURL, logger.environment, logger.isDebug)
// Log sends a simple message to slack.
func (l *SlackLogger) Log(message string) {
l.log(context.Background(), message)
}

func (logger *SlackLogger) Write(message []byte) (int, error) {
logger.Log(string(message))
// LogContext sends a simple message to slack with the given context.Context.
func (l *SlackLogger) LogContext(ctx context.Context, message string) {
l.log(ctx, message)
}

// Write implements the io.Writer interface.
func (l *SlackLogger) Write(message []byte) (int, error) {
l.Log(string(message))

return len(message), nil
}

func LogWithURL(message, url, env string, isDebug bool) {
if env != "" {
message = fmt.Sprintf("env=%s, %s", env, message)
// Send sends the given payload to slack.
func (l *SlackLogger) Send(ctx context.Context, payload *Payload) {
const errMessage = "error while logging to slack"

if err := l.send(ctx, payload); err != nil {
slog.Error(errMessage, ErrorAttr(err))
}
}

func (l *SlackLogger) log(ctx context.Context, message string) {
const (
errMessage = "error while logging to slack"
envPrefixFormat = "environment: %s\n\n, %s"
debugMessage = "pretending to log to slack"
)

if l.environment != "" {
message = fmt.Sprintf(envPrefixFormat, l.environment, message)
}

if isDebug {
fmt.Println("Debug: Logging to Slack: " + message)
if l.isDebug {
slog.Debug(debugMessage, MessageAttr(message))

return
}

err := Send(url, Payload{Text: message})
if err != nil {
fmt.Printf("Error while logging to Slack: %s\nOriginal message was: %s\n", err, message)
payload := Payload{
Channel: l.channel,
Text: message,
}

if err := l.send(ctx, &payload); err != nil {
slog.Error(errMessage, ErrorAttr(err))
}
}
15 changes: 10 additions & 5 deletions slacklogger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ import (
"os"
"testing"

"github.com/Clarilab/slacklogger/v2"
"github.com/Clarilab/slacklogger/v3"
)

func Test_Log(t *testing.T) {
if testing.Short() {
t.Skip()
}

webhookURL := os.Getenv("WEBHOOK_URL")
if webhookURL == "" {
t.Fatal("webhook url is not set")
url := os.Getenv("SLACK_URL")
if url == "" {
t.Fatal("slack url is not set")
}

logger := slacklogger.NewSlackLogger(webhookURL, "dev", false)
token := os.Getenv("SLACK_TOKEN")
if token == "" {
t.Fatal("slack token is not set")
}

logger := slacklogger.NewSlackLogger(url, "logs-test", slacklogger.WithAuthorization(token))
logger.Log("test message")
}
Loading
Loading