initialize paraclub-ai-mailer project with core components and configuration

This commit is contained in:
2025-03-01 00:32:27 +01:00
commit 34c35c395f
11 changed files with 848 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# Config file with sensitive information
config.yaml
# Binary
paraclub-ai-mailer
# Log files
*.log
# IDE specific files
.idea/
.vscode/
# OS specific files
.DS_Store
*~

160
README.md Normal file
View File

@@ -0,0 +1,160 @@
# ParaClub AI Mailer
An automated email response system that uses OpenRouter AI API to generate context-aware replies to incoming emails. The system fetches emails via IMAP, gathers context from configured URLs, and generates AI-powered responses that are saved as email drafts.
## Features
- Automatic email fetching via IMAP
- Context gathering from configured URLs
- AI-powered response generation using OpenRouter API
- Draft email creation in IMAP folders
- Configurable polling intervals
- TLS support for secure email communication
- Comprehensive logging system
- Graceful shutdown handling
## Prerequisites
- Go 1.19 or later
- IMAP email account credentials
- OpenRouter API key
- Access to the websites you want to use as context sources
## Installation
1. Clone the repository:
```bash
git clone gitea@git.cloonar.com:Paraclub/ai-mailer.git
cd paraclub-ai-mailer
```
2. Install dependencies:
```bash
go mod download
```
3. Build the application:
```bash
go build -o paraclub-ai-mailer ./cmd/paraclub-ai-mailer
```
## Configuration
The application uses a YAML configuration file (`config.yaml`) with the following structure:
```yaml
imap:
server: "imap.example.com"
port: 993
username: "your-email@example.com"
password: "${IMAP_PASSWORD}" # From environment variable
mailbox_in: "INBOX"
draft_box: "Drafts"
use_tls: true
ai:
openrouter_api_key: "${OPENROUTER_API_KEY}" # From environment variable
model: "anthropic/claude-2"
temperature: 0.7
max_tokens: 2000
context:
urls:
- "https://example.com/faq"
- "https://example.com/about"
- "https://example.com/policies"
polling:
interval: "5m" # Duration format (e.g., 30s, 5m, 1h)
logging:
level: "info" # debug, info, warn, error
file_path: "paraclub-ai-mailer.log"
```
### Environment Variables
The following environment variables need to be set:
- `IMAP_PASSWORD`: Your IMAP account password
- `OPENROUTER_API_KEY`: Your OpenRouter API key
## Usage
1. Set up your configuration file:
```bash
cp config.yaml.example config.yaml
# Edit config.yaml with your settings
```
2. Set required environment variables:
```bash
export IMAP_PASSWORD="your-email-password"
export OPENROUTER_API_KEY="your-openrouter-api-key"
```
3. Run the application:
```bash
./paraclub-ai-mailer
```
Or specify a custom config file path:
```bash
./paraclub-ai-mailer -config /path/to/config.yaml
```
## How It Works
1. The application polls the IMAP server at configured intervals
2. For each unprocessed email:
- Fetches the email content
- Retrieves HTML content from configured URLs
- Sends combined content to OpenRouter AI for response generation
- Saves the AI-generated response as a draft email
- Marks the original email as processed
## Logging
Logs are written to both stdout and the configured log file. Log levels can be set to:
- debug: Detailed debugging information
- info: General operational information
- warn: Warning messages
- error: Error conditions
## Security Considerations
- Use TLS for IMAP connections (set `use_tls: true`)
- Store sensitive credentials in environment variables
- Regularly rotate API keys and passwords
- Monitor log files for unusual activity
## Error Handling
The application implements:
- Automatic retries for transient failures
- Graceful shutdown on system signals
- Comprehensive error logging
- Email processing state tracking to prevent duplicates
## Limitations
- Processes emails sequentially
- HTML content is fetched without following links
- Requires stable internet connection
- API rate limits may apply (OpenRouter)
## Contributing
1. Fork the repository
2. Create a feature branch
3. Commit your changes
4. Push to the branch
5. Create a Pull Request
## License
[Your chosen license]
## Support
[Your support information]

