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

mendersoftware / mender / 2297324832

30 Jan 2026 08:10PM UTC coverage: 81.656%. First build
2297324832

push

gitlab-ci

vpodzime
fix: Make JSONL parsing to deployment log more robust

Changelog: Sanitize deployments logs from disk before sending them to
the server. Fixes the issue that a corrupted log file will trigger HTTP
400 Bad Request from the server and the logs would never be submitted.
With this fix the corrupted line(s) are replaced with a known error
entry "(corrupted log)", savaging the rest of the well-formatted logs.

Ticket: MEN-9128

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Lluis Campos <lluis.campos@northern.tech>
Signed-off-by: Vratislav Podzimek <vratislav.podzimek+auto-signed@northern.tech>

85 of 102 new or added lines in 2 files covered. (83.33%)

8992 of 11012 relevant lines covered (81.66%)

19928.79 hits per line

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

80.43
/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
const DeploymentsErrorCategoryClass DeploymentsErrorCategory;
54

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

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

76
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
52✔
77
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
66✔
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(
10✔
84
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
85
        auto ex_compatible_type = ctx.GetCompatibleType();
20✔
86
        if (!ex_compatible_type) {
10✔
87
                return ex_compatible_type.error();
4✔
88
        }
89
        string compatible_type = ex_compatible_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;
6✔
101
        ss << R"({"device_provides":{)";
6✔
102
        ss << R"("device_type":")";
6✔
103
        ss << json::EscapeString(compatible_type);
12✔
104

105
        for (const auto &kv : provides) {
14✔
106
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
8✔
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]() {
54✔
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"])
12✔
127
                                         + "&device_type=" + http::URLEncode(compatible_type);
18✔
128
        auto v1_req = make_shared<api::APIRequest>();
6✔
129
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
6✔
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(
×
142
                                        CheckUpdatesAPIResponseError {status, nullopt, ex_j.error()}));
×
143
                        }
144
                } else if (status == http::StatusNoContent) {
2✔
145
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
146
                } else {
147
                        log::Warning(
×
148
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
149
                                + to_string(status));
×
150
                        api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
×
151
                                status,
152
                                nullopt,
153
                                MakeError(
154
                                        DeploymentAbortedError,
155
                                        "received unhandled HTTP response: " + to_string(status))}));
×
156
                }
157
        };
10✔
158

159
        http::ResponseHandler header_handler =
160
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
161
                        this->HeaderHandler(received_body, api_handler, exp_resp);
27✔
162
                };
15✔
163

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

176
                        // StatusTooManyRequests must have been handled in HeaderHandler already
177
                        assert(status != http::StatusTooManyRequests);
178

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

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

215
                // StatusTooManyRequests must have been handled in HeaderHandler already
216
                assert(status != http::StatusTooManyRequests);
217

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

245
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
246
}
12✔
247

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

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

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

285
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
286
static const string status_uri_suffix = "/status";
287

288
string DeploymentStatusString(DeploymentStatus status) {
501✔
289
        return deployment_status_strings[static_cast<int>(status)];
505✔
290
}
291

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

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

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

331
                        auto resp = exp_resp.value();
4✔
332
                        auto status = resp->GetStatusCode();
4✔
333

334
                        // StatusTooManyRequests must have been handled in PushStatusHeaderHandler already
335
                        assert(status != http::StatusTooManyRequests);
336

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

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

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

402
using mender::common::expected::ExpectedSize;
403

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

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

423
const vector<uint8_t> JsonLogMessagesReader::header_ = {
424
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
425
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
426
const string JsonLogMessagesReader::default_tstamp_ = "1970-01-01T00:00:00.000000000Z";
427
const string JsonLogMessagesReader::bad_data_msg_tmpl_ =
428
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},)d";
429

430
JsonLogMessagesReader::~JsonLogMessagesReader() {
42✔
431
        reader_.reset();
432
        if (!prepared_fpath_.empty()) {
14✔
433
                if (path::FileExists(prepared_fpath_)) {
14✔
434
                        auto del_err = path::FileDelete(prepared_fpath_);
14✔
435
                        if (del_err != error::NoError) {
14✔
NEW
436
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
437
                        }
438
                }
439
        }
440
        prepared_fpath_.erase();
14✔
441
}
28✔
442

