• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender / 2420780216

31 Mar 2026 12:17PM UTC coverage: 75.839%. First build
2420780216

push

gitlab-ci

vpodzime
feat: Large deployment logs are now trimmed to be accepted by the server

Deployment logs larger than 1 MiB are rejected by the
server which leads to two issues:

- excessive bandwith consumption when uploading such large logs
  only to be thrown away, and

- no deployment logs for particular device and particular failed
  deployment available at the server at all.

To prevent this, the client now trims large deployment logs and
only sends the biggest possible part of the logs from their end.

Ticket: MEN-9415
Changelog: title
Signed-off-by: Vratislav Podzimek <vratislav.podzimek+auto-signed@northern.tech>
(cherry picked from commit cecd63689)

46 of 55 new or added lines in 1 file covered. (83.64%)

7549 of 9954 relevant lines covered (75.84%)

14343.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.6
/src/mender-update/deployments/deployments.cpp
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
#include <mender-update/deployments.hpp>
16

17
#include <algorithm>
18
#include <sstream>
19
#include <string>
20

21
#include <api/api.hpp>
22
#include <api/client.hpp>
23
#include <common/common.hpp>
24
#include <common/error.hpp>
25
#include <common/events.hpp>
26
#include <common/expected.hpp>
27
#include <common/http.hpp>
28
#include <common/io.hpp>
29
#include <common/json.hpp>
30
#include <common/log.hpp>
31
#include <common/optional.hpp>
32
#include <common/path.hpp>
33
#include <mender-update/context.hpp>
34

35
namespace mender {
36
namespace update {
37
namespace deployments {
38

39
using namespace std;
40

41
namespace api = mender::api;
42
namespace common = mender::common;
43
namespace context = mender::update::context;
44
namespace error = mender::common::error;
45
namespace events = mender::common::events;
46
namespace expected = mender::common::expected;
47
namespace http = mender::common::http;
48
namespace io = mender::common::io;
49
namespace json = mender::common::json;
50
namespace log = mender::common::log;
51
namespace path = mender::common::path;
52

53
using ExpectedOffset = expected::expected<ifstream::off_type, error::Error>;
54

55
const DeploymentsErrorCategoryClass DeploymentsErrorCategory;
56

57
const char *DeploymentsErrorCategoryClass::name() const noexcept {
×
58
        return "DeploymentsErrorCategory";
×
59
}
60

61
string DeploymentsErrorCategoryClass::message(int code) const {
3✔
62
        switch (code) {
3✔
63
        case NoError:
64
                return "Success";
×
65
        case InvalidDataError:
66
                return "Invalid data error";
×
67
        case BadResponseError:
68
                return "Bad response error";
×
69
        case DeploymentAbortedError:
70
                return "Deployment was aborted on the server";
3✔
71
        }
72
        assert(false);
73
        return "Unknown";
×
74
}
75

76
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
30✔
77
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
35✔
78
}
79

80
static const string check_updates_v1_uri = "/api/devices/v1/deployments/device/deployments/next";
81
static const string check_updates_v2_uri = "/api/devices/v2/deployments/device/deployments/next";
82

83
error::Error DeploymentClient::CheckNewDeployments(
7✔
84
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
85
        auto ex_dev_type = ctx.GetDeviceType();
7✔
86
        if (!ex_dev_type) {
7✔
87
                return ex_dev_type.error();
1✔
88
        }
89
        string device_type = ex_dev_type.value();
6✔
90

91
        auto ex_provides = ctx.LoadProvides();
6✔
92
        if (!ex_provides) {
6✔
93
                return ex_provides.error();
×
94
        }
95
        auto provides = ex_provides.value();
6✔
96
        if (provides.find("artifact_name") == provides.end()) {
12✔
97
                return MakeError(InvalidDataError, "Missing artifact name data");
×
98
        }
99

100
        stringstream ss;
12✔
101
        ss << R"({"device_provides":{)";
6✔
102
        ss << R"("device_type":")";
6✔
103
        ss << json::EscapeString(device_type);
12✔
104

105
        for (const auto &kv : provides) {
14✔
106
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
16✔
107
                ss << json::EscapeString(kv.second);
16✔
108
        }
109

110
        ss << R"("}})";