157
Specification.md Normal file
View File

@@ -0,0 +1,157 @@
# Specification for Email Auto-Responder Service in Golang
## 1. Overview
This software is designed to automatically process incoming emails by:
- Fetching emails from an IMAP server on a configurable schedule.
- Fetching HTML content from a set of configurable URLs, without following any links.
- Using the OpenRouter AI API—with a configurable model—to generate a reply that uses both the emails content and the website context.
- Saving the AI-generated reply as a draft in a designated IMAP folder.
The service runs continuously and processes emails sequentially, ensuring that each email is handled only once.
## 2. Functional Requirements
### 2.1 Email Processing
- **Email Fetching:**
- Connect to an IMAP server using provided credentials.
- Poll the server every *x* minutes (configurable via a config file).
- Retrieve new emails from a specified mailbox (e.g., INBOX) that have not yet been processed.
- **Uniqueness Handling:**
- Use IMAPs built-in flags or markers (e.g., a custom flag) to mark emails as processed so that no email is processed more than once.
- **Draft Creation:**
- Generate a reply based on AI output and save it as a draft in the designated IMAP Draft folder.
- Format the draft email as a standard email (similar to Outlook formatting).
### 2.2 Context URL Fetching
- **Configurable URLs:**
- The software will read a list of context URLs from the configuration file.
- Each URLs HTML content will be fetched using an HTTP client.
- The fetched content will be used as context for the AI generation.
- **No Link Following:**
- When retrieving the HTML content, the software should not follow or process any hyperlinks found within the content.
### 2.3 AI Integration
- **Input Composition:**
- Combine the content of the email with the HTML content fetched from the configurable URLs.
- Send this combined input to the OpenRouter AI API.
- **Model Selection:**
- The AI model used for generating the reply should be selectable via a configuration setting.
- The model identifier (or name) is passed as a parameter to the OpenRouter AI API call.
- **Output:**
- Receive the generated reply that addresses the emails content with the provided context.
- Allow configuration of additional AI parameters (e.g., temperature, max tokens) through the config file.
## 3. Non-Functional Requirements
### 3.1 Configuration
- **Config File:**
- All parameters, including IMAP server details, polling interval, list of context URLs, API keys, AI model and parameters, and logging preferences, must be settable via an external configuration file.
- **Security:**
- Secure handling of sensitive data such as IMAP credentials and API keys (e.g., via environment variables or secure configuration management).
- Ensure all network communications (IMAP over TLS and HTTPS for website/API calls) use secure protocols.
### 3.2 Error Handling & Logging
- **Error Handling:**
- Implement robust error handling at every stage: IMAP connection, context URL fetching, AI integration, and draft creation.
- Utilize retry mechanisms with exponential backoff for transient failures (e.g., network issues).
- **Logging:**
- Provide detailed logging with configurable log levels (e.g., info, debug, error).
- Log significant events such as successful email processing, context fetching outcomes, AI responses, errors, and retries.
### 3.3 Concurrency and Performance
- **Sequential Processing:**
- Process emails sequentially to simplify the design, given the expected load.
- **Long-Lived Service:**
- The software should operate continuously, handling graceful startup, shutdown, and configuration reloads as needed.
## 4. System Architecture
### 4.1 Modules/Components
1. **Configuration Manager:**
- Loads and parses the configuration file.
- Provides configuration data (e.g., IMAP details, context URLs, AI parameters, model selection) to other components.
2. **IMAP Client Module:**
- Manages connections to the IMAP server.
- Fetches new emails from the designated mailbox.
- Marks emails as processed using IMAP flags or markers.
- Saves generated replies to the Draft folder.
3. **Context URL Fetcher Module:**
- Reads a list of URLs from the configuration.
- Uses an HTTP client to fetch HTML content for each URL.
- Ensures that no further links are followed during content retrieval.
- Processes and optionally sanitizes the HTML for use as context.
4. **AI Processor Module:**
- Combines the email content with the fetched context HTML.
- Sends the combined content to the OpenRouter AI API with parameters (including the selected model and other settings) as specified in the configuration.
- Receives and parses the AI-generated response.
5. **Processing Controller:**
- Orchestrates the overall workflow.
- Initiates email polling at the configured interval.
- For each unprocessed email, performs:
- Fetching the email.
- Fetching context from all configurable URLs.
- Invoking the AI processor.
- Formatting the AI-generated reply as a standard email.
- Saving the reply as a draft.
- Marking the original email as processed.
6. **Logging and Monitoring Module:**
- Captures and records all operations, errors, and significant events.
- Provides health and status reports for debugging and operational oversight.
### 4.2 Data Flow
1. **Startup:**
- Load configuration and initialize all modules.
- Establish a connection to the IMAP server.
2. **Polling Loop:**
- At every configured interval, poll for new emails.
- For each new email:
1. Retrieve email content.
2. Fetch HTML content from each URL specified in the configuration (without following any links).
3. Combine email content and context HTML.
4. Send the combined data to the OpenRouter AI API, using the selected model.
5. Receive and format the generated reply.
6. Save the reply in the Draft folder.
7. Mark the email as processed.
3. **Error Handling:**
- Log and handle errors at each stage.
- Use retry strategies where applicable to handle transient failures.
## 5. Deployment & Maintenance
- **Deployment:**
- Build the Golang service into a standalone binary.
- Deploy the binary along with its configuration file to the target environment.
- Ensure that secure connections (IMAP over TLS, HTTPS for API and URL fetching) are configured properly.
- **Maintenance:**
- Monitor logs and error reports for issues.
- Update configuration settings as needed without code modifications.
- Implement graceful shutdown and restart mechanisms to avoid processing duplicates.
## 6. Future Considerations
- **Scaling:**
- Although sequential processing is used initially, consider refactoring to support parallel processing if email volume increases.
- **Enhanced Content Processing:**
- Improve HTML content parsing to better extract relevant context or perform additional sanitization if required.
- **Extensibility:**
- Allow dynamic updates to the list of context URLs or integration with other AI models as future enhancements.