443
static error::Error DoPrepareLogs(
18✔
444
        const string &orig_path, const string &new_path, bool &all_valid, string &first_tstamp) {
445
        auto ex_ifs = io::OpenIfstream(orig_path);
18✔
446
        if (!ex_ifs) {
18✔
NEW
447
                return ex_ifs.error();
×
448
        }
449
        auto ex_ofs = io::OpenOfstream(new_path);
18✔
450
        if (!ex_ofs) {
18✔
NEW
451
                return ex_ofs.error();
×
452
        }
453
        auto &ifs = ex_ifs.value();
18✔
454
        auto &ofs = ex_ofs.value();
18✔
455

456
        string last_known_tstamp = first_tstamp;
18✔
457
        const string tstamp_prefix_data = R"d({"timestamp": ")d";
18✔
458
        const string corrupt_msg_suffix_data =
459
                R"d(", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},)d";
18✔
460

461
        string line;
462
        first_tstamp.erase();
18✔
463
        all_valid = true;
18✔
464
        error::Error err;
18✔
465
        while (!ifs.eof()) {
85✔
466
                getline(ifs, line);
67✔
467
                if (!ifs.eof() && !ifs) {
67✔
NEW
468
                        int io_errno = errno;
×
469
                        return error::Error(
NEW
470
                                generic_category().default_error_condition(io_errno),
×
NEW
471
                                "Failed to get line from deployment logs file '" + orig_path
×
NEW
472
                                        + "': " + strerror(io_errno));
×
473
                }
474
                if (line.empty()) {
67✔
475
                        // skip empty lines
476
                        continue;
18✔
477
                }
478
                auto ex_json = json::Load(line);
98✔
479
                if (ex_json) {
49✔
480
                        // valid JSON log line, just replace the newline after it with a comma and save the
481
                        // timestamp for later
482
                        // TODO: handle invalid lines missing required fields?
483
                        auto ex_tstamp = ex_json.value().Get("timestamp").and_then(json::ToString);
82✔
484
                        if (ex_tstamp) {
41✔
485
                                if (first_tstamp.empty()) {
41✔
486
                                        first_tstamp = ex_tstamp.value();
17✔
487
                                }
488
                                last_known_tstamp = std::move(ex_tstamp.value());
41✔
489
                        }
490
                        line.append(1, ',');
41✔
491
                        err = io::WriteStringIntoOfstream(ofs, line);
41✔
492
                        if (err != error::NoError) {
41✔
NEW
493
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
494
                        }
495
                } else {
496
                        all_valid = false;
8✔
497
                        if (first_tstamp.empty()) {
8✔
498
                                // If we still don't have the first valid tstamp, we need to
499
                                // save the last known one (potentially pre-set) as the first
500
                                // one.
501
                                first_tstamp = last_known_tstamp;
502
                        }
503
                        err = io::WriteStringIntoOfstream(
8✔
504
                                ofs, tstamp_prefix_data + last_known_tstamp + corrupt_msg_suffix_data);
16✔
505
                        if (err != error::NoError) {
8✔
NEW
506
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
507
                        }
508
                }
509
        }
510
        return error::NoError;
18✔
511
}
512

513
error::Error JsonLogMessagesReader::PrepareLogs() {
18✔
514
        if (!prepared_fpath_.empty()) {
18✔
NEW
515
                return error::NoError;
×
516
        }
517

518
        string prep_fpath = log_fpath_ + ".prepared";
18✔
519
        string first_tstamp = default_tstamp_;
18✔
520
        auto err = DoPrepareLogs(log_fpath_, prep_fpath, clean_logs_, first_tstamp);
18✔
521
        if (err != error::NoError) {
18✔
NEW
522
                if (path::FileExists(prep_fpath)) {
×
NEW
523
                        auto del_err = path::FileDelete(prep_fpath);
×
NEW
524
                        if (del_err != error::NoError) {
×
NEW
525
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
526
                        }
527
                }
528
        } else {
529
                prepared_fpath_ = std::move(prep_fpath);
18✔
530
                reader_ = make_unique<io::FileReader>(prepared_fpath_);
36✔
531
                auto ex_sz = GetLogFileDataSize(prepared_fpath_);
18✔
532
                if (!ex_sz) {
18✔
NEW
533
                        return ex_sz.error().WithContext("Failed to determine deployment logs size");
×
534
                }
535
                raw_data_size_ = ex_sz.value();
18✔
536
                rem_raw_data_size_ = raw_data_size_;
18✔
537
                if (!clean_logs_) {
18✔
538
                        auto bad_data_msg_tstamp_start =
539
                                bad_data_msg_.begin() + 15; // len(R"({"timestamp": ")")
540
                        copy_n(first_tstamp.cbegin(), first_tstamp.size(), bad_data_msg_tstamp_start);
7✔
541
                }
542
        }
543
        return err;
18✔
544
}
545

