Skip to content

Commit 6783f1a

Browse files
committed
feat: add configurabile logging
1 parent 67b706b commit 6783f1a

File tree

8 files changed

+362
-12
lines changed

8 files changed

+362
-12
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The proxy is configured via environment variables. Minimal required env:
1616
- `SUPER_ADMIN_TOKEN`: the Application Service `as_token` from your registration file
1717
- `PROXY_PORT` (optional): port to listen on (default: `8080`)
1818
- `AS_USER_ID` (optional): the user ID of the Application Service bot (default: `@_acrobits_proxy:matrix.example`)
19+
- `LOGLEVEL` (optional): logging verbosity level - `DEBUG`, `INFO`, `WARNING`, `CRITICAL` (default: `INFO`)
1920

2021
Building and running
2122

@@ -28,9 +29,21 @@ export MATRIX_HOMESERVER_URL="https://matrix.your-homeserver-name.com"
2829
export SUPER_ADMIN_TOKEN="YOUR_SECURE_APPLICATION_SERVICE_TOKEN"
2930
export PROXY_PORT=8080
3031
export AS_USER_ID="@_acrobits_proxy:your-homeserver-name.com"
32+
export LOGLEVEL=INFO
3133
./matrix2acrobits
3234
```
3335

36+
### Logging Levels
37+
38+
The `LOGLEVEL` environment variable controls the verbosity of application logs:
39+
40+
- **DEBUG**: Detailed information for diagnosing issues (shows all API calls, mapping lookups, Matrix operations)
41+
- **INFO**: General informational messages (successful operations, server startup) - **Default**
42+
- **WARNING**: Warning messages for potentially problematic situations
43+
- **CRITICAL**: Only critical errors
44+
45+
For debugging mapping and API issues, set `LOGLEVEL=DEBUG` to see detailed trace information.
46+
3447
## Extra info
3548

3649
- [Deploying with NS8](docs/DEPLOY_NS8.md)

agents.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The proxy translates Acrobits' proprietary HTTP API calls for messaging into sta
2121
- **Language:** Golang (1.23+)
2222
- **Web Framework:** Standard Echo (v4) framework
2323
- **Matrix SDK:** `mautrix-go` (recommended)
24+
- **Logging:** `zerolog` for structured logging with configurable levels
2425
- **Testing:** Standard testing package with `testify/assert` and `testify/mock`
2526

2627
---
@@ -53,6 +54,7 @@ The proxy translates Acrobits' proprietary HTTP API calls for messaging into sta
5354

5455
```
5556
cmd/ # Main application entry point
57+
internal/logger/ # Structured logging with configurable levels (DEBUG, INFO, WARNING, CRITICAL)
5658
internal/api/ # HTTP handlers for Acrobits endpoints
5759
internal/matrix/ # Matrix client wrapper and logic
5860
internal/service/ # Business logic (translation between Acrobits <-> Matrix models)
@@ -74,6 +76,7 @@ go build -o matrix2acrobits .
7476
export MATRIX_HOMESERVER_URL="https://matrix.your-homeserver-name.com"
7577
export SUPER_ADMIN_TOKEN="YOUR_SECURE_APPLICATION_SERVICE_TOKEN"
7678
export PROXY_PORT=8080
79+
export LOGLEVEL=INFO # DEBUG, INFO, WARNING, or CRITICAL
7780
./matrix2acrobits
7881
```
7982

api/routes.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/labstack/echo/v4"
9+
"github.com/nethesis/matrix2acrobits/logger"
910
"github.com/nethesis/matrix2acrobits/models"
1011
"github.com/nethesis/matrix2acrobits/service"
1112
)
@@ -29,26 +30,38 @@ type handler struct {
2930
func (h handler) sendMessage(c echo.Context) error {
3031
var req models.SendMessageRequest
3132
if err := c.Bind(&req); err != nil {
33+
logger.Warn().Str("endpoint", "send_message").Err(err).Msg("invalid request payload")
3234
return echo.NewHTTPError(http.StatusBadRequest, "invalid payload")
3335
}
3436

37+
logger.Debug().Str("endpoint", "send_message").Str("from", req.From).Str("to", req.SMSTo).Msg("processing send message request")
38+
3539
resp, err := h.svc.SendMessage(c.Request().Context(), &req)
3640
if err != nil {
41+
logger.Error().Str("endpoint", "send_message").Str("from", req.From).Str("to", req.SMSTo).Err(err).Msg("failed to send message")
3742
return mapServiceError(err)
3843
}
44+
45+
logger.Info().Str("endpoint", "send_message").Str("from", req.From).Str("to", req.SMSTo).Str("sms_id", resp.SMSID).Msg("message sent successfully")
3946
return c.JSON(http.StatusOK, resp)
4047
}
4148

4249
func (h handler) fetchMessages(c echo.Context) error {
4350
var req models.FetchMessagesRequest
4451
if err := c.Bind(&req); err != nil {
52+
logger.Warn().Str("endpoint", "fetch_messages").Err(err).Msg("invalid request payload")
4553
return echo.NewHTTPError(http.StatusBadRequest, "invalid payload")
4654
}
4755

56+
logger.Debug().Str("endpoint", "fetch_messages").Str("username", req.Username).Str("last_id", req.LastID).Msg("processing fetch messages request")
57+
4858
resp, err := h.svc.FetchMessages(c.Request().Context(), &req)
4959
if err != nil {
60+
logger.Error().Str("endpoint", "fetch_messages").Str("username", req.Username).Err(err).Msg("failed to fetch messages")
5061
return mapServiceError(err)
5162
}
63+
64+
logger.Info().Str("endpoint", "fetch_messages").Str("username", req.Username).Int("received", len(resp.ReceivedSMSS)).Int("sent", len(resp.SentSMSS)).Msg("messages fetched successfully")
5265
return c.JSON(http.StatusOK, resp)
5366
}
5467

@@ -59,12 +72,19 @@ func (h handler) postMapping(c echo.Context) error {
5972

6073
var req models.MappingRequest
6174
if err := c.Bind(&req); err != nil {
75+
logger.Warn().Str("endpoint", "post_mapping").Err(err).Msg("invalid request payload")
6276
return echo.NewHTTPError(http.StatusBadRequest, "invalid payload")
6377
}
78+
79+
logger.Debug().Str("endpoint", "post_mapping").Str("sms_number", req.SMSNumber).Str("room_id", req.RoomID).Msg("saving mapping")
80+
6481
resp, err := h.svc.SaveMapping(&req)
6582
if err != nil {
83+
logger.Error().Str("endpoint", "post_mapping").Str("sms_number", req.SMSNumber).Err(err).Msg("failed to save mapping")
6684
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
6785
}
86+
87+
logger.Info().Str("endpoint", "post_mapping").Str("sms_number", req.SMSNumber).Str("room_id", req.RoomID).Msg("mapping saved successfully")
6888
return c.JSON(http.StatusOK, resp)
6989
}
7090

@@ -75,21 +95,30 @@ func (h handler) getMapping(c echo.Context) error {
7595

7696
smsNumber := strings.TrimSpace(c.QueryParam("sms_number"))
7797
if smsNumber == "" {
98+
logger.Debug().Str("endpoint", "get_mapping").Msg("listing all mappings")
7899
// return full mappings list when sms_number is not provided
79100
respList, err := h.svc.ListMappings()
80101
if err != nil {
102+
logger.Error().Str("endpoint", "get_mapping").Err(err).Msg("failed to list mappings")
81103
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
82104
}
105+
logger.Info().Str("endpoint", "get_mapping").Int("count", len(respList)).Msg("listed all mappings")
83106
return c.JSON(http.StatusOK, respList)
84107
}
85108

109+
logger.Debug().Str("endpoint", "get_mapping").Str("sms_number", smsNumber).Msg("looking up mapping")
110+
86111
resp, err := h.svc.LookupMapping(smsNumber)
87112
if err != nil {
88113
if errors.Is(err, service.ErrMappingNotFound) {
114+
logger.Warn().Str("endpoint", "get_mapping").Str("sms_number", smsNumber).Msg("mapping not found")
89115
return echo.NewHTTPError(http.StatusNotFound, err.Error())
90116
}
117+
logger.Error().Str("endpoint", "get_mapping").Str("sms_number", smsNumber).Err(err).Msg("failed to lookup mapping")
91118
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
92119
}
120+
121+
logger.Info().Str("endpoint", "get_mapping").Str("sms_number", smsNumber).Str("room_id", resp.RoomID).Msg("mapping found")
93122
return c.JSON(http.StatusOK, resp)
94123
}
95124

logger/logger.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package logger
2+
3+
import (
4+
"io"
5+
"os"
6+
"strings"
7+
"time"
8+
9+
"github.com/rs/zerolog"
10+
)
11+
12+
var log zerolog.Logger
13+
14+
// Level represents the logging level
15+
type Level string
16+
17+
const (
18+
LevelDebug Level = "DEBUG"
19+
LevelInfo Level = "INFO"
20+
LevelWarning Level = "WARNING"
21+
LevelCritical Level = "CRITICAL"
22+
)
23+
24+
// Init initializes the global logger with the specified level
25+
func Init(level Level) {
26+
var zLevel zerolog.Level
27+
switch strings.ToUpper(string(level)) {
28+
case string(LevelDebug):
29+
zLevel = zerolog.DebugLevel
30+
case string(LevelInfo):
31+
zLevel = zerolog.InfoLevel
32+
case string(LevelWarning):
33+
zLevel = zerolog.WarnLevel
34+
case string(LevelCritical):
35+
zLevel = zerolog.ErrorLevel
36+
default:
37+
zLevel = zerolog.InfoLevel
38+
}
39+
40+
zerolog.SetGlobalLevel(zLevel)
41+
// Configure zerolog for human-readable output
42+
log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).With().Timestamp().Logger()
43+
}
44+
45+
// InitWithWriter initializes the logger with a custom writer (useful for testing)
46+
func InitWithWriter(level Level, w io.Writer) {
47+
var zLevel zerolog.Level
48+
switch strings.ToUpper(string(level)) {
49+
case string(LevelDebug):
50+
zLevel = zerolog.DebugLevel
51+
case string(LevelInfo):
52+
zLevel = zerolog.InfoLevel
53+
case string(LevelWarning):
54+
zLevel = zerolog.WarnLevel
55+
case string(LevelCritical):
56+
zLevel = zerolog.ErrorLevel
57+
default:
58+
zLevel = zerolog.InfoLevel
59+
}
60+
61+
zerolog.SetGlobalLevel(zLevel)
62+
// Configure zerolog for human-readable output
63+
log = zerolog.New(zerolog.ConsoleWriter{Out: w, TimeFormat: time.RFC3339}).With().Timestamp().Logger()
64+
}
65+
66+
// Debug logs a debug message
67+
func Debug() *zerolog.Event {
68+
return log.Debug()
69+
}
70+
71+
// Info logs an info message
72+
func Info() *zerolog.Event {
73+
return log.Info()
74+
}
75+
76+
// Warn logs a warning message
77+
func Warn() *zerolog.Event {
78+
return log.Warn()
79+
}
80+
81+
// Error logs an error message (CRITICAL level)
82+
func Error() *zerolog.Event {
83+
return log.Error()
84+
}
85+
86+
// Fatal logs a fatal message and exits
87+
func Fatal() *zerolog.Event {
88+
return log.Fatal()
89+
}
90+
91+
// With creates a child logger with additional fields
92+
func With() zerolog.Context {
93+
return log.With()
94+
}

logger/logger_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package logger
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestInit(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
level Level
15+
}{
16+
{
17+
name: "Debug level",
18+
level: LevelDebug,
19+
},
20+
{
21+
name: "Info level",
22+
level: LevelInfo,
23+
},
24+
{
25+
name: "Warning level",
26+
level: LevelWarning,
27+
},
28+
{
29+
name: "Critical level",
30+
level: LevelCritical,
31+
},
32+
}
33+
34+
for _, tt := range tests {
35+
t.Run(tt.name, func(t *testing.T) {
36+
// Just ensure it doesn't panic
37+
Init(tt.level)
38+
})
39+
}
40+
}
41+
42+
func TestLogLevels(t *testing.T) {
43+
var buf bytes.Buffer
44+
InitWithWriter(LevelDebug, &buf)
45+
46+
Debug().Msg("debug message")
47+
assert.Contains(t, buf.String(), "debug message")
48+
assert.Contains(t, buf.String(), "DBG")
49+
50+
buf.Reset()
51+
Info().Msg("info message")
52+
assert.Contains(t, buf.String(), "info message")
53+
assert.Contains(t, buf.String(), "INF")
54+
55+
buf.Reset()
56+
Warn().Msg("warning message")
57+
assert.Contains(t, buf.String(), "warning message")
58+
assert.Contains(t, buf.String(), "WRN")
59+
60+
buf.Reset()
61+
Error().Msg("error message")
62+
assert.Contains(t, buf.String(), "error message")
63+
assert.Contains(t, buf.String(), "ERR")
64+
}
65+
66+
func TestLogLevelFiltering(t *testing.T) {
67+
var buf bytes.Buffer
68+
InitWithWriter(LevelWarning, &buf)
69+
70+
// Debug and Info should not be logged at WARNING level
71+
Debug().Msg("debug message")
72+
assert.Empty(t, buf.String())
73+
74+
Info().Msg("info message")
75+
assert.Empty(t, buf.String())
76+
77+
// Warning should be logged
78+
Warn().Msg("warning message")
79+
assert.Contains(t, buf.String(), "warning message")
80+
81+
buf.Reset()
82+
Error().Msg("error message")
83+
assert.Contains(t, buf.String(), "error message")
84+
}
85+
86+
func TestLogWithFields(t *testing.T) {
87+
var buf bytes.Buffer
88+
InitWithWriter(LevelInfo, &buf)
89+
90+
Info().Str("user", "@alice:example.com").Int("count", 42).Msg("user action")
91+
92+
output := buf.String()
93+
assert.Contains(t, output, "user action")
94+
assert.Contains(t, output, "@alice:example.com")
95+
assert.Contains(t, output, "42")
96+
}
97+
98+
func TestDefaultLevel(t *testing.T) {
99+
var buf bytes.Buffer
100+
// Invalid level should default to INFO
101+
InitWithWriter("INVALID", &buf)
102+
103+
Debug().Msg("debug message")
104+
assert.Empty(t, buf.String())
105+
106+
Info().Msg("info message")
107+
assert.Contains(t, buf.String(), "info message")
108+
}
109+
110+
func TestCaseInsensitiveLevel(t *testing.T) {
111+
tests := []struct {
112+
name string
113+
level Level
114+
expected string
115+
}{
116+
{
117+
name: "lowercase debug",
118+
level: Level(strings.ToLower(string(LevelDebug))),
119+
expected: "debug",
120+
},
121+
{
122+
name: "mixed case info",
123+
level: Level("InFo"),
124+
expected: "info",
125+
},
126+
}
127+
128+
for _, tt := range tests {
129+
t.Run(tt.name, func(t *testing.T) {
130+
var buf bytes.Buffer
131+
InitWithWriter(tt.level, &buf)
132+
// Just ensure it doesn't panic
133+
assert.NotPanics(t, func() {
134+
Info().Msg("test")
135+
})
136+
})
137+
}
138+
}

0 commit comments

Comments
 (0)