Skip to content

Commit 475f62f

Browse files
committed
Support JSON logging format in operator
1 parent 27f03e3 commit 475f62f

9 files changed

Lines changed: 461 additions & 13 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ zed --insecure --endpoint=localhost:50051 --token=averysecretpresharedkey schema
8080

8181
## Where To Go From Here
8282

83-
- Check out the [examples](examples) directory to see how to configure `SpiceDBCluster` for production, including datastore backends, TLS, and Ingress.
83+
- Check out the [examples](examples) directory to see how to configure `SpiceDBCluster` for production, including datastore backends, TLS, Ingress, and operator features like JSON logging.
8484
- Learn how to use SpiceDB via the [docs](https://docs.authzed.com/) and [playground](https://play.authzed.com/).
8585
- Ask questions and join the community in [discord](https://authzed.com/discord).
8686

examples/json-logging/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# JSON Logging for SpiceDB Operator
2+
3+
This example demonstrates how to configure the SpiceDB Operator to output logs in JSON format, which is useful for integration with log aggregation systems like ELK (Elasticsearch, Logstash, Kibana) stack, Splunk, or other structured logging solutions.
4+
5+
## Overview
6+
7+
By default, the SpiceDB Operator uses a text-based log format. However, for production environments where logs need to be parsed and analyzed by log aggregation systems, JSON format is often preferred.
8+
9+
## Configuration
10+
11+
### Using the Command Line Flag
12+
13+
The simplest way to enable JSON logging is by using the `--log-format` flag when starting the operator:
14+
15+
```bash
16+
spicedb-operator run --log-format=json
17+
```
18+
19+
### In Kubernetes Deployment
20+
21+
To enable JSON logging in a Kubernetes deployment, modify the operator's deployment manifest:
22+
23+
```yaml
24+
apiVersion: apps/v1
25+
kind: Deployment
26+
metadata:
27+
name: spicedb-operator
28+
namespace: spicedb-operator
29+
spec:
30+
template:
31+
spec:
32+
containers:
33+
- name: spicedb-operator
34+
image: authzed/spicedb-operator:latest
35+
args:
36+
- "run"
37+
- "--log-format=json"
38+
# ... other flags
39+
```
40+
41+
## Log Format Comparison
42+
43+
### Text Format (Default)
44+
```
45+
2024-01-15T10:30:45.123Z INFO controller Reconciling SpiceDBCluster {"namespace": "default", "name": "my-spicedb"}
46+
2024-01-15T10:30:45.456Z INFO controller Created deployment {"namespace": "default", "name": "my-spicedb-spicedb"}
47+
```
48+
49+
### JSON Format
50+
```json
51+
{"level":"info","ts":"2024-01-15T10:30:45.123Z","logger":"controller","msg":"Reconciling SpiceDBCluster","namespace":"default","name":"my-spicedb"}
52+
{"level":"info","ts":"2024-01-15T10:30:45.456Z","logger":"controller","msg":"Created deployment","namespace":"default","name":"my-spicedb-spicedb"}
53+
```
54+
55+
## Integration with ELK Stack
56+
57+
When using JSON logging with the ELK stack:
58+
59+
1. **Filebeat Configuration**: Configure Filebeat to read the operator logs:
60+
```yaml
61+
filebeat.inputs:
62+
- type: container
63+
paths:
64+
- /var/log/containers/spicedb-operator-*.log
65+
processors:
66+
- decode_json_fields:
67+
fields: ["message"]
68+
target: ""
69+
overwrite_keys: true
70+
```
71+
72+
2. **Logstash Pipeline**: No special parsing is needed as the logs are already structured.
73+
74+
3. **Kibana**: You can directly query and visualize the structured log fields.
75+
76+
## Benefits
77+
78+
- **Structured Data**: Each log entry is a valid JSON object with consistent fields
79+
- **Easy Parsing**: No need for complex regular expressions to parse log entries
80+
- **Better Search**: Log aggregation systems can index individual fields for faster searching
81+
- **Standardized Format**: Compatible with most modern logging infrastructure
82+
- **Machine-Readable**: Easier to process logs programmatically for alerting or analysis
83+
84+
## Complete Example
85+
86+
See the [operator-with-json-logging.yaml](operator-with-json-logging.yaml) file for a complete example of deploying the SpiceDB Operator with JSON logging enabled.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Example deployment of SpiceDB Operator with JSON logging enabled
2+
#
3+
# This manifest shows how to configure the operator to output logs in JSON format
4+
# for better integration with log aggregation systems like ELK stack.
5+
6+
apiVersion: v1
7+
kind: Namespace
8+
metadata:
9+
name: spicedb-operator
10+
---
11+
apiVersion: v1
12+
kind: ServiceAccount
13+
metadata:
14+
name: spicedb-operator
15+
namespace: spicedb-operator
16+
---
17+
apiVersion: rbac.authorization.k8s.io/v1
18+
kind: ClusterRole
19+
metadata:
20+
name: spicedb-operator
21+
rules:
22+
- apiGroups: [""]
23+
resources: ["namespaces", "secrets", "services", "serviceaccounts", "endpoints", "events", "configmaps", "pods"]
24+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
25+
- apiGroups: ["apps"]
26+
resources: ["deployments", "replicasets", "statefulsets"]
27+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
28+
- apiGroups: ["batch"]
29+
resources: ["jobs"]
30+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
31+
- apiGroups: ["policy"]
32+
resources: ["poddisruptionbudgets"]
33+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
34+
- apiGroups: ["rbac.authorization.k8s.io"]
35+
resources: ["roles", "rolebindings"]
36+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
37+
- apiGroups: ["authzed.com"]
38+
resources: ["spicedbclusters"]
39+
verbs: ["get", "list", "watch", "update", "patch"]
40+
- apiGroups: ["authzed.com"]
41+
resources: ["spicedbclusters/status"]
42+
verbs: ["get", "update", "patch"]
43+
---
44+
apiVersion: rbac.authorization.k8s.io/v1
45+
kind: ClusterRoleBinding
46+
metadata:
47+
name: spicedb-operator
48+
roleRef:
49+
apiGroup: rbac.authorization.k8s.io
50+
kind: ClusterRole
51+
name: spicedb-operator
52+
subjects:
53+
- kind: ServiceAccount
54+
name: spicedb-operator
55+
namespace: spicedb-operator
56+
---
57+
apiVersion: apps/v1
58+
kind: Deployment
59+
metadata:
60+
name: spicedb-operator
61+
namespace: spicedb-operator
62+
labels:
63+
app: spicedb-operator
64+
spec:
65+
replicas: 1
66+
selector:
67+
matchLabels:
68+
app: spicedb-operator
69+
template:
70+
metadata:
71+
labels:
72+
app: spicedb-operator
73+
spec:
74+
serviceAccountName: spicedb-operator
75+
containers:
76+
- name: spicedb-operator
77+
image: authzed/spicedb-operator:latest
78+
args:
79+
- "run"
80+
- "--log-format=json" # Enable JSON logging
81+
- "--debug-address=:8080"
82+
env:
83+
- name: POD_NAMESPACE
84+
valueFrom:
85+
fieldRef:
86+
fieldPath: metadata.namespace
87+
ports:
88+
- name: metrics
89+
containerPort: 8080
90+
protocol: TCP
91+
livenessProbe:
92+
httpGet:
93+
path: /healthz
94+
port: 8080
95+
initialDelaySeconds: 15
96+
periodSeconds: 20
97+
readinessProbe:
98+
httpGet:
99+
path: /healthz
100+
port: 8080
101+
initialDelaySeconds: 5
102+
periodSeconds: 10
103+
resources:
104+
limits:
105+
memory: "256Mi"
106+
cpu: "500m"
107+
requests:
108+
memory: "128Mi"
109+
cpu: "100m"
110+
---
111+
# Optional: Service to expose metrics endpoint
112+
apiVersion: v1
113+
kind: Service
114+
metadata:
115+
name: spicedb-operator-metrics
116+
namespace: spicedb-operator
117+
labels:
118+
app: spicedb-operator
119+
spec:
120+
ports:
121+
- name: metrics
122+
port: 8080
123+
targetPort: 8080
124+
selector:
125+
app: spicedb-operator
126+
---
127+
# Optional: ServiceMonitor for Prometheus Operator
128+
apiVersion: monitoring.coreos.com/v1
129+
kind: ServiceMonitor
130+
metadata:
131+
name: spicedb-operator
132+
namespace: spicedb-operator
133+
labels:
134+
app: spicedb-operator
135+
spec:
136+
selector:
137+
matchLabels:
138+
app: spicedb-operator
139+
endpoints:
140+
- port: metrics
141+
path: /metrics
142+
interval: 30s

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ require (
1010
github.com/evanphx/json-patch v5.9.11+incompatible
1111
github.com/fatih/camelcase v1.0.0
1212
github.com/go-logr/logr v1.4.2
13+
github.com/go-logr/zapr v1.3.0
1314
github.com/gosimple/slug v1.15.0
1415
github.com/jzelinskie/stringz v0.0.3
1516
github.com/magefile/mage v1.15.0
1617
github.com/samber/lo v1.49.1
1718
github.com/spf13/cobra v1.9.1
1819
github.com/stretchr/testify v1.10.0
1920
go.uber.org/atomic v1.11.0
21+
go.uber.org/zap v1.27.0
2022
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
2123
k8s.io/api v0.34.0-alpha.2
2224
k8s.io/apimachinery v0.34.0-alpha.2
@@ -120,7 +122,6 @@ require (
120122
go.opentelemetry.io/otel/trace v1.35.0 // indirect
121123
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
122124
go.uber.org/multierr v1.11.0 // indirect
123-
go.uber.org/zap v1.27.0 // indirect
124125
go.yaml.in/yaml/v2 v2.4.2 // indirect
125126
go.yaml.in/yaml/v3 v3.0.3 // indirect
126127
golang.org/x/crypto v0.38.0 // indirect

pkg/cmd/run/run.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ package run
22

33
import (
44
"context"
5+
"fmt"
56

7+
"github.com/go-logr/logr"
8+
"github.com/go-logr/zapr"
69
"github.com/spf13/cobra"
10+
"go.uber.org/zap"
711
corev1 "k8s.io/api/core/v1"
812
"k8s.io/apimachinery/pkg/labels"
913
"k8s.io/apimachinery/pkg/util/errors"
@@ -48,6 +52,9 @@ type Options struct {
4852
MetricNamespace string
4953

5054
WatchNamespaces []string
55+
56+
// LogFormat specifies the log format: "text" or "json"
57+
LogFormat string
5158
}
5259

5360
// RecommendedOptions builds a new options config with default values
@@ -57,6 +64,7 @@ func RecommendedOptions() *Options {
5764
DebugFlags: ctrlmanageropts.RecommendedDebuggingOptions(),
5865
DebugAddress: ":8080",
5966
MetricNamespace: "spicedb_operator",
67+
LogFormat: "text",
6068
}
6169
}
6270

@@ -89,6 +97,7 @@ func NewCmdRun(o *Options) *cobra.Command {
8997
globalflag.AddGlobalFlags(globalFlags, cmd.Name())
9098
globalFlags.StringVar(&o.OperatorConfigPath, "config", "", "set a path to the operator's config file (configure registries, image tags, etc)")
9199
globalFlags.StringVar(&o.BaseImage, "base-image", "", "default base image for SpiceDB containers")
100+
globalFlags.StringVar(&o.LogFormat, "log-format", o.LogFormat, "log format: text or json")
92101

93102
for _, f := range namedFlagSets.FlagSets {
94103
cmd.Flags().AddFlagSet(f)
@@ -102,7 +111,15 @@ func NewCmdRun(o *Options) *cobra.Command {
102111

103112
// Validate checks the set of flags provided by the user.
104113
func (o *Options) Validate() error {
105-
return errors.NewAggregate(o.DebugFlags.Validate())
114+
validationErrors := []error{}
115+
116+
// Allow empty string to use default
117+
if o.LogFormat != "" && o.LogFormat != "text" && o.LogFormat != "json" {
118+
validationErrors = append(validationErrors, fmt.Errorf("invalid log format %q: must be 'text' or 'json'", o.LogFormat))
119+
}
120+
121+
validationErrors = append(validationErrors, o.DebugFlags.Validate()...)
122+
return errors.NewAggregate(validationErrors)
106123
}
107124

108125
// Run performs the apply operation.
@@ -113,7 +130,27 @@ func (o *Options) Run(ctx context.Context, f cmdutil.Factory) error {
113130
}
114131
DisableClientRateLimits(restConfig)
115132

116-
logger := textlogger.NewLogger(textlogger.NewConfig())
133+
// Configure logger based on format
134+
// Use default if empty
135+
logFormat := o.LogFormat
136+
if logFormat == "" {
137+
logFormat = "text"
138+
}
139+
140+
var logger logr.Logger
141+
if logFormat == "json" {
142+
// Use zap with JSON encoder
143+
zapConfig := zap.NewProductionConfig()
144+
zapConfig.DisableStacktrace = true
145+
zapLog, err := zapConfig.Build()
146+
if err != nil {
147+
return fmt.Errorf("failed to create JSON logger: %w", err)
148+
}
149+
logger = zapr.NewLogger(zapLog)
150+
} else {
151+
// Use default text logger
152+
logger = textlogger.NewLogger(textlogger.NewConfig())
153+
}
117154

118155
dclient, err := dynamic.NewForConfig(restConfig)
119156
if err != nil {
@@ -155,7 +192,7 @@ func (o *Options) Run(ctx context.Context, f cmdutil.Factory) error {
155192
controllers = append(controllers, staticSpiceDBController)
156193
}
157194

158-
ctrl, err := controller.NewController(ctx, registry, dclient, kclient, resources, o.OperatorConfigPath, o.BaseImage, broadcaster, o.WatchNamespaces)
195+
ctrl, err := controller.NewController(ctx, logger, registry, dclient, kclient, resources, o.OperatorConfigPath, o.BaseImage, broadcaster, o.WatchNamespaces)
159196
if err != nil {
160197
return err
161198
}

0 commit comments

Comments
 (0)