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

mendersoftware / mender / 2410916884

26 Mar 2026 01:11PM UTC coverage: 81.405% (-0.3%) from 81.686%
2410916884

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>

21 of 59 new or added lines in 2 files covered. (35.59%)

2 existing lines in 2 files now uncovered.

9027 of 11089 relevant lines covered (81.4%)

19799.17 hits per line

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

75.17
/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 {
33✔
62
        switch (code) {
33✔
63
        case NoError:
64
                return "Success";
×
65
        case InvalidDataError:
66
                return "Invalid data error";
×
67
        case BadResponseError:
68
                return "Bad response error";
4✔
69
        case DeploymentAbortedError:
70
                return "Deployment was aborted on the server";
3✔
71
        case TooManyRequestsError:
72
                return "Too many requests";
26✔
73
        case RequestBodyTooLargeError:
74
                return "Request body too large";
×
75
        }
76
        assert(false);
77
        return "Unknown";
×
78
}
79

80
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
89✔
81
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
104✔
82
}
83

84
static const string check_updates_v1_uri = "/api/devices/v1/deployments/device/deployments/next";
85
static const string check_updates_v2_uri = "/api/devices/v2/deployments/device/deployments/next";
86

87
error::Error DeploymentClient::CheckNewDeployments(
10✔
88
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
89
        auto ex_compatible_type = ctx.GetCompatibleType();
20✔
90
        if (!ex_compatible_type) {
10✔
91
                return ex_compatible_type.error();
4✔
92
        }
93
        string compatible_type = ex_compatible_type.value();
6✔
94

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

104
        stringstream ss;
6✔
105
        ss << R"({"device_provides":{)";
6✔
106
        ss << R"("device_type":")";
6✔
107
        ss << json::EscapeString(compatible_type);
12✔
108

109
        for (const auto &kv : provides) {
14✔
110
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
8✔
111
                ss << json::EscapeString(kv.second);
16✔
112
        }
113

114
        ss << R"("}})";
6✔
115

116
        string v2_payload = ss.str();
117
        log::Debug("deployments/next v2 payload " + v2_payload);
6✔
118
        http::BodyGenerator payload_gen = [v2_payload]() {
54✔
119
                return make_shared<io::StringReader>(v2_payload);
6✔
120
        };
6✔
121

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

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

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

163
        http::ResponseHandler header_handler =
164
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
165
                        this->HeaderHandler(received_body, api_handler, exp_resp);
27✔
166
                };
15✔
167

168
        http::ResponseHandler v1_body_handler =
169
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
15✔
170
                        if (!exp_resp) {
3✔
171
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
172
                                CheckUpdatesAPIResponse response = expected::unexpected(
×
173
                                        CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
174
                                api_handler(response);
×
175
                                return;
176
                        }
177
                        auto resp = exp_resp.value();
3✔
178
                        auto status = resp->GetStatusCode();
3✔
179

180
                        // StatusTooManyRequests must have been handled in HeaderHandler already
181
                        assert(status != http::StatusTooManyRequests);
182

183
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
184
                                handle_data(status);
2✔
185
                        } else {
186
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
187
                                string err_str;
188
                                if (ex_err_msg) {
1✔
189
                                        err_str = ex_err_msg.value();
×
190
                                } else {
191
                                        err_str = resp->GetStatusMessage();
2✔
192
                                }
193
                                api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
3✔
194
                                        status,
195
                                        nullopt,
196
                                        MakeError(
197
                                                BadResponseError,
198
                                                "Got unexpected response " + to_string(status) + ": " + err_str)}));
2✔
199
                        }
200
                };
6✔
201