6✔
111

112
        string v2_payload = ss.str();
113
        log::Debug("deployments/next v2 payload " + v2_payload);
6✔
114
        http::BodyGenerator payload_gen = [v2_payload]() {
48✔
115
                return make_shared<io::StringReader>(v2_payload);
6✔
116
        };
6✔
117

118
        auto v2_req = make_shared<api::APIRequest>();
6✔
119
        v2_req->SetPath(check_updates_v2_uri);
120
        v2_req->SetMethod(http::Method::POST);
6✔
121
        v2_req->SetHeader("Content-Type", "application/json");
12✔
122
        v2_req->SetHeader("Content-Length", to_string(v2_payload.size()));
12✔
123
        v2_req->SetHeader("Accept", "application/json");
12✔
124
        v2_req->SetBodyGenerator(payload_gen);
6✔
125

126
        string v1_args = "artifact_name=" + http::URLEncode(provides["artifact_name"])
18✔
127
                                         + "&device_type=" + http::URLEncode(device_type);
18✔
128
        auto v1_req = make_shared<api::APIRequest>();
6✔
129
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
12✔
130
        v1_req->SetMethod(http::Method::GET);
6✔
131
        v1_req->SetHeader("Accept", "application/json");
12✔
132

133
        auto received_body = make_shared<vector<uint8_t>>();
6✔
134
        auto handle_data = [received_body, api_handler](unsigned status) {
4✔
135
                if (status == http::StatusOK) {
4✔
136
                        auto ex_j = json::Load(common::StringFromByteVector(*received_body));
4✔
137
                        if (ex_j) {
2✔
138
                                CheckUpdatesAPIResponse response {optional<json::Json> {ex_j.value()}};
2✔
139
                                api_handler(response);
4✔
140
                        } else {
141
                                api_handler(expected::unexpected(ex_j.error()));
×
142
                        }
143
                } else if (status == http::StatusNoContent) {
2✔
144
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
145
                } else {
146
                        log::Warning(
×
147
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
148
                                + to_string(status));
×
149
                        api_handler(expected::unexpected(MakeError(
×
150
                                DeploymentAbortedError, "received unhandled HTTP response: " + to_string(status))));
×
151
                }
152
        };
16✔
153

154
        http::ResponseHandler header_handler =
155
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
9✔
156
                        if (!exp_resp) {
9✔
157
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
158
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
159
                                api_handler(response);
×
160
                                return;
161
                        }
162

163
                        auto resp = exp_resp.value();
9✔
164
                        received_body->clear();
9✔
165
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
9✔
166
                        body_writer->SetUnlimited(true);
9✔
167
                        resp->SetBodyWriter(body_writer);
18✔
168
                };
12✔
169

170
        http::ResponseHandler v1_body_handler =
171
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
172
                        if (!exp_resp) {
3✔
173
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
174
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
175
                                api_handler(response);
×
176
                                return;
177
                        }
178
                        auto resp = exp_resp.value();
3✔
179
                        auto status = resp->GetStatusCode();
3✔
180
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
181
                                handle_data(status);
2✔
182
                        } else {
183
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
184
                                string err_str;
185
                                if (ex_err_msg) {
1✔
186
                                        err_str = ex_err_msg.value();
×
187
                                } else {
188
                                        err_str = resp->GetStatusMessage();
2✔
189
                                }
190
                                api_handler(expected::unexpected(MakeError(
2✔
191
                                        BadResponseError,
192
                                        "Got unexpected response " + to_string(status) + ": " + err_str)));
4✔
193
                        }
194
                };
12✔
195

