Skip to content

Commit bb15a99

Browse files
authored
Merge pull request #44 from mathworks/changes_after_v_1_3_0
Changes after v 1 3 0
2 parents e2525ed + ac78aac commit bb15a99

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1368
-407
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ jobs:
66
build-and-run-tests:
77
runs-on: ubuntu-latest
88
env:
9-
OPENTELEMETRY_CPP_INSTALL: "${{ github.workspace }}/otel_cpp_install"
109
OPENTELEMETRY_MATLAB_INSTALL: "${{ github.workspace }}/otel_matlab_install"
11-
OPENTELEMETRY_COLLECTOR_INSTALL: "${{ github.workspace }}/otelcol"
1210
SYSTEM_LIBSTDCPP_PATH: "/usr/lib/x86_64-linux-gnu/libstdc++.so.6"
1311
steps:
1412
- name: Download OpenTelemetry-Matlab source
@@ -17,15 +15,10 @@ jobs:
1715
path: opentelemetry-matlab
1816
- name: Install MATLAB
1917
uses: matlab-actions/setup-matlab@v1
20-
- name: Download OpenTelemetry Collector binary
21-
run: |
22-
mkdir otelcol && cd otelcol
23-
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.75.0/otelcol_0.75.0_linux_amd64.tar.gz
24-
tar -xzf otelcol_0.75.0_linux_amd64.tar.gz
2518
- name: Build OpenTelemetry-Matlab
2619
run: |
2720
cd opentelemetry-matlab
28-
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ env.OPENTELEMETRY_MATLAB_INSTALL }}
21+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DWITH_EXAMPLES=ON -DCMAKE_INSTALL_PREFIX=${{ env.OPENTELEMETRY_MATLAB_INSTALL }}
2922
cmake --build build --config Release --target install
3023
- name: Run tests
3124
env:

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# build directory
1+
# build directories
22
build
33

44
# Autosave files

CMakeLists.txt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ else()
5656
endif()
5757

5858
if(NOT DEFINED VCPKG_INSTALLED_DIR)
59-
set(DVCPKG_INSTALLED_DIR ${CMAKE_BINARY_DIR}/vcpkg_installed)
59+
set(VCPKG_INSTALLED_DIR ${CMAKE_BINARY_DIR}/vcpkg_installed)
6060
endif()
6161

6262
# ######################################
@@ -71,6 +71,7 @@ endif()
7171
if(APPLE)
7272
option(SKIP_OTEL_CPP_PATCH "Whether to skip patching OpenTelemetry-cpp" OFF)
7373
endif()
74+
option(WITH_EXAMPLES "Whether to build examples" OFF)
7475

7576
# set vcpkg features depending on specified options
7677
set(VCPKG_MANIFEST_FEATURES "") # start with empty
@@ -269,7 +270,7 @@ endif()
269270
target_compile_options(${OPENTELEMETRY_PROXY_LIBRARY_NAME} PRIVATE ${OTLP_MACROS} ${CUSTOM_CXX_FLAGS})
270271