202
        http::ResponseHandler v2_body_handler = [received_body,
18✔
203
                                                                                         v1_req,
204
                                                                                         header_handler,
205
                                                                                         v1_body_handler,
206
                                                                                         api_handler,
207
                                                                                         handle_data,
208
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
209
                if (!exp_resp) {
6✔
210
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
211
                        CheckUpdatesAPIResponse response = expected::unexpected(
×
212
                                CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
213
                        api_handler(response);
×
214
                        return;
215
                }
216
                auto resp = exp_resp.value();
6✔
217
                auto status = resp->GetStatusCode();
6✔
218

219
                // StatusTooManyRequests must have been handled in HeaderHandler already
220
                assert(status != http::StatusTooManyRequests);
221

222
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
223
                        handle_data(status);
2✔
224
                } else if (status == http::StatusNotFound) {
4✔
225
                        log::Debug(
3✔
226
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
227
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
228
                        if (err != error::NoError) {
3✔
229
                                api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
×
230
                                        status, nullopt, err.WithContext("While calling v1 endpoint")}));
×
231
                        }
232
                } else {
233
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
234
                        string err_str;
235
                        if (ex_err_msg) {
1✔
236
                                err_str = ex_err_msg.value();
1✔
237
                        } else {
238
                                err_str = resp->GetStatusMessage();
×
239
                        }
240
                        api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
3✔
241
                                status,
242
                                nullopt,
243
                                MakeError(
244
                                        BadResponseError,
245
                                        "Got unexpected response " + to_string(status) + ": " + err_str)}));
2✔
246
                }
247
        };
6✔
248

249
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
250
}
12✔
251

252
void DeploymentClient::HeaderHandler(
12✔
253
        shared_ptr<vector<uint8_t>> received_body,
254
        CheckUpdatesAPIResponseHandler api_handler,
255
        http::ExpectedIncomingResponsePtr exp_resp) {
256
        if (!exp_resp) {
12✔
257
                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
258
                CheckUpdatesAPIResponse response =
259
                        expected::unexpected(CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
260
                api_handler(response);
×
261
                return;
262
        }
263

264
        auto resp = exp_resp.value();
12✔
265
        auto status = resp->GetStatusCode();
12✔
266
        if (status == http::StatusTooManyRequests) {
12✔
267
                CheckUpdatesAPIResponse response = expected::unexpected(CheckUpdatesAPIResponseError {
6✔
268
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")});
9✔
269
                api_handler(response);
6✔
270
        }
271
        received_body->clear();
12✔
272
        auto body_writer = make_shared<io::ByteWriter>(received_body);
12✔
273
        body_writer->SetUnlimited(true);
12✔
274
        resp->SetBodyWriter(body_writer);
24✔
275
}
276

277
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
278
        "installing",
279
        "pause_before_installing",
280
        "downloading",
281
        "pause_before_rebooting",
282
        "rebooting",
283
        "pause_before_committing",
284
        "success",
285
        "failure",
286
        "already-installed"};
287

288
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
289
static const string status_uri_suffix = "/status";
290

291
string DeploymentStatusString(DeploymentStatus status) {
501✔
292
        return deployment_status_strings[static_cast<int>(status)];
505✔
293
}
294

295
error::Error DeploymentClient::PushStatus(
4✔
296
        const string &deployment_id,
297
        DeploymentStatus status,
298
        const string &substate,
299
        api::Client &client,
300
        StatusAPIResponseHandler api_handler) {
301
        // Cannot push a status update without a deployment ID
302
        AssertOrReturnError(deployment_id != "");
4✔
303
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
4✔
304
        if (substate != "") {
4✔
305
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
306
        } else {
307
                payload += "}";
1✔
308
        }
309
        http::BodyGenerator payload_gen = [payload]() {
36✔
310
                return make_shared<io::StringReader>(payload);
4✔
311
        };
4✔
312

313
        auto req = make_shared<api::APIRequest>();
4✔
314
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
4✔
315
        req->SetMethod(http::Method::PUT);
4✔
316
        req->SetHeader("Content-Type", "application/json");
8✔
317
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
318
        req->SetHeader("Accept", "application/json");
8✔
319
        req->SetBodyGenerator(payload_gen);
4✔
320

321
        auto received_body = make_shared<vector<uint8_t>>();
4✔
322
        return client.AsyncCall(
16✔
323
                req,
324
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
325
                        this->PushStatusHeaderHandler(received_body, api_handler, exp_resp);
12✔
326
                },
4✔
327
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
328
                        if (!exp_resp) {
4✔
329
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
330
                                api_handler(StatusAPIResponse {nullopt, nullopt, exp_resp.error()});
×
331
                                return;
×
332
                        }
333

334
                        auto resp = exp_resp.value();
4✔
335
                        auto status = resp->GetStatusCode();
4✔
336

337
                        // StatusTooManyRequests must have been handled in PushStatusHeaderHandler already
338
                        assert(status != http::StatusTooManyRequests);
339

340
                        if (status == http::StatusNoContent) {
4✔
341
                                api_handler(StatusAPIResponse {status, nullopt, error::NoError});
2✔
342
                        } else if (status == http::StatusConflict) {
2✔
343
                                api_handler(StatusAPIResponse {
2✔
344
                                        status,
345
                                        nullopt,
346
                                        MakeError(DeploymentAbortedError, "Could not send status update to server")});
2✔
347
                        } else {
348
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
349
                                string err_str;
350
                                if (ex_err_msg) {
1✔
351
                                        err_str = ex_err_msg.value();
1✔
352
                                } else {
353
                                        err_str = resp->GetStatusMessage();
×
354
                                }
355
                                api_handler(StatusAPIResponse {
2✔
356
                                        status,
357
                                        nullopt,
358
                                        MakeError(
359
                                                BadResponseError,
360
                                                "Got unexpected response " + to_string(status)
1✔
361
                                                        + " from status API: " + err_str)});
2✔
362
                        }
363
                });
4✔
364
}
365