196
        http::ResponseHandler v2_body_handler = [received_body,
6✔
197
                                                                                         v1_req,
198
                                                                                         header_handler,
199
                                                                                         v1_body_handler,
200
                                                                                         api_handler,
201
                                                                                         handle_data,
202
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
203
                if (!exp_resp) {
6✔
204
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
205
                        CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
206
                        api_handler(response);
×
207
                        return;
208
                }
209
                auto resp = exp_resp.value();
6✔
210
                auto status = resp->GetStatusCode();
6✔
211
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
212
                        handle_data(status);
2✔
213
                } else if (status == http::StatusNotFound) {
4✔
214
                        log::Debug(
3✔
215
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
6✔
216
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
217
                        if (err != error::NoError) {
3✔
218
                                api_handler(expected::unexpected(err.WithContext("While calling v1 endpoint")));
×
219
                        }
220
                } else {
221
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
222
                        string err_str;
223
                        if (ex_err_msg) {
1✔
224
                                err_str = ex_err_msg.value();
1✔
225
                        } else {
226
                                err_str = resp->GetStatusMessage();
×
227
                        }
228
                        api_handler(expected::unexpected(MakeError(
2✔
229
                                BadResponseError,
230
                                "Got unexpected response " + to_string(status) + ": " + err_str)));
4✔
231
                }
232
        };
6✔
233

234
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
235
}
236

237
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
238
        "installing",
239
        "pause_before_installing",
240
        "downloading",
241
        "pause_before_rebooting",
242
        "rebooting",
243
        "pause_before_committing",
244
        "success",
245
        "failure",
246
        "already-installed"};
247

248
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
249
static const string status_uri_suffix = "/status";
250

251
string DeploymentStatusString(DeploymentStatus status) {
501✔
252
        return deployment_status_strings[static_cast<int>(status)];
505✔
253
}
254

255
error::Error DeploymentClient::PushStatus(
4✔
256
        const string &deployment_id,
257
        DeploymentStatus status,
258
        const string &substate,
259
        api::Client &client,
260
        StatusAPIResponseHandler api_handler) {
261
        // Cannot push a status update without a deployment ID
262
        AssertOrReturnError(deployment_id != "");
4✔
263
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
8✔
264
        if (substate != "") {
4✔
265
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
266
        } else {
267
                payload += "}";
1✔
268
        }
269
        http::BodyGenerator payload_gen = [payload]() {
32✔
270
                return make_shared<io::StringReader>(payload);
4✔
271
        };
4✔
272

273
        auto req = make_shared<api::APIRequest>();
4✔
274
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
8✔
275
        req->SetMethod(http::Method::PUT);
4✔
276
        req->SetHeader("Content-Type", "application/json");
8✔
277
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
278
        req->SetHeader("Accept", "application/json");
8✔
279
        req->SetBodyGenerator(payload_gen);
4✔
280

281
        auto received_body = make_shared<vector<uint8_t>>();
4✔
282
        return client.AsyncCall(
283
                req,
284
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
285
                        if (!exp_resp) {
4✔
286
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
287
                                api_handler(exp_resp.error());
×
288
                                return;
×
289
                        }
290

291
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
4✔
292
                        auto resp = exp_resp.value();
4✔
293
                        auto content_length = resp->GetHeader("Content-Length");
8✔
294
                        if (!content_length) {
4✔
295
                                log::Debug(
×
296
                                        "Failed to get content length from the deployment status API response headers: "
297
                                        + content_length.error().String());
×
298
                                body_writer->SetUnlimited(true);
×
299
                        } else {
300
                                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
301
                                if (!ex_len) {
4✔
302
                                        log::Error(
×
303
                                                "Failed to convert the content length from the deployment status API response headers to an integer: "
304
                                                + ex_len.error().String());
×
305
                                        body_writer->SetUnlimited(true);
×
306
                                } else {
307
                                        received_body->resize(ex_len.value());
4✔
308
                                }
309
                        }
310
                        resp->SetBodyWriter(body_writer);
8✔
311
                },
312
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
313
                        if (!exp_resp) {
4✔
314
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
315
                                api_handler(exp_resp.error());
×
316
                                return;
×
317
                        }
318

319
                        auto resp = exp_resp.value();
4✔
320
                        auto status = resp->GetStatusCode();
4✔
321
                        if (status == http::StatusNoContent) {
4✔
322
                                api_handler(error::NoError);
4✔
323
                        } else if (status == http::StatusConflict) {
2✔
324
                                api_handler(
1✔
325
                                        MakeError(DeploymentAbortedError, "Could not send status update to server"));
2✔
326
                        } else {
327
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
328
                                string err_str;
329
                                if (ex_err_msg) {
1✔
330
                                        err_str = ex_err_msg.value();
1✔
331
                                } else {
332
                                        err_str = resp->GetStatusMessage();
×
333
                                }
334
                                api_handler(MakeError(
2✔
335
                                        BadResponseError,
336
                                        "Got unexpected response " + to_string(status)
2✔
337
                                                + " from status API: " + err_str));
2✔
338
                        }
339
                });