27
config.yaml.example Normal file
View File

@@ -0,0 +1,27 @@
imap:
server: "imap.gmail.com" # Example for Gmail
port: 993
username: "your-email@gmail.com"
password: "${IMAP_PASSWORD}" # Will be read from environment variable
mailbox_in: "INBOX"
draft_box: "Drafts"
use_tls: true
ai:
openrouter_api_key: "${OPENROUTER_API_KEY}" # Will be read from environment variable
model: "anthropic/claude-2" # Other options: "openai/gpt-4", "google/palm-2"
temperature: 0.7 # 0.0 to 1.0, lower for more focused responses
max_tokens: 2000 # Adjust based on your needs and model limits
context:
urls:
- "https://your-company.com/faq"
- "https://your-company.com/about"
- "https://your-company.com/support"
polling:
interval: "5m" # Examples: "30s", "1m", "1h"
logging:
level: "info" # Options: "debug", "info", "warn", "error"
file_path: "paraclub-ai-mailer.log"

60
config/config.go Normal file
View File

@@ -0,0 +1,60 @@
package config
import (
"os"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
IMAP IMAPConfig `yaml:"imap"`
AI AIConfig `yaml:"ai"`
Context ContextConfig `yaml:"context"`
Polling PollingConfig `yaml:"polling"`
Logging LoggingConfig `yaml:"logging"`
}
type IMAPConfig struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
MailboxIn string `yaml:"mailbox_in"`
DraftBox string `yaml:"draft_box"`
UseTLS bool `yaml:"use_tls"`
}
type AIConfig struct {
OpenRouterAPIKey string `yaml:"openrouter_api_key"`
Model string `yaml:"model"`
Temperature float32 `yaml:"temperature"`
MaxTokens int `yaml:"max_tokens"`
}
type ContextConfig struct {
URLs []string `yaml:"urls"`
}
type PollingConfig struct {
Interval time.Duration `yaml:"interval"`
}
type LoggingConfig struct {
Level string `yaml:"level"`
FilePath string `yaml:"file_path"`
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}