366
void DeploymentClient::PushStatusHeaderHandler(
7✔
367
        shared_ptr<vector<uint8_t>> received_body,
368
        StatusAPIResponseHandler api_handler,
369
        http::ExpectedIncomingResponsePtr exp_resp) {
370
        if (!exp_resp) {
7✔
371
                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
372
                api_handler(StatusAPIResponse {nullopt, nullopt, exp_resp.error()});
×
373
                return;
×
374
        }
375

376
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
377
        auto resp = exp_resp.value();
7✔
378
        auto status = resp->GetStatusCode();
7✔
379
        if (status == http::StatusTooManyRequests) {
7✔
380
                StatusAPIResponse response = {
381
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
382
                api_handler(response);
3✔
383
        }
3✔
384
        auto content_length = resp->GetHeader("Content-Length");
14✔
385
        if (!content_length) {
7✔
386
                log::Debug(
3✔
387
                        "Failed to get content length from the deployment status API response headers: "
388
                        + content_length.error().String());
6✔
389
                body_writer->SetUnlimited(true);
3✔
390
        } else {
391
                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
392
                if (!ex_len) {
4✔
393
                        log::Error(
×
394
                                "Failed to convert the content length from the deployment status API response headers to an integer: "
395
                                + ex_len.error().String());
×
396
                        body_writer->SetUnlimited(true);
×
397
                } else {
398
                        received_body->resize(ex_len.value());
4✔
399
                }
400
        }
401
        resp->SetBodyWriter(body_writer);
14✔
402
}
403

404
using mender::common::expected::ExpectedSize;
405

406
static ExpectedSize GetLogFileDataSize(const string &path) {
22✔
407
        auto ex_istr = io::OpenIfstream(path);
22✔
408
        if (!ex_istr) {
22✔
409
                return expected::unexpected(ex_istr.error());
×
410
        }
411
        auto istr = std::move(ex_istr.value());
22✔
412

413
        // We want the size of the actual data without a potential trailing
414
        // comma. So let's seek one byte before the end of file, check if the last
415
        // byte is a comma and return the appropriate number.
416
        istr.seekg(-1, ios_base::end);
22✔
417
        int c = istr.get();
22✔
418
        if (c == ',') {
22✔
419
                return istr.tellg() - static_cast<ifstream::off_type>(1);
22✔
420
        } else {
421
                return istr.tellg();
×
422
        }
423
}
22✔
424

425
// Find the beginning of the next log message JSON after offset
NEW
426
static ExpectedOffset FindNextMsgAfter(const string &path, ifstream::off_type offset) {
×
NEW
427
        auto ex_is = io::OpenIfstream(path);
×
NEW
428
        if (!ex_is) {
×
NEW
429
                return expected::unexpected(ex_is.error());
×
430
        }
NEW
431
        auto is = std::move(ex_is.value());
×
NEW
432
        is.seekg(offset);
×
NEW
433
        int io_errno = errno;
×
NEW
434
        if (!is) {
×
NEW
435
                return expected::unexpected(error::Error(
×
NEW
436
                        generic_category().default_error_condition(io_errno),
×
NEW
437
                        "Failed to seek to truncated logs offset in '" + path + "'"));
×
438
        }
439

440
        // Now that we have seeked to the starting offset, we need to find the next
441
        // boundary between JSON log entries.
NEW
442
        const string pattern = R"(},{"timestamp")";
×
443
        string buf(1024, '\0');
NEW
444
        while (is.read(buf.data(), 1024)) {
×
445
                // make sure we don't work with some stale data from the previous read()
NEW
446
                buf.resize(is.gcount());
×
447

448
                // XXX: This is not perfect as it can fail to find the next boundary if
449
                //      it's split between the end of the current buffer contents and
450
                //      the start of the next chunk read from the file. However, with a
451
                //      1K buffer, this is very unlikely to happen and even if it does
452
                //      happen, the logs will just be trimmed a bit more than
453
                //      necessary. For the sake of incomparably simpler code that is
454
                //      easy to test.
NEW
455
                auto pos = buf.find(pattern);
×
NEW
456
                if (pos != string::npos) {
×
NEW
457
                        offset += pos + 2; // skip "},"
×
NEW
458
                        break;
×
459
                }
NEW
460
                offset += buf.size();
×
461
        }
NEW
462
        io_errno = errno;
×
NEW
463
        if (!is && !is.eof()) {
×
NEW
464
                return expected::unexpected(error::Error(
×
NEW
465
                        generic_category().default_error_condition(io_errno),
×
NEW
466
                        "Failed to read logs from '" + path + "'"));
×
467
        }
468

469
        // In case we read the whole rest of the file not finding the next JSON log
470
        // entry, there must be something really wrong with the log and it should be
471
        // fully skipped. The user will still get the information that the log was
472
        // truncated and they will still have the full log on the device.
473
        return offset;
NEW
474
}
×
475

476
const vector<uint8_t> JsonLogMessagesReader::header_ = {
477
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
478
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
479
const string JsonLogMessagesReader::default_tstamp_ = "1970-01-01T00:00:00.000000000Z";
480
const string JsonLogMessagesReader::bad_data_msg_tmpl_ =
481
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},)d";
482
const string JsonLogMessagesReader::too_much_data_msg_tmpl_ =
483
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS WERE TOO BIG, THIS LOG IS TRUNCATED. The full log can be found on the device)"},)d";
484