16✔
340
}
341

342
using mender::common::expected::ExpectedSize;
343

344
static ExpectedSize GetLogFileDataSize(const string &path) {
21✔
345
        auto ex_istr = io::OpenIfstream(path);
21✔
346
        if (!ex_istr) {
21✔
347
                return expected::unexpected(ex_istr.error());
×
348
        }
349
        auto istr = std::move(ex_istr.value());
42✔
350

351
        // We want the size of the actual data without a potential trailing
352
        // comma. So let's seek one byte before the end of file, check if the last
353
        // byte is a comma and return the appropriate number.
354
        istr.seekg(-1, ios_base::end);
21✔
355
        int c = istr.get();
21✔
356
        if (c == ',') {
21✔
357
                return istr.tellg() - static_cast<ifstream::off_type>(1);
21✔
358
        } else {
359
                return istr.tellg();
×
360
        }
361
}
362

363
// Find the beginning of the next log message JSON after offset
364
static ExpectedOffset FindNextMsgAfter(const string &path, ifstream::off_type offset) {
1✔
365
        auto ex_is = io::OpenIfstream(path);
1✔
366
        if (!ex_is) {
1✔
NEW
367
                return expected::unexpected(ex_is.error());
×
368
        }
369
        auto is = std::move(ex_is.value());
2✔
370
        is.seekg(offset);
1✔
371
        int io_errno = errno;
1✔
372
        if (!is) {
1✔
NEW
373
                return expected::unexpected(error::Error(
×
NEW
374
                        generic_category().default_error_condition(io_errno),
×
NEW
375
                        "Failed to seek to truncated logs offset in '" + path + "'"));
×
376
        }
377

378
        // Now that we have seeked to the starting offset, we need to find the next
379
        // boundary between JSON log entries.
380
        const string pattern = R"(},{"timestamp")";
1✔
381
        string buf(1024, '\0');
382
        while (is.read(buf.data(), 1024)) {
1✔
383
                // make sure we don't work with some stale data from the previous read()
384
                buf.resize(is.gcount());
1✔
385

386
                // XXX: This is not perfect as it can fail to find the next boundary if
387
                //      it's split between the end of the current buffer contents and
388
                //      the start of the next chunk read from the file. However, with a
389
                //      1K buffer, this is very unlikely to happen and even if it does
390
                //      happen, the logs will just be trimmed a bit more than
391
                //      necessary. For the sake of incomparably simpler code that is
392
                //      easy to test.
393
                auto pos = buf.find(pattern);
1✔
394
                if (pos != string::npos) {
1✔
395
                        offset += pos + 2; // skip "},"
1✔
396
                        break;
1✔
397
                }
NEW
398
                offset += buf.size();
×
399
        }
400
        io_errno = errno;
1✔
401
        if (!is && !is.eof()) {
1✔
NEW
402
                return expected::unexpected(error::Error(
×
NEW
403
                        generic_category().default_error_condition(io_errno),
×
NEW
404
                        "Failed to read logs from '" + path + "'"));
×
405
        }
406

407
        // In case we read the whole rest of the file not finding the next JSON log
408
        // entry, there must be something really wrong with the log and it should be
409
        // fully skipped. The user will still get the information that the log was
410
        // truncated and they will still have the full log on the device.
411
        return offset;
412
}
413

