Repository: amir20/dozzle (2026-04-11T13:47:16Z)

Issue:
Grouped logs breaks alerting 
### 🔍 Check for existing issues

- [x] Completed

### How is Dozzle deployed?

Standalone Deployment

### 📦 Dozzle version

v10.3.0

### ✅ Command used to run Dozzle

  socket-proxy:
    image: tecnativa/docker-socket-proxy:latest
    container_name: socket-proxy
    restart: unless-stopped
    mem_limit: 32m
    environment:
      CONTAINERS: 1
      INFO: 1
      EVENTS: 1
      LOGS: 1
      STATS: 1
      VERSION: 1
      POST: 0
      AUTH: 0
      SECRETS: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - socket-proxy-net
    logging:
      driver: "json-file"
      options:
        max-size: "5m"
        max-file: "3"

  dozzle:
    image: amir20/dozzle:latest
    container_name: dozzle
    restart: unless-stopped
    mem_limit: 64m
    environment:
      DOCKER_HOST: tcp://socket-proxy:2375
    volumes:
      - dozzle_data:/data
    networks:
      - socket-proxy-net
      - monitoring-net
    logging:
      driver: "json-file"
      options:
        max-size: "5m"
        max-file: "3"

### 🐛 Describe the bug / provide steps to reproduce it

Log alerts are not working properly. The notification channel (Telegram) is correctly configured and functional, as metric and event alerts work perfectly. However, I cannot get log alerts to trigger.

While the container filter works as expected, the log filter always returns a validation error: "Ningún registro coincide con este filtro".

I have tested several simple filters without success:
- message contains "backend"
- message contains "info"
- message contains "registros"

I also tried expressions from the documentation, but the result is the same:
- level == "error"
- message.status >= 500
- stream == "stderr"

Logs from all containers are displayed correctly in the Dozzle UI. I can manually search for the same keywords (backend, info, registros) within the UI, and they return matching records.

Example of a log line copied directly from the UI that should match:
2026-04-11T07:46:48.304Z  INFO 1 --- [backend] [           main] e.l.backend.services.ElasticSyncService  : Sincronizado lote de 1000 registros (Total: 710000)

**Edited:**
I have two discoveries: first, the contains function is case-sensitive, which is not an issue for my use case. However, the real problem is that if multiple logs occur within the same second, they are ignored by the alerts. Essentially, when logs share the exact same second, the alert fails to trigger as if those logs didn't exist, whereas they work perfectly fine if they are recorded one second apart. For example:
**KO**
2026-04-11T07:46:**48.304Z**  INFO 1 --- [backend] [           main] e.l.backend.services.ElasticSyncService  : Sincronizado lote de 1000 registros (Total: 710000)
2026-04-11T07:46:**48.504Z**  INFO 1 --- [backend] [           main] e.l.backend.services.ElasticSyncService  : Sincronizado lote de 1000 registros (Total: 711000)
**OK**
2026-04-11T07:46:**48.304Z**  INFO 1 --- [backend] [           main] e.l.backend.services.ElasticSyncService  : Sincronizado lote de 1000 registros (Total: 710000)
2026-04-11T07:46:**49.304Z**  INFO 1 --- [backend] [           main] e.l.backend.services.ElasticSyncService  : Sincronizado lote de 1000 registros (Total: 711000)


### 💻 Environment

Client: Docker Engine - Community
 Version:    29.3.0
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.31.1
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v5.1.0
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 10
  Running: 10
  Paused: 0
  Stopped: 0
 Images: 16
 Server Version: 29.3.0
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: dea7da592f5d1d2b7755e3a161be07f43fad8f75
 runc version: v1.3.4-0-gd6d73eb8
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.8.0-101-generic
 Operating System: Ubuntu 24.04.4 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 7.57GiB
 Name: xxx-server
 ID: 7c3a8146-64c5-46ae-b551-1bc69980f3e8
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Username: xxx
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
  ::1/128
 Live Restore Enabled: false
 Firewall Backend: iptables

### 📜 Dozzle logs are required for debugging purposes. You may need to enable debug mode. See https://dozzle.dev/guide/debugging.

{"level":"debug","version":"v10.3.0","id":"ab8f8072bde2","from":"2026-04-11T06:14:11Z","to":"2026-04-11T08:14:11Z","stdType":"all","host":"l12s-server","time":"2026-04-11T08:14:11Z","message":"Fetching logs between dates for container"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is map[string]interface {}, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}
{"level":"debug","version":"v10.3.0","error":"interface conversion: interface {} is []container.LogFragment, not string (1:9)\n | message contains \"info\"\n | ........^","expression":"message contains \"info\"","time":"2026-04-11T08:14:11Z","message":"log expression evaluation error"}