485
JsonLogMessagesReader::~JsonLogMessagesReader() {
51✔
486
        reader_.reset();
487
        if (!sanitized_fpath_.empty() && path::FileExists(sanitized_fpath_)) {
17✔
488
                auto del_err = path::FileDelete(sanitized_fpath_);
17✔
489
                if (del_err != error::NoError) {
17✔
490
                        log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
491
                }
492
        }
493
        sanitized_fpath_.erase();
17✔
494
}
34✔
495

496
static error::Error DoSanitizeLogs(
22✔
497
        const string &orig_path, const string &new_path, bool &all_valid, string &first_tstamp) {
498
        auto ex_ifs = io::OpenIfstream(orig_path);
22✔
499
        if (!ex_ifs) {
22✔
500
                return ex_ifs.error();
×
501
        }
502
        auto ex_ofs = io::OpenOfstream(new_path);
22✔
503
        if (!ex_ofs) {
22✔
504
                return ex_ofs.error();
×
505
        }
506
        auto &ifs = ex_ifs.value();
22✔
507
        auto &ofs = ex_ofs.value();
22✔
508

509
        string last_known_tstamp = first_tstamp;
22✔
510
        const string tstamp_prefix_data = R"d({"timestamp": ")d";
22✔
511
        const string corrupt_msg_suffix_data =
