Skip to content
Merged
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
2 changes: 1 addition & 1 deletion crypto/pem/pem.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func EncodePrivateKey(key any) ([]byte, error) {
)

switch key := key.(type) {
case *ecdsa.PrivateKey, *ed25519.PrivateKey:
case *ecdsa.PrivateKey, ed25519.PrivateKey, *rsa.PrivateKey:
keyBytes, err = x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, err
Expand Down
133 changes: 133 additions & 0 deletions crypto/pem/pem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package pem

import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"strings"
"testing"
)

func TestEncodePrivateKey(t *testing.T) {
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate ECDSA key: %v", err)
}

rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("failed to generate RSA key: %v", err)
}

_, ed25519Key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("failed to generate Ed25519 key: %v", err)
}

tests := []struct {
name string
key any
wantErr bool
errSubstr string
}{
{
name: "ECDSA P-256",
key: ecKey,
},
{
name: "RSA 2048",
key: rsaKey,
},
{
name: "Ed25519",
key: ed25519Key,
},
{
name: "unsupported type",
key: "not a key",
wantErr: true,
errSubstr: "unsupported key type",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
encoded, err := EncodePrivateKey(tt.key)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
if tt.errSubstr != "" && !strings.Contains(err.Error(), tt.errSubstr) {
t.Fatalf("expected error containing %q, got %q", tt.errSubstr, err.Error())
}
return
}

if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(encoded) == 0 {
t.Fatal("encoded output is empty")
}

decoded, err := DecodePEMPrivateKey(encoded)
if err != nil {
t.Fatalf("roundtrip decode failed: %v", err)
}

if !keysEqual(t, tt.key, decoded) {
t.Fatal("roundtrip key does not match original")
}
})
}
}

// keysEqual compares the original key with the decoded signer using
// each key type's Equal method.
func keysEqual(t *testing.T, original any, decoded crypto.Signer) bool {
t.Helper()

switch orig := original.(type) {
case *ecdsa.PrivateKey:
d, ok := decoded.(*ecdsa.PrivateKey)
if !ok {
t.Errorf("decoded key type %T, want *ecdsa.PrivateKey", decoded)
return false
}
return orig.Equal(d)
case *rsa.PrivateKey:
d, ok := decoded.(*rsa.PrivateKey)
if !ok {
t.Errorf("decoded key type %T, want *rsa.PrivateKey", decoded)
return false
}
return orig.Equal(d)
case ed25519.PrivateKey:
d, ok := decoded.(ed25519.PrivateKey)
if !ok {
t.Errorf("decoded key type %T, want ed25519.PrivateKey", decoded)
return false
}
return orig.Equal(d)
default:
t.Errorf("unknown key type %T", original)
return false
}
}
62 changes: 62 additions & 0 deletions logger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ package logger

import (
"fmt"
"io"
"os"
"sync"
)

const (
Expand All @@ -23,6 +26,12 @@ const (
undefinedAppID = ""
)

var (
// logOutputMu protects logOutputFile from concurrent access.
logOutputMu sync.Mutex
logOutputFile *os.File
)

// Options defines the sets of options for Dapr logging.
type Options struct {
// appID is the unique id of Dapr Application
Expand All @@ -33,6 +42,9 @@ type Options struct {

// OutputLevel is the level of logging
OutputLevel string

// OutputFile is the destination file path for logs.
OutputFile string
}

// SetOutputLevel sets the log output level.
Expand Down Expand Up @@ -60,6 +72,11 @@ func (o *Options) AttachCmdFlags(
"log-level",
defaultOutputLevel,
"Options are debug, info, warn, error, or fatal (default info)")
stringVar(
&o.OutputFile,
"log-file",
"",
"Path to a file where logs will be written")
}
if boolVar != nil {
boolVar(
Expand All @@ -76,6 +93,7 @@ func DefaultOptions() Options {
JSONFormatEnabled: defaultJSONOutput,
appID: undefinedAppID,
OutputLevel: defaultOutputLevel,
OutputFile: "",
}
}

Expand All @@ -100,5 +118,49 @@ func ApplyOptionsToLoggers(options *Options) error {
for _, v := range internalLoggers {
v.SetOutputLevel(daprLogLevel)
}

if err := setLogOutput(options.OutputFile, internalLoggers); err != nil {
return err
}

return nil
}

// setLogOutput configures log output destination. If path is non-empty, logs
// are written to the file at that path. If empty, output reverts to stdout.
// The new file is opened before closing the previous one so that loggers are
// never left pointing at a closed file descriptor.
func setLogOutput(path string, loggers map[string]Logger) error {
logOutputMu.Lock()
defer logOutputMu.Unlock()

var (
out io.Writer = os.Stdout
newFile *os.File
)

if path != "" {
var err error

newFile, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return fmt.Errorf("failed to open log file %q: %w", path, err)
}

out = newFile
}

// Switch all loggers to the new output before closing the old file.
for _, v := range loggers {
v.SetOutput(out)
}

// Close the previous log file after loggers have been redirected.
if logOutputFile != nil {
logOutputFile.Close()
}

logOutputFile = newFile

return nil
}
78 changes: 78 additions & 0 deletions logger/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ limitations under the License.
package logger

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -26,6 +28,7 @@ func TestOptions(t *testing.T) {
assert.Equal(t, defaultJSONOutput, o.JSONFormatEnabled)
assert.Equal(t, undefinedAppID, o.appID)
assert.Equal(t, defaultOutputLevel, o.OutputLevel)
assert.Empty(t, o.OutputFile)
})