### 📸 If applicable, add screenshots to help explain your bug

<img width="991" height="716" alt="Image" src="https://github.com/user-attachments/assets/edc6a347-517d-4390-94f0-79df6300eb04" />


Discussion:
> I have two discoveries: first, the contains function is case-sensitive, which is not an issue for my use case. However, the real problem is that if multiple logs occur within the same second, they are ignored by the alerts. Essentially, when logs share the exact same second, the alert fails to trigger as if those logs didn't exist, whereas they work perfectly fine if they are recorded one second apart. For example:

Just to confirm, alerts work for you? Except when they are happening at the same second. 


> message contains "info"

It is case sensitive so this should be INFO. 

> level == "info"

This should work. 

If you confirm what the actual bug is I can try replicating with your sample logs and see what happens. 
Ah after some exploration I think I see what's happening. Dozzle groups similar logs together. If you show me a screenshot we can validate by seeing the UI. 

Try https://github.com/amir20/dozzle/pull/4612 `amir20/dozzle:pr-4612`
Tested the amir20/dozzle:pr-4612, and is working correctly now.  Thank!



Patch:
diff --git a/internal/notification/types.go b/internal/notification/types.go
index 175c8cf7bbe7..ad8c384f46be 100644
--- a/internal/notification/types.go
+++ b/internal/notification/types.go
@@ -58,11 +58,14 @@ func extractMessage(l container.LogEvent) any {
 	case string:
 		return container.StripANSI(v)
 	case []container.LogFragment:
-		cleaned := make([]container.LogFragment, len(v))
+		var sb strings.Builder
 		for i, fragment := range v {
-			cleaned[i] = container.LogFragment{Message: container.StripANSI(fragment.Message)}
+			if i > 0 {
+				sb.WriteString("\n")
+			}
+			sb.WriteString(container.StripANSI(fragment.Message))
 		}
-		return cleaned
+		return sb.String()
 	case *orderedmap.OrderedMap[string, any]:
 		// Convert OrderedMap to regular map for expr compatibility
 		result := make(map[string]any)


Test patch:
diff --git a/internal/notification/types_test.go b/internal/notification/types_test.go
index ebec716ce22d..47923a74e9fe 100644
--- a/internal/notification/types_test.go
+++ b/internal/notification/types_test.go
@@ -285,6 +285,71 @@ func TestSubscription_EventCooldown(t *testing.T) {
 	})
 }
 
+func TestFromLogEvent_GroupedLogFragments(t *testing.T) {
+	tests := []struct {
+		name       string
+		expression string
+		logEvent   container.LogEvent
+		want       bool
+	}{
+		{
+			name:       "grouped log fragments - matches contains",
+			expression: `message contains "error"`,
+			logEvent: container.LogEvent{
+				Type: container.LogTypeGroup,
+				Message: []container.LogFragment{
+					{Message: "first line"},
+					{Message: "error occurred here"},
+					{Message: "third line"},
+				},
+			},
+			want: true,
+		},
+		{
+			name:       "grouped log fragments - does not match",
+			expression: `message contains "fatal"`,
+			logEvent: container.LogEvent{
+				Type: container.LogTypeGroup,
+				Message: []container.LogFragment{
+					{Message: "first line"},
+					{Message: "second line"},
+				},
+			},
+			want: false,
+		},
+		{
+			name:       "grouped log fragments - single fragment matches",
+			expression: `message contains "info"`,
+			logEvent: container.LogEvent{
+				Type: container.LogTypeGroup,
+				Message: []container.LogFragment{
+					{Message: "info: something happened"},
+				},
+			},
+			want: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			notificationLog := FromLogEvent(tt.logEvent)
+
+			_, isString := notificationLog.Message.(string)
+			assert.True(t, isString, "grouped log message should be converted to string")
+
+			program, err := expr.Compile(tt.expression, expr.Env(types.NotificationLog{}), expr.AsBool())
+			require.NoError(t, err, "failed to compile expression")
+
+			sub := &Subscription{
+				LogProgram: program,
+			}
+
+			got := sub.MatchesLog(notificationLog)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
 func TestFromLogEvent_OrderedMapConversion(t *testing.T) {
 	tests := []struct {
 		name       string