512
                R"d(", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},)d";
22✔
513

514
        string line;
515
        first_tstamp.erase();
22✔
516
        all_valid = true;
22✔
517
        error::Error err;
22✔
518
        while (!ifs.eof()) {
104✔
519
                getline(ifs, line);
82✔
520
                if (!ifs.eof() && !ifs) {
82✔
521
                        int io_errno = errno;
×
522
                        return error::Error(
523
                                generic_category().default_error_condition(io_errno),
×
524
                                "Failed to get line from deployment logs file '" + orig_path
×
525
                                        + "': " + strerror(io_errno));
×
526
                }
527
                if (line.empty()) {
82✔
528
                        // skip empty lines
529
                        continue;
22✔
530
                }
531
                auto ex_json = json::Load(line);
120✔
532
                if (ex_json) {
60✔
533
                        // valid JSON log line, just replace the newline after it with a comma and save the
534
                        // timestamp for later
535
                        auto ex_tstamp = ex_json.value().Get("timestamp").and_then(json::ToString);
94✔
536
                        if (ex_tstamp) {
47✔
537
                                if (first_tstamp.empty()) {
47✔
538
                                        first_tstamp = ex_tstamp.value();
21✔
539
                                }
540
                                last_known_tstamp = std::move(ex_tstamp.value());
47✔
541
                        }
542
                        line.append(1, ',');
47✔
543
                        err = io::WriteStringIntoOfstream(ofs, line);
47✔
544
                        if (err != error::NoError) {
47✔
545
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
546
                        }
547
                } else {
548
                        all_valid = false;
13✔
549
                        if (first_tstamp.empty()) {
13✔
550
                                // If we still don't have the first valid tstamp, we need to
551
                                // save the last known one (potentially pre-set) as the first
552
                                // one.
553
                                first_tstamp = last_known_tstamp;
554
                        }
555
                        err = io::WriteStringIntoOfstream(
13✔
556
                                ofs, tstamp_prefix_data + last_known_tstamp + corrupt_msg_suffix_data);
26✔
557
                        if (err != error::NoError) {
13✔
558
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
559
                        }
560
                }
561
        }
562
        return error::NoError;
22✔
563
}
564

565
static void ReplaceTimestampInMsgData(
9✔
566
        vector<uint8_t> &msg_data, string &timestamp, size_t default_tstamp_size) {
567
        auto msg_data_tstamp_start = msg_data.begin() + 15; // len(R"({"timestamp": ")")
568

569
        // The actual timestamp from logs can potentially have a different
570
        // (likely lower) time resolution and thus length than our default.
571
        const auto timestamp_size = timestamp.size();
572
        if (timestamp_size > default_tstamp_size) {
9✔
573
                // In case the time resolution is higher and the timestamp
574
                // longer (unlikely to happen)
575
                if (timestamp[timestamp_size - 1] == 'Z') {
1✔
576
                        timestamp[default_tstamp_size - 1] = 'Z';
1✔
577
                }
578
                timestamp.resize(default_tstamp_size);
1✔
579
        }
580
        copy_n(timestamp.cbegin(), timestamp.size(), msg_data_tstamp_start);
9✔
581
        if (timestamp.size() < default_tstamp_size) {
9✔
582
                // Add a closing '"' right after the timestamp and fill in the
583
                // rest of the space in the template with spaces that have no
584
                // effect in JSON.
585
                msg_data_tstamp_start[timestamp.size()] = '"';
1✔
586
                for (auto it = msg_data_tstamp_start + timestamp.size() + 1;
1✔
587
                         it < msg_data_tstamp_start + default_tstamp_size + 1;
4✔
588
                         it++) {
589
                        *it = ' ';
3✔
590
                }
591
        }
592
}
9✔
593