546
error::Error JsonLogMessagesReader::Rewind() {
4✔
547
        AssertOrReturnError(!prepared_fpath_.empty());
4✔
548
        header_rem_ = header_.size();
4✔
549
        closing_rem_ = closing_.size();
4✔
550
        bad_data_msg_rem_ = bad_data_msg_.size();
4✔
551

552
        // release/close the file first so that the FileDelete() below can actually
553
        // delete it and free space up
554
        reader_.reset();
555
        auto del_err = path::FileDelete(prepared_fpath_);
4✔
556
        if (del_err != error::NoError) {
4✔
NEW
557
                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
558
        }
559
        prepared_fpath_.erase();
4✔
560
        return PrepareLogs();
4✔
561
}
562

563
int64_t JsonLogMessagesReader::TotalDataSize() {
14✔
564
        assert(!prepared_fpath_.empty());
565

566
        auto ret = raw_data_size_ + header_.size() + closing_.size();
14✔
567
        if (!clean_logs_) {
14✔
568
                ret += bad_data_msg_.size();
7✔
569
        }
570
        return ret;
14✔
571
}
572

573
ExpectedSize JsonLogMessagesReader::Read(
147✔
574
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
575
        AssertOrReturnUnexpected(!prepared_fpath_.empty());
147✔
576

577
        if (header_rem_ > 0) {
147✔
578
                io::Vsize target_size = end - start;
16✔
579
                auto copy_end = copy_n(
16✔
580
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
16✔
581
                auto n_copied = copy_end - start;
582
                header_rem_ -= n_copied;
16✔
583
                return static_cast<size_t>(n_copied);
584
        } else if (!clean_logs_ && (bad_data_msg_rem_ > 0)) {
131✔
585
                io::Vsize target_size = end - start;
14✔
586
                auto copy_end = copy_n(
14✔
587
                        bad_data_msg_.begin() + (bad_data_msg_.size() - bad_data_msg_rem_),
14✔
588
                        min(bad_data_msg_rem_, target_size),
589
                        start);
590
                auto n_copied = copy_end - start;
591
                bad_data_msg_rem_ -= n_copied;
14✔
592
                return static_cast<size_t>(n_copied);
593
        } else if (rem_raw_data_size_ > 0) {
117✔
594
                if (end - start > rem_raw_data_size_) {
87✔
595
                        end = start + static_cast<size_t>(rem_raw_data_size_);
596
                }
597
                auto ex_sz = reader_->Read(start, end);
87✔
598
                if (!ex_sz) {
87✔
599
                        return ex_sz;
600
                }
601
                auto n_read = ex_sz.value();
87✔
602
                rem_raw_data_size_ -= n_read;
87✔
603

604
                // We control how much we read from the file so we should never read
605
                // 0 bytes (meaning EOF reached). If we do, it means the file is
606
                // smaller than what we were told.
607
                assert(n_read > 0);
608
                if (n_read == 0) {
87✔
609
                        return expected::unexpected(
×
610
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
611
                }
612
                return n_read;
613
        } else if (closing_rem_ > 0) {
30✔
614
                io::Vsize target_size = end - start;
15✔
615
                auto copy_end = copy_n(
15✔
616
                        closing_.begin() + (closing_.size() - closing_rem_),
15✔
617
                        min(closing_rem_, target_size),
618
                        start);
619
                auto n_copied = copy_end - start;
620
                closing_rem_ -= n_copied;
15✔
621
                return static_cast<size_t>(copy_end - start);
622
        } else {
623
                return 0;
624
        }
625
};
626

627
static const string logs_uri_suffix = "/log";
628

629
error::Error DeploymentClient::PushLogs(
3✔
630
        const string &deployment_id,
631
        const string &log_file_path,
632
        api::Client &client,
633
        LogsAPIResponseHandler api_handler) {
634
        auto logs_reader = make_shared<JsonLogMessagesReader>(log_file_path);
3✔
635
        auto err = logs_reader->PrepareLogs();
3✔
636
        if (err != error::NoError) {
3✔
NEW
637
                return err;
×
638
        }
639

640
        auto req = make_shared<api::APIRequest>();
3✔
641
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
3✔
642
        req->SetMethod(http::Method::PUT);
3✔
643
        req->SetHeader("Content-Type", "application/json");
6✔
644
        req->SetHeader("Content-Length", to_string(logs_reader->TotalDataSize()));
6✔
645
        req->SetHeader("Accept", "application/json");
6✔
646
        req->SetBodyGenerator([logs_reader]() {
15✔
647
                logs_reader->Rewind();
6✔
648
                return logs_reader;
3✔
649
        });
650

651
        auto received_body = make_shared<vector<uint8_t>>();
3✔
652
        return client.AsyncCall(
12✔
653
                req,
654
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
6✔
655
                        this->PushLogsHeaderHandler(received_body, api_handler, exp_resp);
9✔
656
                },
3✔
657
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
9✔
658
                        if (!exp_resp) {
3✔
659
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
660
                                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
661
                                return;
×
662
                        }
663

664
                        auto resp = exp_resp.value();
3✔
665
                        auto status = resp->GetStatusCode();
3✔
666

667
                        // StatusTooManyRequests must have been handled in PushLogsHeaderHandler already
668
                        assert(status != http::StatusTooManyRequests);
669

670
                        if (status == http::StatusNoContent) {
3✔
671
                                api_handler(LogsAPIResponse {status, nullopt, error::NoError});
2✔
672
                        } else {
673
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
674
                                string err_str;
675
                                if (ex_err_msg) {
1✔
676
                                        err_str = ex_err_msg.value();
1✔
677
                                } else {
678
                                        err_str = resp->GetStatusMessage();
×
679
                                }
680
                                api_handler(LogsAPIResponse {
2✔
681
                                        status,
682
                                        nullopt,
683
                                        MakeError(
684
                                                BadResponseError,
685
                                                "Got unexpected response " + to_string(status)
1✔
686
                                                        + " from logs API: " + err_str)});
2✔
687
                        }
688
                });
3✔
689
}
690