414
const vector<uint8_t> JsonLogMessagesReader::header_ = {
415
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
416
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
417
const string JsonLogMessagesReader::default_tstamp_ = "1970-01-01T00:00:00.000000000Z";
418
const string JsonLogMessagesReader::bad_data_msg_tmpl_ =
419
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},)d";
420
const string JsonLogMessagesReader::too_much_data_msg_tmpl_ =
421
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "WARNING", "message": "(THE ORIGINAL LOGS WERE TOO BIG, THIS LOG IS TRUNCATED. The full log can be found on the device)"},)d";
422

423
JsonLogMessagesReader::~JsonLogMessagesReader() {
51✔
424
        reader_.reset();
425
        if (!sanitized_fpath_.empty() && path::FileExists(sanitized_fpath_)) {
17✔
426
                auto del_err = path::FileDelete(sanitized_fpath_);
17✔
427
                if (del_err != error::NoError) {
17✔
428
                        log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
429
                }
430
        }
431
        sanitized_fpath_.erase();
17✔
432
}
17✔
433

434
static error::Error DoSanitizeLogs(
21✔
435
        const string &orig_path, const string &new_path, bool &all_valid, string &first_tstamp) {
436
        auto ex_ifs = io::OpenIfstream(orig_path);
21✔
437
        if (!ex_ifs) {
21✔
438
                return ex_ifs.error();
×
439
        }
440
        auto ex_ofs = io::OpenOfstream(new_path);
21✔
441
        if (!ex_ofs) {
21✔
442
                return ex_ofs.error();
×
443
        }
444
        auto &ifs = ex_ifs.value();
21✔
445
        auto &ofs = ex_ofs.value();
21✔
446

447
        string last_known_tstamp = first_tstamp;
21✔
448
        const string tstamp_prefix_data = R"d({"timestamp": ")d";
21✔
449
        const string corrupt_msg_suffix_data =
450
                R"d(", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},)d";
21✔
451

452
        string line;
453
        first_tstamp.erase();
21✔
454
        all_valid = true;
21✔
455
        error::Error err;
21✔
456
        while (!ifs.eof()) {
20,103✔
457
                getline(ifs, line);
20,082✔
458
                if (!ifs.eof() && !ifs) {
20,082✔
459
                        int io_errno = errno;
×
460
                        return error::Error(
461
                                generic_category().default_error_condition(io_errno),
×
462
                                "Failed to get line from deployment logs file '" + orig_path
×
463
                                        + "': " + strerror(io_errno));
×
464
                }
465
                if (line.empty()) {
20,082✔
466
                        // skip empty lines
467
                        continue;
21✔
468
                }
469
                auto ex_json = json::Load(line);
40,122✔
470
                if (ex_json) {
20,061✔
471
                        // valid JSON log line, just replace the newline after it with a comma and save the
472
                        // timestamp for later
473
                        auto ex_tstamp = ex_json.value().Get("timestamp").and_then(json::ToString);
40,096✔
474
                        if (ex_tstamp) {
20,048✔
475
                                if (first_tstamp.empty()) {
20,048✔
476
                                        first_tstamp = ex_tstamp.value();
20✔
477
                                }
478
                                last_known_tstamp = std::move(ex_tstamp.value());
20,048✔
479
                        }
480
                        line.append(1, ',');
20,048✔
481
                        err = io::WriteStringIntoOfstream(ofs, line);
20,048✔
482
                        if (err != error::NoError) {
20,048✔
483
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
484
                        }
485
                } else {
486
                        all_valid = false;
13✔
487
                        if (first_tstamp.empty()) {
13✔
488
                                // If we still don't have the first valid tstamp, we need to
489
                                // save the last known one (potentially pre-set) as the first
490
                                // one.
491
                                first_tstamp = last_known_tstamp;
492
                        }
493
                        err = io::WriteStringIntoOfstream(
13✔
494
                                ofs, tstamp_prefix_data + last_known_tstamp + corrupt_msg_suffix_data);
26✔
495
                        if (err != error::NoError) {
13✔
496
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
497
                        }
498
                }
499
        }