594
error::Error JsonLogMessagesReader::SanitizeLogs() {
22✔
595
        if (!sanitized_fpath_.empty()) {
22✔
596
                return error::NoError;
×
597
        }
598

599
        string prep_fpath = log_fpath_ + ".sanitized";
22✔
600
        string first_tstamp = default_tstamp_;
22✔
601
        auto err = DoSanitizeLogs(log_fpath_, prep_fpath, clean_logs_, first_tstamp);
22✔
602
        if (err != error::NoError) {
22✔
603
                if (path::FileExists(prep_fpath)) {
×
604
                        auto del_err = path::FileDelete(prep_fpath);
×
605
                        if (del_err != error::NoError) {
×
606
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
607
                        }
608
                }
609
        } else {
610
                sanitized_fpath_ = std::move(prep_fpath);
22✔
611
                reader_ = make_unique<io::FileReader>(sanitized_fpath_);
44✔
612
                auto ex_sz = GetLogFileDataSize(sanitized_fpath_);
22✔
613
                if (!ex_sz) {
22✔
614
                        return ex_sz.error().WithContext("Failed to determine deployment logs size");
×
615
                }
616

617
                raw_data_size_ = ex_sz.value();
22✔
618
                if (raw_data_size_ > maximum_log_size_) {
22✔
NEW
619
                        large_logs_ = true;
×
620
                        auto ex_off = FindNextMsgAfter(
621
                                sanitized_fpath_,
NEW
622
                                (raw_data_size_ + too_much_data_msg_tmpl_.size() - maximum_log_size_));
×
NEW
623
                        if (!ex_off) {
×
NEW
624
                                return ex_off.error().WithContext(
×
NEW
625
                                        "Failed to determine start offset in too large deployment logs");
×
626
                        }
NEW
627
                        reader_ = make_unique<io::FileReader>(sanitized_fpath_, ex_off.value());
×
NEW
628
                        raw_data_size_ -= ex_off.value();
×
629
                }
630
                rem_raw_data_size_ = raw_data_size_;
22✔
631
                if (!clean_logs_) {
22✔
632
                        ReplaceTimestampInMsgData(bad_data_msg_, first_tstamp, default_tstamp_.size());
9✔
633
                }
634
                if (large_logs_) {
22✔
NEW
635
                        ReplaceTimestampInMsgData(too_much_data_msg_, first_tstamp, default_tstamp_.size());
×
636
                }
637
        }
638
        return err;
22✔
639
}
640

641
error::Error JsonLogMessagesReader::Rewind() {
5✔
642
        AssertOrReturnError(!sanitized_fpath_.empty());
5✔
643
        header_rem_ = header_.size();
5✔
644
        closing_rem_ = closing_.size();
5✔
645
        bad_data_msg_rem_ = bad_data_msg_.size();
5✔
646
        too_much_data_msg_rem_ = too_much_data_msg_.size();
5✔
647

648
        // release/close the file first so that the FileDelete() below can actually
649
        // delete it and free space up
650
        reader_.reset();
651
        auto del_err = path::FileDelete(sanitized_fpath_);
5✔
652
        if (del_err != error::NoError) {
5✔
653
                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
2✔
654
        }
655
        sanitized_fpath_.erase();
5✔
656
        return SanitizeLogs();
5✔
657
}
658