12
go.mod Normal file
View File

@@ -0,0 +1,12 @@
module paraclub-ai-mailer
go 1.23.5
require (
github.com/emersion/go-imap v1.2.1 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

24
go.sum Normal file
View File

@@ -0,0 +1,24 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

99
internal/ai/ai.go Normal file
View File

@@ -0,0 +1,99 @@
package ai
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"paraclub-ai-mailer/config"
)
type AI struct {
config config.AIConfig
client *http.Client
}
type OpenRouterRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type OpenRouterResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
func New(cfg config.AIConfig) *AI {
return &AI{
config: cfg,
client: &http.Client{},
}
}
func (a *AI) GenerateReply(emailContent string, contextContent map[string]string) (string, error) {
// Prepare context from all URLs
var context string
for url, content := range contextContent {
context += fmt.Sprintf("\nContext from %s:\n%s\n", url, content)
}
// Prepare the system message and user message
systemMsg := "You are a helpful assistant. Use the provided context to help answer the email professionally and accurately."
userMsg := fmt.Sprintf("Using the following context:\n%s\n\nPlease generate a reply for this email:\n%s", context, emailContent)
messages := []Message{
{Role: "system", Content: systemMsg},
{Role: "user", Content: userMsg},
}
reqBody := OpenRouterRequest{
Model: a.config.Model,
Messages: messages,
Temperature: a.config.Temperature,
MaxTokens: a.config.MaxTokens,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.config.OpenRouterAPIKey))
resp, err := a.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("OpenRouter API returned status code: %d", resp.StatusCode)
}
var result OpenRouterResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if len(result.Choices) == 0 {
return "", fmt.Errorf("no response generated")
}
return result.Choices[0].Message.Content, nil
}

View File