500
        return error::NoError;
21✔
501
}
502

503
static void ReplaceTimestampInMsgData(
10✔
504
        vector<uint8_t> &msg_data, string &timestamp, size_t default_tstamp_size) {
505
        auto msg_data_tstamp_start = msg_data.begin() + 15; // len(R"({"timestamp": ")")
506

507
        // The actual timestamp from logs can potentially have a different
508
        // (likely lower) time resolution and thus length than our default.
509
        const auto timestamp_size = timestamp.size();
510
        if (timestamp_size > default_tstamp_size) {
10✔
511
                // In case the time resolution is higher and the timestamp
512
                // longer (unlikely to happen)
513
                if (timestamp[timestamp_size - 1] == 'Z') {
1✔
514
                        timestamp[default_tstamp_size - 1] = 'Z';
1✔
515
                }
516
                timestamp.resize(default_tstamp_size);
1✔
517
        }
518
        copy_n(timestamp.cbegin(), timestamp.size(), msg_data_tstamp_start);
10✔
519
        if (timestamp.size() < default_tstamp_size) {
10✔
520
                // Add a closing '"' right after the timestamp and fill in the
521
                // rest of the space in the template with spaces that have no
522
                // effect in JSON.
523
                msg_data_tstamp_start[timestamp.size()] = '"';
1✔
524
                for (auto it = msg_data_tstamp_start + timestamp.size() + 1;
525
                         it < msg_data_tstamp_start + default_tstamp_size + 1;
4✔
526
                         it++) {
527
                        *it = ' ';
3✔
528
                }
529
        }
530
}
10✔
531

532
error::Error JsonLogMessagesReader::SanitizeLogs() {
21✔
533
        if (!sanitized_fpath_.empty()) {
21✔
534
                return error::NoError;
×
535
        }
536

537
        string prep_fpath = log_fpath_ + ".sanitized";
21✔
538
        string first_tstamp = default_tstamp_;
21✔
539
        auto err = DoSanitizeLogs(log_fpath_, prep_fpath, clean_logs_, first_tstamp);
21✔
540
        if (err != error::NoError) {
21✔
541
                if (path::FileExists(prep_fpath)) {
×
542
                        auto del_err = path::FileDelete(prep_fpath);
×
543
                        if (del_err != error::NoError) {
×
544
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
545
                        }
546
                }
547
        } else {
548
                sanitized_fpath_ = std::move(prep_fpath);
21✔
549
                reader_ = make_unique<io::FileReader>(sanitized_fpath_);
42✔
550
                auto ex_sz = GetLogFileDataSize(sanitized_fpath_);
21✔
551
                if (!ex_sz) {
21✔
552
                        return ex_sz.error().WithContext("Failed to determine deployment logs size");
×
553
                }
554

555
                raw_data_size_ = ex_sz.value();
21✔
556
                if (raw_data_size_ > maximum_log_size_) {
21✔
557
                        large_logs_ = true;
1✔
558
                        // Make sure we end up with less data than the limit with all the
559
                        // potential extra messages added in JsonLogMessagesReader::Read()
560
                        // below.
561
                        auto ex_off = FindNextMsgAfter(
562
                                sanitized_fpath_,
563
                                (raw_data_size_ + too_much_data_msg_tmpl_.size() + bad_data_msg_tmpl_.size()
1✔
564
                                 - maximum_log_size_));
1✔
565
                        if (!ex_off) {
1✔
566
                                return ex_off.error().WithContext(
NEW
567
                                        "Failed to determine start offset in too large deployment logs");
×
568
                        }
569
                        reader_ = make_unique<io::FileReader>(sanitized_fpath_, ex_off.value());
2✔
570
                        raw_data_size_ -= ex_off.value();
1✔
571
                }
572
                rem_raw_data_size_ = raw_data_size_;
21✔
573
                if (!clean_logs_) {
21✔
574
                        ReplaceTimestampInMsgData(bad_data_msg_, first_tstamp, default_tstamp_.size());
9✔
575
                }
576
                if (large_logs_) {
21✔
577
                        ReplaceTimestampInMsgData(too_much_data_msg_, first_tstamp, default_tstamp_.size());
1✔
578
                }
579
        }
580
        return err;
21✔
581
}
582