659
int64_t JsonLogMessagesReader::TotalDataSize() {
17✔
660
        assert(!sanitized_fpath_.empty());
661

662
        auto ret = raw_data_size_ + header_.size() + closing_.size();
17✔
663
        if (!clean_logs_) {
17✔
664
                ret += bad_data_msg_.size();
9✔
665
        }
666
        if (large_logs_) {
17✔
NEW
667
                ret += too_much_data_msg_.size();
×
668
        }
669
        return ret;
17✔
670
}
671

672
ExpectedSize JsonLogMessagesReader::Read(
161✔
673
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
674
        AssertOrReturnUnexpected(!sanitized_fpath_.empty());
161✔
675

676
        if (header_rem_ > 0) {
161✔
677
                io::Vsize target_size = end - start;
19✔
678
                auto copy_end = copy_n(
19✔
679
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
19✔
680
                auto n_copied = copy_end - start;
681
                header_rem_ -= n_copied;
19✔
682
                return static_cast<size_t>(n_copied);
683
        } else if (!clean_logs_ && (bad_data_msg_rem_ > 0)) {
142✔
684
                io::Vsize target_size = end - start;
16✔
685
                auto copy_end = copy_n(
16✔
686
                        bad_data_msg_.begin() + (bad_data_msg_.size() - bad_data_msg_rem_),
16✔
687
                        min(bad_data_msg_rem_, target_size),
688
                        start);
689
                auto n_copied = copy_end - start;
690
                bad_data_msg_rem_ -= n_copied;
16✔
691
                return static_cast<size_t>(n_copied);
692
        } else if (large_logs_ && (too_much_data_msg_rem_ > 0)) {
126✔
NEW
693
                io::Vsize target_size = end - start;
×
NEW
694
                auto copy_end = copy_n(
×
NEW
695
                        too_much_data_msg_.begin() + (too_much_data_msg_.size() - too_much_data_msg_rem_),
×
696
                        min(too_much_data_msg_rem_, target_size),
697
                        start);
698
                auto n_copied = copy_end - start;
NEW
699
                too_much_data_msg_rem_ -= n_copied;
×
700
                return static_cast<size_t>(n_copied);
701
        } else if (rem_raw_data_size_ > 0) {
126✔
702
                if (end - start > rem_raw_data_size_) {
90✔
703
                        end = start + static_cast<size_t>(rem_raw_data_size_);
704
                }
705
                auto ex_sz = reader_->Read(start, end);
90✔
706
                if (!ex_sz) {
90✔
707
                        return ex_sz;
708
                }
709
                auto n_read = ex_sz.value();
90✔
710
                rem_raw_data_size_ -= n_read;
90✔
711

712
                // We control how much we read from the file so we should never read
713
                // 0 bytes (meaning EOF reached). If we do, it means the file is
714
                // smaller than what we were told.
715
                assert(n_read > 0);
716
                if (n_read == 0) {
90✔
717
                        return expected::unexpected(
×
718
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
719
                }
720
                return n_read;
721
        } else if (closing_rem_ > 0) {
36✔
722
                io::Vsize target_size = end - start;
18✔
723
                auto copy_end = copy_n(
18✔
724
                        closing_.begin() + (closing_.size() - closing_rem_),
18✔
725
                        min(closing_rem_, target_size),
726
                        start);
727
                auto n_copied = copy_end - start;
728
                closing_rem_ -= n_copied;
18✔
729
                return static_cast<size_t>(copy_end - start);
730
        } else {
731
                return 0;
732
        }
733
};
734

735
static const string logs_uri_suffix = "/log";
736

737
error::Error DeploymentClient::PushLogs(
4✔
738
        const string &deployment_id,
739
        const string &log_file_path,
740
        api::Client &client,
741
        LogsAPIResponseHandler api_handler) {
742
        auto logs_reader = make_shared<JsonLogMessagesReader>(log_file_path);
4✔
743
        auto err = logs_reader->SanitizeLogs();
4✔
744
        if (err != error::NoError) {
4✔
745
                return err;
×
746
        }
747

748
        auto req = make_shared<api::APIRequest>();
4✔
749
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
4✔
750
        req->SetMethod(http::Method::PUT);
4✔
751
        req->SetHeader("Content-Type", "application/json");
8✔
752
        req->SetHeader("Content-Length", to_string(logs_reader->TotalDataSize()));
8✔
753
        req->SetHeader("Accept", "application/json");
8✔
754
        req->SetBodyGenerator([logs_reader]() {
20✔
755
                logs_reader->Rewind();
8✔
756
                return logs_reader;
4✔
757
        });
758

759
        auto received_body = make_shared<vector<uint8_t>>();
4✔
760
        return client.AsyncCall(
16✔
761
                req,
762
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
763
                        this->PushLogsHeaderHandler(received_body, api_handler, exp_resp);
12✔
764
                },
4✔
765
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
11✔
766
                        if (!exp_resp) {
3✔
767
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
768
                                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
769
                                return;
×
770
                        }
771

772
                        auto resp = exp_resp.value();
3✔
773
                        auto status = resp->GetStatusCode();
3✔
774

775
                        // StatusTooManyRequests must have been handled in PushLogsHeaderHandler already
776
                        assert(status != http::StatusTooManyRequests);
777
                        // StatusRequestBodyTooLarge must have been handled in PushLogsHeaderHandler already
778
                        assert(status != http::StatusRequestBodyTooLarge);
779

780
                        if (status == http::StatusNoContent) {
3✔
781
                                api_handler(LogsAPIResponse {status, nullopt, error::NoError});
2✔
782
                        } else {
783
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
784
                                string err_str;
785
                                if (ex_err_msg) {
1✔
786
                                        err_str = ex_err_msg.value();
1✔
787
                                } else {
788
                                        err_str = resp->GetStatusMessage();
×
789
                                }
790
                                api_handler(LogsAPIResponse {
2✔
791
                                        status,
792
                                        nullopt,
793
                                        MakeError(
794
                                                BadResponseError,
795
                                                "Got unexpected response " + to_string(status)
1✔
796
                                                        + " from logs API: " + err_str)});
2✔
797
                        }
798
                });
4✔
799
}
800