691
void DeploymentClient::PushLogsHeaderHandler(
6✔
692
        shared_ptr<vector<uint8_t>> received_body,
693
        LogsAPIResponseHandler api_handler,
694
        http::ExpectedIncomingResponsePtr exp_resp) {
695
        if (!exp_resp) {
6✔
696
                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
697
                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
698
                return;
3✔
699
        }
700

701
        auto body_writer = make_shared<io::ByteWriter>(received_body);
6✔
702
        auto resp = exp_resp.value();
6✔
703
        auto status = resp->GetStatusCode();
6✔
704
        if (status == http::StatusTooManyRequests) {
6✔
705
                LogsAPIResponse response = {
706
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
707
                api_handler(response);
3✔
708
                return;
709
        }
3✔
710
        auto content_length = resp->GetHeader("Content-Length");
6✔
711
        if (!content_length) {
3✔
712
                log::Debug(
×
713
                        "Failed to get content length from the deployment log API response headers: "
714
                        + content_length.error().String());
×
715
                body_writer->SetUnlimited(true);
×
716
        } else {
717
                auto ex_len = common::StringTo<size_t>(content_length.value());
3✔
718
                if (!ex_len) {
3✔
719
                        log::Error(
×
720
                                "Failed to convert the content length from the deployment log API response headers to an integer: "
721
                                + ex_len.error().String());
×
722
                        body_writer->SetUnlimited(true);
×
723
                } else {
724
                        received_body->resize(ex_len.value());
3✔
725
                }
726
        }
727
        resp->SetBodyWriter(body_writer);
6✔
728
}
729

730
} // namespace deployments
731
} // namespace update
732
} // 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