t.Run("set dapr ID", func(t *testing.T) {
Expand All @@ -40,10 +43,15 @@ func TestOptions(t *testing.T) {
o := DefaultOptions()

logLevelAsserted := false
logFileAsserted := false
testStringVarFn := func(p *string, name string, value string, usage string) {
if name == "log-level" && value == defaultOutputLevel {
logLevelAsserted = true
}

if name == "log-file" && value == "" {
logFileAsserted = true
}
}

logAsJSONAsserted := false
Expand All @@ -57,6 +65,7 @@ func TestOptions(t *testing.T) {

// assert
assert.True(t, logLevelAsserted)
assert.True(t, logFileAsserted)
assert.True(t, logAsJSONAsserted)
})
}
Expand Down Expand Up @@ -92,3 +101,72 @@ func TestApplyOptionsToLoggers(t *testing.T) {
(l.(*daprLogger)).logger.Logger.GetLevel())
}
}

func TestApplyOptionsToLoggersFileOutput(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "dapr.log")

testOptions := Options{
OutputLevel: "debug",
OutputFile: logPath,
}

l := NewLogger("testLoggerFileOutput")

require.NoError(t, ApplyOptionsToLoggers(&testOptions))
t.Cleanup(func() {
// Revert to stdout, which also closes the log file.
require.NoError(t, ApplyOptionsToLoggers(&Options{
OutputLevel: "info",
}))
})

dl, ok := l.(*daprLogger)
require.True(t, ok)
fileOut, ok := dl.logger.Logger.Out.(*os.File)
require.True(t, ok)
assert.Equal(t, logPath, fileOut.Name())

msg := "log-file-test-message"
l.Info(msg)

b, err := os.ReadFile(logPath)
require.NoError(t, err)
assert.Contains(t, string(b), msg)
}

func TestApplyOptionsToLoggersFileOutputReapply(t *testing.T) {
dir := t.TempDir()
logPath1 := filepath.Join(dir, "dapr1.log")
logPath2 := filepath.Join(dir, "dapr2.log")

l := NewLogger("testLoggerReapply")

t.Cleanup(func() {
require.NoError(t, ApplyOptionsToLoggers(&Options{
OutputLevel: "info",
}))
})

// Apply first file output.
require.NoError(t, ApplyOptionsToLoggers(&Options{
OutputLevel: "debug",
OutputFile: logPath1,
}))
l.Info("message-one")

// Re-apply with a different file — should close the first.
require.NoError(t, ApplyOptionsToLoggers(&Options{
OutputLevel: "debug",
OutputFile: logPath2,
}))
l.Info("message-two")

b1, err := os.ReadFile(logPath1)
require.NoError(t, err)
assert.Contains(t, string(b1), "message-one")
assert.NotContains(t, string(b1), "message-two")

b2, err := os.ReadFile(logPath2)
require.NoError(t, err)
assert.Contains(t, string(b2), "message-two")
}
Loading