801
void DeploymentClient::PushLogsHeaderHandler(
7✔
802
        shared_ptr<vector<uint8_t>> received_body,
803
        LogsAPIResponseHandler api_handler,
804
        http::ExpectedIncomingResponsePtr exp_resp) {
805
        if (!exp_resp) {
7✔
806
                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
807
                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
808
                return;
×
809
        }
810

811
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
812
        auto resp = exp_resp.value();
7✔
813
        auto status = resp->GetStatusCode();
7✔
814
        if (status == http::StatusTooManyRequests) {
7✔
815
                LogsAPIResponse response = {
816
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
817
                api_handler(response);
3✔
818
        } else if (status == http::StatusRequestBodyTooLarge) {
7✔
819
                LogsAPIResponse response = {
820
                        status,
821
                        resp->GetHeaders(),
822
                        MakeError(RequestBodyTooLargeError, "Could not send logs to server")};
1✔
823
                api_handler(response);
1✔
824
        }
1✔
825
        auto content_length = resp->GetHeader("Content-Length");
14✔
826
        if (!content_length) {
7✔
827
                log::Debug(
3✔
828
                        "Failed to get content length from the deployment log API response headers: "
829
                        + content_length.error().String());
6✔
830
                body_writer->SetUnlimited(true);
3✔
831
        } else {
832
                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
833
                if (!ex_len) {
4✔
834
                        log::Error(
×
835
                                "Failed to convert the content length from the deployment log API response headers to an integer: "
836
                                + ex_len.error().String());
×
837
                        body_writer->SetUnlimited(true);
×
838
                } else {
839
                        received_body->resize(ex_len.value());
4✔
840
                }
841
        }
842
        resp->SetBodyWriter(body_writer);
14✔
843
}
844

845
} // namespace deployments
846
} // namespace update
847
} // 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