Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Praposal for logging support for msal-go #535

Open
wants to merge 33 commits into
base: andyohart/managed-identity
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a58420b
Added MD file
4gust Nov 19, 2024
57f484f
Updated callback support in 1.21 as well
4gust Nov 21, 2024
deb5d6e
Updated logging with files
4gust Nov 29, 2024
e855af1
* Pushes changes needed for logging, including removing support for 1…
AndyOHart Dec 19, 2024
0ba0540
* Changes how logger works so that we can create a logger implementat…
AndyOHart Dec 23, 2024
77d735e
* remove unneeded log
AndyOHart Dec 23, 2024
e6aa078
* moves context.Background to internal implementation
AndyOHart Dec 23, 2024
dffa979
* Makes changes so WithLogger is only accessible for 1.21 and above
AndyOHart Jan 6, 2025
aa59b0f
* Updates logic to create default logger instance and remove returnin…
AndyOHart Jan 7, 2025
da84180
* Updates to remove LoggerInterface
AndyOHart Jan 8, 2025
435bbb6
* Adds missing functions to help compile go1.18
AndyOHart Jan 8, 2025
1a2ccbf
* Updates logger to remove LoggerInterface
AndyOHart Jan 8, 2025
bc17181
* Updates packages to use slog instead of logger
AndyOHart Jan 9, 2025
f4639c5
* Updates to use pointer
AndyOHart Jan 9, 2025
fca7a8a
* update documentation
AndyOHart Jan 9, 2025
61b5358
* Removed some uneeded fileds and information
AndyOHart Jan 9, 2025
c9b47f5
* More PR Changes
AndyOHart Jan 13, 2025
6d69a31
Changes to how logger works
AndyOHart Jan 17, 2025
ca09100
Merge remote-tracking branch 'origin/andyohart/managed-identity' into…
AndyOHart Jan 17, 2025
41d9c77
* Adds logger to managed identity
AndyOHart Jan 20, 2025
281d376
* Some small fixes such as extra lines and some comments being removed
AndyOHart Jan 22, 2025
0ef46fe
Update documentation WithLogger
AndyOHart Jan 22, 2025
7232018
Documentation update
AndyOHart Jan 22, 2025
dee0a5d
Address PR comments
AndyOHart Jan 26, 2025
2f88c40
* Add ability to pass WithPiiLogging
AndyOHart Jan 27, 2025
21ab563
* Update documentation
AndyOHart Jan 27, 2025
6a8d621
* Update documentation
AndyOHart Jan 27, 2025
f284171
* Removes separate WithPiiLogging to add bool to WithLogger
AndyOHart Jan 28, 2025
b37530d
Merge remote-tracking branch 'origin/andyohart/managed-identity' into…
AndyOHart Jan 28, 2025
2da41d2
* PR Changes
AndyOHart Feb 4, 2025
72611ca
Updated the file name
4gust Feb 6, 2025
0122e72
* Address some PR Comments
AndyOHart Feb 12, 2025
6d52633
* Use context of the function rather than the logOnce
AndyOHart Feb 13, 2025
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
8 changes: 8 additions & 0 deletions apps/confidential/confidential.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/exported"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/logger"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
Expand Down Expand Up @@ -250,6 +251,7 @@ type clientOptions struct {
capabilities []string
disableInstanceDiscovery, sendX5C bool
httpClient ops.HTTPClient
logger logger.LoggerInterface
}

// Option is an optional argument to New().
Expand Down Expand Up @@ -318,12 +320,17 @@ func New(authority, clientID string, cred Credential, options ...Option) (Client
return Client{}, err
}
autoEnabledRegion := os.Getenv("MSAL_FORCE_REGION")
defaultLogger, err := logger.New(nil)
if err != nil {
return Client{}, err
}
opts := clientOptions{
authority: authority,
// if the caller specified a token provider, it will handle all details of authentication, using Client only as a token cache
disableInstanceDiscovery: cred.tokenProvider != nil,
httpClient: shared.DefaultClient,
azureRegion: autoEnabledRegion,
logger: defaultLogger,
}
for _, o := range options {
o(&opts)
Expand All @@ -345,6 +352,7 @@ func New(authority, clientID string, cred Credential, options ...Option) (Client
}
base.AuthParams.IsConfidentialClient = true

opts.logger.Log(logger.Info, "Created confidential client", logger.Field("clientID", clientID))
return Client{base: base, cred: internalCred}, nil
}

Expand Down
21 changes: 21 additions & 0 deletions apps/confidential/confidential_121_plus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//go:build go1.21

package confidential

import (
"fmt"

"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/logger"
)

//  WithLogger allows for a custom logger to be set.
func WithLogger(l interface{}) Option {
return func(o *clientOptions) {
logInstance, err := logger.New(l)
if err != nil {
fmt.Println("Error creating logger with slog:", err)
return
}
o.logger = logInstance
}
}
36 changes: 36 additions & 0 deletions apps/design/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Logging

GO 1.21 introduces enhanced features like structured logging with `log/slog`.
As part of MSAL GO, we have decided to only support logging using slog from version 1.21.
The reason for this is `log/slog` greatly enhances the debugging and monitoring capabilities compared to the old SDK. This is especially useful in production environments where accurate, traceable logs are crucial for maintaining application health and troubleshooting issues. For versions older than 1.21, the user can specify *nil* to the *New()* function and a no-op is returned

## **Expected Output**

```plaintext
time=2024-12-19T01:25:58.730Z level=INFO msg="This is an info message via slog." username=john_doe age=30
time=2024-12-19T01:25:58.730Z level=ERROR msg="This is an error message via slog." module=user-service retry=3
time=2024-12-19T01:25:58.730Z level=WARN msg="Disk space is low." free_space_mb=100
time=2024-12-19T01:25:58.730Z level=INFO msg="Default log message." module=main
```