583
error::Error JsonLogMessagesReader::Rewind() {
4✔
584
        AssertOrReturnError(!sanitized_fpath_.empty());
4✔
585
        header_rem_ = header_.size();
4✔
586
        closing_rem_ = closing_.size();
4✔
587
        bad_data_msg_rem_ = bad_data_msg_.size();
4✔
588
        too_much_data_msg_rem_ = too_much_data_msg_.size();
4✔
589

590
        // release/close the file first so that the FileDelete() below can actually
591
        // delete it and free space up
592
        reader_.reset();
593
        auto del_err = path::FileDelete(sanitized_fpath_);
4✔
594
        if (del_err != error::NoError) {
4✔
595
                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
596
        }
597
        sanitized_fpath_.erase();
4✔
598
        return SanitizeLogs();
4✔
599
}
600

601
int64_t JsonLogMessagesReader::TotalDataSize() {
17✔
602
        assert(!sanitized_fpath_.empty());
603

604
        auto ret = raw_data_size_ + header_.size() + closing_.size();
17✔
605
        if (!clean_logs_) {
17✔
606
                ret += bad_data_msg_.size();
9✔
607
        }
608
        if (large_logs_) {
17✔
609
                ret += too_much_data_msg_.size();
1✔
610
        }
611
        return ret;
17✔
612
}
613