271272
# link against OpenTelemetry-cpp libraries and their dependencies
272-
target_link_libraries(${OPENTELEMETRY_PROXY_LIBRARY_NAME} PRIVATE ${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_common${CMAKE_STATIC_LIBRARY_SUFFIX}
273+
set(OTEL_CPP_LINK_LIBRARIES ${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_common${CMAKE_STATIC_LIBRARY_SUFFIX}
273274
${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_otlp_recordable${CMAKE_STATIC_LIBRARY_SUFFIX}
274275
${OTEL_CPP_PREFIX}/lib/${CMAKE_SHARED_LIBRARY_PREFIX}opentelemetry_proto${OTEL_PROTO_LIBRARY_SUFFIX}
275276
${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_resources${CMAKE_STATIC_LIBRARY_SUFFIX}
@@ -278,19 +279,21 @@ target_link_libraries(${OPENTELEMETRY_PROXY_LIBRARY_NAME} PRIVATE ${OTEL_CPP_PRE
278279
${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_logs${CMAKE_STATIC_LIBRARY_SUFFIX}
279280
${Protobuf_LIBRARIES})
280281
if(WITH_OTLP_HTTP)
281-
target_link_libraries(${OPENTELEMETRY_PROXY_LIBRARY_NAME} PRIVATE ${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_exporter_otlp_http${CMAKE_STATIC_LIBRARY_SUFFIX}
282+
set(OTEL_CPP_LINK_LIBRARIES ${OTEL_CPP_LINK_LIBRARIES} ${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_exporter_otlp_http${CMAKE_STATIC_LIBRARY_SUFFIX}
282283
${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_exporter_otlp_http_client${CMAKE_STATIC_LIBRARY_SUFFIX}
283284
${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_http_client_curl${CMAKE_STATIC_LIBRARY_SUFFIX}
284285
${CURL_LIBRARIES})
285286
endif()
286287
if(WITH_OTLP_GRPC)
287-
target_link_libraries(${OPENTELEMETRY_PROXY_LIBRARY_NAME} PRIVATE ${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_exporter_otlp_grpc${CMAKE_STATIC_LIBRARY_SUFFIX}
288+
set(OTEL_CPP_LINK_LIBRARIES ${OTEL_CPP_LINK_LIBRARIES} ${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_exporter_otlp_grpc${CMAKE_STATIC_LIBRARY_SUFFIX}
288289
${OTEL_CPP_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}opentelemetry_exporter_otlp_grpc_client${CMAKE_STATIC_LIBRARY_SUFFIX}
289290
${OTEL_CPP_PREFIX}/lib/${CMAKE_SHARED_LIBRARY_PREFIX}opentelemetry_proto_grpc${OTEL_PROTO_LIBRARY_SUFFIX}
290291
gRPC::grpc++
291292
absl::synchronization)
292293
endif()
293294

295+
target_link_libraries(${OPENTELEMETRY_PROXY_LIBRARY_NAME} PRIVATE ${OTEL_CPP_LINK_LIBRARIES})
296+
294297
# On Linux, when linking with certain static libraries, need to force include entire archive to avoid the linker mistakenly leaving out symbols
295298
if(UNIX AND NOT APPLE AND NOT CYGWIN)
296299
set(OPENTELEMETRY_PROXY_LINK_OPTIONS -Wl,--whole-archive
@@ -394,4 +397,12 @@ if(WITH_OTLP_GRPC)
394397
endif()
395398

396399
# Install dependent runtime libraries
397-
install(FILES ${OPENTELEMETRY_PROXY_RUNTIME_LIBRARIES} DESTINATION +libmexclass/+proxy)
400+
set(LIBMEXCLASS_PROXY_INSTALLED_DIR +libmexclass/+proxy)
401+
install(FILES ${OPENTELEMETRY_PROXY_RUNTIME_LIBRARIES} DESTINATION ${LIBMEXCLASS_PROXY_INSTALLED_DIR})
402+
403+
# ##############################
404+
# Subdirectories
405+
# ##############################
406+
if(WITH_EXAMPLES)
407+
add_subdirectory(examples)
408+
endif()

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# MATLAB Interface to OpenTelemetry
2+
[![View OpenTelemetry-Matlab on File Exchange](https://www.mathworks.com/matlabcentral/images/matlab-file-exchange.svg)](https://www.mathworks.com/matlabcentral/fileexchange/130979-opentelemetry-matlab) [![MATLAB](https://github.com/mathworks/OpenTelemetry-Matlab/actions/workflows/build.yml/badge.svg)](https://github.com/mathworks/OpenTelemetry-Matlab/actions/workflows/build.yml)
23

34
MATLAB® interface to [OpenTelemetry™](https://opentelemetry.io/), based on the [OpenTelemetry Specification](https://opentelemetry.io/docs/reference/specification/). OpenTelemetry is an observability framework for creating and managing telemetry data, such as traces, metrics, and logs. This data can then be sent to an observability back-end for monitoring, alerts, and analysis.
45

@@ -12,7 +13,8 @@ Requires MATLAB release R2022b or newer
1213
- [MATLAB](https://www.mathworks.com/products/matlab.html)
1314

1415
### 3rd Party Products:
15-
- [Opentelemetry C++](https://github.com/open-telemetry/opentelemetry-cpp)
16+
- [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector-releases/releases)
17+
- [OpenTelemetry C++](https://github.com/open-telemetry/opentelemetry-cpp)
1618
- [vcpkg C/C++ dependency manager](https://vcpkg.io)
1719

1820
## Installation
@@ -57,6 +59,8 @@ otelcol --config <otelcol-config-yaml>
5759
```
5860
4. If your collector is configured to display the data, you should see your span displayed.
5961

62+
For more examples, see the "examples" folder.
63+
6064
## Help
6165
To view documentation of individual function, type "help \<function_name>\". For example,
6266
```

api/trace/+opentelemetry/+trace/Span.m

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ function setStatus(obj, status, description)
132132
% new status is not valid, ignore
133133
return
134134
end
135-
description = opentelemetry.common.mustBeScalarString(description);
135+
if nargin < 3
136+
description = "";
137+
else
138+
description = opentelemetry.common.mustBeScalarString(description);
139+
end
136140
obj.Proxy.setStatus(status, description);
137141
end
138142

@@ -181,8 +185,7 @@ function setStatus(obj, status, description)
181185
function attrs = processAttributes(attrsin)
182186
import opentelemetry.trace.Span.processAttribute
183187

184-
nin = length(attrsin);
185-
if nin == 1 && isa(attrsin{1}, "dictionary")
188+
if isscalar(attrsin) && isa(attrsin{1}, "dictionary")
186189
% dictionary case
187190
attrtbl = entries(attrsin{1});
188191
nattr = height(attrtbl);
@@ -203,6 +206,7 @@ function setStatus(obj, status, description)
203206
attrs = attrs(:);
204207
else
205208
% NV pairs
209+
nin = length(attrsin);
206210
if rem(nin,2) ~= 0
207211
% Mismatched name-value pairs. Ignore all attributes.
208212
attrs = cell(1,0);

examples/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
add_subdirectory(context_propagation)
3+
add_subdirectory(webread)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
# C++ target
3+
set(CONTEXTPROP_EXAMPLE_TARGET contextprop_example_client)
4+
add_executable(${CONTEXTPROP_EXAMPLE_TARGET} cpp/client.cc)
5+
6+
target_include_directories(${CONTEXTPROP_EXAMPLE_TARGET} PRIVATE ${OTEL_CPP_PREFIX}/include)
7+
target_link_libraries(${CONTEXTPROP_EXAMPLE_TARGET} PRIVATE ${OTEL_CPP_LINK_LIBRARIES})
8+
if(UNIX AND NOT APPLE AND NOT CYGWIN)
9+
target_link_options(${CONTEXTPROP_EXAMPLE_TARGET} PRIVATE ${OPENTELEMETRY_PROXY_LINK_OPTIONS})
10+
elseif(APPLE)
11+
set_target_properties(${CONTEXTPROP_EXAMPLE_TARGET} PROPERTIES BUILD_RPATH "${CMAKE_INSTALL_PREFIX}/${LIBMEXCLASS_PROXY_INSTALLED_DIR}")
12+
endif()
13+
# use the same C++ standard as OpenTelemetry-cpp
14+
target_compile_features(${CONTEXTPROP_EXAMPLE_TARGET} PRIVATE cxx_std_${OTEL_CPP_CXX_STANDARD})
15+
16+
# MATLAB target
17+
find_package(Matlab REQUIRED COMPONENTS MCC_COMPILER MAIN_PROGRAM)
18+
19+
set(CONTEXTPROP_EXAMPLE_DEPLOYNAME mymagic)
20+
set(CONTEXTPROP_EXAMPLE_MATLAB_SOURCE ${CMAKE_CURRENT_LIST_DIR}/matlab/${CONTEXTPROP_EXAMPLE_DEPLOYNAME}.m)
21+
set(CONTEXTPROP_EXAMPLE_ROUTES ../../../examples/context_propagation/matlab/routes.json) #somehow, only relative paths are allowed
22+
matlab_get_version_from_matlab_run(${Matlab_MAIN_PROGRAM} Matlab_LIST_VERSION)
23+
if(DEFINED Matlab_LIST_VERSION AND ${Matlab_LIST_VERSION} VERSION_GREATER_EQUAL 23.2.0)
24+
# since MATLAB R2023b, route mapping can be specified at the archive level
25+
set(ARCHIVE_ROUTES ",ROUTES:${CONTEXTPROP_EXAMPLE_ROUTES}")
26+
else()
27+
set(ARCHIVE_ROUTES "")
28+
endif()
29+
install(CODE "execute_process(COMMAND ${Matlab_MCC_COMPILER} -W CTF:${CONTEXTPROP_EXAMPLE_DEPLOYNAME}${ARCHIVE_ROUTES} -U ${CONTEXTPROP_EXAMPLE_MATLAB_SOURCE} -a ${CMAKE_INSTALL_PREFIX} -a ${CMAKE_INSTALL_PREFIX}/+libmexclass/+proxy WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Context Propagation Example
2+
3+
In this example, a C++ client calls a MATLAB function hosted on MATLAB Production Server that returns a magic square matrix. Both the C++ client and the MATLAB code are instrumented with OpenTelemetry, and their generated spans form a single trace.
4+
5+
## Building the Example
6+
1. Enable WITH_EXAMPLES when building OpenTelemetry-Matlab
7+
```
8+
cmake -S . -B build -DWITH_EXAMPLES=ON -DCMAKE_INSTALL_PREFIX=<opentelemetry-matlab-installdir>
9+
cmake --build build --config Release
10+
```
11+
The built examples can be found in build/examples/context_propagation and subdirectories.
12+
2. [Create](https://www.mathworks.com/help/mps/server/creating-a-server.html) and [start](https://www.mathworks.com/help/mps/qs/starting-and-stopping.html) a MATLAB Production Server instance.
13+
3. [Deploy](https://www.mathworks.com/help/mps/qs/share-a-ctf-archive-on-the-server-instance.html) archive to server instance by copying to the auto_deploy directory.
14+
4. If using a MATLAB release before R2023b, [copy](https://www.mathworks.com/help/mps/server/use-web-handler-for-custom-routes-and-custom-payloads.html) matlab/routes.json to the config directory of the server instance.
15+
6. Start an instance of [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector).
16+
7. Start the C++ client.
17+
```
18+
cd cpp/build/Release
19+
http_client
20+
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2023 The MathWorks, Inc.
2+
3+
#pragma once
4+
5+
#include <string>
6+
#include "opentelemetry/nostd/string_view.h"
7+
#include "opentelemetry/trace/propagation/http_trace_context.h"
8+
9+
namespace
10+
{
11+
12+
template <typename T>
13+
class HttpTextMapCarrier : public opentelemetry::context::propagation::TextMapCarrier
14+
{
15+
public:
16+
HttpTextMapCarrier(T &headers) : headers_(headers) {}
17+
HttpTextMapCarrier() = default;
18+
virtual opentelemetry::nostd::string_view Get(
19+
opentelemetry::nostd::string_view key) const noexcept override
20+
{
21+
std::string key_to_compare = key.data();
22+
auto it = headers_.find(key_to_compare);
23+
if (it != headers_.end())
24+
{
25+
return it->second;
26+
}
27+
return "";
28+
}
29+
30+
virtual void Set(opentelemetry::nostd::string_view key,
31+
opentelemetry::nostd::string_view value) noexcept override
32+
{
33+
headers_.insert(std::pair<std::string, std::string>(std::string(key), std::string(value)));
34+
}
35+
36+
T headers_;
37+
};
38+
39+
} // namespace
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2023 The MathWorks, Inc.
2+
3+
#include "opentelemetry/ext/http/client/http_client_factory.h"
4+
#include "opentelemetry/ext/http/common/url_parser.h"
5+
#include "opentelemetry/trace/semantic_conventions.h"
6+
#include "HttpTextMapCarrier.h"
7+
8+
#include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h"
9+
#include "opentelemetry/sdk/trace/simple_processor_factory.h"
10+
#include "opentelemetry/sdk/trace/tracer_context.h"
11+
#include "opentelemetry/sdk/trace/tracer_context_factory.h"
12+
#include "opentelemetry/sdk/trace/tracer_provider_factory.h"
13+
#include "opentelemetry/trace/provider.h"
14+
15+
#include "opentelemetry/context/propagation/global_propagator.h"
16+
#include "opentelemetry/context/propagation/text_map_propagator.h"
17+
18+
#include <vector>
19+
#include "opentelemetry/ext/http/client/http_client.h"
20+
#include "opentelemetry/nostd/shared_ptr.h"
21+
22+
namespace
23+
{
24+
25+
using namespace opentelemetry::trace;
26+
namespace http_client = opentelemetry::ext::http::client;
27+
namespace context = opentelemetry::context;
28+
namespace nostd = opentelemetry::nostd;
29+
30+
void InitTracer()
31+
{
32+
auto exporter = opentelemetry::exporter::otlp::OtlpHttpExporterFactory::Create();
33+
auto processor =
34+
opentelemetry::sdk::trace::SimpleSpanProcessorFactory::Create(std::move(exporter));
35+
std::vector<std::unique_ptr<opentelemetry::sdk::trace::SpanProcessor>> processors;
36+
processors.push_back(std::move(processor));
37+
std::unique_ptr<opentelemetry::sdk::trace::TracerContext> context =
38+
opentelemetry::sdk::trace::TracerContextFactory::Create(std::move(processors));
39+
std::shared_ptr<opentelemetry::trace::TracerProvider> provider =
40+
opentelemetry::sdk::trace::TracerProviderFactory::Create(std::move(context));
41+
// Set the global trace provider
42+
opentelemetry::trace::Provider::SetTracerProvider(provider);
43+
44+
// set global propagator
45+
opentelemetry::context::propagation::GlobalTextMapPropagator::SetGlobalPropagator(
46+
opentelemetry::nostd::shared_ptr<opentelemetry::context::propagation::TextMapPropagator>(
47+
new opentelemetry::trace::propagation::HttpTraceContext()));
48+
}
49+
50+
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer> get_tracer(std::string tracer_name)
51+
{
52+
auto provider = opentelemetry::trace::Provider::GetTracerProvider();
53+
return provider->GetTracer(tracer_name);
54+
}
55+
56+
void sendRequest(const std::string &url)
57+
{
58+
auto http_client = http_client::HttpClientFactory::CreateSync();
59+
// define input to post to destination
60+
std::vector<uint8_t> body;
61+
uint8_t magic_square_size = 3; // request 3x3 magic square
62+
body.push_back(magic_square_size);
63+
64+
// start active span
65+
StartSpanOptions options;
66+
options.kind = SpanKind::kClient; // client
67+
opentelemetry::ext::http::common::UrlParser url_parser(url);
68+
69+
std::string span_name = url_parser.path_;
70+
auto span = get_tracer("http-client")
71+
->StartSpan(span_name,
72+
{{SemanticConventions::kUrlFull, url_parser.url_},
73+
{SemanticConventions::kUrlScheme, url_parser.scheme_},
74+
{SemanticConventions::kHttpRequestMethod, "POST"}},
75+
options);
76+
auto scope = get_tracer("http-client")->WithActiveSpan(span);
77+
78+
// inject current context into http header
79+
auto current_ctx = context::RuntimeContext::GetCurrent();
80+
HttpTextMapCarrier<http_client::Headers> carrier;
81+
auto prop = context::propagation::GlobalTextMapPropagator::GetGlobalPropagator();
82+
prop->Inject(carrier, current_ctx);
83+
84+
// send http request
85+
http_client::Result result = http_client->Post(url, body, carrier.headers_);
86+
if (result)
87+
{
88+
// set span attributes
89+
auto status_code = result.GetResponse().GetStatusCode();
90+
span->SetAttribute(SemanticConventions::kHttpResponseStatusCode, status_code);
91+
result.GetResponse().ForEachHeader(
92+
[&span](nostd::string_view header_name, nostd::string_view header_value) {
93+
span->SetAttribute("http.header." + std::string(header_name.data()), header_value);
94+
return true;
95+
});
96+
97+
if (status_code >= 400)
98+
{
99+
span->SetStatus(StatusCode::kError);
100+
}
101+
}
102+
else
103+
{
104+
span->SetStatus(
105+
StatusCode::kError,
106+
"Response Status :" +
107+
std::to_string(
108+
static_cast<typename std::underlying_type<http_client::SessionState>::type>(
109+
result.GetSessionState())));
110+
}
111+
// end span and export data
112+
span->End();
113+
}
114+
115+
void CleanupTracer()
116+
{
117+
std::shared_ptr<opentelemetry::trace::TracerProvider> none;
118+
opentelemetry::trace::Provider::SetTracerProvider(none);
119+
}
120+
121+
} // namespace
122+
123+
int main(int argc, char *argv[])
124+
{
125+
InitTracer();
126+
constexpr char default_host[] = "localhost";
127+
constexpr char default_path[] = "/mymagic/magic";
128+
constexpr uint16_t default_port = 9910;
129+
uint16_t port;
130+
131+
// The port the validation service listens to can be specified via the command line.
132+
if (argc > 1)
133+
{
134+
port = (uint16_t)(atoi(argv[1]));
135+
}
136+
else
137+
{
138+
port = default_port;
139+
}
140+
141+
std::string url = "http://" + std::string(default_host) + ":" + std::to_string(port) +
142+
std::string(default_path);
143+
sendRequest(url);
144+
CleanupTracer();
145+
}

0 commit comments

Comments
 (0)