## Key Pointers

1. **For Go <= 1.20**:
- User should pass *nil* to `logger.New(nil)`
- No-op is returned and can be handled by user

2. **Full `slog` Support for Go 1.21+**:
- The `logger.go` file leverages the `slog` package, supporting structured logs, multiple log levels (`info`, `error`, `warn`), and fields.

3. **Structured Logging**:
- You can pass key-value pairs using `slog.String`, `slog.Int`, etc., for structured logging, which is handled by `slog` in Go 1.21 and later.

### **Sample**

A sample of the logger can be found in the following location:

```plaintext
microsoft-authentication-library-for-go/apps/devapps
├── logger/
│ ├── logger_sample.go
```
19 changes: 19 additions & 0 deletions apps/internal/logger/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package logger

type Level string

const (
Info Level = "info"
Err Level = "error"
Warn Level = "warn"
Debug Level = "debug"
)

// LoggerInterface defines the methods that a logger should implement
type LoggerInterface interface {
Log(level Level, message string, fields ...any)
}

func New(loggerInterface interface{}) (LoggerInterface, error) {
return NewLogger(loggerInterface)
}
19 changes: 19 additions & 0 deletions apps/internal/logger/logger_121_below.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build go1.18 && !go1.21

package logger

type logger struct{}

func NewLogger(loggerInterface interface{}) (LoggerInterface, error) {
return &logger{}, nil
}

// Log method for Go 1.21+ with full support for structured logging and multiple log levels.
func (a *logger) Log(level Level, message string, fields ...any) {
return
}

// Field creates a slog field for any value
func Field(key string, value any) any {
return ""
}
60 changes: 60 additions & 0 deletions apps/internal/logger/logger_121_plus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//go:build go1.21

package logger

import (
"context"
"fmt"
"log/slog"
)

// logger struct for Go 1.21+ with full `slog` logging support.
type logger struct {
logging *slog.Logger
}

// New creates a new logger instance
func NewLogger(loggerInterface interface{}) (LoggerInterface, error) {
if loggerInterface == nil {
return &logger{logging: nil}, nil
}

if loggerInterface, ok := loggerInterface.(*slog.Logger); ok {
return &logger{logging: loggerInterface}, nil
}

return nil, fmt.Errorf("invalid input for Go 1.21+; expected *slog.Logger")
}

// Log method for Go 1.21+ with full support for structured logging and multiple log levels.
func (a *logger) Log(level Level, message string, fields ...any) {
if a == nil || a.logging == nil {
return
}
var slogLevel slog.Level
switch level {
case Info:
slogLevel = slog.LevelInfo
case Err:
slogLevel = slog.LevelError
case Warn:
slogLevel = slog.LevelWarn
case Debug:
slogLevel = slog.LevelDebug
default:
slogLevel = slog.LevelInfo
}

// Log the entry with the message and fields
a.logging.Log(
context.Background(),
slogLevel,
message,
fields...,
)
}

// Field creates a slog field for any value
func Field(key string, value any) any {
return slog.Any(key, value)
}
58 changes: 58 additions & 0 deletions apps/internal/logger/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//go:build go1.21

package logger

import (
"bytes"
"log/slog"
"testing"
)

func TestLogger_Log_ConsoleOutput(t *testing.T) {
// Capture the console output
var buf bytes.Buffer
handler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelDebug, // Set the log level to Debug to capture all log levels
})

// Create a new logger instance
slogLogger := slog.New(handler)
logInstance, err := New(slogLogger)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

// Log messages
logInstance.Log(Info, "This is an info message via slog.", Field("username", "john_doe"), slog.Int("age", 30))
logInstance.Log(Err, "This is an error message via slog.", slog.String("module", "user-service"), slog.Int("retry", 3))
logInstance.Log(Warn, "This is a warn message via slog.", slog.Int("free_space_mb", 100))
logInstance.Log(Debug, "This is a debug message via slog.", slog.String("module", "main"))

// Check the output
output := buf.String()
expectedMessages := []string{
"This is an info message via slog.",
"This is an error message via slog.",
"This is a warn message via slog.",
"This is a debug message via slog.",
}

for _, msg := range expectedMessages {
if !bytes.Contains([]byte(output), []byte(msg)) {
t.Errorf("expected log message %q not found in output", msg)
}
}
}

// This test is to emulate what happens if the user has a go version < 1.21
// In this case, they will not have access to slog and will need to pass nil to the New function
func TestLogger_New_NilLogger(t *testing.T) {
// Attempt to create a new logger instance with nil slog.Logger
logInstance, err := New(nil)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if logInstance == nil {
t.Fatalf("expected non-nil logInstance, got nil")
}
}
25 changes: 25 additions & 0 deletions apps/tests/devapps/logger/logger_sample.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"fmt"
"log/slog"
"os"

"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/logger"
)

func main() {
// Test for Go 1.21+
handlerOptions := &slog.HandlerOptions{}
slogLogger := slog.New(slog.NewTextHandler(os.Stdout, handlerOptions))
logInstance, err := logger.New(slogLogger)
if err != nil {
fmt.Println("Error creating logger with slog:", err)
return
}

logInstance.Log(logger.Info, "This is a info message via slog.", slog.String("username", "john_doe"), slog.Int("age", 30))
logInstance.Log(logger.Err, "This is a error message via slog.", slog.String("module", "user-service"), slog.Int("retry", 3))
logInstance.Log(logger.Warn, "This is a warn message via slog.", slog.Int("free_space_mb", 100))
logInstance.Log(logger.Debug, "This is a debug message via slog.", slog.String("module", "main"))
}
Loading