614
ExpectedSize JsonLogMessagesReader::Read(
1,137✔
615
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
616
        AssertOrReturnUnexpected(!sanitized_fpath_.empty());
1,137✔
617

618
        if (header_rem_ > 0) {
1,137✔
619
                io::Vsize target_size = end - start;
19✔
620
                auto copy_end = copy_n(
621
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
20✔
622
                auto n_copied = copy_end - start;
623
                header_rem_ -= n_copied;
19✔
624
                return static_cast<size_t>(n_copied);
625
        } else if (large_logs_ && (too_much_data_msg_rem_ > 0)) {
1,118✔
626
                io::Vsize target_size = end - start;
1✔
627
                auto copy_end = copy_n(
628
                        too_much_data_msg_.begin() + (too_much_data_msg_.size() - too_much_data_msg_rem_),
1✔
629
                        min(too_much_data_msg_rem_, target_size),
1✔
630
                        start);
1✔
631
                auto n_copied = copy_end - start;
632
                too_much_data_msg_rem_ -= n_copied;
1✔
633
                return static_cast<size_t>(n_copied);
634
        } else if (!clean_logs_ && (bad_data_msg_rem_ > 0)) {
1,117✔
635
                io::Vsize target_size = end - start;
16✔
636
                auto copy_end = copy_n(
637
                        bad_data_msg_.begin() + (bad_data_msg_.size() - bad_data_msg_rem_),
16✔
638
                        min(bad_data_msg_rem_, target_size),
16✔
639
                        start);
16✔
640
                auto n_copied = copy_end - start;
641
                bad_data_msg_rem_ -= n_copied;
16✔
642
                return static_cast<size_t>(n_copied);
643
        } else if (rem_raw_data_size_ > 0) {
1,101✔
644
                if (end - start > rem_raw_data_size_) {
1,065✔
645
                        end = start + static_cast<size_t>(rem_raw_data_size_);
646
                }
647
                auto ex_sz = reader_->Read(start, end);
1,065✔
648
                if (!ex_sz) {
1,065✔
649
                        return ex_sz;
650
                }
651
                auto n_read = ex_sz.value();
1,065✔
652
                rem_raw_data_size_ -= n_read;
1,065✔
653

654
                // We control how much we read from the file so we should never read
655
                // 0 bytes (meaning EOF reached). If we do, it means the file is
656
                // smaller than what we were told.
657
                assert(n_read > 0);
658
                if (n_read == 0) {
1,065✔
659
                        return expected::unexpected(
×
660
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
661
                }
662
                return n_read;
663
        } else if (closing_rem_ > 0) {
36✔
664
                io::Vsize target_size = end - start;
18✔
665
                auto copy_end = copy_n(
666
                        closing_.begin() + (closing_.size() - closing_rem_),
18✔
667
                        min(closing_rem_, target_size),
18✔
668
                        start);
18✔
669
                auto n_copied = copy_end - start;
670
                closing_rem_ -= n_copied;
18✔
671
                return static_cast<size_t>(copy_end - start);
672
        } else {
673
                return 0;
674
        }
675
};
676

677
static const string logs_uri_suffix = "/log";
678

679
error::Error DeploymentClient::PushLogs(
3✔
680
        const string &deployment_id,
681
        const string &log_file_path,
682
        api::Client &client,
683
        LogsAPIResponseHandler api_handler) {
684
        auto logs_reader = make_shared<JsonLogMessagesReader>(log_file_path);
3✔
685
        auto err = logs_reader->SanitizeLogs();
3✔
686
        if (err != error::NoError) {
3✔
687
                return err;
×
688
        }
689

690
        auto req = make_shared<api::APIRequest>();
3✔
691
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
6✔
692
        req->SetMethod(http::Method::PUT);
3✔
693
        req->SetHeader("Content-Type", "application/json");
6✔
694
        req->SetHeader("Content-Length", to_string(logs_reader->TotalDataSize()));
6✔
695
        req->SetHeader("Accept", "application/json");
6✔
696
        req->SetBodyGenerator([logs_reader]() {
18✔
697
                logs_reader->Rewind();
6✔
698
                return logs_reader;
3✔
699
        });
6✔
700

701
        auto received_body = make_shared<vector<uint8_t>>();
3✔
702
        return client.AsyncCall(
703
                req,
704
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
705
                        if (!exp_resp) {
3✔
706
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
707
                                api_handler(exp_resp.error());
×
708
                                return;
×
709
                        }
710

711
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
3✔
712
                        auto resp = exp_resp.value();
3✔
713
                        auto content_length = resp->GetHeader("Content-Length");
6✔
714
                        if (!content_length) {
3✔
715
                                log::Debug(
×
716
                                        "Failed to get content length from the deployment log API response headers: "
717
                                        + content_length.error().String());
×
718
                                body_writer->SetUnlimited(true);
×
719
                        } else {
720
                                auto ex_len = common::StringTo<size_t>(content_length.value());
3✔
721
                                if (!ex_len) {
3✔
722
                                        log::Error(
×
723
                                                "Failed to convert the content length from the deployment log API response headers to an integer: "
724
                                                + ex_len.error().String());
×
725
                                        body_writer->SetUnlimited(true);
×
726
                                } else {
727
                                        received_body->resize(ex_len.value());
3✔
728
                                }
729
                        }
730
                        resp->SetBodyWriter(body_writer);
6✔
731
                },
732
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
733
                        if (!exp_resp) {
3✔
734
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
735
                                api_handler(exp_resp.error());
×
736
                                return;
×
737
                        }
738

739
                        auto resp = exp_resp.value();
3✔
740
                        auto status = resp->GetStatusCode();
3✔
741
                        if (status == http::StatusNoContent) {
3✔
742
                                api_handler(error::NoError);
4✔
743
                        } else {
744
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
745
                                string err_str;
746
                                if (ex_err_msg) {
1✔
747
                                        err_str = ex_err_msg.value();
1✔
748
                                } else {
749
                                        err_str = resp->GetStatusMessage();
×
750
                                }
751
                                api_handler(MakeError(
2✔
752
                                        BadResponseError,
753
                                        "Got unexpected response " + to_string(status) + " from logs API: " + err_str));
2✔
754
                        }
755
                });
12✔
756
}
757

758
} // namespace deployments
759
} // namespace update
760
} // namespace mender
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc