Repository: nginx/kubernetes-ingress

Issue:
[Bug]: logFormat=json creates events without contextual information
### Version

edge

### What Kubernetes platforms are you running on?

Kind

### Steps to reproduce

No file and line present in NIC logs when logFormat is "json". File and line are present when "glog" is configured.

Structured logging in JSON is nice but without file and line it is not very practial to use.


Discussion:
@github-actions: Hi @hafe thanks for reporting! 

 Be sure to check out the [docs](https://docs.nginx.com/nginx-ingress-controller) and the [Contributing Guidelines](https://github.com/nginx/kubernetes-ingress/blob/main/CONTRIBUTING.md) while you wait for a human to take a look at this :slightly_smiling_face:

 Cheers!@vepatel: @hafe Can you please provide exact repro steps for this please with the event you're observing in logs?@hafe: just switched logFormat from glog to json and file/line is no longer present in log messages@hafe: Example with glog:

I0330 07:06:24.132037       1 leaderelection.go:257] attempting to acquire leader lease default/nic-nginx-ingress-leader-election...
I20250330 07:06:24.140270   1 service.go:26] Adding service: kubernetes
I20250330 07:06:24.140661   1 service.go:26] Adding service: nic-nginx-ingress-controller
I20250330 07:06:24.140742   1 service.go:26] Adding service: kube-dns
I20250330 07:06:24.232862   1 service.go:142] Syncing service default/nic-nginx-ingress-controller
I20250330 07:06:24.472901   1 main.go:102] Event(v1.ObjectReference{Kind:"ConfigMap", Namespace:"default", Name:"nic-nginx-ingress", UID:"e7afc234-0c83-4e1a-b570-386c1cb67235", APIVersion:"v1", ResourceVersion:"561", FieldPath:""}): type: 'Normal' reason: 'Updated' ConfigMap default/nic-nginx-ingress updated without error
I0330 07:06:59.678082       1 leaderelection.go:271] successfully acquired lease default/nic-nginx-ingress-leader-election

Example with json:

I0330 07:03:07.868148       1 leaderelection.go:257] attempting to acquire leader lease default/nic-nginx-ingress-leader-election...
{"time":"2025-03-30T07:03:07.875732211Z","level":"INFO","msg":"Adding service: kubernetes"}
{"time":"2025-03-30T07:03:07.87576521Z","level":"INFO","msg":"Adding service: nic-nginx-ingress-controller"}
{"time":"2025-03-30T07:03:07.875813377Z","level":"INFO","msg":"Adding service: kube-dns"}
{"time":"2025-03-30T07:03:07.96753629Z","level":"INFO","msg":"Syncing service default/nic-nginx-ingress-controller"}
{"time":"2025-03-30T07:03:08.186180215Z","level":"INFO","msg":"Event(v1.ObjectReference{Kind:\"ConfigMap\", Namespace:\"default\", Name:\"nic-nginx-ingress\", UID:\"e7afc234-0c83-4e1a-b570-386c1cb67235\", APIVersion:\"v1\", ResourceVersion:\"561\", FieldPath:\"\"}): type: 'Normal' reason: 'Updated' ConfigMap default/nic-nginx-ingress updated without error"}
I0330 07:03:45.948189       1 leaderelection.go:271] successfully acquired lease default/nic-nginx-ingress-leader-election

No file and line and leader election still uses glog it seems
@AlexFenlon: Hi @hafe ,

Thank you for the update on this, we will investigate this soon.@AlexFenlon: Hi @hafe,

After looking into this, we have discovered that the `leaderelection..go` is not a file controlled by NIC and unfortunately for now will have to stay as the glog format.


> ile/line is no longer present in log messages

In relation to this, this would require some minor code changes.

Solution patch:
diff --git a/cmd/nginx-ingress/main.go b/cmd/nginx-ingress/main.go
index 75c4be031..09b156fe7 100644
--- a/cmd/nginx-ingress/main.go
+++ b/cmd/nginx-ingress/main.go
@@ -1156,13 +1156,29 @@ func logEventAndExit(ctx context.Context, eventLog record.EventRecorder, obj pkg
 func initLogger(logFormat string, level slog.Level, out io.Writer) context.Context {
 	programLevel := new(slog.LevelVar) // Info by default
 	var h slog.Handler
+
+	opts := &slog.HandlerOptions{
+		Level:     programLevel,
+		AddSource: true,
+		ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
+			if a.Key == slog.SourceKey {
+				if src, ok := a.Value.Any().(*slog.Source); ok {
+					src.Function = ""
+					src.File = filepath.Base(src.File)
+					a.Value = slog.AnyValue(src)
+				}
+			}
+			return a
+		},
+	}
+
 	switch {
 	case logFormat == "glog":
 		h = nic_glog.New(out, &nic_glog.Options{Level: programLevel})
 	case logFormat == "json":
-		h = slog.NewJSONHandler(out, &slog.HandlerOptions{Level: programLevel})
+		h = slog.NewJSONHandler(out, opts)
 	case logFormat == "text":
-		h = slog.NewTextHandler(out, &slog.HandlerOptions{Level: programLevel})
+		h = slog.NewTextHandler(out, opts)
 	default:
 		h = nic_glog.New(out, &nic_glog.Options{Level: programLevel})
 	}



Test patch:
diff --git a/cmd/nginx-ingress/main_test.go b/cmd/nginx-ingress/main_test.go
index c4de491e6..b15f96863 100644
--- a/cmd/nginx-ingress/main_test.go
+++ b/cmd/nginx-ingress/main_test.go
@@ -30,12 +30,12 @@ func TestLogFormats(t *testing.T) {
 		{
 			name:   "json format message",
 			format: "json",
-			wantre: `^{"time":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+.*","level":"INFO","msg":".*}`,
+			wantre: `^{"time":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+.*","level":"INFO","source":\{"file":"[^"]+\.go","line":\d+\},"msg":".*}`,
 		},
 		{
 			name:   "text format message",
 			format: "text",
-			wantre: `^time=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+.*level=\w+\smsg=\w+`,
+			wantre: `^time=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+.*level=\w+\ssource=[^:]+\.go:\d+\smsg=\w+`,
 		},
 	}
 	t.Parallel()