aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java
blob: 69fe9bb8fa14593642e1f067e0b93511ee29985b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;

import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.ObjectTraverser;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
import com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type;
import com.yahoo.vespa.hosted.controller.deployment.Step;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Serialisation of {@link LogEntry} objects. Not all fields are stored!
 *
 * @author jonmv
 */
class LogSerializer {

    // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
    //          (and rewrite all nodes on startup), changes to the serialized format must be made
    //          such that what is serialized on version N+1 can be read by version N:
    //          - ADDING FIELDS: Always ok
    //          - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
    //          - CHANGING THE FORMAT OF A FIELD: Don't do it bro.

    private static final String idField = "id";
    private static final String typeField = "type";
    private static final String timestampField = "at";
    private static final String messageField = "message";

    byte[] toJson(Map<Step, List<LogEntry>> log) {
        try {
            return SlimeUtils.toJsonBytes(toSlime(log));
        }
        catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    Slime toSlime(Map<Step, List<LogEntry>> log) {
        Slime root = new Slime();
        Cursor logObject = root.setObject();
        log.forEach((step, entries) -> {
            Cursor recordsArray = logObject.setArray(RunSerializer.valueOf(step));
            entries.forEach(entry -> toSlime(entry, recordsArray.addObject()));
        });
        return root;
    }

    private void toSlime(LogEntry entry, Cursor entryObject) {
        entryObject.setLong(idField, entry.id());
        entryObject.setLong(timestampField, entry.at().toEpochMilli());
        entryObject.setString(typeField, valueOf(entry.type()));
        entryObject.setString(messageField, entry.message());
    }

    Map<Step, List<LogEntry>> fromJson(byte[] logJson, long after) {
        return fromJson(Collections.singletonList(logJson), after);
    }

    Map<Step, List<LogEntry>> fromJson(List<byte[]> logJsons, long after) {
        return fromSlime(logJsons.stream()
                                 .map(SlimeUtils::jsonToSlime)
                                 .toList(),
                         after);
    }

    Map<Step, List<LogEntry>> fromSlime(List<Slime> slimes, long after) {
        Map<Step, List<LogEntry>> log = new HashMap<>();
        slimes.forEach(slime -> slime.get().traverse((ObjectTraverser) (stepName, entryArray) -> {
            Step step = RunSerializer.stepOf(stepName);
            List<LogEntry> entries = log.computeIfAbsent(step, __ -> new ArrayList<>());
            entryArray.traverse((ArrayTraverser) (__, entryObject) -> {
                LogEntry entry = fromSlime(entryObject);
                if (entry.id() > after)
                    entries.add(entry);
            });
        }));
        return log;
    }

    private LogEntry fromSlime(Inspector entryObject) {
        return new LogEntry(entryObject.field(idField).asLong(),
                            SlimeUtils.instant(entryObject.field(timestampField)),
                            typeOf(entryObject.field(typeField).asString()),
                            entryObject.field(messageField).asString());
    }

    static String valueOf(Type type) {
        return switch (type) {
            case debug -> "debug";
            case info -> "info";
            case warning -> "warning";
            case error -> "error";
            case html -> "html";
        };
    }

    static Type typeOf(String type) {
        return switch (type) {
            case "debug" -> Type.debug;
            case "info" -> Type.info;
            case "warning" -> Type.warning;
            case "error" -> Type.error;
            case "html" -> Type.html;
            default -> throw new IllegalArgumentException("Unknown log entry type '" + type + "'!");
        };
    }

}