@@ -0,0 +1,48 @@
package fetcher
import (
"io"
"net/http"
"time"
)
type Fetcher struct {
client *http.Client
}
func New() *Fetcher {
return &Fetcher{
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (f *Fetcher) FetchContent(url string) (string, error) {
resp, err := f.client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func (f *Fetcher) FetchAllURLs(urls []string) (map[string]string, error) {
results := make(map[string]string)
for _, url := range urls {
content, err := f.FetchContent(url)
if err != nil {
return nil, err
}
results[url] = content
}
return results, nil
}

187
internal/imap/imap.go Normal file
View File

@@ -0,0 +1,187 @@
package imap
import (
"fmt"
"io"
"paraclub-ai-mailer/config"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
)
type IMAPClient struct {
client *client.Client
config config.IMAPConfig
processed map[string]bool
}
// MessageLiteral implements imap.Literal for draft messages
type MessageLiteral struct {
content []byte
}
func (m *MessageLiteral) Read(p []byte) (n int, err error) {
return copy(p, m.content), io.EOF
}
func (m *MessageLiteral) Len() int {
return len(m.content)
}
func New(cfg config.IMAPConfig) (*IMAPClient, error) {
addr := fmt.Sprintf("%s:%d", cfg.Server, cfg.Port)
var c *client.Client
var err error
if cfg.UseTLS {
c, err = client.DialTLS(addr, nil)
} else {
c, err = client.Dial(addr)
}
if err != nil {
return nil, fmt.Errorf("failed to connect to IMAP server: %v", err)
}
if err := c.Login(cfg.Username, cfg.Password); err != nil {
return nil, fmt.Errorf("failed to login: %v", err)
}
return &IMAPClient{
client: c,
config: cfg,
processed: make(map[string]bool),
}, nil
}
type Email struct {
ID string
Subject string
From string
Body string
}
func (ic *IMAPClient) FetchUnprocessedEmails() ([]Email, error) {
_, err := ic.client.Select(ic.config.MailboxIn, false)
if err != nil {
return nil, err
}
criteria := imap.NewSearchCriteria()
criteria.WithoutFlags = []string{"\\Seen", "Processed"}
uids, err := ic.client.Search(criteria)
if err != nil {
return nil, err
}
if len(uids) == 0 {
return nil, nil
}
seqSet := new(imap.SeqSet)
seqSet.AddNum(uids...)
messages := make(chan *imap.Message, 10)
section := &imap.BodySectionName{Peek: true}
done := make(chan error, 1)
go func() {
done <- ic.client.Fetch(seqSet, []imap.FetchItem{section.FetchItem()}, messages)
}()
var emails []Email
for msg := range messages {
if ic.processed[msg.Envelope.MessageId] {
continue
}
r := msg.GetBody(section)
if r == nil {
continue
}
var bodyBuilder strings.Builder
_, err := io.Copy(&bodyBuilder, r)
if err != nil {
continue
}
emails = append(emails, Email{
ID: msg.Envelope.MessageId,
Subject: msg.Envelope.Subject,
From: msg.Envelope.From[0].Address(),
Body: bodyBuilder.String(),
})
ic.processed[msg.Envelope.MessageId] = true
}
if err := <-done; err != nil {
return nil, err
}
return emails, nil
}
func (ic *IMAPClient) SaveDraft(email Email, response string) error {
_, err := ic.client.Select(ic.config.DraftBox, false)
if err != nil {
return err
}
// Format the draft message
draft := fmt.Sprintf("From: %s\r\n"+
"To: %s\r\n"+
"Subject: Re: %s\r\n"+
"Content-Type: text/plain; charset=UTF-8\r\n"+
"\r\n%s",
ic.config.Username,
email.From,
email.Subject,
response)
// Create literal message
literal := &MessageLiteral{content: []byte(draft)}
// Save the draft
flags := []string{"\\Draft"}
if err := ic.client.Append(ic.config.DraftBox, flags, time.Now(), literal); err != nil {
return err
}
return nil
}
func (ic *IMAPClient) MarkAsProcessed(email Email) error {
_, err := ic.client.Select(ic.config.MailboxIn, false)
if err != nil {
return err
}
criteria := imap.NewSearchCriteria()
criteria.Header.Set("Message-Id", email.ID)
uids, err := ic.client.Search(criteria)
if err != nil {
return err
}
if len(uids) == 0 {
return fmt.Errorf("email not found")
}
seqSet := new(imap.SeqSet)
seqSet.AddNum(uids...)
return ic.client.Store(seqSet, imap.FormatFlagsOp(imap.AddFlags, true), []interface{}{"Processed"}, nil)
}
func (ic *IMAPClient) Close() error {
if ic.client != nil {
ic.client.Logout()
}
return nil
}

58
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,58 @@
package logger
import (
"io"
"os"
"github.com/sirupsen/logrus"
)
var log = logrus.New()
func Init(level string, filePath string) error {
// Set log level
lvl, err := logrus.ParseLevel(level)
if err != nil {
return err
}
log.SetLevel(lvl)
// Configure output
if filePath != "" {
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
log.SetOutput(io.MultiWriter(os.Stdout, file))
}
log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
return nil
}
func Info(args ...interface{}) {
log.Info(args...)
}
func Error(args ...interface{}) {
log.Error(args...)
}
func Debug(args ...interface{}) {
log.Debug(args...)
}
func Warn(args ...interface{}) {
log.Warn(args...)
}
func WithField(key string, value interface{}) *logrus.Entry {
return log.WithField(key, value)
}
func WithFields(fields logrus.Fields) *logrus.Entry {
return log.WithFields(fields)
}