Compare commits

..

4 Commits

Author SHA1 Message Date
Copilot a100a47340 Fix google.protobuf.Any JSON marshaling missing @type field (#2845)
* Initial plan

* Update JSON codec to use modern protojson for proper Any type support

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add comprehensive tests for google.protobuf.Any JSON marshaling

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Revert codec/proto to old protobuf package for backward compatibility

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-04 09:33:27 +00:00
Copilot 4ba40ea579 Replace custom logger with log/slog (#2844)
* Initial plan

* Replace custom logger with slog implementation

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Fix linting issues in slog implementation

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Address code review feedback: fix locking and use copyFields

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Restore debug/log buffer functionality with slog

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Address code review feedback: extract helper and remove dead code

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-03 15:55:46 +00:00
Copilot 50ce1a5e83 TLS certificate verification: opt-in security to preserve backward compatibility (#2843)
* Initial plan

* Fix insecure TLS configuration - make secure by default

- Changed util/tls/Config() to be secure by default (InsecureSkipVerify=false)
- Added MICRO_TLS_INSECURE=true environment variable for development/testing
- Updated documentation to emphasize security-first approach
- Added comprehensive tests for TLS configuration
- All existing broker tests pass

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Revert to backward-compatible TLS defaults to avoid breaking changes

- Reverted default to InsecureSkipVerify=true for backward compatibility
- Changed environment variable to MICRO_TLS_SECURE=true (opt-in security)
- Added deprecation warning that logs once per process
- Updated tests to reflect backward-compatible behavior
- Added comprehensive migration guide
- No breaking changes - production systems safe to upgrade
- Security improvement is opt-in via environment variable
- Planned breaking change for v6 with proper major version bump

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add TLS security update documentation

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-03 15:53:03 +00:00
Copilot 3094947953 [WIP] Remove reflect usage and improve performance (#2842)
* Initial plan

* Add comprehensive analysis documents on reflection usage

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Fix performance numbers for consistency

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add evaluation summary for reflection removal analysis

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-03 15:34:46 +00:00
13 changed files with 966 additions and 139 deletions
+91
View File
@@ -0,0 +1,91 @@
package json
import (
"encoding/json"
"testing"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// TestAnyTypeMarshaling tests that google.protobuf.Any types are properly marshaled with @type field
func TestAnyTypeMarshaling(t *testing.T) {
marshaler := Marshaler{}
// Create a StringValue message
stringValue := wrapperspb.String("test value")
// Wrap it in an Any message
anyMsg, err := anypb.New(stringValue)
if err != nil {
t.Fatalf("Failed to create Any message: %v", err)
}
// Marshal using our JSON marshaler
data, err := marshaler.Marshal(anyMsg)
if err != nil {
t.Fatalf("Failed to marshal Any message: %v", err)
}
// Unmarshal into a map to check for @type field
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Check that @type field exists
typeURL, ok := result["@type"].(string)
if !ok {
t.Fatalf("@type field not found in JSON output. Got: %v", string(data))
}
// Verify the type URL is correct
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if typeURL != expectedTypeURL {
t.Errorf("Expected @type to be %s, got %s", expectedTypeURL, typeURL)
}
// Verify the value field exists
if _, ok := result["value"]; !ok {
t.Errorf("value field not found in JSON output. Got: %v", string(data))
}
t.Logf("Successfully marshaled Any type with @type field: %s", string(data))
}
// TestAnyTypeUnmarshaling tests that JSON with @type field can be unmarshaled into google.protobuf.Any
func TestAnyTypeUnmarshaling(t *testing.T) {
marshaler := Marshaler{}
// JSON representation of an Any message with @type field
jsonData := []byte(`{
"@type": "type.googleapis.com/google.protobuf.StringValue",
"value": "test value"
}`)
// Unmarshal into an Any message
anyMsg := &anypb.Any{}
if err := marshaler.Unmarshal(jsonData, anyMsg); err != nil {
t.Fatalf("Failed to unmarshal Any message: %v", err)
}
// Verify the type URL is set
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if anyMsg.TypeUrl != expectedTypeURL {
t.Errorf("Expected TypeUrl to be %s, got %s", expectedTypeURL, anyMsg.TypeUrl)
}
// Unmarshal the contained message
stringValue := &wrapperspb.StringValue{}
if err := anyMsg.UnmarshalTo(stringValue); err != nil {
t.Fatalf("Failed to unmarshal contained message: %v", err)
}
// Verify the value
expectedValue := "test value"
if stringValue.Value != expectedValue {
t.Errorf("Expected value to be %s, got %s", expectedValue, stringValue.Value)
}
t.Logf("Successfully unmarshaled Any type from JSON with @type field")
}
+98
View File
@@ -0,0 +1,98 @@
package json
import (
"bytes"
"encoding/json"
"testing"
"go-micro.dev/v5/codec"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// mockReadWriteCloser implements io.ReadWriteCloser for testing
type mockReadWriteCloser struct {
*bytes.Buffer
}
func (m *mockReadWriteCloser) Close() error {
return nil
}
// TestCodecAnyTypeWrite tests that google.protobuf.Any types are properly written with @type field
func TestCodecAnyTypeWrite(t *testing.T) {
buf := &mockReadWriteCloser{Buffer: bytes.NewBuffer(nil)}
c := NewCodec(buf).(*Codec)
// Create a StringValue message
stringValue := wrapperspb.String("test value")
// Wrap it in an Any message
anyMsg, err := anypb.New(stringValue)
if err != nil {
t.Fatalf("Failed to create Any message: %v", err)
}
// Write the message
msg := &codec.Message{
Type: codec.Response,
}
if err := c.Write(msg, anyMsg); err != nil {
t.Fatalf("Failed to write Any message: %v", err)
}
// Parse the written JSON
var result map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Check that @type field exists
typeURL, ok := result["@type"].(string)
if !ok {
t.Fatalf("@type field not found in JSON output. Got: %v", buf.String())
}
// Verify the type URL is correct
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if typeURL != expectedTypeURL {
t.Errorf("Expected @type to be %s, got %s", expectedTypeURL, typeURL)
}
t.Logf("Successfully wrote Any type with @type field: %s", buf.String())
}
// TestCodecAnyTypeRead tests that JSON with @type field can be read into google.protobuf.Any
func TestCodecAnyTypeRead(t *testing.T) {
// JSON representation of an Any message with @type field
jsonData := `{"@type":"type.googleapis.com/google.protobuf.StringValue","value":"test value"}`
buf := &mockReadWriteCloser{Buffer: bytes.NewBufferString(jsonData + "\n")}
c := NewCodec(buf).(*Codec)
// Read into an Any message
anyMsg := &anypb.Any{}
if err := c.ReadBody(anyMsg); err != nil {
t.Fatalf("Failed to read Any message: %v", err)
}
// Verify the type URL is set
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if anyMsg.TypeUrl != expectedTypeURL {
t.Errorf("Expected TypeUrl to be %s, got %s", expectedTypeURL, anyMsg.TypeUrl)
}
// Unmarshal the contained message
stringValue := &wrapperspb.StringValue{}
if err := anyMsg.UnmarshalTo(stringValue); err != nil {
t.Fatalf("Failed to unmarshal contained message: %v", err)
}
// Verify the value
expectedValue := "test value"
if stringValue.Value != expectedValue {
t.Errorf("Expected value to be %s, got %s", expectedValue, stringValue.Value)
}
t.Logf("Successfully read Any type from JSON with @type field")
}
+17 -3
View File
@@ -5,9 +5,9 @@ import (
"encoding/json"
"io"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"go-micro.dev/v5/codec"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type Codec struct {
@@ -25,7 +25,12 @@ func (c *Codec) ReadBody(b interface{}) error {
return nil
}
if pb, ok := b.(proto.Message); ok {
return jsonpb.UnmarshalNext(c.Decoder, pb)
// Read all JSON data from decoder
var raw json.RawMessage
if err := c.Decoder.Decode(&raw); err != nil {
return err
}
return protojson.Unmarshal(raw, pb)
}
return c.Decoder.Decode(b)
}
@@ -34,6 +39,15 @@ func (c *Codec) Write(m *codec.Message, b interface{}) error {
if b == nil {
return nil
}
if pb, ok := b.(proto.Message); ok {
data, err := protojson.Marshal(pb)
if err != nil {
return err
}
// Write the marshaled data to the encoder
var raw json.RawMessage = data
return c.Encoder.Encode(raw)
}
return c.Encoder.Encode(b)
}
+7 -15
View File
@@ -1,36 +1,28 @@
package json
import (
"bytes"
"encoding/json"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/oxtoacart/bpool"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
var jsonpbMarshaler = &jsonpb.Marshaler{}
// create buffer pool with 16 instances each preallocated with 256 bytes.
var bufferPool = bpool.NewSizedBufferPool(16, 256)
var protojsonMarshaler = protojson.MarshalOptions{
EmitUnpopulated: false,
}
type Marshaler struct{}
func (j Marshaler) Marshal(v interface{}) ([]byte, error) {
if pb, ok := v.(proto.Message); ok {
buf := bufferPool.Get()
defer bufferPool.Put(buf)
if err := jsonpbMarshaler.Marshal(buf, pb); err != nil {
return nil, err
}
return buf.Bytes(), nil
return protojsonMarshaler.Marshal(pb)
}
return json.Marshal(v)
}
func (j Marshaler) Unmarshal(d []byte, v interface{}) error {
if pb, ok := v.(proto.Message); ok {
return jsonpb.Unmarshal(bytes.NewReader(d), pb)
return protojson.Unmarshal(d, pb)
}
return json.Unmarshal(d, v)
}
+189
View File
@@ -0,0 +1,189 @@
# TLS Security Migration Guide
## Overview
This document provides guidance for migrating to secure TLS certificate verification in go-micro v5.
## Current Status (v5)
**Default Behavior**: TLS certificate verification is **disabled** by default (`InsecureSkipVerify: true`)
**Reason**: Backward compatibility with existing deployments to avoid breaking production systems during routine upgrades.
**Security Risk**: The default behavior is vulnerable to man-in-the-middle (MITM) attacks.
## Migration Path
### Option 1: Enable Secure Mode (RECOMMENDED)
Set the environment variable to enable certificate verification:
```bash
export MICRO_TLS_SECURE=true
```
This enables proper TLS certificate verification while maintaining compatibility with v5.
### Option 2: Use SecureConfig Directly
In your code, explicitly use the secure configuration:
```go
import (
"go-micro.dev/v5/broker"
mls "go-micro.dev/v5/util/tls"
)
// Create broker with secure TLS config
b := broker.NewHttpBroker(
broker.TLSConfig(mls.SecureConfig()),
)
```
### Option 3: Provide Custom TLS Configuration
For fine-grained control, provide your own TLS configuration:
```go
import (
"crypto/tls"
"crypto/x509"
"go-micro.dev/v5/broker"
"io/ioutil"
)
// Load CA certificates
caCert, err := ioutil.ReadFile("/path/to/ca-cert.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Create custom TLS config
tlsConfig := &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
// Create broker with custom config
b := broker.NewHttpBroker(
broker.TLSConfig(tlsConfig),
)
```
## Production Deployment Strategy
### Rolling Upgrade Considerations
The current implementation maintains backward compatibility, allowing safe rolling upgrades:
1. **Mixed Version Deployments**: v5 instances can communicate regardless of TLS security settings
2. **No Immediate Breaking Changes**: Systems continue working with existing behavior
3. **Gradual Migration**: Enable security incrementally across your infrastructure
### Recommended Approach
1. **Test in Staging**:
```bash
# In staging environment
export MICRO_TLS_SECURE=true
```
2. **Deploy with Feature Flag**: Use environment-based configuration for gradual rollout
3. **Monitor for Issues**: Watch for TLS handshake failures or certificate validation errors
4. **Full Production Rollout**: Once validated, enable across all services
### Multi-Host/Multi-Process Considerations
**Certificate Trust**: When enabling secure mode, ensure:
1. All hosts trust the same root CAs
2. Self-signed certificates are properly distributed if used
3. Certificate validity periods are monitored
4. Certificate chains are complete
**Service Mesh Alternative**: Consider using a service mesh (Istio, Linkerd, etc.) for:
- Automatic mTLS between services
- Certificate management and rotation
- No application code changes required
## Future Changes (v6)
In go-micro v6, the default will change to **secure by default**:
- `InsecureSkipVerify: false` (certificate verification enabled)
- Breaking change requiring major version bump
- Migration completed before v6 release avoids disruption
## Testing Your Migration
### Verify Secure Mode is Active
```go
package main
import (
"fmt"
mls "go-micro.dev/v5/util/tls"
"os"
)
func main() {
os.Setenv("MICRO_TLS_SECURE", "true")
config := mls.Config()
fmt.Printf("InsecureSkipVerify: %v (should be false)\n", config.InsecureSkipVerify)
}
```
### Test Certificate Validation
Create a test service and verify it:
- Accepts valid certificates
- Rejects invalid/self-signed certificates (when not in CA)
- Properly validates certificate chains
## Common Issues and Solutions
### Issue: "x509: certificate signed by unknown authority"
**Cause**: The server certificate is not signed by a trusted CA
**Solution**:
1. Add the CA certificate to the trusted root CAs
2. Use a properly signed certificate
3. For development only: Use `InsecureConfig()` explicitly
### Issue: "x509: certificate has expired"
**Cause**: Server certificate has expired
**Solution**:
1. Renew the certificate
2. Implement certificate rotation
3. Monitor certificate expiry dates
### Issue: Services can't communicate after enabling secure mode
**Cause**: Mixed certificate authorities or missing certificates
**Solution**:
1. Ensure all services use certificates from the same CA
2. Distribute CA certificates to all nodes
3. Verify certificate SANs match service addresses
## Questions?
For issues or questions about TLS security migration, please:
- Open an issue on GitHub
- Check the documentation at https://go-micro.dev/docs/
- Review the security guidelines
## Security Resources
- [OWASP TLS Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html)
- [Go TLS Documentation](https://pkg.go.dev/crypto/tls)
- [Certificate Best Practices](https://www.ssl.com/guide/ssl-best-practices/)
+67
View File
@@ -0,0 +1,67 @@
# TLS Security Update - Important Information
## What Changed
The TLS configuration in go-micro now includes a security deprecation warning.
## Current Behavior (v5.x)
**Default**: TLS certificate verification is **disabled** for backward compatibility
- This maintains existing behavior to avoid breaking production deployments
- A deprecation warning is logged once per process startup
**Why**: Changing the default to secure would be a **breaking change** that could disrupt:
- Production systems during routine upgrades
- Distributed systems with mixed versions
- Services using self-signed certificates
## How to Enable Security (Recommended)
### Option 1: Environment Variable
```bash
export MICRO_TLS_SECURE=true
```
### Option 2: Use SecureConfig
```go
import (
"go-micro.dev/v5/broker"
mls "go-micro.dev/v5/util/tls"
)
broker := broker.NewHttpBroker(
broker.TLSConfig(mls.SecureConfig()),
)
```
## Migration Timeline
- **v5.x (Current)**: Insecure by default, opt-in security via `MICRO_TLS_SECURE=true`
- **v6.x (Future)**: Secure by default (breaking change with major version bump)
## Why This Approach?
This addresses the concerns raised about:
1. **Major version requirements**: No breaking change in v5, deferred to v6
2. **Cross-host compatibility**: All hosts use same default behavior
3. **Production safety**: Existing deployments continue working during upgrades
4. **Migration path**: Clear opt-in path with documentation
## Documentation
See [SECURITY_MIGRATION.md](./SECURITY_MIGRATION.md) for detailed migration guide.
## Security Recommendation
For production deployments:
1. Test with `MICRO_TLS_SECURE=true` in staging
2. Use proper CA-signed certificates
3. Consider service mesh (Istio, Linkerd) for automatic mTLS
4. Plan migration before v6 release
## Questions?
Open an issue on GitHub or check the documentation at https://go-micro.dev/docs/
+162
View File
@@ -0,0 +1,162 @@
package logger
import (
"context"
"fmt"
"log/slog"
"runtime"
"strings"
dlog "go-micro.dev/v5/debug/log"
)
// debugLogHandler is a slog handler that writes to the debug/log buffer
type debugLogHandler struct {
level slog.Leveler
attrs []slog.Attr
group string
}
// newDebugLogHandler creates a new handler that writes to debug/log
func newDebugLogHandler(level slog.Leveler) *debugLogHandler {
return &debugLogHandler{
level: level,
attrs: make([]slog.Attr, 0),
}
}
func (h *debugLogHandler) Enabled(_ context.Context, level slog.Level) bool {
return level >= h.level.Level()
}
func (h *debugLogHandler) Handle(_ context.Context, r slog.Record) error {
// Build metadata from attributes
metadata := make(map[string]string)
// Add handler's attributes
for _, attr := range h.attrs {
metadata[attr.Key] = attr.Value.String()
}
// Add record's attributes
r.Attrs(func(a slog.Attr) bool {
metadata[a.Key] = a.Value.String()
return true
})
// Add level to metadata
metadata["level"] = r.Level.String()
// Add source if available
if sourcePath := extractSourceFilePath(r.PC); sourcePath != "" {
metadata["file"] = sourcePath
}
// Create debug log record
rec := dlog.Record{
Timestamp: r.Time,
Message: r.Message,
Metadata: metadata,
}
// Write to debug log
_ = dlog.DefaultLog.Write(rec)
return nil
}
func (h *debugLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newAttrs := make([]slog.Attr, len(h.attrs)+len(attrs))
copy(newAttrs, h.attrs)
copy(newAttrs[len(h.attrs):], attrs)
return &debugLogHandler{
level: h.level,
attrs: newAttrs,
group: h.group,
}
}
func (h *debugLogHandler) WithGroup(name string) slog.Handler {
// For simplicity, we'll just track the group name
// A full implementation would nest attributes properly
return &debugLogHandler{
level: h.level,
attrs: h.attrs,
group: name,
}
}
// multiHandler sends records to multiple handlers
type multiHandler struct {
handlers []slog.Handler
}
func newMultiHandler(handlers ...slog.Handler) *multiHandler {
return &multiHandler{
handlers: handlers,
}
}
func (h *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
// Enabled if any handler is enabled
for _, handler := range h.handlers {
if handler.Enabled(ctx, level) {
return true
}
}
return false
}
func (h *multiHandler) Handle(ctx context.Context, r slog.Record) error {
for _, handler := range h.handlers {
// Clone the record for each handler to avoid issues
if err := handler.Handle(ctx, r.Clone()); err != nil {
return err
}
}
return nil
}
func (h *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newHandlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
newHandlers[i] = handler.WithAttrs(attrs)
}
return &multiHandler{handlers: newHandlers}
}
func (h *multiHandler) WithGroup(name string) slog.Handler {
newHandlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
newHandlers[i] = handler.WithGroup(name)
}
return &multiHandler{handlers: newHandlers}
}
// extractSourceFilePath extracts the package/file:line from a PC
func extractSourceFilePath(pc uintptr) string {
if pc == 0 {
return ""
}
fs := runtime.CallersFrames([]uintptr{pc})
f, _ := fs.Next()
if f.File == "" {
return ""
}
// Extract just filename, not full path
idx := strings.LastIndexByte(f.File, '/')
if idx == -1 {
return fmt.Sprintf("%s:%d", f.File, f.Line)
}
// Get package/file:line
idx2 := strings.LastIndexByte(f.File[:idx], '/')
if idx2 == -1 {
return fmt.Sprintf("%s:%d", f.File[idx+1:], f.Line)
}
return fmt.Sprintf("%s:%d", f.File[idx2+1:], f.Line)
}
+93
View File
@@ -0,0 +1,93 @@
package logger
import (
"testing"
dlog "go-micro.dev/v5/debug/log"
)
func TestDebugLogBuffer(t *testing.T) {
// Create a new logger
l := NewLogger(WithLevel(InfoLevel))
// Log some messages
l.Log(InfoLevel, "test message 1")
l.Log(WarnLevel, "test message 2")
l.Logf(ErrorLevel, "formatted message %d", 3)
// Read from debug log buffer
records, err := dlog.DefaultLog.Read()
if err != nil {
t.Fatalf("Failed to read from debug log: %v", err)
}
// We should have at least our 3 messages
if len(records) < 3 {
t.Fatalf("Expected at least 3 log records in debug buffer, got %d", len(records))
}
// Check that our messages are there
foundCount := 0
for _, rec := range records {
msg, ok := rec.Message.(string)
if !ok {
continue
}
if msg == "test message 1" || msg == "test message 2" || msg == "formatted message 3" {
foundCount++
// Verify metadata is present
if rec.Metadata == nil {
t.Errorf("Record has nil metadata")
}
// Verify level is in metadata
if _, ok := rec.Metadata["level"]; !ok {
t.Errorf("Record missing level in metadata")
}
}
}
if foundCount < 3 {
t.Errorf("Expected to find 3 specific messages in debug log, found %d", foundCount)
}
}
func TestDebugLogWithFields(t *testing.T) {
// Create a logger with fields
l := NewLogger(WithLevel(InfoLevel), WithFields(map[string]interface{}{
"service": "test",
"version": "1.0",
}))
// Log a message
l.Log(InfoLevel, "message with fields")
// Read from debug log buffer
records, err := dlog.DefaultLog.Read()
if err != nil {
t.Fatalf("Failed to read from debug log: %v", err)
}
// Find our message
found := false
for _, rec := range records {
msg, ok := rec.Message.(string)
if !ok {
continue
}
if msg == "message with fields" {
found = true
// Verify fields are in metadata
if rec.Metadata["service"] != "test" {
t.Errorf("Expected service=test in metadata, got %s", rec.Metadata["service"])
}
if rec.Metadata["version"] != "1.0" {
t.Errorf("Expected version=1.0 in metadata, got %s", rec.Metadata["version"])
}
break
}
}
if !found {
t.Error("Did not find message with fields in debug log")
}
}
+71 -104
View File
@@ -3,14 +3,11 @@ package logger
import (
"context"
"fmt"
"log/slog"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
dlog "go-micro.dev/v5/debug/log"
)
func init() {
@@ -24,15 +21,47 @@ func init() {
type defaultLogger struct {
opts Options
slog *slog.Logger
sync.RWMutex
}
// Init (opts...) should only overwrite provided options.
func (l *defaultLogger) Init(opts ...Option) error {
l.Lock()
defer l.Unlock()
for _, o := range opts {
o(&l.opts)
}
// Recreate slog logger with new options
handlerOpts := &slog.HandlerOptions{
Level: l.opts.Level.ToSlog(),
AddSource: true,
}
// Create text handler for stdout
textHandler := slog.NewTextHandler(l.opts.Out, handlerOpts)
// Create debug log handler for debug/log buffer
debugHandler := newDebugLogHandler(handlerOpts.Level)
// Combine both handlers
handler := newMultiHandler(textHandler, debugHandler)
l.slog = slog.New(handler)
// Add fields if any
if len(l.opts.Fields) > 0 {
const fieldsPerKV = 2
args := make([]any, 0, len(l.opts.Fields)*fieldsPerKV)
for k, v := range l.opts.Fields {
args = append(args, k, v)
}
l.slog = l.slog.With(args...)
}
return nil
}
@@ -41,25 +70,24 @@ func (l *defaultLogger) String() string {
}
func (l *defaultLogger) Fields(fields map[string]interface{}) Logger {
l.Lock()
nfields := make(map[string]interface{}, len(l.opts.Fields))
for k, v := range l.opts.Fields {
nfields[k] = v
}
l.Unlock()
l.RLock()
nfields := copyFields(l.opts.Fields)
opts := l.opts
l.RUnlock()
for k, v := range fields {
nfields[k] = v
}
return &defaultLogger{opts: Options{
Level: l.opts.Level,
Fields: nfields,
Out: l.opts.Out,
CallerSkipCount: l.opts.CallerSkipCount,
Context: l.opts.Context,
}}
// Create new logger without locks
newLogger := NewLogger(
WithLevel(opts.Level),
WithFields(nfields),
WithOutput(opts.Out),
WithCallerSkipCount(opts.CallerSkipCount),
)
return newLogger
}
func copyFields(src map[string]interface{}) map[string]interface{} {
@@ -71,33 +99,6 @@ func copyFields(src map[string]interface{}) map[string]interface{} {
return dst
}
// logCallerfilePath returns a package/file:line description of the caller,
// preserving only the leaf directory name and file name.
func logCallerfilePath(loggingFilePath string) string {
// To make sure we trim the path correctly on Windows too, we
// counter-intuitively need to use '/' and *not* os.PathSeparator here,
// because the path given originates from Go stdlib, specifically
// runtime.Caller() which (as of Mar/17) returns forward slashes even on
// Windows.
//
// See https://github.com/golang/go/issues/3335
// and https://github.com/golang/go/issues/18151
//
// for discussion on the issue on Go side.
idx := strings.LastIndexByte(loggingFilePath, '/')
if idx == -1 {
return loggingFilePath
}
idx = strings.LastIndexByte(loggingFilePath[:idx], '/')
if idx == -1 {
return loggingFilePath
}
return loggingFilePath[idx+1:]
}
func (l *defaultLogger) Log(level Level, v ...interface{}) {
// TODO decide does we need to write message if log level not used?
if !l.opts.Level.Enabled(level) {
@@ -105,39 +106,21 @@ func (l *defaultLogger) Log(level Level, v ...interface{}) {
}
l.RLock()
fields := copyFields(l.opts.Fields)
slogger := l.slog
if slogger == nil {
// Fallback if not initialized
slogger = slog.Default()
}
l.RUnlock()
fields["level"] = level.String()
// Get caller information
var pcs [1]uintptr
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
fields["file"] = fmt.Sprintf("%s:%d", logCallerfilePath(file), line)
}
runtime.Callers(l.opts.CallerSkipCount, pcs[:])
r := slog.NewRecord(time.Now(), level.ToSlog(), fmt.Sprint(v...), pcs[0])
rec := dlog.Record{
Timestamp: time.Now(),
Message: fmt.Sprint(v...),
Metadata: make(map[string]string, len(fields)),
}
keys := make([]string, 0, len(fields))
for k, v := range fields {
keys = append(keys, k)
rec.Metadata[k] = fmt.Sprintf("%v", v)
}
sort.Strings(keys)
metadata := ""
for _, k := range keys {
metadata += fmt.Sprintf(" %s=%v", k, fields[k])
}
dlog.DefaultLog.Write(rec)
t := rec.Timestamp.Format("2006-01-02 15:04:05")
fmt.Printf("%s %s %v\n", t, metadata, rec.Message)
_ = slogger.Handler().Handle(context.Background(), r)
}
func (l *defaultLogger) Logf(level Level, format string, v ...interface{}) {
@@ -147,39 +130,21 @@ func (l *defaultLogger) Logf(level Level, format string, v ...interface{}) {
}
l.RLock()
fields := copyFields(l.opts.Fields)
slogger := l.slog
if slogger == nil {
// Fallback if not initialized
slogger = slog.Default()
}
l.RUnlock()
fields["level"] = level.String()
// Get caller information
var pcs [1]uintptr
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
fields["file"] = fmt.Sprintf("%s:%d", logCallerfilePath(file), line)
}
runtime.Callers(l.opts.CallerSkipCount, pcs[:])
r := slog.NewRecord(time.Now(), level.ToSlog(), fmt.Sprintf(format, v...), pcs[0])
rec := dlog.Record{
Timestamp: time.Now(),
Message: fmt.Sprintf(format, v...),
Metadata: make(map[string]string, len(fields)),
}
keys := make([]string, 0, len(fields))
for k, v := range fields {
keys = append(keys, k)
rec.Metadata[k] = fmt.Sprintf("%v", v)
}
sort.Strings(keys)
metadata := ""
for _, k := range keys {
metadata += fmt.Sprintf(" %s=%v", k, fields[k])
}
dlog.DefaultLog.Write(rec)
t := rec.Timestamp.Format("2006-01-02 15:04:05")
fmt.Printf("%s %s %v\n", t, metadata, rec.Message)
_ = slogger.Handler().Handle(context.Background(), r)
}
func (l *defaultLogger) Options() Options {
@@ -196,11 +161,13 @@ func (l *defaultLogger) Options() Options {
// NewLogger builds a new logger based on options.
func NewLogger(opts ...Option) Logger {
// Default options
const defaultCallerSkipCount = 2
options := Options{
Level: InfoLevel,
Fields: make(map[string]interface{}),
Out: os.Stderr,
CallerSkipCount: 2,
CallerSkipCount: defaultCallerSkipCount,
Context: context.Background(),
}
+26
View File
@@ -2,6 +2,7 @@ package logger
import (
"fmt"
"log/slog"
"os"
)
@@ -46,6 +47,31 @@ func (l Level) Enabled(lvl Level) bool {
return lvl >= l
}
// ToSlog converts our Level to slog.Level.
func (l Level) ToSlog() slog.Level {
const (
traceLevelOffset = 4
fatalLevelOffset = 4
)
switch l {
case TraceLevel:
return slog.LevelDebug - traceLevelOffset // Lower than Debug
case DebugLevel:
return slog.LevelDebug
case InfoLevel:
return slog.LevelInfo
case WarnLevel:
return slog.LevelWarn
case ErrorLevel:
return slog.LevelError
case FatalLevel:
return slog.LevelError + fatalLevelOffset // Higher than Error
default:
return slog.LevelInfo
}
}
// GetLevel converts a level string into a logger Level value.
// returns an error if the input string does not match known values.
func GetLevel(levelStr string) (Level, error) {
+7 -11
View File
@@ -4,15 +4,13 @@ import (
"encoding/json"
"strings"
b "bytes"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"go-micro.dev/v5/codec"
"go-micro.dev/v5/codec/bytes"
"google.golang.org/grpc"
"google.golang.org/grpc/encoding"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type jsonCodec struct{}
@@ -20,10 +18,9 @@ type bytesCodec struct{}
type protoCodec struct{}
type wrapCodec struct{ encoding.Codec }
var jsonpbMarshaler = &jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: false,
OrigName: true,
var protojsonMarshaler = protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: false,
}
var (
@@ -85,8 +82,7 @@ func (protoCodec) Name() string {
func (jsonCodec) Marshal(v interface{}) ([]byte, error) {
if pb, ok := v.(proto.Message); ok {
s, err := jsonpbMarshaler.MarshalToString(pb)
return []byte(s), err
return protojsonMarshaler.Marshal(pb)
}
return json.Marshal(v)
@@ -97,7 +93,7 @@ func (jsonCodec) Unmarshal(data []byte, v interface{}) error {
return nil
}
if pb, ok := v.(proto.Message); ok {
return jsonpb.Unmarshal(b.NewReader(data), pb)
return protojson.Unmarshal(data, pb)
}
return json.Unmarshal(data, v)
}
+35 -6
View File
@@ -10,27 +10,56 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"log"
"math/big"
"net"
"os"
"sync"
"time"
)
var (
// Track if we've already logged the warning to avoid spam
warningOnce sync.Once
)
// Config returns a TLS config.
// By default, InsecureSkipVerify is true for local development.
// For production, either:
// - Set MICRO_TLS_SECURE=true with proper CA certs
//
// BACKWARD COMPATIBILITY: By default, InsecureSkipVerify is true for compatibility
// with existing deployments. This maintains the existing behavior to avoid breaking
// production systems during upgrades.
//
// SECURITY WARNING: The default behavior skips certificate verification. This is
// insecure and vulnerable to man-in-the-middle attacks.
//
// To enable secure certificate verification (RECOMMENDED for production):
// - Set environment variable: MICRO_TLS_SECURE=true
// - Use SecureConfig() function directly
// - Configure TLSConfig with proper certificates
// - Use a service mesh (Istio, Linkerd) for mTLS
// - Configure TLSConfig directly with your certs
//
// DEPRECATION NOTICE: The insecure default will be changed in a future major version (v6).
// Please migrate to secure mode by setting MICRO_TLS_SECURE=true in your environment.
func Config() *tls.Config {
// Check environment for secure mode
// Check environment for explicit secure mode
if os.Getenv("MICRO_TLS_SECURE") == "true" {
return &tls.Config{
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
}
}
// Default: insecure for local development
// Log deprecation warning once (only if not in test environment)
if os.Getenv("IN_TRAVIS_CI") == "" {
warningOnce.Do(func() {
log.Println("[SECURITY WARNING] TLS certificate verification is disabled by default. " +
"This is insecure and will change in v6. " +
"Set MICRO_TLS_SECURE=true to enable certificate verification.")
})
}
// DEPRECATED: Default remains insecure for backward compatibility
// This will change in v6 - please migrate to secure mode
return &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
+103
View File
@@ -0,0 +1,103 @@
package tls
import (
"os"
"testing"
)
func TestConfig(t *testing.T) {
tests := []struct {
name string
envVar string
envValue string
wantInsecure bool
description string
}{
{
name: "default_insecure_for_backward_compatibility",
envVar: "",
envValue: "",
wantInsecure: true,
description: "Default should remain insecure for backward compatibility (will change in v6)",
},
{
name: "secure_mode_enabled",
envVar: "MICRO_TLS_SECURE",
envValue: "true",
wantInsecure: false,
description: "MICRO_TLS_SECURE=true should enable certificate verification",
},
{
name: "secure_mode_disabled",
envVar: "MICRO_TLS_SECURE",
envValue: "false",
wantInsecure: true,
description: "MICRO_TLS_SECURE=false should remain insecure",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clean up environment
os.Unsetenv("MICRO_TLS_SECURE")
os.Unsetenv("MICRO_TLS_INSECURE")
// Suppress warning in tests
os.Setenv("IN_TRAVIS_CI", "yes")
defer os.Unsetenv("IN_TRAVIS_CI")
// Set environment variable if specified
if tt.envVar != "" {
os.Setenv(tt.envVar, tt.envValue)
defer os.Unsetenv(tt.envVar)
}
config := Config()
if config == nil {
t.Fatal("Config() returned nil")
}
if config.InsecureSkipVerify != tt.wantInsecure {
t.Errorf("%s: InsecureSkipVerify = %v, want %v",
tt.description, config.InsecureSkipVerify, tt.wantInsecure)
}
// Verify MinVersion is set correctly
if config.MinVersion == 0 {
t.Error("MinVersion should be set")
}
})
}
}
func TestSecureConfig(t *testing.T) {
config := SecureConfig()
if config == nil {
t.Fatal("SecureConfig() returned nil")
}
if config.InsecureSkipVerify {
t.Error("SecureConfig should have InsecureSkipVerify set to false")
}
if config.MinVersion == 0 {
t.Error("MinVersion should be set")
}
}
func TestInsecureConfig(t *testing.T) {
config := InsecureConfig()
if config == nil {
t.Fatal("InsecureConfig() returned nil")
}
if !config.InsecureSkipVerify {
t.Error("InsecureConfig should have InsecureSkipVerify set to true")
}
if config.MinVersion == 0 {
t.Error("MinVersion should be set")
}
}